Quicksort: escolhendo o pivô

109

Ao implementar o Quicksort, uma das coisas que você deve fazer é escolher um pivô. Mas quando vejo um pseudocódigo como o mostrado abaixo, não fica claro como devo escolher o pivô. Primeiro elemento da lista? Algo mais?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

Alguém pode me ajudar a entender o conceito de escolher um pivô e se diferentes cenários exigem ou não estratégias diferentes.

Jacob T. Nielsen
fonte

Respostas:

87

A escolha de um pivô aleatório minimiza a chance de você encontrar o desempenho O (n 2 ) de pior caso (sempre escolher o primeiro ou o último causaria o desempenho de pior caso para dados classificados quase ou classificados de forma reversa). A escolha do elemento intermediário também seria aceitável na maioria dos casos.

Além disso, se você estiver implementando isso sozinho, existem versões do algoritmo que funcionam no local (ou seja, sem criar duas novas listas e depois concatená-las).

Kip
fonte
10
Eu apoiaria a noção de que implementar uma pesquisa sozinho pode não valer o esforço. Além disso, tome cuidado ao escolher números aleatórios, já que geradores de números aleatórios às vezes são lentos.
PeterAllenWebb,
A resposta de @Jonathan Leffler é melhor
Nathan
60

Depende dos seus requisitos. A escolha de um pivô aleatoriamente torna mais difícil criar um conjunto de dados que gere desempenho O (N ^ 2). 'Média de três' (primeiro, último, meio) também é uma forma de evitar problemas. No entanto, tenha cuidado com o desempenho relativo das comparações; se suas comparações forem caras, o Mo3 fará mais comparações do que escolher (um único valor de pivô) aleatoriamente. Os registros do banco de dados podem ser caros para comparar.


Atualização: Colocando comentários em resposta.

mdkess afirmou:

'Mediana de 3' NÃO é o primeiro último meio. Escolha três índices aleatórios e obtenha o valor médio deles. O ponto principal é ter certeza de que sua escolha de pivôs não seja determinística - se for, os dados do pior caso podem ser gerados facilmente.

Ao que respondi:

  • Análise do algoritmo Find de Hoare com partição de mediana de três (1997) por P Kirschenhofer, H Prodinger, C Martínez apóia sua tese (que 'mediana de três' é três itens aleatórios).

  • Há um artigo descrito em portal.acm.org que é sobre 'The Worst Case Permutation for Median-of-Three Quicksort' por Hannu Erkiö, publicado no The Computer Journal, Vol 27, No 3, 1984. [Atualização 2012-02- 26: Recebi o texto do artigo . Seção 2 'O algoritmo' começa: ' Usando a mediana do primeiro, do meio e do último elemento de A [L: R], partições eficientes em partes de tamanhos razoavelmente iguais podem ser obtidas na maioria das situações práticas. 'Portanto, está discutindo a abordagem Mo3 primeiro-meio-último.]

  • Outro artigo curto que é interessante é de MD McIlroy, "A Killer Adversary for Quicksort" , publicado em Software-Practice and Experience, Vol. 29 (0), 1–4 (0 1999). Ele explica como fazer quase qualquer Quicksort se comportar quadraticamente.

  • AT&T Bell Labs Tech Journal, outubro de 1984 "Teoria e prática na construção de uma rotina de classificação de trabalho" afirma "Hoare sugeriu o particionamento em torno da mediana de várias linhas selecionadas aleatoriamente. Sedgewick [...] recomendou escolher a mediana das primeiras [. ..] passado [...] e meio ". Isso indica que ambas as técnicas para 'mediana de três' são conhecidas na literatura. (Atualização 2014-11-23: O artigo parece estar disponível no IEEE Xplore ou na Wiley - se você for membro ou estiver disposto a pagar uma taxa).

  • 'Engineering a Sort Function' por JL Bentley e MD McIlroy, publicado em Software Practice and Experience, Vol 23 (11), novembro de 1993, entra em uma ampla discussão sobre os problemas e eles escolheram um algoritmo de particionamento adaptativo baseado em parte no tamanho do conjunto de dados. Há muita discussão sobre compensações para várias abordagens.

  • Uma pesquisa no Google por 'mediana de três' funciona muito bem para rastreamento posterior.

Obrigado pela informação; Eu só havia encontrado a 'mediana de três' determinística antes.

