Algoritmos 2Revisão em C

Aula 5 de 6

Insertion sort

Inserções em uma parte já ordenada.

Ao final desta aula você será capaz de
  • Executar o insertion sort à mão, acompanhando a chave, os deslocamentos e o buraco.
  • Implementá-lo em C para números, structs e textos.
  • Usar a mesma ideia para manter uma lista ordenada enquanto os dados chegam, como em uma fila de atendimento.
  • Explicar, com medições, por que ele é a melhor escolha para listas pequenas ou quase ordenadas.

Palavras novas nesta aula: Deslocamento, Vetor k-ordenado. Cada uma tem uma explicação curta no glossário.

O problema

Ao organizar cartas na mão, você pega uma carta nova e a encaixa entre as que já estão em ordem. As cartas maiores escorregam um pouco para a direita para abrir espaço.

Em um vetor não há espaço livre entre as posições. Como abrir uma vaga sem perder nenhuma carta?

Considere as cartas 2, 5 e 8 já em ordem. Onde entra a carta 4, e quais cartas precisam se mover?

Como funciona

O insertion sort divide o vetor em duas partes: à esquerda, um trecho já ordenado; à direita, os elementos que ainda faltam. No começo, o trecho ordenado tem só o primeiro elemento, porque um elemento sozinho está sempre em ordem. A cada etapa, o próximo elemento é inserido no lugar certo do trecho ordenado, em três passos:

  1. Guarde a chave. Copie o próximo elemento para uma variável, a chave. A posição dele no vetor agora pode ser sobrescrita.
  2. Desloque os maiores. Da direita para a esquerda, cada elemento maior que a chave é copiado uma posição para a direita. O “buraco” vai andando para a esquerda.
  3. Insira. Ao encontrar um elemento que não é maior que a chave, ou ao chegar ao início, escreva a chave no buraco.
Inserindo a chave 4 no trecho ordenado 2, 5, 8 Passo 1: a chave 4 é guardada fora do vetor. Passo 2: como 8 é maior que 4, o 8 é copiado uma posição para a direita, e a posição 2 vira o buraco. Passo 3: como 5 é maior que 4, o 5 é copiado para a direita, e o buraco passa para a posição 1. Passo 4: como 2 não é maior que 4, a busca para e a chave é escrita no buraco. Resultado: 2, 4, 5, 8. 1. Guarde a chavea posição 3 pode ser usada2584chave = 4 2. 8 > 4: desloque o 8a posição 2 vira o buraco2588chave = 4 3. 5 > 4: desloque o 5agora o buraco é a posição 12558chave = 4 4. 2 ≤ 4: pare e insiraa chave cai no buraco2458✓ [2, 4, 5, 8] A chave fica guardada fora do vetor (à direita) enquanto os maiores escorregam. Pontilhado: o buraco, que pode ser sobrescrito.
Inserindo a carta 4 em [2, 5, 8]. Durante os deslocamentos, o mesmo valor aparece duas vezes por um instante. Não há problema, porque a chave está guardada em segurança.

Repetindo essa inserção para cada elemento, da esquerda para a direita, o vetor inteiro fica ordenado:

Insertion sort completo no vetor 5, 2, 4, 1 Início: o 5 sozinho é um trecho ordenado. Com i igual a 1, a chave 2 entra antes do 5, com 1 deslocamento: 2, 5, 4, 1. Com i igual a 2, a chave 4 entra entre 2 e 5, com 1 deslocamento: 2, 4, 5, 1. Com i igual a 3, a chave 1 vai para o início, com 3 deslocamentos: 1, 2, 4, 5. Total: 5 deslocamentos, igual ao número de inversões. Início5241o 5 sozinho já é um trecho ordenado i = 1chave 225411 deslocamento (o 5) i = 2chave 424511 deslocamento (o 5) i = 3chave 112453 deslocamentos (5, 4 e 2) Total: 5 deslocamentos, exatamente o número de inversões de [5, 2, 4, 1].Azul: trecho ordenado (ainda pode mudar). Amarelo: onde a chave entrou. A linha vertical separa o trecho ordenado do que falta.
O trecho ordenado cresce uma posição por etapa. Repare que o 5 muda de lugar três vezes. Os valores do trecho azul estão em ordem, mas ainda não estão no lugar definitivo.
O que fica garantido a cada etapa

