Insertion Sort em Rust

Aprenda Insertion Sort em Rust com diagramas visuais, análise Big-O, código genérico e otimizações. Guia completo em português.

O Insertion Sort (ordenação por inserção) é um algoritmo de ordenação simples que constrói a sequência ordenada um elemento por vez. Sua lógica é similar à forma como a maioria das pessoas organiza cartas na mão: você pega uma carta de cada vez e a insere na posição correta entre as cartas já organizadas.

Entre os algoritmos O(n²), o Insertion Sort é o mais utilizado na prática. Ele é estável, adaptativo (aproveita dados parcialmente ordenados) e tem excelente desempenho para vetores pequenos. Não é coincidência que algoritmos de ordenação sofisticados como o TimSort (usado em Python e Java) e o sort() da biblioteca padrão do Rust utilizem o Insertion Sort como caso base para partições pequenas.

Como Funciona

O algoritmo percorre o vetor da esquerda para a direita. Para cada elemento, ele o compara com os elementos anteriores (já ordenados) e o desloca para a posição correta, movendo os maiores uma posição à direita.

Vetor inicial: [7, 3, 5, 1, 9, 2]

=== Passo 1: inserir o 3 ===
  Parte ordenada: [7]  |  Elemento: 3
  3 < 7 → desloca 7 para a direita
  Insere 3 na posição 0
  [3, 7, 5, 1, 9, 2]
   ✓  ✓

=== Passo 2: inserir o 5 ===
  Parte ordenada: [3, 7]  |  Elemento: 5
  5 < 7 → desloca 7 para a direita
  5 > 3 → posição encontrada!
  Insere 5 na posição 1
  [3, 5, 7, 1, 9, 2]
   ✓  ✓  ✓

=== Passo 3: inserir o 1 ===
  Parte ordenada: [3, 5, 7]  |  Elemento: 1
  1 < 7 → desloca 7
  1 < 5 → desloca 5
  1 < 3 → desloca 3
  Insere 1 na posição 0
  [1, 3, 5, 7, 9, 2]
   ✓  ✓  ✓  ✓

=== Passo 4: inserir o 9 ===
  Parte ordenada: [1, 3, 5, 7]  |  Elemento: 9
  9 > 7 → já está na posição correta!
  [1, 3, 5, 7, 9, 2]
   ✓  ✓  ✓  ✓  ✓

=== Passo 5: inserir o 2 ===
  Parte ordenada: [1, 3, 5, 7, 9]  |  Elemento: 2
  2 < 9 → desloca 9
  2 < 7 → desloca 7
  2 < 5 → desloca 5
  2 < 3 → desloca 3
  2 > 1 → posição encontrada!
  Insere 2 na posição 1
  [1, 2, 3, 5, 7, 9]
   ✓  ✓  ✓  ✓  ✓  ✓

Resultado: [1, 2, 3, 5, 7, 9]

Diagrama visual do deslocamento (shift) no passo 5:

  Inserindo o 2 na parte ordenada [1, 3, 5, 7, 9]:

  [1, 3, 5, 7, 9, _]     salvar 2 em variável temporária

  [1, 3, 5, 7, _, 9]     desloca 9 →
  [1, 3, 5, _, 7, 9]     desloca 7 →
  [1, 3, _, 5, 7, 9]     desloca 5 →
  [1, _, 3, 5, 7, 9]     desloca 3 →
  [1, 2, 3, 5, 7, 9]     insere 2 na posição vazia

Complexidade

CasoTempoEspaço
MelhorO(n)O(1)
MédioO(n²)O(1)
PiorO(n²)O(1)
  • Melhor caso O(n): quando o vetor já está ordenado, cada elemento é comparado apenas uma vez com o anterior, totalizando n-1 comparações e zero deslocamentos.
  • Caso médio O(n²): para dados aleatórios, cada elemento é inserido aproximadamente na metade da parte ordenada, resultando em ~n²/4 comparações e deslocamentos.
  • Pior caso O(n²): ocorre quando o vetor está em ordem decrescente. Cada novo elemento precisa ser movido para o início, resultando em ~n²/2 comparações e deslocamentos.
  • Espaço O(1): algoritmo in-place, utiliza apenas uma variável temporária.

