Algoritmos Recursivos em Listas (Merge Sort)

filósofo

O Merge Sort é um algoritmo clássico de ordenação baseado no paradigma “dividir para conquistar”. Em Prolog, sua implementação é particularmente elegante devido à natureza recursiva das listas e ao poder da unificação. Vamos construir o algoritmo passo a passo.

1. O Paradigma “Dividir para Conquistar”

O Merge Sort segue três passos fundamentais:

  1. Distribuir (Dividir): A lista original é dividida em duas sublistas aproximadamente iguais.
  2. Ordenar recursivamente (Conquistar): Cada sublista é ordenada recursivamente usando o mesmo algoritmo.
  3. Intercalar (Combinar): As duas sublistas ordenadas são mescladas em uma única lista ordenada.

2. Passo 1: Distribuir — distribui/3 (Programa 5.4)

O predicado distribui/3 divide uma lista em duas, alternando os elementos entre elas.

Explicação Passo-a-Passo

  • distribui([], [], []): Se a lista está vazia, ambas as sublistas estão vazias.
  • distribui([X], [X], []): Se a lista tem um elemento, ele vai para a primeira lista.
  • distribui([X,Y|Z], [X|A], [Y|B]): O primeiro elemento X vai para A, o segundo Y vai para B, e a recursão distribui o restante Z.

Exemplo de Distribuição

3. Passo 2: Intercalar — intercala/3

O predicado intercala/3 mescla duas listas ordenadas em uma única lista ordenada.

Explicação Passo-a-Passo

  • intercala([], B, B): Se A está vazia, o resultado é B.
  • intercala(A, [], A): Se B está vazia, o resultado é A.
  • intercala([X|A], [Y|B], [X|C]): Se X ≤ Y, X é colocado na frente e a intercalação continua com A e [Y|B].
  • intercala([X|A], [Y|B], [Y|C]): Se X > Y, Y é colocado na frente e a intercalação continua com [X|A] e B.

Exemplo de Intercalação

4. Passo 3: Ordenar — ordena/2

O predicado ordena/2 combina distribuição, ordenação recursiva e intercalação.

5. Fluxo de Execução: ordena([3,5,0,4,1,2], S)

Vamos acompanhar a execução passo a passo:

Consulta Completa

6. Árvore de Recursão do Merge Sort

Visualize a árvore de recursão para ordena([3,5,0,4,1,2], S):

7. Análise de Complexidade

O Merge Sort tem complexidade:

  • Tempo: O(n log n) no pior caso, médio e melhor caso.
  • Espaço: O(n) para armazenar as sublistas durante a intercalação.

Em Prolog, a implementação é particularmente elegante porque:

  • A recursão natural do Prolog se alinha perfeitamente com a estrutura do algoritmo.
  • A unificação simplifica a distribuição e intercalação.
  • O código é declarativo e fácil de ler.

8. Exercícios Propostos

  1. Insertion Sort: Implemente o algoritmo Insertion Sort em Prolog.
    Dica: Use insere/3 para inserir um elemento em uma lista ordenada.
  2. Quick Sort: Implemente o Quick Sort em Prolog.
    Dica: Use um pivô (ex: o primeiro elemento) e divida a lista em menores e maiores.
  3. Bubble Sort: Implemente o Bubble Sort em Prolog.
    Dica: Use troca_uma_vez/2 e repita até a lista ficar ordenada.
  4. Selection Sort: Implemente o Selection Sort.
    Dica: Encontre o menor elemento, remova-o e ordene o resto.
  5. Comparação: Teste os diferentes algoritmos com listas de diferentes tamanhos e compare a eficiência em termos de tempo (use time/1).

9. Conclusão

O Merge Sort é um exemplo perfeito de como o Prolog pode expressar algoritmos complexos de forma elegante e declarativa. Com poucas linhas de código, implementamos um algoritmo eficiente de ordenação que é:

  • Correto: A recursão garante que todas as partes sejam ordenadas.
  • Eficiente: O(log n) para divisão e O(n) para intercalação.
  • Legível: Cada passo do algoritmo é claramente expresso em Prolog.

Dominar o Merge Sort em Prolog é um excelente exercício para entender como a recursão, a unificação e a divisão de listas se combinam para criar soluções poderosas.

