A Distancia de Levenshtein (ou distancia de edicao) mede o numero minimo de operacoes de edicao necessarias para transformar uma string em outra. As operacoes permitidas sao: insercao, remocao e substituicao de um unico caractere. Esse conceito, proposto por Vladimir Levenshtein em 1965, e a base de inumeras aplicacoes praticas: correcao ortografica, busca fuzzy (aproximada), alinhamento de sequencias de DNA, deteccao de plagio e sistemas de autocompletar.
Embora o problema possa ser resolvido recursivamente, a solucao eficiente usa programacao dinamica, preenchendo uma tabela que armazena as distancias entre todos os prefixos das duas strings. A variante Damerau-Levenshtein adiciona uma quarta operacao – transposicao de caracteres adjacentes – que modela melhor os erros de digitacao humanos.
Como Funciona
A Tabela de Programacao Dinamica
Para calcular a distancia entre “gato” e “pato”, construimos uma tabela onde dp[i][j] e a distancia entre os primeiros i caracteres da string de origem e os primeiros j caracteres da string de destino:
"" p a t o
"" 0 1 2 3 4
g 1 1 2 3 4
a 2 2 1 2 3
t 3 3 2 1 2
o 4 4 3 2 1
Resposta: dp[4][4] = 1 (apenas trocar 'g' por 'p')
Regras de Preenchimento
Para cada celula dp[i][j]:
Se s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] (caracteres iguais, sem custo)
Senao:
dp[i][j] = 1 + min(
dp[i-1][j], // remocao (remove s1[i-1])
dp[i][j-1], // insercao (insere s2[j-1])
dp[i-1][j-1] // substituicao (troca s1[i-1] por s2[j-1])
)
Trace Detalhado: “kitten” -> “sitting”
"" s i t t i n g
"" 0 1 2 3 4 5 6 7
k 1 1 2 3 4 5 6 7
i 2 2 1 2 3 4 5 6
t 3 3 2 1 2 3 4 5
t 4 4 3 2 1 2 3 4
e 5 5 4 3 2 2 3 4
n 6 6 5 4 3 3 2 3
Distancia = dp[6][7] = 3
Operacoes:
1. Substituir 'k' por 's': "kitten" -> "sitten"
2. Substituir 'e' por 'i': "sitten" -> "sittin"
3. Inserir 'g' no final: "sittin" -> "sitting"
Caminho na tabela (traceback):
dp[6][7]=3 <- dp[5][6]=3 (insercao de 'g')
dp[5][6]=3 <- dp[4][5]=2 (substituicao 'e'->'i')
dp[4][5]=2 <- dp[3][4]=1 (diagonal, 't'=='t')
dp[3][4]=1 <- dp[2][3]=2? Nao, dp[3][4]=1 <- dp[2][3]=2 nao funciona.
Melhor: dp[3][4]=1 <- dp[2][3]=2... (substituicao? nao, t==t)
dp[3][3]=1 <- dp[2][2]=1 (diagonal, 't'=='t')
dp[2][2]=1 <- dp[1][1]=1 (diagonal, 'i'=='i')
dp[1][1]=1 <- dp[0][0]=0 (substituicao 'k'->'s')
Complexidade
| Variante | Tempo | Espaco |
|---|---|---|
| Tabela completa | O(n*m) | O(n*m) |
| Espaco otimizado | O(n*m) | O(min(n,m)) |
| Damerau-Levenshtein | O(n*m) | O(n*m) |
| Com limiar (threshold) | O(n*k) | O(k) |
Onde n e m sao os comprimentos das strings e k e o limiar maximo de distancia.
Implementacao em Rust
/// Calcula a distancia de Levenshtein entre duas strings.
/// Usa espaco otimizado O(min(n,m)) mantendo apenas duas linhas da tabela.
fn levenshtein(s1: &str, s2: &str) -> usize {
let s1: Vec<char> = s1.chars().collect();
let s2: Vec<char> = s2.chars().collect();
let n = s1.len();
let m = s2.len();
// Otimizacao: usa a string menor como coluna
if n < m {
return levenshtein_chars(&s2, &s1);
}
levenshtein_chars(&s1, &s2)
}
fn levenshtein_chars(s1: &[char], s2: &[char]) -> usize {
let m = s2.len();
// Apenas duas linhas da tabela DP
let mut anterior: Vec<usize> = (0..=m).collect();
let mut atual = vec![0usize; m + 1];
for i in 1..=s1.len() {
atual[0] = i;
for j in 1..=m {
let custo_substituicao = if s1[i - 1] == s2[j - 1] { 0 } else { 1 };
atual[j] = (anterior[j] + 1) // remocao
.min(atual[j - 1] + 1) // insercao
.min(anterior[j - 1] + custo_substituicao); // substituicao
}
std::mem::swap(&mut anterior, &mut atual);
}
anterior[m]
}
/// Calcula a distancia e tambem recupera as operacoes de edicao.
fn levenshtein_com_operacoes(s1: &str, s2: &str) -> (usize, Vec<String>) {
let s1: Vec<char> = s1.chars().collect();
let s2: Vec<char> = s2.chars().collect();
let n = s1.len();
let m = s2.len();
// Tabela completa para traceback
let mut dp = vec![vec![0usize; m + 1]; n + 1];
// Inicializacao
for i in 0..=n {
dp[i][0] = i;
}
for j in 0..=m {
dp[0][j] = j;
}
// Preenchimento
for i in 1..=n {
for j in 1..=m {
let custo = if s1[i - 1] == s2[j - 1] { 0 } else { 1 };
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + custo);
}
}
// Traceback: recupera as operacoes
let mut operacoes = Vec::new();
let mut i = n;
let mut j = m;
while i > 0 || j > 0 {
if i > 0 && j > 0 && s1[i - 1] == s2[j - 1] {
// Caracteres iguais, sem operacao
i -= 1;
j -= 1;
} else if i > 0 && j > 0 && dp[i][j] == dp[i - 1][j - 1] + 1 {
operacoes.push(format!(
"Substituir '{}' por '{}' na posicao {}",
s1[i - 1],
s2[j - 1],
i - 1
));
i -= 1;
j -= 1;
} else if j > 0 && dp[i][j] == dp[i][j - 1] + 1 {
operacoes.push(format!(
"Inserir '{}' na posicao {}",
s2[j - 1],
i
));
j -= 1;
} else if i > 0 && dp[i][j] == dp[i - 1][j] + 1 {
operacoes.push(format!(
"Remover '{}' da posicao {}",
s1[i - 1],
i - 1
));
i -= 1;
} else {
break;
}
}
operacoes.reverse();
(dp[n][m], operacoes)
}
fn main() {
// Exemplo basico
let s1 = "kitten";
let s2 = "sitting";
let distancia = levenshtein(s1, s2);
println!("Distancia entre \"{}\" e \"{}\": {}", s1, s2, distancia);
println!();
// Com operacoes detalhadas
let (dist, ops) = levenshtein_com_operacoes(s1, s2);
println!("Operacoes ({} edicoes):", dist);
for op in &ops {
println!(" {}", op);
}
println!();
// Mais exemplos
let pares = vec![
("gato", "pato"),
("rust", "trust"),
("algoritmo", "logaritmo"),
("", "abc"),
("abc", "abc"),
];
println!("Tabela de distancias:");
for (a, b) in &pares {
println!(" \"{}\" -> \"{}\" = {}", a, b, levenshtein(a, b));
}
}
Exemplo de Execucao
Distancia entre "kitten" e "sitting": 3
Operacoes (3 edicoes):
Substituir 'k' por 's' na posicao 0
Substituir 'e' por 'i' na posicao 4
Inserir 'g' na posicao 6
Tabela de distancias:
"gato" -> "pato" = 1
"rust" -> "trust" = 1
"algoritmo" -> "logaritmo" = 3
"" -> "abc" = 3
"abc" -> "abc" = 0
Variacoes e Otimizacoes
1. Damerau-Levenshtein (com Transposicoes)
A variante Damerau-Levenshtein adiciona transposicao de caracteres adjacentes, modelando melhor erros de digitacao:
/// Distancia de Damerau-Levenshtein: permite insercao, remocao,
/// substituicao e transposicao de caracteres adjacentes.
fn damerau_levenshtein(s1: &str, s2: &str) -> usize {
let s1: Vec<char> = s1.chars().collect();
let s2: Vec<char> = s2.chars().collect();
let n = s1.len();
let m = s2.len();
if n == 0 {
return m;
}
if m == 0 {
return n;
}
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for i in 0..=n {
dp[i][0] = i;
}
for j in 0..=m {
dp[0][j] = j;
}
for i in 1..=n {
for j in 1..=m {
let custo = if s1[i - 1] == s2[j - 1] { 0 } else { 1 };
dp[i][j] = (dp[i - 1][j] + 1) // remocao
.min(dp[i][j - 1] + 1) // insercao
.min(dp[i - 1][j - 1] + custo); // substituicao
// Transposicao de caracteres adjacentes
if i > 1
&& j > 1
&& s1[i - 1] == s2[j - 2]
&& s1[i - 2] == s2[j - 1]
{
dp[i][j] = dp[i][j].min(dp[i - 2][j - 2] + 1);
}
}
}
dp[n][m]
}
fn main() {
// Transposicao: "ab" -> "ba" = 1 (em vez de 2 no Levenshtein puro)
println!(
"Levenshtein(\"ab\", \"ba\") = {}",
levenshtein("ab", "ba")
); // 2
println!(
"Damerau-Levenshtein(\"ab\", \"ba\") = {}",
damerau_levenshtein("ab", "ba")
); // 1
// Erro de digitacao tipico
println!(
"Damerau(\"teh\", \"the\") = {}",
damerau_levenshtein("teh", "the")
); // 1
}
2. Distancia com Limiar (Threshold)
Quando voce so precisa saber se a distancia e menor que um limite, pode cortar cedo:
/// Calcula a distancia de Levenshtein, mas retorna None se exceder o limiar.
/// Mais eficiente quando o limiar e pequeno.
fn levenshtein_com_limiar(s1: &str, s2: &str, limiar: usize) -> Option<usize> {
let s1: Vec<char> = s1.chars().collect();
let s2: Vec<char> = s2.chars().collect();
let n = s1.len();
let m = s2.len();
// Se a diferenca de comprimento ja excede o limiar
if n.abs_diff(m) > limiar {
return None;
}
let mut anterior: Vec<usize> = (0..=m).collect();
let mut atual = vec![0usize; m + 1];
for i in 1..=n {
atual[0] = i;
let mut min_na_linha = i;
for j in 1..=m {
let custo = if s1[i - 1] == s2[j - 1] { 0 } else { 1 };
atual[j] = (anterior[j] + 1)
.min(atual[j - 1] + 1)
.min(anterior[j - 1] + custo);
min_na_linha = min_na_linha.min(atual[j]);
}
// Se o minimo na linha inteira excede o limiar, impossivel obter resultado
if min_na_linha > limiar {
return None;
}
std::mem::swap(&mut anterior, &mut atual);
}
if anterior[m] <= limiar {
Some(anterior[m])
} else {
None
}
}
3. Operacoes com Pesos Customizados
/// Distancia de edicao com pesos customizados para cada operacao.
struct PesosEdicao {
insercao: usize,
remocao: usize,
substituicao: usize,
}
fn levenshtein_ponderado(s1: &str, s2: &str, pesos: &PesosEdicao) -> usize {
let s1: Vec<char> = s1.chars().collect();
let s2: Vec<char> = s2.chars().collect();
let n = s1.len();
let m = s2.len();
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for i in 1..=n {
dp[i][0] = dp[i - 1][0] + pesos.remocao;
}
for j in 1..=m {
dp[0][j] = dp[0][j - 1] + pesos.insercao;
}
for i in 1..=n {
for j in 1..=m {
if s1[i - 1] == s2[j - 1] {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = (dp[i - 1][j] + pesos.remocao)
.min(dp[i][j - 1] + pesos.insercao)
.min(dp[i - 1][j - 1] + pesos.substituicao);
}
}
}
dp[n][m]
}
Aplicacoes Praticas
Corretor Ortografico Simples
/// Corretor ortografico que sugere as palavras mais proximas do dicionario.
fn sugerir_correcoes(palavra: &str, dicionario: &[&str], max_distancia: usize) -> Vec<(String, usize)> {
let mut sugestoes: Vec<(String, usize)> = Vec::new();
for &palavra_dict in dicionario {
if let Some(dist) = levenshtein_com_limiar(palavra, palavra_dict, max_distancia) {
sugestoes.push((palavra_dict.to_string(), dist));
}
}
// Ordena por distancia (menor primeiro)
sugestoes.sort_by_key(|s| s.1);
sugestoes
}
fn main() {
let dicionario = vec![
"rust", "ruby", "python", "java", "javascript",
"rust", "rosto", "rusto", "justo", "custo",
"lista", "pista", "vista", "mista", "fusta",
];
let palavra = "rustp";
let sugestoes = sugerir_correcoes(palavra, &dicionario, 2);
println!("Correcoes para \"{}\":", palavra);
for (sugestao, dist) in &sugestoes {
println!(" \"{}\" (distancia: {})", sugestao, dist);
}
}
Busca Fuzzy em Lista de Nomes
/// Busca fuzzy: encontra os itens mais similares a consulta.
fn busca_fuzzy<'a>(consulta: &str, itens: &[&'a str], max_resultados: usize) -> Vec<(&'a str, usize)> {
let mut resultados: Vec<(&str, usize)> = itens
.iter()
.map(|&item| {
let dist = levenshtein(
&consulta.to_lowercase(),
&item.to_lowercase(),
);
(item, dist)
})
.collect();
resultados.sort_by_key(|r| r.1);
resultados.truncate(max_resultados);
resultados
}
fn main() {
let cidades = vec![
"Sao Paulo", "Rio de Janeiro", "Belo Horizonte",
"Salvador", "Brasilia", "Curitiba", "Porto Alegre",
"Fortaleza", "Recife", "Manaus",
];
let consulta = "Sao Polo";
let resultados = busca_fuzzy(consulta, &cidades, 3);
println!("Busca fuzzy por \"{}\":", consulta);
for (cidade, dist) in &resultados {
println!(" \"{}\" (distancia: {})", cidade, dist);
}
}
Similaridade de Sequencias (DNA)
/// Calcula a similaridade entre duas sequencias como porcentagem.
fn similaridade(s1: &str, s2: &str) -> f64 {
let dist = levenshtein(s1, s2);
let max_len = s1.len().max(s2.len());
if max_len == 0 {
return 100.0;
}
(1.0 - dist as f64 / max_len as f64) * 100.0
}
fn main() {
let seq1 = "ATCGATCG";
let seq2 = "ATCAATCG";
let dist = levenshtein(seq1, seq2);
let sim = similaridade(seq1, seq2);
println!("Sequencia 1: {}", seq1);
println!("Sequencia 2: {}", seq2);
println!("Distancia de edicao: {}", dist);
println!("Similaridade: {:.1}%", sim);
// Visualizacao do alinhamento
println!();
println!("Alinhamento:");
println!(" {}", seq1);
print!(" ");
for (c1, c2) in seq1.chars().zip(seq2.chars()) {
if c1 == c2 {
print!("|");
} else {
print!("x");
}
}
println!();
println!(" {}", seq2);
}
Comparacao com Outros Algoritmos
| Metrica | Operacoes | Tempo | Uso Tipico |
|---|---|---|---|
| Levenshtein | Ins, Rem, Sub | O(n*m) | Correcao ortografica |
| Damerau-Levenshtein | Ins, Rem, Sub, Trans | O(n*m) | Erros de digitacao |
| Hamming | Apenas substituicao | O(n) | Strings mesmo tamanho |
| LCS (subseq. comum) | Ins, Rem (sem sub) | O(n*m) | Diff de arquivos |
| Jaro-Winkler | Transposicoes | O(n*m) | Nomes proprios |
| Needleman-Wunsch | Ins, Rem, Sub (ponderado) | O(n*m) | Alinhamento de DNA |
A Distancia de Levenshtein e a metrica de edicao mais utilizada e serve como base para variantes mais especializadas. Para strings curtas (nomes, palavras), Jaro-Winkler pode ser mais adequado. Para alinhamento biologico, Needleman-Wunsch com matrizes de substituicao (BLOSUM, PAM) e preferido.
Exercicios Praticos
Autocompletar fuzzy: Combine a Distancia de Levenshtein com uma Trie para implementar autocompletar que tolera erros de digitacao. Para cada no da Trie, calcule a distancia ate a consulta e pode os ramos que excedem o limiar.
Diff de linhas: Implemente um diff simplificado (similar ao
diffdo Unix) que mostra insercoes e remocoes entre duas versoes de um arquivo. Use a LCS (Longest Common Subsequence) derivada da tabela de Levenshtein.Agrupamento por similaridade: Dado um conjunto de palavras, agrupe-as por similaridade (distancia <= k). Implemente usando uma matriz de distancias e um algoritmo de agrupamento simples.
Teclado QWERTY ponderado: Modifique a funcao de substituicao para considerar a distancia entre teclas no teclado QWERTY. Teclas adjacentes (como ‘r’ e ’t’) devem ter custo de substituicao menor que teclas distantes (como ‘r’ e ‘p’).
Veja Tambem
- String na Biblioteca Padrao – manipulacao de strings em Rust
- Vec: Vetores Dinamicos – armazenamento da tabela DP
- Tipos Numericos – tipos usados para custos e distancias
- Trie (Arvore de Prefixos) – combine com Levenshtein para autocompletar fuzzy
- Algoritmo KMP – busca exata de padroes (distancia = 0)
- Hashing de Strings – alternativa para comparacao rapida de strings