0% acharam este documento útil (0 voto)
78 visualizações7 páginas

Resolução Do Exercício Proposto - Complexidade Do Quicksort em Ordem Decrescente

O documento analisa a complexidade do algoritmo Quicksort, demonstrando que sua execução é O(n²) quando os elementos estão ordenados em ordem decrescente. A análise inclui a derivação da recorrência T(n) = T(n-1) + Θ(n) e a visualização do processo por meio de uma árvore de recursão. O estudo ressalta a importância de técnicas para evitar o pior caso e a necessidade de considerar o comportamento do algoritmo em diferentes padrões de entrada.
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
78 visualizações7 páginas

Resolução Do Exercício Proposto - Complexidade Do Quicksort em Ordem Decrescente

O documento analisa a complexidade do algoritmo Quicksort, demonstrando que sua execução é O(n²) quando os elementos estão ordenados em ordem decrescente. A análise inclui a derivação da recorrência T(n) = T(n-1) + Θ(n) e a visualização do processo por meio de uma árvore de recursão. O estudo ressalta a importância de técnicas para evitar o pior caso e a necessidade de considerar o comportamento do algoritmo em diferentes padrões de entrada.
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

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.

Você também pode gostar