Resolução do Exercício Proposto:
Complexidade do Quicksort em Ordem
Decrescente
Objetivo: Demonstrar que o tempo de execução do algoritmo Quicksort é O(n²) quando
todos os elementos do vetor A são distintos e estão ordenados em ordem decrescente.
Introdução ao Algoritmo Quicksort
O Quicksort é um algoritmo de ordenação eficiente baseado na estratégia de divisão e
conquista, desenvolvido por Tony Hoare em 1960. Sua ideia fundamental consiste em:
• Selecionar um elemento do vetor como pivô
• Particionar o vetor em dois subvetores: elementos menores que o pivô e elementos
maiores que o pivô
• Recursivamente ordenar os subvetores
Em média, o Quicksort apresenta complexidade O(n log n), tornando-o um dos
algoritmos de ordenação mais eficientes na prática. No entanto, sua eficiência depende
fortemente da escolha do pivô e da distribuição dos elementos no vetor.
Pseudocódigo do Quicksort
Apresentamos abaixo o pseudocódigo do algoritmo Quicksort em sua implementação
padrão, onde o último elemento é escolhido como pivô:
QUICKSORT(A, p, r)
1 if p < r // O(1) - Verificação de condição
2 q ← PARTITION(A, p, r) // O(n) - Particionamento do vetor
3 QUICKSORT(A, p, q-1) // Chamada recursiva para subvetor esquerdo
4 QUICKSORT(A, q+1, r) // Chamada recursiva para subvetor direito
PARTITION(A, p, r)
1 x ← A[r] // O(1) - Seleção do pivô (último elemento)
2 i ← p-1 // O(1) - Inicialização do índice
3 for j ← p to r-1 // O(n) - Loop através do vetor
4 if A[j] ≤ x // O(1) - Comparação
5 i ← i+1 // O(1) - Incremento de índice
6 trocar A[i] com A[j] // O(1) - Troca de elementos
7 trocar A[i+1] com A[r] // O(1) - Posicionamento do pivô
8 return i+1 // O(1) - Retorno da posição do pivô
Complexidade por Linha
Analisando a complexidade de cada linha do algoritmo:
• Quicksort:
• Linha 1: Verificação de condição - O(1)
• Linha 2: Chamada ao procedimento Partition - O(n), onde n = r-p+1 é o tamanho do
subvetor
• Linhas 3-4: Chamadas recursivas - Dependem do tamanho dos subvetores
resultantes
• Partition:
• Linhas 1-2: Operações de atribuição - O(1)
• Linha 3: Loop for - Executa r-p vezes, ou seja, O(n)
• Linhas 4-6: Operações dentro do loop - O(1) por iteração
• Linhas 7-8: Operações finais - O(1)
Invariante de Laço
Para o procedimento Partition, podemos definir a seguinte invariante de laço para o
loop nas linhas 3-6:
Invariante: No início de cada iteração do loop for, para qualquer índice k: - Se p ≤ k ≤ i,
então A[k] ≤ x - Se i+1 ≤ k ≤ j-1, então A[k] > x - Se k = r, então A[k] = x (o pivô)
Inicialização: Antes da primeira iteração, j = p e i = p-1. Não existem elementos no
intervalo [p, i] nem no intervalo [i+1, j-1], então a invariante é trivialmente verdadeira.
Manutenção: Suponha que a invariante seja verdadeira antes da iteração j.
Consideramos dois casos: - Se A[j] ≤ x: Incrementamos i e trocamos A[i] com A[j]. Isso
mantém a propriedade de que todos os elementos em [p, i] são ≤ x, e todos em [i+1, j]
são > x. - Se A[j] > x: Não fazemos nada, e j é incrementado. A invariante é mantida, pois
A[j] é corretamente classificado como > x.
Terminação: O loop termina quando j = r. Neste ponto, todos os elementos em [p, i] são
≤ x e todos em [i+1, r-1] são > x. Ao trocar A[i+1] com A[r], colocamos o pivô na posição
correta, com elementos menores à esquerda e maiores à direita.
Análise da Complexidade do Quicksort
Passo 1: Entender a Recorrência Geral
A complexidade do Quicksort pode ser expressa pela seguinte recorrência:
T(n) = T(q) + T(n-q-1) + Θ(n)
onde: - n é o tamanho do vetor - q é o número de elementos no subvetor esquerdo após
o particionamento - n-q-1 é o número de elementos no subvetor direito - Θ(n) é o custo
do particionamento
O valor de q depende da posição do pivô após o particionamento, que por sua vez
depende da distribuição dos elementos no vetor.
Passo 2: Analisar o Caso Específico (Ordem Decrescente)
Consideremos um vetor A com elementos distintos em ordem decrescente, por exemplo:
A = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Quando o último elemento é escolhido como pivô (implementação padrão), temos:
• Na primeira chamada, o pivô é A[r] = 1 (o menor elemento)
• Durante o particionamento, nenhum elemento satisfaz A[j] ≤ x, pois todos são
maiores que o pivô
• O pivô é colocado na primeira posição, resultando em q = p
• O subvetor esquerdo está vazio (T(0))
• O subvetor direito contém n-1 elementos (T(n-1))
Isso leva à seguinte recorrência para o caso de ordem decrescente:
T(n) = T(0) + T(n-1) + Θ(n)
Como T(0) = Θ(1) (caso base), simplificamos para:
T(n) = T(n-1) + Θ(n)
Passo 3: Resolver a Recorrência
Para resolver esta recorrência, expandimos os termos:
T(n) = T(n-1) + Θ(n) = T(n-2) + Θ(n-1) + Θ(n) = T(n-3) + Θ(n-2) + Θ(n-1) + Θ(n) = T(0) + Θ(1)
+ Θ(2) + ... + Θ(n-1) + Θ(n)
Como T(0) = Θ(1) e Θ(k) = ck para alguma constante c, temos:
T(n) = Θ(1) + Θ(1) + Θ(2) + ... + Θ(n-1) + Θ(n) = Θ(1) + c • (1 + 2 + ... + (n-1) + n) = Θ(1) + c •
(n(n+1)/2) = Θ(1) + Θ(n²) = Θ(n²)
Portanto, T(n) = Θ(n²), o que implica que T(n) = O(n²).
Passo 4: Análise com Árvore de Recursão
Podemos visualizar a execução do Quicksort em um vetor com elementos em ordem
decrescente usando uma árvore de recursão:
T(n), custo: Θ(n)
/ \
T(0), custo: Θ(1) T(n-1), custo: Θ(n-1)
/ \
T(0), custo: Θ(1) T(n-2), custo: Θ(n-2)
/ \
Observações sobre a árvore: - A árvore é altamente desbalanceada, com profundidade n
- Cada nível i tem custo Θ(n-i) - O custo total é a soma dos custos de todos os níveis: Θ(n
+ (n-1) + (n-2) + ... + 1) = Θ(n²)
Passo 5: Análise Detalhada do Particionamento
Vamos analisar em detalhes o comportamento do procedimento Partition quando
aplicado a um vetor em ordem decrescente:
Considere o vetor A = [5, 4, 3, 2, 1] com p = 0 e r = 4:
• Pivô: x = A[r] = 1
• Inicialização: i = -1
• Iteração 1 (j = 0):
• A[j] = 5 > x? Sim
• Nenhuma ação, i permanece -1
• Iteração 2 (j = 1):
• A[j] = 4 > x? Sim
• Nenhuma ação, i permanece -1
• Iteração 3 (j = 2):
• A[j] = 3 > x? Sim
• Nenhuma ação, i permanece -1
• Iteração 4 (j = 3):
• A[j] = 2 > x? Sim
• Nenhuma ação, i permanece -1
• Após o loop: i = -1
• Troca final: A[i+1] = A[0] = 5 é trocado com A[r] = A[4] = 1
• Resultado: A = [1, 4, 3, 2, 5]
• Retorno: i+1 = 0
Nas chamadas recursivas: - Quicksort(A, 0, -1): Subvetor esquerdo vazio - Quicksort(A, 1,
4): Subvetor direito com 4 elementos [4, 3, 2, 5]
Este padrão se repete em cada chamada recursiva, sempre resultando em um subvetor
esquerdo vazio e um subvetor direito com um elemento a menos que o vetor original.
Passo 6: Visualização do Processo Completo
Para ilustrar o processo completo, vamos acompanhar a execução do Quicksort no vetor
A = [5, 4, 3, 2, 1]:
A = [5, 4, 3, 2, 1]
Pivô: 1
Após partição: A = [1, 4, 3, 2, 5], q = 0
├── Subvetor esquerdo: A[0..-1] (Vazio), T(0) = Θ(1)
└── Subvetor direito: A[1..4] = [4, 3, 2, 5]
Pivô: 5
Após partição: A = [1, 4, 3, 2, 5], q = 4
├── Subvetor esquerdo: A[1..3] = [4, 3, 2]
│ Pivô: 2
│ Após partição: A = [1, 2, 3, 4, 5], q = 1
│ ├── Subvetor esquerdo: A[1..0] (Vazio), T(0) = Θ(1)
│ └── Subvetor direito: A[2..3] = [3, 4]
│ Pivô: 4
│ Após partição: A = [1, 2, 3, 4, 5], q = 3
│ ├── Subvetor esquerdo: A[2..2] = [3] (Um elemento), T(1) = Θ(1)
│ └── Subvetor direito: A[4..3] (Vazio), T(0) = Θ(1)
└── Subvetor direito: A[5..4] (Vazio), T(0) = Θ(1)
Observações importantes: - Na primeira chamada, o pivô (1) é o menor elemento,
resultando em um particionamento altamente desbalanceado - Nas chamadas
subsequentes, o padrão se repete, com o pivô sempre sendo colocado em uma posição
extrema - A árvore de recursão é essencialmente uma lista ligada, com profundidade n -
Cada nível da recursão processa um subvetor com um elemento a menos que o nível
anterior
Passo 7: Verificação Intuitiva
Intuitivamente, podemos entender por que o Quicksort tem desempenho O(n²) neste
caso:
• Em um vetor ordenado em ordem decrescente, o último elemento (escolhido como
pivô) é sempre o menor
• Isso resulta no pior caso possível de particionamento: um subvetor vazio e outro
com n-1 elementos
• O algoritmo efetivamente se comporta como um algoritmo de ordenação por
inserção, processando um elemento por vez
• Cada elemento requer O(n) operações para ser posicionado, resultando em O(n²)
operações no total
Passo 8: Comparação com o Caso Médio
Para contextualizar, comparemos com o caso médio do Quicksort:
• No caso médio, o pivô divide o vetor em duas partes aproximadamente iguais
• Isso leva à recorrência T(n) = 2T(n/2) + Θ(n)
• Pelo Teorema Mestre, isso resulta em T(n) = Θ(n log n)
• A diferença entre O(n log n) e O(n²) é significativa para valores grandes de n
Conclusão
Resumo da Demonstração
Demonstramos que o tempo de execução do Quicksort é O(n²) quando todos os
elementos do vetor são distintos e estão ordenados em ordem decrescente, através de:
• Análise detalhada do comportamento do algoritmo neste caso específico
• Derivação e resolução da recorrência T(n) = T(n-1) + Θ(n)
• Visualização do processo usando árvore de recursão
• Verificação intuitiva do comportamento do algoritmo
Implicações Práticas
Esta análise tem importantes implicações práticas:
• Demonstra a vulnerabilidade do Quicksort a certos padrões de entrada
• Justifica a necessidade de técnicas como "escolha aleatória do pivô" ou "mediana
de três" para evitar o pior caso
• Explica por que, em implementações reais, o Quicksort é frequentemente
combinado com outros algoritmos ou otimizações
Observações Finais
O caso analisado (elementos em ordem decrescente) é apenas um dos padrões que
podem levar ao pior caso do Quicksort. Qualquer padrão que resulte em
particionamentos altamente desbalanceados em cada nível da recursão levará a um
desempenho O(n²).
Esta análise reforça um princípio fundamental em algoritmos: a escolha do algoritmo
deve considerar não apenas seu desempenho médio, mas também seu comportamento
no pior caso e a probabilidade de encontrar entradas que provoquem esse
comportamento.