C # Сортування та Порядок порівняння


105

Я можу сортувати список за допомогою Сортування або Упорядкувати. Який швидше? Обидва працюють за одним алгоритмом?

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);
    }
}

22
Я не можу повірити, що жодна з відповідей не згадала про це, але найбільша відмінність полягає в наступному: OrderBy робить відсортовану копію масиву чи списку, а сортування насправді сортує її на місці.
PRMan

2
як заголовок сказати порівняння, я хотів би додати, що OrderBy є стабільним і сортування є стабільним до 16 елементів, оскільки до 16 елементів вставлення сортування використовується, якщо елементів більше, ніж тоді він переходить на інші нестабільні альги Редагувати: стабільний означає підтримання відносного порядку елементів, що мають однаковий ключ.
Еклавьяа

@PRMan Nope, 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
Зауважимо також, що тест тут сортує список перед запуском секундоміра, тому ми порівнюємо порівняння двох алгоритмів, зіткнувшись із сортованим входом. Це може бути зовсім іншим, ніж їх відносна продуктивність з несортованим входом.
фог

3
Ці результати досить дивують IMHO, враховуючи той факт, що LINQдоводиться витрачати додаткову пам’ять порівняно з реальною List<T>.Sortреалізацією. Я не впевнений, чи покращили вони це в нових версіях .NET, але на моїй машині (i7 3-го покоління, 64-розрядний .NET 4.5 випуск) Sortпереважає OrderByу всіх випадках. Крім того, дивлячись на OrderedEnumerable<T>вихідний код, здається, що він створює три додаткові масиви (спочатку a Buffer<T>, потім масив проектованих ключів, потім масив індексів), перш ніж нарешті викликати Quicksort для сортування масиву індексів на місці.
Groo

2
... а потім після всього цього відбувається ToArrayдзвінок, який створює отриманий масив. Операції з пам'яттю та індексація масивів - це надзвичайно швидкі операції, але я все ще не можу знайти логіку цих результатів.
Groo

121

Ні, це не той самий алгоритм. Для початківців LINQ OrderByзадокументовано як стабільний (тобто якщо два елементи однакові Name, вони з'являться у первинному порядку).

Це також залежить від того, чи буферизуєте ви запит проти ітерації його декілька разів (LINQ до об'єктів, якщо ви не буферуєте результат, буде повторно замовлений 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 Я не дуже слідкую за різницею, якби два елементи були рівними, а їх порядок не зберігався. Це, звичайно, не виглядає проблемою для примітивних типів даних. Але навіть для довідкового типу, чому б це мало значення, якщо я б сортував, людина з прізвищем Марк Гравелл з'явилася перед іншою людиною з прізвищем Марк Гравелл (наприклад :))? Я не ставлю під сумнів вашу відповідь / знання, скоріше шукаю застосування цього сценарію.
Мукус

4
@Mukus уявіть, що ви сортуєте адресну книгу компанії за назвою (чи справді за датою народження) - неминуче будуть дублікати. Питання в кінцевому підсумку: що з ними відбувається? Чи визначено підзарядку?
Marc Gravell

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.
довід

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.
фог

14

Коротко :

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

  • Нестабільний сорт.
  • Зроблено на місці.
  • Використовуйте Introsort / Quicksort.
  • Замовне порівняння проводиться шляхом порівняння. Якщо порівняння дороге, воно може бути повільніше, ніж OrderBy () (які дозволяють використовувати клавіші, див. Нижче).

OrderBy / thenBy ():

  • Стабільний сорт.
  • Не на місці.
  • Використовуйте Quicksort. Quicksort - це не стійкий сорт. Ось хитрість: при сортуванні, якщо два елементи мають рівний ключ, він порівнює їх початковий порядок (який було збережено перед сортуванням).
  • Дозволяє використовувати клавіші (за допомогою лямбда) для сортування елементів за їх значеннями (наприклад:) x => x.Id. Усі клавіші витягуються спочатку перед сортуванням. Це може призвести до кращих показників, ніж використання Sort () та спеціального порівняння.

Джерела: MDSN , довідкове джерело та сховище dotnet / coreclr (GitHub).

Деякі з перерахованих вище тверджень засновані на поточній реалізації .NET Framework (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/…
Thor

-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.

Дуже легко.

Я не можу зробити це з роду. Цікаво, чому. Зробіть добре з замовленням.

Що стосується швидкості, це завжди O (n).


3
Питання: Час O (n) (я припускаю) у вашій відповіді відноситься до OrderBy або Порівнятеля? Я не думаю, що швидкий сорт може досягти часу O (N).
Кевман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.