Ciências da computação dia 229
Algoritmos de Ordenação — Bubble Sort, Insertion Sort, Merge Sort e Quick Sort
Os algoritmos de ordenação são fundamentais na ciência da computação. Eles organizam elementos de uma lista em uma sequência específica — geralmente crescente ou decrescente — e são pré-requisito para algoritmos mais avançados como busca binária, análise de dados e otimização de consultas em bancos de dados. Nesta aula, vamos estudar quatro algoritmos clássicos: Bubble Sort, Insertion Sort, Merge Sort e Quick Sort, analisando suas complexidades de tempo e espaço, estabilidade e casos de uso ideais.
Bubble Sort
O Bubble Sort é o algoritmo mais simples de entender e implementar. Ele percorre a lista repetidamente, comparando elementos adjacentes e trocando-os se estiverem na ordem errada. A cada passagem, o maior elemento não ordenado "flutua" para sua posição correta no final da lista, como uma bolha na água — daí o nome.
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr
Complexidade: O(n²) no pior caso (lista invertida), O(n) no melhor caso (lista já ordenada, com a otimização da flag swapped). Espaço auxiliar: O(1) — é in-place. Estável: sim. Apesar de simples e didático, o Bubble Sort raramente é usado em produção devido à baixa eficiência em listas com mais de alguns centenas de elementos.
Insertion Sort
O Insertion Sort constrói a lista ordenada incrementalmente. A cada iteração, um elemento é retirado da posição atual e inserido na posição correta entre os elementos já ordenados à sua esquerda. É exatamente o método que usamos intuitivamente para ordenar cartas de baralho na mão.
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
Complexidade: O(n²) no pior caso (lista invertida), O(n) no melhor caso (lista já ordenada ou quase ordenada). Espaço: O(1). Estável: sim. É eficiente para listas pequenas (menos de 50 elementos) ou quando os dados já estão parcialmente ordenados. Muitas implementações de Timsort usam Insertion Sort como sub-rotina para blocos pequenos.
Merge Sort
O Merge Sort é um algoritmo de dividir para conquistar. Ele divide a lista ao meio recursivamente até obter sublistas de um único elemento (que são naturalmente ordenadas), depois combina (merge) essas sublistas para produzir a lista final ordenada. A operação de mesclagem compara os primeiros elementos de cada sublista e insere o menor no resultado.
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
Complexidade: O(n log n) no pior caso, no melhor caso e no caso médio — desempenho previsível. Espaço: O(n) — não é in-place, pois requer memória auxiliar para as sublistas durante a mesclagem. Estável: sim. É amplamente utilizado quando estabilidade e desempenho previsível são necessários, especialmente em ordenação de listas encadeadas e dados que não cabem em memória principal.
Quick Sort
O Quick Sort também utiliza a estratégia de dividir para conquistar, mas com uma abordagem diferente. Ele seleciona um elemento como pivô, particiona a lista em elementos menores ou iguais e maiores que o pivô, e recursivamente ordena as partições. A escolha do pivô é crucial para o desempenho.
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[-1]
left = [x for x in arr[:-1] if x <= pivot]
right = [x for x in arr[:-1] if x > pivot]
return quick_sort(left) + [pivot] + quick_sort(right)
Complexidade: O(n²) no pior caso (pivô mal escolhido, como pegar o último elemento de uma lista já ordenada), O(n log n) no caso médio e no melhor caso. Espaço: O(log n) para a pilha de recursão. Não é estável na implementação clássica. Na prática, o Quick Sort é geralmente o algoritmo mais rápido para a maioria dos cenários, especialmente com escolha aleatória do pivô ou mediana de três.
Comparação entre os Algoritmos
| Algoritmo | Pior Caso | Melhor Caso | Caso Médio | Espaço | Estável |
|---|---|---|---|---|---|
| Bubble Sort | O(n²) | O(n) | O(n²) | O(1) | Sim |
| Insertion Sort | O(n²) | O(n) | O(n²) | O(1) | Sim |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Sim |
| Quick Sort | O(n²) | O(n log n) | O(n log n) | O(log n) | Não |
Na prática, o Quick Sort é geralmente a escolha mais rápida para listas grandes não ordenadas, enquanto o Insertion Sort é útil para listas pequenas ou quase ordenadas. O Merge Sort é preferido quando a estabilidade é requisito ou quando os dados estão em estrutura que favorece acesso sequencial (listas encadeadas). Para a maioria das aplicações, as bibliotecas padrão das linguagens — como sorted() em Python (Timsort, uma mistura de Merge Sort e Insertion Sort) — já oferecem implementações altamente otimizadas.
Perguntas Frequentes
Qual algoritmo de ordenação devo usar no dia a dia?
Para a maioria dos casos, as funções prontas da linguagem são a melhor escolha. Em Python, o sorted() e o método .sort() implementam Timsort, que combina Merge Sort e Insertion Sort para obter desempenho O(n log n) com estabilidade. Em Java, o Arrays.sort() usa Dual-Pivot Quick Sort para tipos primitivos e Timsort para objetos. Entender os fundamentos ajuda a escolher a ferramenta certa e a escrever código mais eficiente quando você precisa implementar uma ordenação personalizada.
O que significa um algoritmo de ordenação ser estável?
Um algoritmo estável mantém a ordem relativa de elementos com chaves iguais. Se dois registros A e B têm o mesmo valor de ordenação e A aparece antes de B na lista original, após a ordenação A continuará antes de B. Isso é importante quando ordenamos por múltiplos critérios — por exemplo, ordenar uma lista de pessoas por nome e depois por idade: com um algoritmo estável, a ordenação por idade não desfaz a ordenação por nome.
O Bubble Sort tem alguma utilidade prática?
Fora do contexto educacional, o Bubble Sort raramente é usado. Seu principal valor é didático: é o algoritmo mais intuitivo para ensinar conceitos de ordenação, complexidade e otimização. Em cenários com listas muito pequenas (menos de 10 elementos) ou em sistemas embarcados com restrições extremas de memória, seu desempenho simples pode ser aceitável, mas o Insertion Sort quase sempre oferece melhor desempenho com a mesma simplicidade.
Merge Sort ou Quick Sort: qual é melhor?
Depende do contexto. O Quick Sort é geralmente mais rápido na prática para listas em memória (menor constante na notação O), mas tem o risco do pior caso O(n²) e não é estável. O Merge Sort oferece desempenho O(n log n) garantido e estabilidade, mas consome mais memória. Para listas encadeadas, o Merge Sort é naturalmente mais eficiente porque não requer acesso aleatório aos elementos. Em termos de uso geral, o Timsort (usado em Python e Java) combina o melhor dos dois mundos.