Quicksort vs heapsort

Respostas:

60

Este papel tem algumas análises.

Além disso, da Wikipedia:

O concorrente mais direto do quicksort é o heapsort. Heapsort é tipicamente um pouco mais lento do que quicksort, mas o pior caso de tempo de execução é sempre Θ (nlogn). O Quicksort geralmente é mais rápido, embora ainda haja a chance de desempenho no pior caso, exceto na variante introsort, que muda para o heapsort quando um caso ruim é detectado. Se for sabido com antecedência que o heapsort será necessário, usá-lo diretamente será mais rápido do que esperar que o introsort alterne para ele.

DVK
fonte
12
Pode ser importante observar que em implementações típicas, nem quicksort nem heapsort são tipos estáveis.
MjrKusanagi
@DVK, de acordo com seu link cs.auckland.ac.nz/~jmor159/PLDS210/qsort3.html , a classificação de heap leva 2.842 comparações para n = 100, mas leva 53.113 comparações para n = 500. E isso implica que a razão entre n = 500 en = 100 é 18 vezes, e NÃO está combinando o algoritmo de classificação de heap com complexidade O (N logN). Eu acho que é muito provável que sua implementação de classificação de heap tenha algum tipo de bugs.
DU Jiaen
@DUJiaen - lembre-se que O () é sobre comportamento assintótico em grande N e tem um possível multiplicador
DVK
Isso NÃO está relacionado ao multiplicador. Se um algoritmo tem uma complexidade de O (N log N), ele deve seguir uma tendência de Tempo (N) = C1 * N * log (N). E se você pegar Tempo (500) / Tempo (100), é óbvio que C1 vai desaparecer e o resultado deve ser fechado para (500 log500) / (100 log100) = 6,7 Mas de seu link, é 18, que é muito fora de escala.
DU Jiaen
2
O link está morto
PlsWork
123

O Heapsort é O (N log N) garantido, o que é muito melhor do que o pior caso no Quicksort. O Heapsort não precisa de mais memória para outro array colocar os dados ordenados conforme necessário para o Mergesort. Então, por que os aplicativos comerciais ficam com o Quicksort? O que o Quicksort tem de tão especial sobre as outras implementações?

Eu mesmo testei os algoritmos e vi que o Quicksort tem algo realmente especial. Ele é executado rapidamente, muito mais rápido do que os algoritmos Heap e Merge.

O segredo do Quicksort é: ele quase não faz trocas de elementos desnecessárias. A troca é demorada.

Com o Heapsort, mesmo se todos os seus dados já estiverem ordenados, você vai trocar 100% dos elementos para ordenar o array.

Com o Mergesort, é ainda pior. Você vai escrever 100% dos elementos em outro array e escrever de volta no original, mesmo se os dados já estiverem ordenados.

Com Quicksort você não troca o que já foi pedido. Se seus dados estiverem completamente ordenados, você não troca quase nada! Embora haja muita confusão sobre o pior caso, uma pequena melhoria na escolha do pivô, qualquer outra coisa que não seja obter o primeiro ou o último elemento do array, pode evitá-lo. Se você obtiver um pivô do elemento intermediário entre o primeiro, o último e o elemento do meio, é suficiente para evitar o pior caso.

O que é superior no Quicksort não é o pior caso, mas o melhor! Na melhor das hipóteses você faz o mesmo número de comparações, ok, mas você não troca quase nada. Na média dos casos, você troca parte dos elementos, mas não todos os elementos, como no Heapsort e no Mergesort. Isso é o que dá ao Quicksort o melhor tempo. Menos troca, mais velocidade.

A implementação abaixo em C # no meu computador, rodando no modo de lançamento, bate Array.Sort em 3 segundos com o pivô intermediário e em 2 segundos com o pivô aprimorado (sim, há uma sobrecarga para obter um bom pivô).

