Gostaria de acrescentar às ótimas respostas existentes algumas contas sobre o desempenho do QuickSort ao divergir do melhor caso e qual a probabilidade disso, o que espero ajude as pessoas a entender um pouco melhor por que o caso O (n ^ 2) não é real preocupação nas implementações mais sofisticadas do QuickSort.
Fora dos problemas de acesso aleatório, existem dois fatores principais que podem afetar o desempenho do QuickSort e ambos estão relacionados à forma como o pivot se compara aos dados que estão sendo classificados.
1) Um pequeno número de chaves nos dados. Um conjunto de dados com o mesmo valor será classificado em n ^ 2 em um QuickSort de 2 partições de baunilha, porque todos os valores, exceto o local da tabela dinâmica, são colocados em um lado de cada vez. As implementações modernas tratam disso por métodos como o uso de uma classificação de 3 partições. Esses métodos são executados em um conjunto de dados com o mesmo valor em O (n) tempo. Portanto, usar essa implementação significa que uma entrada com um pequeno número de chaves realmente melhora o tempo de desempenho e não é mais uma preocupação.
2) A seleção de pivô extremamente ruim pode causar o pior desempenho possível. Em um caso ideal, o pivô sempre será tal que 50% dos dados são menores e 50% dos dados são maiores, de modo que a entrada será dividida ao meio durante cada iteração. Isso nos dá n comparações e tempos de troca log-2 (n) recursões pelo tempo O (n * logn).
Quanto a seleção de pivô não ideal afeta o tempo de execução?
Vamos considerar um caso em que o pivô é escolhido consistentemente, de modo que 75% dos dados estejam em um lado do pivô. Ainda é O (n * logn), mas agora a base do log mudou para 1 / 0,75 ou 1,33. O relacionamento no desempenho ao alterar a base é sempre uma constante representada por log (2) / log (newBase). Nesse caso, essa constante é 2,4. Portanto, essa qualidade de escolha do pivô leva 2,4 vezes mais tempo do que o ideal.
Quão rápido isso piora?
Não é muito rápido até que a escolha do pivô fique (consistentemente) muito ruim:
- 50% de um lado: (caso ideal)
- 75% de um lado: 2,4 vezes mais
- 90% de um lado: 6,6 vezes mais
- 95% de um lado: 13,5 vezes mais
- 99% de um lado: 69 vezes mais
À medida que nos aproximamos de 100% de um lado, a parte do log da execução se aproxima de n e toda a execução se aproxima assintoticamente de O (n ^ 2).
Em uma implementação ingênua do QuickSort, casos como uma matriz classificada (para o 1º elemento dinâmico) ou uma matriz classificada inversa (para o último elemento dinâmico) produzirão de maneira confiável o pior tempo de execução de O (n ^ 2). Além disso, implementações com uma seleção de pivô previsível podem ser sujeitas a ataques de DoS por dados projetados para produzir a pior execução possível. As implementações modernas evitam isso por uma variedade de métodos, como aleatorizar os dados antes da classificação, escolher a mediana de 3 índices escolhidos aleatoriamente, etc. Com essa aleatorização no mix, temos 2 casos:
- Conjunto de dados pequeno. O pior caso é razoavelmente possível, mas O (n ^ 2) não é catastrófico porque n é pequeno o suficiente para que n ^ 2 também seja pequeno.
- Conjunto de dados grande. O pior caso é possível na teoria, mas não na prática.
Qual a probabilidade de vermos um desempenho terrível?
As chances são muito pequenas . Vamos considerar uma espécie de 5.000 valores:
Nossa implementação hipotética escolherá um pivô usando uma mediana de 3 índices escolhidos aleatoriamente. Consideraremos os pivôs que estão no intervalo de 25% a 75% como "bons" e os pivôs que estão no intervalo de 0% a 25% ou 75% a 100% como "ruins". Se você observar a distribuição de probabilidade usando a mediana de 3 índices aleatórios, cada recursão tem uma chance de 11/16 de terminar com um bom pivô. Vamos fazer 2 suposições conservadoras (e falsas) para simplificar a matemática:
Os bons pivôs estão sempre exatamente em uma divisão de 25% / 75% e operam em caso ideal de 2,4 *. Nunca obtemos uma divisão ideal ou qualquer divisão melhor que 25/75.
Os pivôs ruins são sempre os piores casos e, essencialmente, não contribuem para a solução.
Nossa implementação do QuickSort irá parar em n = 10 e mudar para uma classificação de inserção, portanto, precisamos de 22 partições dinâmicas de 25% / 75% para quebrar a entrada de 5.000 valores até o momento. (10 * 1,333333 ^ 22> 5000) Ou exigimos 4990 pivôs do pior caso. Tenha em mente que se acumulam 22 bons pivôs em qualquer ponto , em seguida, o tipo completará, então pior caso ou qualquer coisa perto que exige extremamente má sorte. Se precisássemos de 88 recursões para atingir os 22 bons pivôs necessários para classificar até n = 10, seria um caso ideal de 4 * 2,4 * ou cerca de 10 vezes o tempo de execução do caso ideal. Qual a probabilidade de não conseguirmos os 22 bons pivôs necessários após 88 recursões?
Distribuições de probabilidade binomial podem responder a isso, e a resposta é de cerca de 10 ^ -18. (n é 88, k é 21, p é 0,6875) Seu usuário tem uma probabilidade mil vezes maior de ser atingido por um raio no primeiro segundo necessário para clicar em [ORDENAR] do que para ver que a classificação de 5.000 itens é pior. de 10 * caso ideal. Essa chance diminui à medida que o conjunto de dados aumenta. Aqui estão alguns tamanhos de matriz e suas chances correspondentes de executar mais de 10 * ideal:
- Matriz de 640 itens: 10 ^ -13 (requer 15 pontos de articulação bons em 60 tentativas)
- Matriz de 5.000 itens: 10 ^ -18 (requer 22 rotações dinâmicas em 88 tentativas)
- Matriz de 40.000 itens: 10 ^ -23 (requer 29 boas pivôs de 116)
Lembre-se de que isso ocorre com 2 suposições conservadoras piores que a realidade. Portanto, o desempenho real é melhor ainda, e o saldo da probabilidade restante está mais próximo do ideal do que não.
Finalmente, como outros já mencionaram, mesmo esses casos absurdamente improváveis podem ser eliminados mudando para uma classificação de heap se a pilha de recursão for muito profunda. Portanto, o TLDR é que, para boas implementações do QuickSort, o pior caso não existe realmente porque foi projetado e a execução é concluída em tempo O (n * logn).
qsort
, Pythonlist.sort
eArray.prototype.sort
JavaScript no Firefox são todos tipos de mesclagem. (GNU STLsort
usa introsort vez, mas isso pode ser porque em C ++, trocando potencialmente ganha grande sobre a cópia.)