C # Sort e comparação OrderBy

105

Posso classificar uma lista usando Sort ou OrderBy. Qual é mais rápido? Ambos estão trabalhando no mesmo algoritmo?

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
fonte
22
Não posso acreditar que nenhuma das respostas mencionou isso, mas a maior diferença é esta: OrderBy faz uma cópia classificada do Array ou List, enquanto Sort realmente classifica no lugar.
PRMan de
2
como o título diz comparação, gostaria de adicionar que OrderBy é estável e a classificação é estável até 16 elementos, já que a classificação por inserção de até 16 elementos é usada se os elementos forem mais do que isso, então ele alterna para outros algoritmos instáveis ​​Edit: stable significa manter a ordem relativa de elementos com a mesma chave.
Eklavyaa
@PRMan Não, OrderBy cria um enumerável preguiçoso. Somente se você chamar um método como ToList no enumerável retornado, você obterá uma cópia classificada.
Stewart
1
@Stewart, Você não considera Array.Copy ou Collection.Copy em TElement [] em Buffer em System.Core / System / Linq / Enumerable.cs uma cópia? E se você chamar ToList no IEnumerable, você pode ter momentaneamente 3 cópias na memória de uma vez. Este é um problema para matrizes muito grandes, o que fazia parte do meu ponto. Além disso, se você precisar da mesma ordem de classificação mais de uma vez, chamar Classificar no local uma vez é muito mais eficiente do que classificar repetidamente a Lista, devido à sua permanência.
PRMan
1
@PRMan Oh, você quis dizer que uma cópia classificada é construída internamente. Ainda assim, isso é impreciso, pois OrderBy não cria a cópia - pelo que posso ver, isso é feito pelo método GetEnumerator quando você realmente começa a percorrer a coleção. Eu apenas tentei percorrer meu código e descobri que o código que preenche uma variável de uma expressão LINQ é executado quase que instantaneamente, mas quando você entra no loop foreach, ele gasta tempo classificando-o. Acho que quando tiver um pouco mais de tempo, devo passar algum tempo tentando descobrir como isso funciona nos bastidores.
Stewart

Respostas:

90

Por que não medir:

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

No meu computador, quando compilado no modo Release, este programa imprime:

Sort: 1162ms
OrderBy: 1269ms

ATUALIZAR:

Conforme sugerido por @Stefan, aqui estão os resultados de classificar uma lista grande menos vezes:

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

Impressões:

Sort: 8965ms
OrderBy: 8460ms

Nesse cenário, parece que o desempenho de OrderBy é melhor.


ATUALIZAÇÃO2:

E usando nomes aleatórios:

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

Onde:

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

Rendimentos:

Sort: 8968ms
OrderBy: 8728ms

Ainda assim, OrderBy é mais rápido

Darin Dimitrov
fonte
2
Eu acho que é muito diferente de classificar uma lista muito pequena (3 itens) 1000000 vezes, ou classificar uma lista muito grande (1000000 itens) apenas algumas vezes. Ambos são muito relevantes. Na prática, o tamanho médio da lista (o que é médio? ... digamos 1000 itens por enquanto) é o mais interessante. IMHO, classificar listas com 3 itens não é muito significativo.
Stefan Steinegger
25
Observe que há uma diferença entre "mais rápido" e "notavelmente mais rápido". Em seu último exemplo, a diferença foi de cerca de um quarto de segundo. O usuário vai notar? É inaceitável que o usuário espere quase nove segundos pelo resultado? Se as respostas a ambas as perguntas forem "não", então realmente não importa qual você escolher do ponto de vista do desempenho.
Eric Lippert
12
Observe também que o teste aqui classifica a lista antes de iniciar o cronômetro, portanto, estamos comparando como os dois algoritmos se comparam quando confrontados com a entrada classificada. Isso pode ser bem diferente de seu desempenho relativo com entrada não classificada.
phoog de
3
Esses resultados são bastante surpreendentes IMHO, considerando o fato de que LINQtem que gastar memória adicional em comparação com uma List<T>.Sortimplementação no local . Não tenho certeza se eles melhoraram isso nas versões mais recentes do .NET, mas na minha máquina (versão i7 do .NET 4.5 de 64 bits de 3ª geração) Sortsupera OrderByem todos os casos. Além disso, ao observar o OrderedEnumerable<T>código-fonte, parece que ele cria três matrizes adicionais (primeiro a Buffer<T>, depois uma matriz de chaves projetadas e, em seguida, uma matriz de índices) antes de finalmente chamar o Quicksort para classificar a matriz de índices no lugar.
Groo
2
... e depois de tudo isso, há a ToArraychamada que cria o array resultante. Operações de memória e indexação de array são operações incrivelmente rápidas, mas ainda não consigo encontrar a lógica por trás desses resultados.
Groo
121

Não, eles não são o mesmo algoritmo. Para começar, o LINQ OrderByé documentado como estável (ou seja, se dois itens tiverem o mesmo Name, eles aparecerão em sua ordem original).

Também depende de você armazenar em buffer a consulta em vez de iterá-la várias vezes (LINQ-to-Objects, a menos que você armazene o resultado em buffer, será reordenado por foreach).

Para a OrderByconsulta, também ficaria tentado a usar:

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

(para {yourchoice}um de CurrentCulture, Ordinalou InvariantCulture).

List<T>.Sort

Este método usa Array.Sort, que usa o algoritmo QuickSort. Essa implementação executa uma classificação instável; ou seja, se dois elementos forem iguais, sua ordem pode não ser preservada. Em contraste, uma classificação estável preserva a ordem dos elementos que são iguais.

Enumerable.OrderBy