Propriedade adaptativa: o Insertion Sort executa em O(n + d), onde d é o número de inversões no vetor. Para vetores quase ordenados (poucas inversões), o desempenho se aproxima de O(n).

Implementação em Rust

/// Insertion Sort genérico.
/// Ordena o slice in-place em ordem crescente.
/// Algoritmo estável e adaptativo.
fn insertion_sort<T: PartialOrd>(vetor: &mut [T]) {
    let n = vetor.len();

    for i in 1..n {
        // O elemento na posição `i` precisa ser inserido
        // na posição correta dentro de vetor[0..i]
        let mut j = i;

        // Desloca elementos maiores para a direita
        while j > 0 && vetor[j - 1] > vetor[j] {
            vetor.swap(j - 1, j);
            j -= 1;
        }
    }
}

fn main() {
    // Exemplo com inteiros
    let mut numeros = vec![7, 3, 5, 1, 9, 2];
    println!("Antes:  {:?}", numeros);
    insertion_sort(&mut numeros);
    println!("Depois: {:?}", numeros);

    // Exemplo com strings
    let mut nomes = vec!["Rust", "Python", "Go", "C", "Java"];
    println!("\nAntes:  {:?}", nomes);
    insertion_sort(&mut nomes);
    println!("Depois: {:?}", nomes);

    // Exemplo com vetor quase ordenado (caso ideal)
    let mut quase = vec![1, 2, 4, 3, 5, 6, 8, 7];
    println!("\nAntes:  {:?}", quase);
    insertion_sort(&mut quase);
    println!("Depois: {:?}", quase);
}

Exemplo de Execução

Antes:  [7, 3, 5, 1, 9, 2]
Depois: [1, 2, 3, 5, 7, 9]

Antes:  ["Rust", "Python", "Go", "C", "Java"]
Depois: ["C", "Go", "Java", "Python", "Rust"]

Antes:  [1, 2, 4, 3, 5, 6, 8, 7]
Depois: [1, 2, 3, 4, 5, 6, 7, 8]

Versão com rastreamento detalhado:

fn insertion_sort_rastreado(vetor: &mut Vec<i32>) {
    let n = vetor.len();

    for i in 1..n {
        let valor = vetor[i];
        let mut j = i;
        let mut deslocamentos = 0;

        while j > 0 && vetor[j - 1] > vetor[j] {
            vetor.swap(j - 1, j);
            j -= 1;
            deslocamentos += 1;
        }

        println!(
            "Passo {}: inserir {} → posição {} ({} deslocamentos) → {:?}",
            i, valor, j, deslocamentos, vetor
        );
    }
}

fn main() {
    let mut v = vec![7, 3, 5, 1, 9, 2];
    println!("Entrada: {:?}\n", v);
    insertion_sort_rastreado(&mut v);
    println!("\nResultado: {:?}", v);
}

Saída esperada:

Entrada: [7, 3, 5, 1, 9, 2]

Passo 1: inserir 3 → posição 0 (1 deslocamentos) → [3, 7, 5, 1, 9, 2]
Passo 2: inserir 5 → posição 1 (1 deslocamentos) → [3, 5, 7, 1, 9, 2]
Passo 3: inserir 1 → posição 0 (3 deslocamentos) → [1, 3, 5, 7, 9, 2]
Passo 4: inserir 9 → posição 4 (0 deslocamentos) → [1, 3, 5, 7, 9, 2]
Passo 5: inserir 2 → posição 1 (4 deslocamentos) → [1, 2, 3, 5, 7, 9]

Resultado: [1, 2, 3, 5, 7, 9]

Variações e Otimizações

1. Insertion Sort com Busca Binária