static void Main(string[] args)
{
    int[] arrToSort = new int[100000000];
    var r = new Random();
    for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);

    Console.WriteLine("Press q to quick sort, s to Array.Sort");
    while (true)
    {
        var k = Console.ReadKey(true);
        if (k.KeyChar == 'q')
        {
            // quick sort
            Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            QuickSort(arrToSort, 0, arrToSort.Length - 1);
            Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
        else if (k.KeyChar == 's')
        {
            Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            Array.Sort(arrToSort);
            Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
    }
}

static public void QuickSort(int[] arr, int left, int right)
{
    int begin = left
        , end = right
        , pivot
        // get middle element pivot
        //= arr[(left + right) / 2]
        ;

    //improved pivot
    int middle = (left + right) / 2;
    int
        LM = arr[left].CompareTo(arr[middle])
        , MR = arr[middle].CompareTo(arr[right])
        , LR = arr[left].CompareTo(arr[right])
        ;
    if (-1 * LM == LR)
        pivot = arr[left];
    else
        if (MR == -1 * LR)
            pivot = arr[right];
        else
            pivot = arr[middle];
    do
    {
        while (arr[left] < pivot) left++;
        while (arr[right] > pivot) right--;

        if(left <= right)
        {
            int temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;

            left++;
            right--;
        }
    } while (left <= right);

    if (left < end) QuickSort(arr, left, end);
    if (begin < right) QuickSort(arr, begin, right);
}
Marquinho Peli
fonte
10
1 para considerações sobre o no. de troca, operações de leitura / gravação necessárias para diferentes algoritmos de classificação
ycy
2
Para qualquer estratégia de seleção de pivô de tempo constante determinística, você pode encontrar uma matriz que produz o pior caso O (n ^ 2). Não basta eliminar apenas o mínimo. Você tem que escolher pivôs de forma confiável que estejam dentro de uma certa faixa pecentil.
Antimônio
1
Estou curioso para saber se este é o código exato que você executou para suas simulações entre a classificação rápida codificada manualmente e o C # Array.sort integrado? Testei esse código e, em todos os meus testes, na melhor das hipóteses, a classificação rápida codificada manualmente foi a mesma que Array.sort. Uma coisa que controlei em meus testes disso foi fazer duas cópias idênticas do array aleatório. Afinal, uma dada randomização poderia ser potencialmente mais favorável (tendendo para o melhor caso) do que outra randomização. Então, executei conjuntos idênticos em cada um. Array.sort empatado ou vencido todas as vezes (versão de lançamento btw).
Chris
1
A classificação por mesclagem não precisa copiar 100% dos elementos, a menos que seja uma implementação muito ingênua de um livro-texto. É simples implementá-lo, de forma que você só precisa copiar 50% deles (o lado esquerdo das duas matrizes mescladas). Também é trivial adiar a cópia até que você realmente precise "trocar" dois elementos, portanto, com os dados já classificados, você não terá nenhuma sobrecarga de memória. Portanto, mesmo os 50% são, na verdade, o pior caso, e você pode ter algo entre isso e 0%.
ddekany
1
@MarquinhoPeli Eu quis dizer que você só precisa de 50% a mais de memória disponível em comparação com o tamanho da lista classificada, não 100%, o que parece ser um equívoco comum. Eu estava falando sobre o pico de uso de memória. Não posso dar um link, mas é fácil ver se você tentar mesclar os dois já classificados metade de um array no lugar (apenas a metade esquerda tem o problema de sobrescrever elementos que ainda não consumiu). A quantidade de cópias de memória que você precisa fazer durante todo o processo de classificação é outra questão, mas obviamente o pior caso não pode estar abaixo de 100% para qualquer algoritmo de classificação.
ddekany
15

Para a maioria das situações, ter rápido vs. um pouco mais rápido é irrelevante ... você simplesmente nunca quer que ocasionalmente fique muito lento. Embora você possa ajustar o QuickSort para evitar situações lentas, você perde a elegância do QuickSort básico. Então, para a maioria das coisas, eu realmente prefiro HeapSort ... você pode implementá-lo em toda sua elegância simples e nunca obter uma classificação lenta.

Para situações em que você deseja a velocidade máxima na maioria dos casos, QuickSort pode ser preferido em vez de HeapSort, mas nenhuma pode ser a resposta certa. Para situações críticas de velocidade, vale a pena examinar de perto os detalhes da situação. Por exemplo, em alguns dos meus códigos de velocidade crítica, é muito comum que os dados já estejam classificados ou quase classificados (é a indexação de vários campos relacionados que muitas vezes movem para cima e para baixo juntos OU movem para cima e para baixo opostos um ao outro, então, uma vez que você classifica por um, os outros são classificados ou classificados de forma reversa ou próximos ... qualquer um dos quais pode matar QuickSort). Para esse caso, eu não implementei nenhum ... em vez disso, implementei o SmoothSort de Dijkstra ... uma variante HeapSort que é O (N) quando já classificada ou quase classificada ... não é tão elegante, não é muito fácil de entender, mas rápido ... leiahttp://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF se quiser algo um pouco mais desafiador para codificar.

Brian Kennedy
fonte
6

Os híbridos Quicksort-Heapsort no local também são realmente interessantes, já que a maioria deles só precisa de n * log n comparações no pior caso (eles são ótimos em relação ao primeiro termo dos assintóticos, então evitam os piores cenários do Quicksort), O (log n) espaço extra e preservam pelo menos "metade" do bom comportamento do Quicksort com relação ao conjunto de dados já ordenado. Um algoritmo extremamente interessante é apresentado por Dikert e Weiss em http://arxiv.org/pdf/1209.4214v1.pdf :

  • Selecione um pivô p como a mediana de uma amostra aleatória de elementos sqrt (n) (isso pode ser feito em no máximo 24 comparações sqrt (n) através do algoritmo de Tarjan & co, ou 5 comparações sqrt (n) através da aranha muito mais complicada -algoritmo de fábrica de Schonhage);
  • Particione seu array em duas partes como na primeira etapa do Quicksort;
  • Heapify a menor parte e use O (log n) bits extras para codificar um heap em que cada filho à esquerda tem um valor maior do que seu irmão;
  • Extraia recursivamente a raiz da pilha, peneire a lacuna deixada pela raiz até chegar a uma folha da pilha e, em seguida, preencha a lacuna com um elemento apropriado retirado da outra parte da matriz;
  • Recorre sobre a parte não ordenada restante da matriz (se p for escolhido como a mediana exata, não haverá recursão alguma).
Jack D'Aurizio
fonte
2

Comp. entre quick sorte merge sortuma vez que ambos são do tipo de classificação local, há uma diferença entre o tempo de execução do caso errado para classificação rápida O(n^2)e para classificação de heap ainda é O(n*log(n))e para uma quantidade média de dados a classificação rápida será mais útil. Uma vez que é um algoritmo aleatório, então a probabilidade de obter ans corretos. em menos tempo dependerá da posição do elemento pivô que você escolher.

Então um

Boa escolha: os tamanhos de L e G são cada um menores que 3s / 4

Má chamada: um de L e G tem tamanho maior que 3s / 4

para uma pequena quantidade, podemos ir para a classificação por inserção e para uma quantidade muito grande de dados ir para a classificação por heap.

vicky garg
fonte
Embora a classificação por mesclagem possa ser implementada com a classificação local, a implementação é complexa. AFAIK, a maioria das implementações de merge sort não estão no local, mas são estáveis.
MjrKusanagi
2

O Heapsort tem a vantagem de ter um pior caso de execução de O (n * log (n)), portanto, nos casos em que o quicksort provavelmente terá um desempenho insatisfatório (geralmente conjuntos de dados classificados principalmente), o heapsort é o preferido.

Zellio
fonte
4
Quicksort só funciona mal em um conjunto de dados classificado principalmente se um método de escolha de pivô ruim for escolhido. Ou seja, o método de escolha do pivô ruim seria sempre escolher o primeiro ou o último elemento como o pivô. Se um pivô aleatório for escolhido a cada vez e um bom método de manipulação de elementos repetidos for usado, a chance de um quicksort de pior caso é muito pequena.
Justin Peel,
1
@ Justin - Isso é verdade, eu estava falando sobre uma implementação ingênua.
zellio
1
@ Justin: Verdade, mas a chance de uma grande desaceleração sempre existe, por menor que seja. Para alguns aplicativos, posso querer garantir o comportamento O (n log n), mesmo que seja mais lento.
David Thornley
2

Bem, se você for para o nível de arquitetura ... usamos a estrutura de dados da fila na memória cache. Então, o que quer que esteja disponível na fila será classificado. Como na classificação rápida, não temos nenhum problema em dividir a matriz em qualquer comprimento ... mas em heap sort (usando array) pode acontecer que o pai não esteja presente no sub array disponível no cache e então ele tem que trazê-lo para a memória cache ... o que é demorado. Esse quicksort é o melhor !! 😀

Manav Jain
fonte
1

O Heapsort cria um heap e extrai repetidamente o item máximo. Seu pior caso é O (n log n).

Mas se você ver o pior caso de classificação rápida , que é O (n2), perceberia que a classificação rápida não seria uma escolha tão boa para dados grandes.

Portanto, isso torna a classificação uma coisa interessante; Acredito que a razão de tantos algoritmos de classificação existirem hoje é porque todos eles são 'melhores' em seus melhores lugares. Por exemplo, a classificação por bolha pode realizar uma classificação rápida se os dados forem classificados. Ou, se sabemos algo sobre os itens a serem classificados, provavelmente podemos fazer melhor.

Isso pode não responder sua pergunta diretamente, pensei em acrescentar meus dois centavos.

KMån
fonte
1
Nunca use o tipo bolha. Se você acha razoavelmente que seus dados serão classificados, você pode usar a classificação por inserção ou mesmo testar os dados para ver se eles estão classificados. Não use o bubblesort.
vy32
se você tiver um conjunto de dados RANDOM muito grande, sua melhor aposta é o quicksort. Se parcialmente ordenado, então não, mas se você começar a trabalhar com grandes conjuntos de dados, você deve saber pelo menos isso sobre eles.
Kobor42,
1

Heap Sort é uma aposta segura ao lidar com entradas muito grandes. A análise assintótica revela a ordem de crescimento do Heapsort no pior caso Big-O(n logn), que é melhor do que o Quicksort no Big-O(n^2)pior caso. No entanto, Heapsort é um pouco mais lento na prática na maioria das máquinas do que uma classificação rápida bem implementada. O Heapsort também não é um algoritmo de classificação estável.

O motivo pelo qual o heapsort é mais lento na prática do que o quicksort é devido à melhor localidade de referência (" https://en.wikipedia.org/wiki/Locality_of_reference ") no quicksort, onde os elementos de dados estão em locais de armazenamento relativamente próximos. Os sistemas que exibem forte localidade de referência são ótimos candidatos para otimização de desempenho. A classificação de heap, no entanto, lida com saltos maiores. Isso torna o quicksort mais favorável para entradas menores.

Benn
fonte
2
A classificação rápida também não é estável.
Antimônio
1

Para mim, há uma diferença fundamental entre o heapsort e o quicksort: o último usa uma recursão. Em algoritmos recursivos, o heap aumenta com o número de recursões. Isso não importa se n for pequeno, mas agora estou classificando duas matrizes com n = 10 ^ 9 !!. O programa ocupa quase 10 GB de RAM e qualquer memória extra fará com que meu computador comece a trocar para memória de disco virtual. Meu disco é um disco RAM, mas mesmo assim a troca para ele faz uma grande diferença na velocidade . Portanto, em um statpack codificado em C ++ que inclui matrizes de dimensão ajustáveis, com tamanho desconhecido de antemão para o programador, e tipo de classificação estatística não paramétrica, prefiro o heapsort para evitar atrasos no uso com matrizes de dados muito grandes.

csevcik
fonte
1
Você só precisa de memória O (logn) em média. A sobrecarga de recursão é trivial, supondo que você não tenha azar com os pivôs e, nesse caso, você terá problemas maiores com que se preocupar.
Antimônio
-1

Para responder à pergunta original e abordar alguns dos outros comentários aqui:

Eu apenas comparei as implementações de seleção, rápida, mesclagem e classificação de heap para ver como eles se comparam. A resposta é que todos eles têm suas desvantagens.

TL; DR: Quick é o melhor tipo de uso geral (razoavelmente rápido, estável e principalmente no local). Pessoalmente, prefiro o tipo heap, a menos que precise de um tipo estável.

Seleção - N ^ 2 - É realmente bom apenas para menos de 20 elementos ou mais, então é superado. A menos que seus dados já estejam classificados, ou quase isso. N ^ 2 fica muito lento muito rápido.

Rápido, na minha experiência, não é verdade que a rápida o tempo todo. Os bônus por usar a classificação rápida como uma classificação geral são que ela é razoavelmente rápida e estável. É também um algoritmo local, mas como geralmente é implementado recursivamente, ele ocupará espaço de pilha adicional. Ele também fica em algum lugar entre O (n log n) e O (n ^ 2). O tempo em alguns tipos parece confirmar isso, especialmente quando os valores estão dentro de uma faixa estreita. É muito mais rápido do que a classificação por seleção em 10.000.000 de itens, mas mais lento do que a fusão ou a pilha.

A classificação por mesclagem é garantida O (n log n), pois sua classificação não depende dos dados. Ele simplesmente faz o que faz, independentemente dos valores que você atribuiu a ele. Também é estável, mas tipos muito grandes podem explodir sua pilha se você não tiver cuidado com a implementação. Existem algumas implementações de classificação de mesclagem complexas no local, mas geralmente você precisa de outro array em cada nível para mesclar seus valores. Se essas matrizes estiverem na pilha, você poderá ter problemas.

A classificação de heap é max O (n log n), mas em muitos casos é mais rápida, dependendo de quanto você precisa mover seus valores para cima no heap de log n profundo. O heap pode ser facilmente implementado no local no array original, portanto, não precisa de memória adicional e é iterativo, portanto, não se preocupe com o estouro de pilha durante a recorrência. A grande desvantagem da classificação de heap é que não é uma classificação estável, o que significa que está pronta se você precisar disso.

Timothy Renner
fonte
A classificação rápida não é uma classificação estável. Além disso, perguntas dessa natureza encorajam respostas baseadas em opinião e podem levar a editar guerras e discussões. Perguntas que exigem respostas baseadas em opinião são explicitamente desencorajadas pelas diretrizes do SO. Os respondentes devem evitar a tentação de respondê-las, mesmo que tenham experiência e sabedoria significativas na área. Sinalize-os para fechamento ou espere que alguém com reputação suficiente sinalize e feche. Este comentário não é um reflexo do seu conhecimento ou da validade da sua resposta.
MikeC de