Jonathan Leffler
fonte
4
A mediana de 3 NÃO é o primeiro último meio. Escolha três índices aleatórios e obtenha o valor médio deles. O ponto principal é ter certeza de que sua escolha de pivôs não seja determinística - se for, os dados do pior caso podem ser gerados facilmente.
mindvirus
Eu estava lendo o abt introsort, que combina bons recursos do quicksort e do heapsort. A abordagem para selecionar o pivô usando a mediana de três nem sempre pode ser favorável.
Sumit Kumar Saha
4
O problema de escolher índices aleatórios é que os geradores de números aleatórios são muito caros. Embora não aumente o custo big-O de classificação, provavelmente tornará as coisas mais lentas do que se você tivesse apenas escolhido o primeiro, o último e o elemento intermediário. (No mundo real, aposto que ninguém está criando situações inventadas para desacelerar sua escolha rápida.)
Kevin Chen
20

Heh, acabei de dar essa aula.

Existem várias opções.
Simples: escolha o primeiro ou o último elemento do intervalo. (ruim na entrada parcialmente classificada) Melhor: Escolha o item no meio do intervalo. (melhor na entrada parcialmente classificada)

No entanto, escolher qualquer elemento arbitrário corre o risco de particionar mal o array de tamanho n em dois arrays de tamanho 1 e n-1. Se você fizer isso com frequência suficiente, seu quicksort corre o risco de se tornar O (n ^ 2).

Uma melhoria que vi foi escolher a mediana (primeiro, último, meio); No pior caso, ele ainda pode ir para O (n ^ 2), mas probabilisticamente, este é um caso raro.

Para a maioria dos dados, escolher o primeiro ou o último é suficiente. Mas, se você descobrir que está se deparando com os piores cenários com frequência (entrada parcialmente classificada), a primeira opção seria escolher o valor central (que é um pivô estatisticamente bom para dados parcialmente classificados).

Se ainda tiver problemas, siga o caminho do meio.

Chris Cudmore
fonte
1
Fizemos um experimento em nossa classe, obtendo os k menores elementos de um array em ordem classificada. Geramos matrizes aleatórias e, em seguida, usamos uma pilha mínima ou uma seleção aleatória e uma classificação rápida de pivô fixo e contamos o número de comparações. Com esses dados "aleatórios", a segunda solução teve desempenho pior, em média, do que a primeira. Mudar para um pivô aleatório resolve o problema de desempenho. Portanto, mesmo para dados supostamente aleatórios, o pivô fixo tem um desempenho significativamente pior do que o pivô aleatório.
Robert S. Barnes
Por que particionar o array de tamanho n em dois arrays de tamanho 1 en-1 corre o risco de se tornar O (n ^ 2)?
Aaron Franke
Suponha um Array de tamanho N. Particione em tamanhos [1, N-1]. A próxima etapa é particionar a metade direita em [1, N-2]. e assim por diante, até termos N partições de tamanho 1. Mas, se partíssemos ao meio, estaríamos fazendo 2 partições de N / 2 cada etapa, levando ao termo Log (n) da complexidade;
Chris Cudmore
11

Nunca, jamais escolha um pivô fixo - ele pode ser atacado para explorar o pior caso de tempo de execução O (n ^ 2) do seu algoritmo, que está apenas pedindo problemas. O pior caso de tempo de execução do Quicksort ocorre quando o particionamento resulta em uma matriz de 1 elemento e uma matriz de n-1 elementos. Suponha que você escolha o primeiro elemento como sua partição. Se alguém alimentar seu algoritmo com uma matriz que está em ordem decrescente, seu primeiro pivô será o maior, de modo que todo o resto da matriz se moverá para a esquerda. Então, quando você recursa, o primeiro elemento será o maior novamente, então, mais uma vez, você coloca tudo à esquerda dele e assim por diante.

Uma técnica melhor é o método da mediana de 3, em que você escolhe três elementos aleatoriamente e escolhe o meio. Você sabe que o elemento que você escolher não será o primeiro ou o último, mas também, pelo teorema do limite central, a distribuição do elemento do meio será normal, o que significa que você tenderá para o meio (e, portanto, , n lg n tempo).

Se você realmente deseja garantir o tempo de execução O (nlgn) para o algoritmo, o método de colunas de 5 para encontrar a mediana de uma matriz é executado no tempo O (n), o que significa que a equação de recorrência para quicksort no pior caso seja T (n) = O (n) (encontre a mediana) + O (n) (partição) + 2T (n / 2) (retroceda à esquerda e à direita.) Pelo Teorema Mestre, este é O (n lg n) . No entanto, o fator constante será enorme e se o desempenho do pior caso for sua preocupação principal, use uma classificação por mesclagem, que é apenas um pouco mais lenta do que a classificação rápida em média e garante o tempo O (nlgn) (e será muito mais rápido do que este quicksort mediano coxo).

Explicação do Algoritmo da Mediana das Medianas

vírus da mente
fonte
6