Podemos usar busca binária para encontrar a posição de inserção, reduzindo o número de comparações de O(n) para O(log n) por elemento. Porém, o número de deslocamentos continua O(n), então a complexidade total permanece O(n²).

/// Insertion Sort com busca binária para encontrar posição de inserção.
/// Reduz comparações, mas mantém O(n²) por causa dos deslocamentos.
fn binary_insertion_sort<T: Ord>(vetor: &mut [T]) {
    let n = vetor.len();

    for i in 1..n {
        // Usar busca binária para encontrar a posição de inserção
        // partition_point retorna o índice onde o elemento deve ser inserido
        let pos = vetor[..i].partition_point(|x| x < &vetor[i]);

        // Rotacionar os elementos para abrir espaço
        // rotate_right move os elementos para a direita
        vetor[pos..=i].rotate_right(1);
    }
}

fn main() {
    let mut v = vec![7, 3, 5, 1, 9, 2];
    println!("Antes:  {:?}", v);
    binary_insertion_sort(&mut v);
    println!("Depois: {:?}", v);

    // Comparação: ambos produzem o mesmo resultado
    let mut v2 = vec![50, 20, 40, 10, 30];
    println!("\nAntes:  {:?}", v2);
    binary_insertion_sort(&mut v2);
    println!("Depois: {:?}", v2);
}

2. Shell Sort (Insertion Sort com Gap)

O Shell Sort é uma generalização do Insertion Sort que compara e troca elementos distantes, reduzindo progressivamente a distância (gap) até chegar a 1, quando se torna um Insertion Sort padrão sobre dados quase ordenados.

/// Shell Sort — Insertion Sort com gap decrescente.
/// Usa a sequência de gaps de Knuth: 1, 4, 13, 40, 121, ...
fn shell_sort<T: PartialOrd>(vetor: &mut [T]) {
    let n = vetor.len();

    // Calcular o maior gap da sequência de Knuth
    let mut gap = 1;
    while gap < n / 3 {
        gap = gap * 3 + 1; // 1, 4, 13, 40, 121, ...
    }

    while gap >= 1 {
        // Insertion Sort com o gap atual
        for i in gap..n {
            let mut j = i;
            while j >= gap && vetor[j - gap] > vetor[j] {
                vetor.swap(j - gap, j);
                j -= gap;
            }
        }
        gap /= 3;
    }
}

fn main() {
    let mut v = vec![35, 33, 42, 10, 14, 19, 27, 44, 26, 31];
    println!("Antes:  {:?}", v);
    shell_sort(&mut v);
    println!("Depois: {:?}", v);
}

3. Insertion Sort como Caso Base em Algoritmos Híbridos

Na prática, o Insertion Sort é usado como caso base quando o tamanho da partição fica pequeno em algoritmos como Quick Sort e Merge Sort. Isso explora o fato de que o Insertion Sort tem menor overhead que algoritmos recursivos para poucos elementos.

/// Exemplo de como Insertion Sort é usado como caso base.
/// Ordena a sub-fatia usando Insertion Sort quando o tamanho é pequeno.
fn insertion_sort_faixa<T: PartialOrd>(vetor: &mut [T], inicio: usize, fim: usize) {
    for i in (inicio + 1)..=fim {
        let mut j = i;
        while j > inicio && vetor[j - 1] > vetor[j] {
            vetor.swap(j - 1, j);
            j -= 1;
        }
    }
}

/// Quick Sort híbrido que usa Insertion Sort para partições pequenas.
const LIMIAR_INSERCAO: usize = 16;

fn quick_sort_hibrido<T: PartialOrd>(vetor: &mut [T]) {
    quick_sort_interno(vetor, 0, vetor.len().saturating_sub(1));
}

