Introdução

A ordenação de dados é uma das operações mais fundamentais na computação. Algoritmos de ordenação são essenciais para organizar informações, otimizar buscas e servir de base para outros algoritmos. Nesta aula, estudamos os principais algoritmos de ordenação, analisamos suas complexidades de tempo e espaço e discutimos quando cada um é mais adequado.

Bubble Sort

O Bubble Sort é um algoritmo simples que percorre repetidamente a lista, compara elementos adjacentes e os troca se estiverem na ordem errada. A cada passagem, o maior elemento "flutua" para o final da lista, reduzindo o número de elementos a verificar. Sua complexidade é O(n²) no pior e no caso médio, e O(n) no melhor caso (quando a lista já está ordenada e utilizamos uma otimização com flag). É um algoritmo estável (a ordem relativa de elementos iguais é preservada) e in-place.

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

Selection Sort

O Selection Sort divide a lista em duas partes: uma sublista ordenada no início e outra não ordenada. A cada iteração, seleciona o menor elemento da parte não ordenada e o troca com o primeiro elemento não ordenado. Sua complexidade é O(n²) em todos os casos, pois sempre percorre a lista em busca do menor elemento. É um algoritmo não estável (a troca pode alterar a ordem relativa de elementos iguais) e in-place.

def selection_sort(arr):
    for i in range(len(arr)):
        min_idx = i
        for j in range(i+1, len(arr)):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr>

Insertion Sort

O Insertion Sort constrói a lista ordenada um elemento de cada vez, inserindo cada novo elemento na posição correta entre os já ordenados. É eficiente para listas pequenas ou parcialmente ordenadas, com complexidade O(n) no melhor caso (lista ordenada) e O(n²) no pior caso. É um algoritmo estável e in-place.

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i-1
        while j >= 0 and key < arr[j]:
            arr[j+1] = arr[j]
            j -= 1
        arr[j+1] = key
    return arr>

Merge Sort

O Merge Sort é um algoritmo de divisão e conquista. Ele divide recursivamente a lista ao meio até que cada sublista tenha um único elemento, depois mescla as sublistas de forma ordenada. Sua complexidade é O(n log n) nos três casos (melhor, médio e pior). É um algoritmo estável e requer memória extra O(n) para as operações de mesclagem. É ideal para cenários onde a estabilidade e a previsibilidade são importantes.

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

Quick Sort

O Quick Sort também utiliza divisão e conquista, mas de forma diferente: escolhe um pivô e particiona a lista em duas sublistas (elementos menores e maiores que o pivô), depois ordena recursivamente as sublistas. Sua complexidade média é O(n log n), mas no pior caso (geralmente quando o pivô é o menor ou maior elemento) chega a O(n²). É um algoritmo in-place, porém não estável. Estratégias como escolha aleatória do pivô ou mediana de três ajudam a evitar o pior caso.

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr)//2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

Comparação dos Algoritmos

A tabela a seguir resume as principais características dos algoritmos estudados:

AlgoritmoMelhor CasoCaso MédioPior CasoEstávelIn-Place
Bubble SortO(n)O(n²)O(n²)SimSim
Selection SortO(n²)O(n²)O(n²)NãoSim
Insertion SortO(n)O(n²)O(n²)SimSim
Merge SortO(n log n)O(n log n)O(n log n)SimNão (memória extra O(n))
Quick SortO(n log n)O(n log n)O(n²)NãoSim

Quando usar cada algoritmo?

  • Bubble Sort: raramente usado na prática, exceto para fins didáticos. Sua ineficiência o torna inadequado para listas grandes.
  • Selection Sort: útil quando o custo de troca é muito alto, pois minimiza o número de trocas (O(n)), mas ainda O(n²) comparações.
  • Insertion Sort: excelente para listas pequenas ou quase ordenadas. Muito usado como base em algoritmos híbridos (ex.: TimSort).
  • Merge Sort: indicado quando a estabilidade é essencial e a memória extra é aceitável. É previsível e eficiente para grandes conjuntos.
  • Quick Sort: geralmente o mais rápido na prática, especialmente para listas grandes. Escolha do pivô é crucial para evitar o pior caso.

Perguntas Frequentes

Qual algoritmo de ordenação é o mais rápido?

Não há uma resposta absoluta. O Quick Sort costuma ser o mais rápido em média para a maioria dos cenários, mas pode degenerar para O(n²). Merge Sort e Heap Sort (não abordado aqui) oferecem O(n log n) garantido. A escolha depende do contexto: tamanho dos dados, necessidade de estabilidade, uso de memória e distribuição dos valores.

O Quick Sort sempre é a melhor escolha?

Não. Em listas ordenadas ou inversamente ordenadas, sem um bom pivô, o Quick Sort pode ter desempenho O(n²). A implementação com mediana de três ou pivô aleatório reduz essa probabilidade. Além disso, o Merge Sort é mais adequado quando a estabilidade é obrigatória ou a memória não é problema.

Por que o Insertion Sort é eficiente para listas pequenas?

Porque tem baixo overhead (não usa recursão nem alocação extra) e seu deslocamento de elementos é eficiente para listas pequenas. Muitos algoritmos de ordenação híbridos usam Insertion Sort para sublistas de tamanho reduzido (tipicamente até 16-64 elementos).

Conclusão

Nesta aula, vimos os principais algoritmos de ordenação, suas complexidades e características. A escolha do algoritmo adequado depende do problema: tipo e volume dos dados, requisitos de estabilidade, memória disponível e desempenho esperado. O conhecimento desses algoritmos é fundamental para qualquer profissional de computação. Para mais conteúdos, explore outros artigos na seção Posts ou navegue pelas Tags do blog.