Não tente ser muito inteligente e combinar estratégias de pivô. Se você combinou mediana de 3 com pivô aleatório escolhendo a mediana do primeiro, último e um índice aleatório no meio, então você ainda estará vulnerável a muitas das distribuições que enviam mediana de 3 quadrático (então é realmente pior do que pivô aleatório simples)

Por exemplo, uma distribuição de órgão de tubos (1,2,3 ... N / 2..3,2,1) primeiro e último serão ambos 1 e o índice aleatório será algum número maior que 1, tomando a mediana resulta em 1 ( seja o primeiro ou o último) e você obterá um particionamento extremamente desequilibrado.

cavalo de papel
fonte
2

É mais fácil quebrar o quicksort em três seções fazendo isso

  1. Função de elemento de troca ou troca de dados
  2. A função de partição
  3. Processando as partições

É apenas um pouco mais ineficiente do que uma função longa, mas é muito mais fácil de entender.

Código a seguir:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};
Uglybb
fonte
1

Para começar, é totalmente dependente de como seus dados são classificados. Se você acha que será pseudoaleatório, sua melhor aposta é escolher uma seleção aleatória ou escolher o meio.

Joe Phillips
fonte
1

Se você estiver classificando uma coleção acessível aleatoriamente (como uma matriz), geralmente é melhor escolher o item físico do meio. Com isso, se o array estiver todo ordenado (ou quase ordenado), as duas partições ficarão quase iguais e você obterá a melhor velocidade.

Se você estiver classificando algo apenas com acesso linear (como uma lista vinculada), é melhor escolher o primeiro item, porque é o item mais rápido de acessar. Aqui, porém, se a lista já estiver ordenada, você está ferrado - uma partição sempre será nula e a outra terá tudo, produzindo o pior momento.

No entanto, para uma lista vinculada, escolher qualquer coisa além do primeiro só tornará as coisas piores. Ele escolhe o item do meio em uma lista listada, você terá que passar por ele em cada etapa de partição - adicionando uma operação O (N / 2) que é feita logN vezes fazendo com que o tempo total O (1,5 N * log N) e isso se soubermos quanto tempo a lista é antes de começarmos - geralmente não sabemos, então teríamos que percorrer todo o caminho para contá-los, depois percorrer meio caminho para encontrar o meio e, em seguida, percorrer um terceira vez para fazer a partição real: O (2,5N * log N)

James Curran
fonte
0

Idealmente, o pivô deve ser o valor do meio em toda a matriz. Isso reduzirá as chances de obter o pior desempenho possível.

Faizan
fonte
1
carroça na frente do cavalo aqui.
ncmathsadist
0

A complexidade da classificação rápida varia muito com a seleção do valor de pivô. por exemplo, se você sempre escolher o primeiro elemento como um pivô, a complexidade do algoritmo se torna tão pior quanto O (n ^ 2). aqui está um método inteligente para escolher o elemento pivô - 1. escolha o primeiro, o meio e o último elemento do array. 2. compare esses três números e encontre o número que é maior que um e menor que o outro, ou seja, a mediana. 3. faça este elemento como elemento pivô.

escolher o pivô por este método divide o array em quase duas metades e, portanto, a complexidade se reduz a O (nlog (n)).

vivek
fonte
0

Em média, a mediana de 3 é boa para n pequeno. A mediana de 5 é um pouco melhor para n maior. O ninther, que é a "mediana de três medianas de três", é ainda melhor para n muito grande.

Quanto mais alto você vai com a amostragem, melhor você obtém à medida que n aumenta, mas a melhoria diminui drasticamente conforme você aumenta as amostras. E você incorre na sobrecarga de amostragem e classificação de amostras.

S0lo
fonte
0

Eu recomendo usar o índice do meio, pois pode ser calculado facilmente.

Você pode calculá-lo arredondando (array.length / 2).

Milesman34
fonte
-1

Em uma implementação verdadeiramente otimizada, o método para escolher o pivô deve depender do tamanho do array - para um array grande, vale a pena gastar mais tempo escolhendo um bom pivô. Sem fazer uma análise completa, eu diria que "meio de elementos O (log (n))" é um bom começo, e isso tem a vantagem adicional de não exigir nenhuma memória extra: usar a chamada final na partição maior e Para colocar o particionamento, usamos a mesma O (log (n)) memória extra em quase todos os estágios do algoritmo.

Morten Kloster
fonte
1
Encontrar o meio de 3 elementos pode ser feito em tempo constante. Mais, e essencialmente temos que classificar a submatriz. À medida que n se torna grande, voltamos imediatamente ao problema de classificação.
Chris Cudmore de