Os elementos já processados estão em ordem entre si, mas não necessariamente no lugar final: um valor menor que chegue depois ainda pode empurrá-los. É a diferença em relação ao bubble sort, em que cada passagem deixa um valor no lugar definitivo.

Deslocar, não trocar

O bubble sort e o insertion sort mexem nos mesmos pares de elementos, mas de jeitos diferentes. Para levar o 1 do fim ao início de [2, 4, 5, 1]:

Levar o 1 do fim ao início: trocas do bubble sort contra deslocamentos do insertion sort Bubble sort: o 1 precisa de 3 trocas para ir do fim ao início, e cada troca escreve em 2 posições do vetor, totalizando 6 escritas no vetor. Insertion sort: o 1 é guardado como chave, fora do vetor, os valores 5, 4 e 2 são deslocados uma vez cada, e o 1 é escrito no início: 4 escritas no vetor. Bubble sort: trocar vizinhosInsertion sort: deslocar 2451troca 5 e 1: 2 escritas 2415troca 4 e 1: 2 escritas 2145troca 2 e 1: 2 escritas 1245 2451guarda a chave 1 (fora do vetor) 24551 escrita: desloca o 5 24451 escrita: desloca o 4 22451 escrita: desloca o 2 12451 escrita: insere o 1 6 escritas no vetor4 escritas no vetor
Cada troca do bubble sort faz três atribuições (temp = a; a = b; b = temp), mas só duas escrevem no vetor: a de temp fica fora dele. O insertion sort guarda a chave fora do vetor e faz uma única escrita no vetor por deslocamento, mais uma para inserir a chave.
Deslocamentos = inversões

Cada deslocamento passa um elemento maior para depois da chave, ou seja, conserta exatamente um par fora de ordem. Como no bubble sort, o número de deslocamentos é igual ao número de inversões. A quantidade de “consertos” é a mesma nos dois algoritmos, mas cada conserto do insertion sort custa uma escrita no vetor, contra duas de uma troca do bubble sort. Na figura, levar o 1 ao início conserta 3 inversões: o bubble sort gasta 2 × 3 = 6 escritas, e o insertion sort gasta 3 deslocamentos + 1 inserção = 4. Generalizando, para uma lista com I inversões e n elementos, o bubble sort faz 2·I escritas no vetor, e esta versão do insertion sort faz I + (n − 1): uma por deslocamento e uma por inserção. Pouca bagunça significa pouco trabalho.

Na prática

Laboratório de ordenação

