Implementação do algoritmo Quicksort em Java

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

  1. Nós escolhemos um elemento da lista, chamado de pivô. Vamos usá-lo para dividir a lista em duas sublistas.

  2. 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.

  3. 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}
  1. Vamos supor que escolhemos 5 como o pivô para simplificar

  2. Primeiro, colocaremos todos os elementos menores que 5 na primeira posição da matriz: \ {3, 4, 5, 6, 5, 9}

  3. Vamos então repetir para a submatriz esquerda \ {3,4}, tomando 3 como o pivô

  4. Não há elementos menores que 3

  5. Aplicamos o quicksort no subconjunto à direita do pivô, ou seja, {4}

  6. Esse sub-array consiste em apenas um elemento classificado

  7. 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.