Implementação do algoritmo Quicksort em Java
1. Visão geral
Neste tutorial, exploraremos o algoritmo QuickSort em detalhes, com foco em sua implementação Java.
Também discutiremos suas vantagens e desvantagens e, em seguida, analisaremos sua complexidade de tempo.
2. Algoritmo QuickSort
Quicksort is a sorting algorithm, which is leveraging the divide-and-conquer principle. Tem uma complexidade média deO(n log n) e é um dos algoritmos de classificação mais usados, especialmente para grandes volumes de dados.
It’s important to remember that Quicksort isn’t a stable algorithm. Um algoritmo de classificação estável é um algoritmo em que os elementos com os mesmos valores aparecem na mesma ordem na saída classificada como aparecem na lista de entrada.
A lista de entrada é dividida em duas sub-listas por um elemento chamado pivô; uma sub-lista com elementos menores que o pivô e outra com elementos maiores que o pivô. Esse processo se repete para cada sub-lista.
Finalmente, todas as sub-listas classificadas se fundem para formar a saída final.
2.1. Etapas do Algoritmo
-
Nós escolhemos um elemento da lista, chamado de pivô. Vamos usá-lo para dividir a lista em duas sublistas.
-
Reordenamos todos os elementos ao redor do pivô - aqueles com menor valor são colocados antes dele e todos os elementos maiores que o pivô depois dele. Após esta etapa, o pivô está em sua posição final. Este é o passo importante da partição.
-
Aplicamos as etapas acima recursivamente às duas sub-listas à esquerda e à direita do pivô.
Como podemos ver,quicksort is naturally a recursive algorithm, like every divide and conquer approach.
Vamos dar um exemplo simples para entender melhor esse algoritmo.
Arr[] = {5, 9, 4, 6, 5, 3}
-
Vamos supor que escolhemos 5 como o pivô para simplificar
-
Primeiro, colocaremos todos os elementos menores que 5 na primeira posição da matriz: \ {3, 4, 5, 6, 5, 9}
-
Vamos então repetir para a submatriz esquerda \ {3,4}, tomando 3 como o pivô
-
Não há elementos menores que 3
-
Aplicamos o quicksort no subconjunto à direita do pivô, ou seja, {4}
-
Esse sub-array consiste em apenas um elemento classificado
-
Continuamos com a parte direita da matriz original, \ {6, 5, 9} até obtermos a matriz ordenada final
2.2. Escolhendo o Pivô Ideal
O ponto crucial no QuickSort é escolher o melhor pivô. O elemento do meio é, obviamente, o melhor, pois dividiria a lista em duas sub-listas iguais.
Mas encontrar o elemento do meio a partir de uma lista não ordenada é difícil e demorado, por isso, tomamos como pivô o primeiro elemento, o último elemento, a mediana ou qualquer outro elemento aleatório.
3. Implementação em Java
O primeiro método équickSort(), que toma como parâmetros o array a ser classificado, o primeiro e o último índice. Primeiro, verificamos os índices e continuamos apenas se ainda houver elementos a serem classificados.
Obtemos o índice do pivô classificado e o usamos para chamar recursivamente o métodopartition() com os mesmos parâmetros do métodoquickSort(), mas com índices diferentes:
public void quickSort(int arr[], int begin, int end) {
if (begin < end) {
int partitionIndex = partition(arr, begin, end);
quickSort(arr, begin, partitionIndex-1);
quickSort(arr, partitionIndex+1, end);
}
}
Vamos continuar com o métodopartition(). Para simplificar, essa função usa o último elemento como o pivô. Em seguida, verifica cada elemento e o troca antes do pivô, se seu valor for menor.
Ao final do particionamento, todos os elementos abaixo do pivô ficam à esquerda e todos os elementos maiores que o pivô ficam à direita. O pivô está em sua posição final classificada e a função retorna esta posição:
private int partition(int arr[], int begin, int end) {
int pivot = arr[end];
int i = (begin-1);
for (int j = begin; j < end; j++) {
if (arr[j] <= pivot) {
i++;
int swapTemp = arr[i];
arr[i] = arr[j];
arr[j] = swapTemp;
}
}
int swapTemp = arr[i+1];
arr[i+1] = arr[end];
arr[end] = swapTemp;
return i+1;
}
4. Análise de algoritmo
4.1. Complexidade temporal
Na melhor das hipóteses, o algoritmo dividirá a lista em duas sub-listas de tamanho igual. Portanto, a primeira iteração da lista completa den precisa deO(n). Classificar as duas sublistas restantes com elementosn/2 leva2*O(n/2) cada. Como resultado, o algoritmo QuickSort tem a complexidade deO(n log n).
No pior caso, o algoritmo selecionará apenas um elemento em cada iteração, entãoO(n) + O(n-1) + … + O(1), que é igual aO(n2).
Em média, o QuickSort tem complexidade deO(n log n), o que o torna adequado para grandes volumes de dados.
4.2. QuickSort vs MergeSort
Vamos discutir em quais casos devemos escolher QuickSort em vez de MergeSort.
Embora Quicksort e Mergesort tenham uma complexidade de tempo média deO(n log n), Quicksort é o algoritmo preferido, pois tem uma complexidade de espaço deO(log(n)). O Mergesort, por outro lado, requerO(n) de armazenamento extra, o que o torna bastante caro para os arrays.
O Quicksort requer acesso a índices diferentes para suas operações, mas esse acesso não é diretamente possível em listas vinculadas, pois não há blocos contínuos; portanto, para acessar um elemento, precisamos percorrer cada nó desde o início da lista vinculada. Além disso, Mergesort é implementado sem espaço extra paraLinkedLists.
Nesse caso, o aumento de despesas gerais para o Quicksort e o Mergesort é geralmente preferido.
5. Conclusão
O Quicksort é um algoritmo de classificação elegante que é muito útil na maioria dos casos.
Geralmente é um algoritmo "no local", com a complexidade de tempo média deO(n log n).
Outro ponto interessante a ser mencionado é que o métodoArrays.sort() de Java usa Quicksort para classificar matrizes de primitivas. A implementação usa dois pivôs e tem um desempenho muito melhor do que nossa solução simples, por isso, para código de produção, geralmente é melhor usar métodos de biblioteca.
Como sempre, o código para a implementação deste algoritmo pode ser encontrado emon our GitHub repository.