C # Сортировка и сортировка по сравнению

105

Я могу отсортировать список с помощью Sort или OrderBy. Какой из них быстрее? Оба работают по одному и тому же алгоритму?

List<Person> persons = new List<Person>();
persons.Add(new Person("P005", "Janson"));
persons.Add(new Person("P002", "Aravind"));
persons.Add(new Person("P007", "Kazhal"));

1.

persons.Sort((p1,p2)=>string.Compare(p1.Name,p2.Name,true));

2.

var query = persons.OrderBy(n => n.Name, new NameComparer());

class NameComparer : IComparer<string>
{
    public int Compare(string x,string y)
    {
      return  string.Compare(x, y, true);
    }
}
user215675
источник
23
Я не могу поверить, что ни один из ответов не упомянул об этом, но самая большая разница заключается в следующем: OrderBy создает отсортированную копию массива или списка, тогда как Sort фактически сортирует его на месте.
PRMan
2
в качестве заголовка говорится, что сравнение, я хотел бы добавить, что OrderBy стабилен, а сортировка стабильна до 16 элементов, так как сортировка вставки до 16 элементов используется, если элементов больше, чем это, тогда он переключается на другие нестабильные алгоритмы Изменить: стабильный означает поддержание относительного порядка элементов, имеющих одинаковый ключ.
Eklavyaa
@PRMan Нет, OrderBy создает ленивое перечисление. Только если вы вызовете такой метод, как ToList, для возвращаемого перечислимого объекта, вы получите отсортированную копию.
Стюарт
1
@Stewart, вы не считаете, что Array.Copy или Collection.Copy в TElement [] в буфере в System.Core / System / Linq / Enumerable.cs копией? И если вы вызовете ToList в IEnumerable, у вас может быть сразу 3 копии в памяти одновременно. Это проблема для очень больших массивов, что было частью моей точки зрения. Кроме того, если вам нужен один и тот же отсортированный порядок более одного раза, то однократный вызов сортировки на месте намного эффективнее, чем повторная сортировка списка, из-за его постоянства.
PRMan
1
@PRMan О, вы имели в виду, что отсортированная копия создается внутренне. Тем не менее, это неточно, поскольку OrderBy не создает копию - насколько я могу судить, это выполняется методом GetEnumerator, когда вы фактически начинаете перебирать коллекцию в цикле. Я просто попробовал пошагово выполнить свой код и обнаружил, что код, который заполняет переменную из выражения LINQ, выполняется почти мгновенно, но когда вы переходите в цикл foreach, он тратит время на его сортировку. Думаю, когда у меня будет немного больше времени, я должен потратить немного времени, пытаясь выяснить, как это работает за кулисами.
Стюарт

Ответы:

90

Почему бы не измерить:

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
    }

    static void Main()
    {
        List<Person> persons = new List<Person>();
        persons.Add(new Person("P005", "Janson"));
        persons.Add(new Person("P002", "Aravind"));
        persons.Add(new Person("P007", "Kazhal"));

        Sort(persons);
        OrderBy(persons);

        const int COUNT = 1000000;
        Stopwatch watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            Sort(persons);
        }
        watch.Stop();
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            OrderBy(persons);
        }
        watch.Stop();
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }
}

На моем компьютере при компиляции в режиме выпуска эта программа печатает:

Sort: 1162ms
OrderBy: 1269ms

ОБНОВИТЬ:

Как предложил @Stefan, вот результаты сортировки большого списка меньшее количество раз:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), "Janson" + i.ToString()));
}

Sort(persons);
OrderBy(persons);

const int COUNT = 30;
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    Sort(persons);
}
watch.Stop();
Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    OrderBy(persons);
}
watch.Stop();
Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

Печать:

Sort: 8965ms
OrderBy: 8460ms

В этом сценарии похоже, что OrderBy работает лучше.


ОБНОВЛЕНИЕ2:

И используя случайные имена:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
}

Куда:

private static Random randomSeed = new Random();
public static string RandomString(int size, bool lowerCase)
{
    var sb = new StringBuilder(size);
    int start = (lowerCase) ? 97 : 65;
    for (int i = 0; i < size; i++)
    {
        sb.Append((char)(26 * randomSeed.NextDouble() + start));
    }
    return sb.ToString();
}

Урожайность:

Sort: 8968ms
OrderBy: 8728ms

Все еще OrderBy быстрее