Informe de 2 a 8 inteiros entre −99 e 99, separados por vírgula. Teste um vetor já ordenado, um decrescente e um com só um elemento fora do lugar. Compare os contadores.

    • Posições envolvidas
    • Trecho ordenado (ainda pode mudar)
    • Ordenação concluída

    Durante os deslocamentos, a chave fica guardada fora do vetor. Por isso, o mesmo valor pode aparecer duas vezes por um instante.

    Implementação em C

    Funções: copie para um programa que tenha main
    void insertion_sort(int v[], int n) {
        for (int i = 1; i < n; i++) {          // v[0..i-1] já está ordenado
            int chave = v[i];                   // 1) guarda a chave
            int j = i - 1;
            while (j >= 0 && v[j] > chave) {   // j >= 0 PRIMEIRO: evita ler v[-1]
                v[j + 1] = v[j];                // 2) desloca o maior para a direita
                j--;
            }
            v[j + 1] = chave;                   // 3) j+1 é o buraco livre
        }
    }

    Para usar: declare int v[] = {5, 2, 4, 1}; e chame insertion_sort(v, 4);. O resultado é [1, 2, 4, 5], com os 5 deslocamentos da figura.

    Armadilhas frequentes
    • Inverter as condições para v[j] > chave && j >= 0 lê v[-1] quando a chave é a menor de todas. O && avalia da esquerda para a direita e para no primeiro falso, então a ordem importa.
    • Esquecer de guardar a chave: o primeiro deslocamento sobrescreve v[i], e o valor some.
    • Inserir em v[j] em vez de v[j + 1]: quando o laço termina, j aponta para o elemento que não é maior que a chave, e o buraco está logo depois dele.
    • Usar >= no laço desloca também os iguais e perde a estabilidade, como no bubble sort.

    Exemplo real: a fila do pronto-socorro

    Em um pronto-socorro, os pacientes são atendidos por gravidade (de 1, leve, a 5, crítico). Com gravidade igual, quem chegou antes é atendido antes. Os pacientes não chegam todos de uma vez: a fila precisa estar correta a cada chegada.

    Não é preciso reordenar a fila inteira a cada paciente. A fila já está ordenada, então basta fazer uma única inserção, exatamente um passo do insertion sort:

    Programa completo: compile e execute
    #include <stdio.h>
    
    typedef struct {
        char nome[20];
        int gravidade;              // 1 (leve) a 5 (crítico)
    } Paciente;
    
    // Insere p na fila já ordenada: mais grave primeiro; empate, quem chegou antes.
    // Pré-condição: a fila tem espaço para mais um paciente.
    void chegar(Paciente fila[], int *n, Paciente p) {
        int j = *n - 1;
        while (j >= 0 && fila[j].gravidade < p.gravidade) {   // < estrito: estável
            fila[j + 1] = fila[j];                              // abre espaço
            j--;
        }
        fila[j + 1] = p;
        (*n)++;
    }
    
    int main(void) {
        Paciente fila[10];
        int n = 0;
        Paciente chegadas[] = {{"Ana", 2}, {"Bruno", 4}, {"Carla", 2}, {"Davi", 5}, {"Eva", 4}};
    
        for (int i = 0; i < 5; i++) {
            chegar(fila, &n, chegadas[i]);
            printf("Chegou %-5s (%d): ", chegadas[i].nome, chegadas[i].gravidade);
            for (int k = 0; k < n; k++) {
                printf("%s%s", fila[k].nome, k < n - 1 ? ", " : "\n");
            }
        }
        return 0;
    }
    Chegou Ana (2): Ana Chegou Bruno (4): Bruno, Ana Chegou Carla (2): Bruno, Ana, Carla Chegou Davi (5): Davi, Bruno, Ana, Carla Chegou Eva (4): Davi, Bruno, Eva, Ana, Carla

    Esse programa junta duas aulas anteriores com esta: struct para o paciente, ponteiro para que chegar atualize o tamanho n do chamador, e a inserção ordenada desta aula. Repare em Eva: ela tem a mesma gravidade de Bruno, mas chegou depois e ficou atrás dele. É o < estrito garantindo a estabilidade. Por que <, e não > como no insertion_sort? Porque aqui a fila vai do mais grave para o menos grave: o laço continua deslocando enquanto o paciente da fila for menos grave que o novo.

    Cada chegada custa no máximo um deslocamento por paciente já na fila, e normalmente bem menos. Reordenar tudo a cada chegada seria muito mais caro. Sempre que os dados chegam aos poucos e a lista precisa estar sempre pronta, a inserção ordenada é a ferramenta certa.

    Na vida real

    Bibliotecas reais usam algoritmos mais sofisticados, mas várias delas recorrem a variantes do insertion sort para trechos pequenos, de algumas dezenas de elementos. Por exemplo, o Timsort do Python (CPython) usa inserção binária para estender trechos curtos, e a ordenação de tipos primitivos do Java (OpenJDK) usa variantes de inserção em trechos pequenos. Os detalhes mudam de versão para versão. Nesse tamanho, a simplicidade dele vence os algoritmos “rápidos”.

    Quanto custa na prática?

    Medimos o insertion sort com quatro tipos de entrada e comparamos com o bubble sort da aula anterior nas mesmas listas:

    Medições reais com 50 000 elementos
    EntradaDeslocamentosInsertionBubble
    Já ordenada0< 0,1 ms< 0,1 ms
    Quase ordenada (cada item a até 10 posições do lugar)125 mil0,4 ms0,9 ms
    Aleatória625 milhões0,21 s2,9 s
    Ordem decrescente1,25 bilhão0,43 s1,4 s
    1. Quase ordenada é o ponto forte. Cada elemento só anda algumas casas, e o trabalho fica proporcional ao tamanho da lista. Nos testes, 1 milhão de elementos quase ordenados foram ordenados em cerca de 9 ms.
    2. Na lista aleatória, cerca de 14 vezes mais rápido que o bubble sort, com o mesmo número de “consertos”. As escritas pela metade e um laço interno que para assim que encontra o lugar da chave explicam umas 2 a 4 vezes. O resto vem do efeito visto na aula de bubble sort: em dados aleatórios, o processador erra muito ao tentar adivinhar o if da troca do bubble sort. Por isso, no bubble, a lista aleatória é mais lenta até que a decrescente.
    3. Na lista bagunçada e grande, continua lento. O custo cresce com o quadrado do tamanho, como no bubble sort. Nos testes, 1 milhão de elementos aleatórios levou cerca de 1 minuto e 53 segundos, contra uns 9 milissegundos para 1 milhão quase ordenado.

    E se a busca do lugar fosse binária? Aprofundamento

    O trecho à esquerda está ordenado, então dá para achar o lugar da chave com busca binária (aula de recursão), em vez de andar casa por casa. Mas atenção ao que ela economiza:

    Funções: copie para um programa que tenha main
    void insertion_binaria(int v[], int n) {
        for (int i = 1; i < n; i++) {
            int chave = v[i];
            int ini = 0, fim = i;               // procura em v[0..i-1]
            while (ini < fim) {
                int meio = ini + (fim - ini) / 2;
                if (v[meio] <= chave) ini = meio + 1;
                else fim = meio;
            }                                   // ini: primeira posição com valor > chave
            for (int j = i; j > ini; j--) v[j] = v[j - 1];   // deslocamentos: os mesmos!
            v[ini] = chave;
        }
    }
    Comparações medidas, 50 000 elementos, com as mesmas entradas da tabela anterior
    EntradaBusca casa a casaBusca binária
    Aleatória625 milhões711 mil
    Quase ordenada175 mil700 mil

    A busca binária reduz muito as comparações em listas bagunçadas, mas os deslocamentos continuam os mesmos, então o tempo continua crescendo com o quadrado. Em listas quase ordenadas, ela até piora: a busca casa a casa encontra o lugar, em média, em uns 3 ou 4 passos (2 ou 3 deslocamentos), enquanto a binária faz cerca de log₂ i comparações para um trecho de i elementos. Lembre: log₂ i é quantas vezes dá para dividir i ao meio até chegar a 1, ou seja, uns 16 para 50 mil e uns 20 para 1 milhão. Isso acontece mesmo com a chave já perto do lugar. As primeiras inserções são baratas, mas perto do fim de uma lista de 50 mil são umas 16 por elemento. Ela vale a pena quando comparar é caro, como ao comparar textos longos, e mover é barato.

    Comparação com as outras ordenações da revisão

    Comportamento das três ordenações elementares
    AlgoritmoLista já ordenadaLista bagunçadaMovimentos de dadosEstável
    Bubble (com parada)uma passagem, rápidoquadrático, lentomuitos: uma troca por inversãoSim
    Insertionuma passagem, rápidoquadrático; nas medições, o mais rápido dos trêsum deslocamento por inversão, mais barato que uma trocaSim
    Selection (próxima aula)quadrático, mesmo ordenadaquadrático, lentopoucos: no máximo n − 1 trocasNão

    Exercícios progressivos

    Use as figuras da aula como modelo: desenhe o vetor, a chave guardada ao lado e o buraco andando. A dica não entrega a resposta: ela indica a pergunta certa a fazer.

    1. A carta nova Básico

    Você tem as cartas [3, 6, 9] em ordem na mão e compra um 5. Mostre o vetor depois de cada atribuição do insertion sort, começando por [3, 6, 9, 5]. Quantos deslocamentos foram necessários, e em que posição o 5 terminou?

    Ver uma dica
    Siga a figura “Inserindo a carta 4”. O primeiro passo não altera o vetor: só guarda a chave. Depois, compare a chave com os elementos da direita para a esquerda. Em cada comparação, pergunte: “este é maior que a chave?”
    Conferir a resposta
    1. Guarda chave = 5. O vetor continua [3, 6, 9, 5].
    2. 9 > 5: desloca o 9, e o vetor fica [3, 6, 9, 9].
    3. 6 > 5: desloca o 6, e o vetor fica [3, 6, 6, 9].
    4. 3 não é maior que 5: para e insere a chave no buraco, a posição 1. O vetor fica [3, 5, 6, 9].

    Foram 2 deslocamentos, e o 5 terminou na posição 1. Os valores repetidos nos passos 2 e 3 não são um erro: é o buraco andando para a esquerda.

    2. O paciente mais grave Básico

    Um colega reescreveu a condição da função chegar como while (fila[j].gravidade < p.gravidade && j >= 0). A fila funciona em quase todos os testes, mas o programa às vezes trava ou corrompe dados quando chega um paciente mais grave que todos os da fila. Por quê?

    Ver uma dica
    Simule a chegada de um paciente com gravidade 5 em uma fila em que todos têm gravidade menor. Até que valor j desce? Na versão do colega, qual condição é avaliada primeiro quando j vale −1?
    Conferir a resposta

    Se o novo paciente é mais grave que todos, o laço desloca a fila inteira, e j chega a −1. Na versão original, j >= 0 é testado primeiro, dá falso, e o && nem avalia a segunda condição.

    Na versão do colega, a primeira condição avaliada é fila[-1].gravidade: uma leitura antes do início do vetor, em memória que pertence a outra coisa. Isso é comportamento indefinido. O programa pode travar, ler lixo e continuar deslocando para posições inválidas, ou funcionar por acaso. Por isso o bug aparece “às vezes”, justamente no caso mais importante da fila.

    Regra: em a && b, coloque em a o teste que torna b seguro.

    3. Contando o trabalho Intermediário

    Um sistema embarcado grava os dados em memória flash, que se desgasta a cada escrita. O vetor [4, 3, 2] precisa ser ordenado. Quantos deslocamentos o insertion sort faz? Quantas escritas no vetor ele faz no total, contando também a inserção da chave? E quantas escritas no vetor faz o bubble sort, lembrando que cada troca escreve em duas posições?

    Ver uma dica
    Conte as inversões primeiro: elas dão o número de deslocamentos e de trocas. Depois, lembre que o insertion sort faz uma escrita extra por etapa, ao colocar a chave no buraco. Quantas etapas há para 3 elementos?
    Conferir a resposta

    Inversões: (4,3), (4,2) e (3,2), ou seja, 3. Então são 3 deslocamentos.

    • Inserir 3: desloca o 4 (1 escrita) e insere o 3 (1 escrita), resultando em [3, 4, 2].
    • Inserir 2: desloca o 4 e o 3 (2 escritas) e insere o 2 (1 escrita), resultando em [2, 3, 4].

    Total do insertion sort: 5 escritas no vetor. O bubble sort faz as mesmas 3 correções, mas como trocas, e cada troca escreve em 2 posições do vetor: 3 × 2 = 6 escritas no vetor. (Contando também a variável temporária temp, seriam 9 atribuições, mas temp não fica na memória flash.) Em memória que se desgasta, isso faz diferença real. A próxima aula mostra um algoritmo que escreve ainda menos.

    4. Espiando no meio do caminho Intermediário

    Um aluno pausou o depurador no meio do insertion sort de [3, 4, 5, 1], logo depois da etapa i = 2. Ele vê [3, 4, 5, 1] e conclui: “os três primeiros já estão nas posições finais”. Ele está certo? Compare com o que aconteceria no bubble sort.

    Ver uma dica
    Simule a última etapa, com chave = 1. Para onde vão o 3, o 4 e o 5? Depois, lembre o que fica garantido ao final de cada passagem do bubble sort.
    Conferir a resposta

    Ele está errado. Na etapa i = 3, a chave 1 é menor que todos, então 5, 4 e 3 se deslocam uma posição para a direita, e o vetor fica [1, 3, 4, 5]. Nenhum dos três estava no lugar final.

    O insertion sort garante que o trecho processado está em ordem, mas não no lugar definitivo. O bubble sort garante o contrário para o fim do vetor: após cada passagem, o maior valor restante está no lugar definitivo. Saber exatamente o que cada algoritmo garante é o que permite prever o estado do vetor em qualquer ponto da execução.

    5. Agenda de contatos Avançado

    Um aplicativo de agenda guarda os nomes como um vetor de ponteiros para texto: const char *nomes[] = {"Carla", "Ana", "Bruno", "Ana Clara"};. Escreva void ordenar_nomes(const char *nomes[], int n) usando o insertion sort, para deixar os nomes em ordem alfabética. A saída esperada é Ana, Ana Clara, Bruno, Carla.

    Ver uma dica
    Quem faz o papel do > quando os valores são textos? (Reveja a aula de Struct.) E o que a chave guarda: o texto inteiro ou só o endereço dele? Pense no que é mais barato mover: um ponteiro de 8 bytes ou um nome inteiro.
    Conferir a resposta
    Funções: copie para um programa que tenha main
    #include <string.h>
    
    void ordenar_nomes(const char *nomes[], int n) {
        for (int i = 1; i < n; i++) {
            const char *chave = nomes[i];                  // guarda o ENDEREÇO do nome
            int j = i - 1;
            while (j >= 0 && strcmp(nomes[j], chave) > 0) {   // nomes[j] vem depois?
                nomes[j + 1] = nomes[j];                   // desloca só o ponteiro
                j--;
            }
            nomes[j + 1] = chave;
        }
    }

    strcmp(a, b) > 0 é o “a é maior que b” para textos: verdadeiro quando a vem depois de b no alfabeto. “Ana” vem antes de “Ana Clara” porque é um prefixo dela.

    O detalhe mais importante: o algoritmo nunca copia os textos. Ele só reorganiza os ponteiros, que têm sizeof(const char *) bytes cada (8 em um sistema de 64 bits), não importa o tamanho do nome. Com nomes longos ou registros grandes, ordenar um vetor de ponteiros é muito mais barato que mover os dados. É a mesma técnica que a função qsort da biblioteca padrão permite usar.

    ./desafio --nivel=hacker

    Desafio hacker: o sensor quase ordenado

    • Tipo: opcional
    • Dificuldade: ★★★★
    • Tema: análise de algoritmos
    • Escala: 1 milhão de leituras, k ≤ 10

    Um sensor envia 1 milhão de leituras com marca de tempo. A rede atrasa alguns pacotes, mas nenhuma leitura chega mais de k posições longe de onde deveria estar. Esse vetor é chamado de k-ordenado. Considere as marcas de tempo todas diferentes: assim, a posição final de cada leitura é única.

    Um colega diz que o insertion sort é lento demais para 1 milhão de elementos e quer implementar algo sofisticado. Você responde: “o insertion sort resolve isso em milissegundos”.

    1. Explique por que, em um vetor k-ordenado, cada inserção faz no máximo k deslocamentos.
    2. Estime o trabalho total para 1 milhão de leituras com k = 10 e compare com um vetor aleatório do mesmo tamanho.
    3. Outro colega sugere usar a inserção binária para acelerar. Ela ajuda neste caso?
    Exemplo, k = 2[2, 0, 1, 4, 3, 5]
    Deslocamentos por inserção1 + 1 + 0 + 1 + 0 = 3
    Dica 1: o que conta um deslocamento?

    Ao inserir um elemento x, o número de deslocamentos é a quantidade de elementos que estão antes de x e são maiores que x. A pergunta vira: quantos elementos maiores podem estar antes de x em um vetor k-ordenado?

    Dica 2: quem é obrigado a estar antes de x?

    Suponha que x esteja na posição 10 e que k = 3. Pense nos valores que, na lista ordenada, pertencem às posições 0 a 6. Eles podem estar no máximo 3 casas longe do seu lugar. Onde eles podem estar? São maiores ou menores que x?

    Solução comentada

    1. Por que no máximo k. Comece pelo exemplo pequeno, com k = 2: [2, 0, 1, 4, 3, 5]. Ao inserir o 3, que está na posição p = 4, há 4 elementos antes dele. Os valores que pertencem às posições 0 e 1 da lista ordenada (o 0 e o 1) não podem estar a mais de 2 casas do lugar, então com certeza já estão antes do 3, e são menores que ele. Sobram só 2 lugares (k = 2) onde pode haver alguém maior. De fato, só o 4 é maior: 1 deslocamento. A figura mostra o mesmo argumento com x na posição p = 10 e k = 3:

    Por que, em um vetor k-ordenado, no máximo k elementos maiores ficam antes de x Um elemento x está na posição 10, com k igual a 3. Os 7 valores que pertencem às posições 0 a 6 da lista ordenada não podem estar a mais de k casas do seu lugar, então já estão antes de x, e são menores que ele. Como o trecho antes de x já foi ordenado, eles ocupam as posições 0 a 6 desse trecho. Sobram apenas as 3 posições 7, 8 e 9 onde pode haver valores maiores que x. Por isso, inserir x desloca no máximo 3 elementos. 0123456789x111213 p − k = 7 posiçõesno trecho já ordenado: menores que x k = 3 posiçõesúnicas que podem ser maiores posição painda não processados Um valor que pertence a uma posição antes de 7 não pode estar mais de k = 3 casas longe dela,então ele já está em algum lugar antes de x. Isso ocupa 7 das 10 posições anteriores a x.Resultado: ao inserir x, no máximo k = 3 elementos são deslocados.
    Das 10 posições antes de x, 7 estão obrigatoriamente ocupadas por valores menores. Só sobram 3, ou seja, k, onde pode haver alguém maior, e só esses são deslocados.

    Em geral, os valores que pertencem às primeiras p − k posições da lista ordenada estão no máximo a k casas do seu lugar. Portanto, já estão antes da posição p, e são todos menores que x, porque x pertence à posição p − k ou depois dela. Eles ocupam p − k das p posições anteriores a x. Sobram no máximo k posições onde pode haver um elemento maior que x. Logo, cada inserção desloca no máximo k elementos, e o total é no máximo k × n.

    2. Os números. Com 1 milhão de leituras e k = 10, são no máximo 10 milhões de deslocamentos. No teste desta aula, foram 2,5 milhões, em cerca de 9 ms na máquina descrita em “Como medimos”. Um vetor aleatório do mesmo tamanho tem cerca de 250 bilhões de inversões, umas 100 mil vezes mais trabalho: nos testes, cerca de 1 minuto e 53 segundos.

    Funções: copie para um programa que tenha main
    // Insertion sort instrumentado: devolve o número de deslocamentos.
    long long insertion_contando(int v[], int n) {
        long long desl = 0;
        for (int i = 1; i < n; i++) {
            int chave = v[i], j = i - 1;
            while (j >= 0 && v[j] > chave) {
                v[j + 1] = v[j];
                j--;
                desl++;
            }
            v[j + 1] = chave;
        }
        return desl;
    }

    Para testar, gere vetores k-ordenados embaralhando blocos de k + 1 elementos consecutivos de uma lista ordenada. Assim, nenhum elemento sai mais de k casas do lugar. Confira que insertion_contando nunca passa de k × n.

    3. A inserção binária piora. Perto do fim de uma lista de 1 milhão, ela faz cerca de 20 comparações por elemento (log₂ do tamanho do trecho já ordenado), mesmo quando a chave está a duas casas do lugar certo. A busca casa a casa, da direita para a esquerda, faz no máximo k + 1 = 11. Nas medições desta aula, com 50 mil elementos quase ordenados, a versão binária fez 4 vezes mais comparações. Os deslocamentos são idênticos nas duas. A lição: o melhor algoritmo depende da forma dos dados, e não só do pior caso.

    Indo além: para k grande (centenas ou milhares), existe uma técnica com uma estrutura chamada heap que ordena vetores k-ordenados ainda mais depressa. Ela aparece em cursos de estruturas de dados.

    Revisão rápida

    • Guardar a chave, deslocar os maiores e inserir no buraco.
    • O trecho processado fica em ordem, mas ainda pode mudar de lugar.
    • Deslocamentos = inversões, e cada deslocamento custa uma escrita no vetor, contra duas de uma troca.
    • j >= 0 vem antes de v[j] > chave. O > estrito mantém a estabilidade.
    • Uma única inserção mantém uma lista ordenada enquanto os dados chegam, como na fila do pronto-socorro.
    • É geralmente a melhor escolha entre os três para listas pequenas ou quase ordenadas: nos testes, 1 milhão de itens quase ordenados em cerca de 9 ms.