fn quick_sort_interno<T: PartialOrd>(vetor: &mut [T], inicio: usize, fim: usize) {
    if inicio >= fim {
        return;
    }

    // Para partições pequenas, usar Insertion Sort
    if fim - inicio + 1 <= LIMIAR_INSERCAO {
        insertion_sort_faixa(vetor, inicio, fim);
        return;
    }

    // Particionamento simples (Lomuto)
    let pivo_pos = fim;
    let mut i = inicio;
    for j in inicio..fim {
        if vetor[j] <= vetor[pivo_pos] {
            vetor.swap(i, j);
            i += 1;
        }
    }
    vetor.swap(i, pivo_pos);

    if i > 0 {
        quick_sort_interno(vetor, inicio, i.saturating_sub(1));
    }
    quick_sort_interno(vetor, i + 1, fim);
}

fn main() {
    let mut v = vec![38, 27, 43, 3, 9, 82, 10, 15, 7, 1, 45, 22, 5, 19, 33, 12, 50, 41, 8, 6];
    println!("Antes:  {:?}", v);
    quick_sort_hibrido(&mut v);
    println!("Depois: {:?}", v);
}

Quando Usar

O Insertion Sort e a escolha ideal nos seguintes cenarios:

  • Vetores pequenos (n < 50): o baixo overhead do Insertion Sort supera a vantagem teórica de algoritmos O(n log n) para poucos elementos.
  • Dados quase ordenados: se o vetor tem poucas inversões (elementos fora de lugar), o desempenho se aproxima de O(n). Ideal para manter dados que recebem atualizações incrementais.
  • Ordenação online: quando os dados chegam um a um (streaming), o Insertion Sort insere cada novo elemento na posição correta de forma natural.
  • Caso base de algoritmos híbridos: usado internamente pelo TimSort, Introsort e pela implementação de sort() do Rust.
  • Estabilidade necessária: quando a ordem relativa de elementos iguais deve ser preservada.

Evite o Insertion Sort quando:

  • O volume de dados for grande e desordenado — prefira Merge Sort ou Quick Sort.
  • O desempenho garantido O(n log n) for necessário — considere o Heap Sort.

Comparação com Outros Algoritmos

CaracterísticaInsertion SortBubble SortSelection SortMerge Sort
Complexidade (média)O(n²)O(n²)O(n²)O(n log n)
Melhor casoO(n) ✓O(n) ✓O(n²)O(n log n)
EstávelSim ✓Sim ✓NãoSim ✓
AdaptativoSim ✓SimNãoNão
In-placeSim ✓Sim ✓Sim ✓Não (O(n))
Uso práticoCaso baseEducacionalTrocas carasGrandes volumes

O Insertion Sort domina entre os O(n²) por ser o mais eficiente na prática: tem o melhor padrão de acesso à memória (acesso sequencial favorável ao cache), é adaptativo e estável. É por isso que algoritmos profissionais o utilizam como caso base.

Exercícios Práticos

  1. Contagem de inversões: O número de deslocamentos no Insertion Sort é igual ao número de inversões no vetor (pares (i, j) onde i < j mas vetor[i] > vetor[j]). Implemente uma função que conta inversões usando Insertion Sort. Depois, implemente uma versão O(n log n) usando Merge Sort modificado e compare os resultados.

  2. Insertion Sort para listas ligadas: Implemente uma lista ligada simples em Rust e um Insertion Sort para ela. Qual é a vantagem da lista ligada sobre o vetor para o Insertion Sort? Dica: na lista, a inserção é O(1) mas a busca é O(n).

  3. Ordenação parcial: Modifique o Insertion Sort para ordenar apenas os primeiros k menores elementos do vetor. Compare a complexidade com o Selection Sort para a mesma tarefa.

  4. Benchmark: quase ordenado vs. aleatório: Gere vetores de tamanho 10.000 com três níveis de “desordem”: 0 trocas (ordenado), 10 trocas aleatórias e 100 trocas aleatórias. Meça o tempo do Insertion Sort em cada caso usando std::time::Instant. Plote mentalmente a curva de desempenho em função do número de inversões.

Veja Também