Дарин Димитров
источник
2
Я думаю, это сильно отличается от сортировки очень маленького списка (3 элемента) 1000000 раз или сортировки очень большого списка (1000000 элементов) всего несколько раз. Оба очень актуальны. На практике наибольший интерес представляет средний размер списка (какой? ... допустим, сейчас 1000 пунктов). ИМХО, сортировка списков из 3 пунктов не имеет большого смысла.
Стефан Штайнеггер,
25
Обратите внимание, что есть разница между «быстрее» и «заметно быстрее». В вашем последнем примере разница составляла около четверти секунды. Заметит ли пользователь? Недопустимо, чтобы пользователь ждал результата почти девять секунд? Если на оба вопроса ответят «нет», то с точки зрения производительности не имеет значения, какой из них вы выберете.
Эрик Липперт
12
Также обратите внимание, что тест здесь сортирует список перед запуском секундомера, поэтому мы сравниваем, как два алгоритма сравниваются при работе с отсортированным вводом. Это может сильно отличаться от их относительной производительности с несортированным вводом.
phoog
3
Эти результаты довольно удивительны, IMHO, учитывая тот факт, что LINQприходится тратить дополнительную память по сравнению с реализацией на месте List<T>.Sort. Я не уверен, улучшили ли они это в более новых версиях .NET, но на моей машине (i7 третьего поколения 64-битная версия .NET 4.5) Sortпревосходит OrderByво всех случаях. Кроме того, глядя на OrderedEnumerable<T>исходный код, кажется, что он создает три дополнительных массива (сначала a Buffer<T>, затем массив спроецированных ключей, затем массив индексов), прежде чем, наконец, вызвать Quicksort для сортировки массива индексов на месте.
Groo
2
... а затем после всего этого происходит ToArrayвызов, который создает результирующий массив. Операции с памятью и индексация массивов - невероятно быстрые операции, но я все еще не могу найти логику этих результатов.
Groo
121

Нет, это разные алгоритмы. Для начала, LINQ OrderByзадокументирован как стабильный (т. Е. Если два элемента имеют одинаковые значения Name, они будут отображаться в исходном порядке).

Это также зависит от того, буферизуете ли вы запрос или повторяете его несколько раз (LINQ-to-Objects, если вы не буферизуете результат, будет переупорядочен для каждого foreach).

Для OrderByзапроса я также хотел бы использовать:

OrderBy(n => n.Name, StringComparer.{yourchoice}IgnoreCase);

(для {yourchoice}одного из CurrentCulture, Ordinalили InvariantCulture).

List<T>.Sort

Этот метод использует Array.Sort, который использует алгоритм QuickSort. Эта реализация выполняет нестабильную сортировку; то есть, если два элемента равны, их порядок может не сохраниться. Напротив, стабильная сортировка сохраняет порядок равных элементов.

Enumerable.OrderBy

Этот метод выполняет стабильную сортировку; то есть, если ключи двух элементов равны, порядок элементов сохраняется. Напротив, нестабильная сортировка не сохраняет порядок элементов с одинаковым ключом. Сортировать; то есть, если два элемента равны, их порядок может не сохраниться. Напротив, стабильная сортировка сохраняет порядок равных элементов.

Марк Гравелл
источник
5
Если вы используете .NET Reflector или ILSpy для взлома Enumerable.OrderByи детализации его внутренней реализации, вы можете увидеть, что алгоритм сортировки OrderBy - это вариант QuickSort, который выполняет стабильную сортировку. (См System.Linq.EnumerableSorter<TElement>.) Таким образом, Array.Sortи Enumerable.OrderByоба могут иметь время выполнения O (N log N) , где N - количество элементов в коллекции.
Джон Бейер
@Marc Я не совсем понимаю, какая была бы разница, если бы два элемента были равны и их порядок не сохранялся. Это определенно не похоже на проблему для примитивных типов данных. Но даже для ссылочного типа, какое это имеет значение, если я буду сортировать, человек с именем Марк Гравелл появится перед другим человеком с именем Марк Гравелл (например :))? Я не подвергаю сомнению ваш ответ / знания, скорее ищу применение этого сценария.
Mukus
4
@Mukus представьте, что вы сортируете адресную книгу компании по имени (или даже по дате рождения) - там неизбежно будут дубликаты. В конечном итоге возникает вопрос: что с ними происходит? Определен ли подзаказ?
Марк Гравелл
55

Ответ Дарина Димитрова показывает, что OrderByэто немного быстрее, чем List.Sortпри работе с уже отсортированным вводом. Я изменил его код, так что он неоднократно сортирует несортированные данные и OrderByв большинстве случаев работает немного медленнее.

Кроме того, OrderByтест использует ToArrayдля принудительного перечисления перечислителя Linq, но он, очевидно, возвращает тип ( Person[]), который отличается от входного типа ( List<Person>). Поэтому я повторно запустил тест, используя ToListвместо, ToArrayи получил еще большую разницу:

Sort: 25175ms
OrderBy: 30259ms
OrderByWithToList: 31458ms