Este método executa uma classificação estável; ou seja, se as chaves de dois elementos são iguais, a ordem dos elementos é preservada. Em contraste, uma classificação instável não preserva a ordem dos elementos que possuem a mesma chave. ordenar; ou seja, se dois elementos forem iguais, sua ordem pode não ser preservada. Em contraste, uma classificação estável preserva a ordem dos elementos que são iguais.

Marc Gravell
fonte
5
Se você usar o .NET Reflector ou ILSpy para abrir Enumerable.OrderBye detalhar sua implementação interna, poderá ver que o algoritmo de classificação OrderBy é uma variante do QuickSort que faz uma classificação estável. (Veja System.Linq.EnumerableSorter<TElement>.) Assim, Array.Sorte Enumerable.OrderBypode-se esperar que ambos tenham O (N log N) tempos de execução, onde N é o número de elementos na coleção.
John Beyer
@Marc Não estou entendendo bem qual seria a diferença se dois elementos fossem iguais e sua ordem não fosse preservada. Isso certamente não parece um problema para tipos de dados primitivos. Mas mesmo para um tipo de referência, por que importaria, se eu fosse classificar, uma pessoa com nome Marc Gravell apareceu antes de outra pessoa com nome Marc Gravell (por exemplo :))? Não estou questionando sua resposta / conhecimento, mas procuro uma aplicação deste cenário.
Mukus
4
@Mukus imagine que você classifica o catálogo de endereços de uma empresa por nome (ou mesmo por data de nascimento) - inevitavelmente haverá duplicatas. A questão é: o que acontece com eles? A sub-ordem está definida?
Marc Gravell
55

A resposta de Darin Dimitrov mostra que isso OrderByé um pouco mais rápido do que List.Sortquando confrontado com uma entrada já classificada. Modifiquei seu código para que classifique repetidamente os dados não classificados e, OrderByna maioria dos casos, é um pouco mais lento.

Além disso, o OrderByteste usa ToArraypara forçar a enumeração do enumerador Linq, mas isso obviamente retorna um tipo ( Person[]) que é diferente do tipo de entrada ( List<Person>). Portanto, executei novamente o teste usando em ToListvez de ToArraye obtive uma diferença ainda maior:

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

O código:

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();
    }
}
phoog
fonte
2
Eu executo o código de teste agora no LinqPad 5 (.net 5) e OrderByWithToListleva o mesmo tempo que OrderBy.
dovid
38

Acho importante notar outra diferença entre Sorte OrderBy:

Suponha que exista um Person.CalculateSalary()método que leve muito tempo; possivelmente mais do que a operação de classificação de uma grande lista.

Comparar

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

A opção 2 pode ter desempenho superior, pois chama o CalculateSalarymétodo apenas n vezes, enquanto a Sortopção pode chamar CalculateSalaryaté 2 n log ( n ) vezes, dependendo do sucesso do algoritmo de ordenação.

Omer Raviv
fonte
4
Isso é verdade, embora haja uma solução para esse problema, a saber, manter os dados em um array e usar a sobrecarga Array.Sort que leva dois arrays, um de chaves e outro de valores. Ao preencher o array de chaves, você chamará CalculateSalary ntimes. Obviamente, isso não é tão conveniente quanto usar OrderBy.
phoog de
14

Resumindo:

Classificar por lista / matriz ():

  • Tipo instável.
  • Feito no local.
  • Use o Introsort / Quicksort.
  • A comparação personalizada é feita fornecendo um comparador. Se a comparação for cara, pode ser mais lenta do que OrderBy () (que permite o uso de chaves, veja abaixo).

OrderBy / ThenBy ():

  • Tipo estável.
  • Não está no lugar.
  • Use Quicksort. Quicksort não é um tipo estável. Aqui está o truque: ao classificar, se dois elementos tiverem chaves iguais, ele compara sua ordem inicial (que foi armazenada antes da classificação).
  • Permite usar chaves (usando lambdas) para classificar os elementos em seus valores (por exemplo x => x.Id:). Todas as chaves são extraídas antes da classificação. Isso pode resultar em melhor desempenho do que usar Sort () e um comparador personalizado.

Fontes: MDSN , fonte de referência e repositório dotnet / coreclr (GitHub).

Algumas das declarações listadas acima são baseadas na implementação atual do .NET framework (4.7.2). Isso pode mudar no futuro.

tigrou
fonte
0

você deve calcular a complexidade dos algoritmos usados ​​pelos métodos OrderBy e Sort. QuickSort tem uma complexidade de n (log n), como me lembro, onde n é o comprimento da matriz.

Também procurei o Orderby's, mas não consegui encontrar nenhuma informação, mesmo na biblioteca do msdn. se você não tiver os mesmos valores e classificação relacionada a apenas uma propriedade, prefiro usar o método Sort (); se não, use OrderBy.

icaptan
fonte
1
De acordo com a documentação atual do MSDN, o Sort usa três algoritmos de classificação diferentes com base na entrada. Entre os quais está o QuickSort. A pergunta sobre o algoritmo OrderBy () está aqui (Quicksort): stackoverflow.com/questions/2792074/…
Thor
-1

Só quero acrescentar que o pedido por é muito mais útil.

Por quê? Porque eu posso fazer isso:

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

Por que comparador complicado? Basta classificar com base em um campo. Aqui estou classificando com base no TotalBalance.

Muito fácil.

Eu não posso fazer isso com sorte. Eu quero saber porque. Faça bem com orderBy.

Quanto à velocidade, é sempre O (n).

user4951
fonte
3
Pergunta: O tempo O (n) (presumo) em sua resposta se refere a OrderBy ou Comparer? Eu não acho que a classificação rápida pode atingir o tempo O (N).
Kevman