Predicados Clássicos para Listas

filósofo

As listas são a estrutura de dados mais importante em Prolog. Para manipulá-las, utilizamos predicados recursivos que exploram a natureza recursiva das listas. Neste artigo, vamos estudar os predicados mais fundamentais: membro/2, anexa/3, e alguns outros como ultimo/2, tam/2 e soma/2.

1. O Predicado membro/2 (Programa 5.2)

O predicado membro/2 verifica se um elemento pertence a uma lista. Ele tem duas cláusulas:

Explicação Passo-a-Passo

  1. Primeira cláusula: membro(X, [X|_]). — X é membro se ele é a cabeça da lista. A variável anônima _ ignora a cauda.
  2. Segunda cláusula: membro(X, [_|Y]) :- membro(X, Y). — X é membro se ele está na cauda. Ignoramos a cabeça com _ e chamamos membro recursivamente na cauda Y.

A recursão percorre a lista elemento por elemento até encontrar X ou chegar ao fim da lista.

Usos de membro/2

1. Testar pertinência:

2. Gerar elementos da lista:

3. Verificar se uma lista contém todos os elementos de outra:

2. O Predicado anexa/3 (Programa 5.3)

O predicado anexa/3 (também conhecido como append/3) concatena duas listas. Ele tem dois casos:

Explicação Passo-a-Passo

  1. Caso base: anexa([], B, B). — Se a primeira lista é vazia, o resultado é a segunda lista.
  2. Passo recursivo: anexa([X|A], B, [X|C]) :- anexa(A, B, C). — Remove a cabeça X da primeira lista, concatena A com B recursivamente (resultando em C), e depois coloca X de volta na frente.

Usos de anexa/3

1. Concatenar duas listas:

2. Dividir uma lista (encontrar prefixos):

3. Encontrar o último elemento (via divisão):

4. Remover um elemento (encontrando o que está antes e depois):

3. Exercícios do PDF (5.1, 5.2, 5.3)

Exercício 5.1: último/2

Problema: Encontrar o último elemento de uma lista.

Como funciona: A recursão percorre a lista até chegar ao último elemento, que é retornado.

Exercício 5.2: tam/2

Problema: Calcular o tamanho (comprimento) de uma lista.

Como funciona: A cada chamada recursiva, removemos a cabeça da lista (ignorando-a com _) e adicionamos 1 ao resultado da chamada recursiva.

Exercício 5.3: soma/2

Problema: Calcular a soma dos elementos de uma lista de números.

Como funciona: Percorre a lista recursivamente, somando a cabeça ao resultado da cauda.

4. Recursão e Listas: O Padrão Fundamental

Todos os predicados que vimos seguem o mesmo padrão:

Esse padrão é universal em Prolog para manipulação de listas. A recursão percorre a lista até o fim, processando cada elemento (ou acumulando resultados) e depois retorna o resultado combinado.

5. Exercícios Adicionais

Pratique com estes desafios:

  1. maximo/2: Encontre o maior elemento de uma lista de números.
    Dica: Caso base: lista com um elemento. Passo: compare a cabeça com o máximo da cauda.
  2. minimo/2: Encontre o menor elemento de uma lista de números.
  3. remover/3: remover(X, L, R) — R é a lista L sem a primeira ocorrência de X.
    Dica: Use anexa/3 para encontrar X e construir o resultado.
  4. remover_todos/3: Remova todas as ocorrências de X da lista L.
  5. inverter/2: inverter(L, R) — R é a lista L invertida.
    Dica: Use anexa/3 para construir a lista invertida.
  6. pertence_todos/2: pertence_todos(Xs, Ys) — todos os elementos de Xs estão em Ys.

6. Conclusão

As listas são a estrutura de dados fundamental em Prolog. Através da notação cabeça-cauda [X|Y] e da recursão, podemos implementar uma vasta gama de operações:

  • Busca: membro/2
  • Combinação: anexa/3
  • Medidas: tam/2, soma/2, ultimo/2
  • Transformações: inverter/2, remover/3

Dominar esses predicados fundamentais é o primeiro passo para se tornar um programador Prolog proficiente. Pratique com os exercícios e explore como a recursão pode ser aplicada a problemas cada vez mais complexos.