Код:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
        public override string ToString()
        {
            return Id + ": " + Name;
        }
    }

    private static Random randomSeed = new Random();
    public static string RandomString(int size, bool lowerCase)
    {
        var sb = new StringBuilder(size);
        int start = (lowerCase) ? 97 : 65;
        for (int i = 0; i < size; i++)
        {
            sb.Append((char)(26 * randomSeed.NextDouble() + start));
        }
        return sb.ToString();
    }

    private class PersonList : List<Person>
    {
        public PersonList(IEnumerable<Person> persons)
           : base(persons)
        {
        }

        public PersonList()
        {
        }

        public override string ToString()
        {
            var names = Math.Min(Count, 5);
            var builder = new StringBuilder();
            for (var i = 0; i < names; i++)
                builder.Append(this[i]).Append(", ");
            return builder.ToString();
        }
    }

    static void Main()
    {
        var persons = new PersonList();
        for (int i = 0; i < 100000; i++)
        {
            persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
        } 

        var unsortedPersons = new PersonList(persons);

        const int COUNT = 30;
        Stopwatch watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            Sort(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderBy(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderByWithToList(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderByWithToList: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }

    static void OrderByWithToList(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToList();
    }
}
фог
источник
2
Я запускаю тестовый код сейчас в LinqPad 5 (.net 5), и он OrderByWithToListзанимает то же время, что и OrderBy.
dovid 06
38

Думаю, важно отметить еще одно различие между Sortи OrderBy:

Предположим, существует Person.CalculateSalary()метод, который требует много времени; возможно, больше, чем даже операция по сортировке большого списка.

Сравнить

// Option 1
persons.Sort((p1, p2) => Compare(p1.CalculateSalary(), p2.CalculateSalary()));
// Option 2
var query = persons.OrderBy(p => p.CalculateSalary()); 

Вариант 2 может иметь превосходную производительность, поскольку он вызывает CalculateSalaryметод только n раз, тогда как Sortопция может вызывать CalculateSalaryдо 2 n log ( n ) раз, в зависимости от успеха алгоритма сортировки.

Омер Равив
источник
4
Это правда, хотя есть решение этой проблемы, а именно, хранить данные в массиве и использовать перегрузку Array.Sort, которая принимает два массива, один из ключей, а другой из значений. Заполняя массив ключей, вы вызовете CalculateSalary ntimes. Очевидно, что это не так удобно, как использование OrderBy.
phoog 09
14

В двух словах :

Список / Сортировка массива ():

  • Нестабильный сорт.
  • Готово на месте.
  • Используйте Introsort / Quicksort.
  • Пользовательское сравнение выполняется с помощью средства сравнения. Если сравнение стоит дорого, оно может быть медленнее, чем OrderBy () (который позволяет использовать ключи, см. Ниже).

OrderBy / ThenBy ():

  • Стабильный сорт.
  • Не на месте.
  • Используйте Quicksort. Быстрая сортировка не является стабильной. Вот уловка: при сортировке, если два элемента имеют одинаковый ключ, он сравнивает их начальный порядок (который был сохранен перед сортировкой).
  • Позволяет использовать ключи (с использованием лямбда-выражений) для сортировки элементов по их значениям (например:) x => x.Id. Все ключи извлекаются перед сортировкой. Это может привести к более высокой производительности, чем использование Sort () и настраиваемого компаратора.

Источники: MDSN , справочный источник и репозиторий dotnet / coreclr (GitHub).

Некоторые из перечисленных выше утверждений основаны на текущей реализации платформы .NET (4.7.2). Это может измениться в будущем.

тигру
источник
0

следует рассчитать сложность алгоритмов, используемых методами OrderBy и Sort. Насколько я помню, QuickSort имеет сложность n (log n), где n - длина массива.

Я тоже искал orderby, но не смог найти никакой информации даже в библиотеке msdn. если у вас нет одинаковых значений и сортировки, относящейся только к одному свойству, я предпочитаю использовать метод Sort (); если нет, то используйте OrderBy.

икаптан
источник
1
Согласно текущей документации MSDN, Sort использует 3 разных алгоритма сортировки на основе входных данных. Среди них QuickSort. Вопрос по алгоритму OrderBy () здесь (Quicksort): stackoverflow.com/questions/2792074/…
Тор
-1

Я просто хочу добавить, что orderby намного полезнее.

Зачем? Потому что я могу это сделать:

Dim thisAccountBalances = account.DictOfBalances.Values.ToList
thisAccountBalances.ForEach(Sub(x) x.computeBalanceOtherFactors())
thisAccountBalances=thisAccountBalances.OrderBy(Function(x) x.TotalBalance).tolist
listOfBalances.AddRange(thisAccountBalances)

Почему сложный компаратор? Просто отсортируйте по полю. Здесь я сортирую по TotalBalance.

Очень просто.

Я не могу этого сделать с сортировкой. Интересно, почему. Ладно с orderBy.

Что касается скорости, это всегда O (n).

user4951
источник
3
Вопрос: Время O (n) (я полагаю) в вашем ответе относится к OrderBy или Comparer? Я не думаю, что быстрая сортировка может достичь O (N) времени.
Кевман