Estrutura de dados usando Typescript
Do inglês Trie ou prefix tree, uma árvore de prefixos, ou também uma árvore digital, é uma estrutura de dados usada para implementar Tabelas de Símbolos de strings. Muito útil para buscas de sequências de caracteres por prefixos, como nomes de pessoas, palavras de um livro e DNA.
É ainda conhecida como R-way trie (RT), cada nó pode ter R filhos. Onde o alfabeto das chaves tem R caracteres e que R = 256 (às vezes só 26, mas não 65536).
Os caracteres das chaves podem ser examinados um a um. Coisa que não faz sentido com chaves de tipo genérico
Cada chave tem um comprimento (número de caracteres).
O alfabeto (conjunto de caracteres) das chaves é conhecido a priori.
O desempenho de uma Tabela de Símbolos de strings é medido em termos de número N de chaves, comprimento máximo W das chaves, comprimento médio w das chaves e tamanho R do alfabeto.
Em muitas aplicações, o tempo de get(key) não depende de N mas é proporcional ao comprimento de key no caso de busca bem-sucedida essencialmente constante em busca malsucedida (porque nem chega a examinar todos os caracteres da chave).
Conceitualmente, uma árvore é composta por uma cadeia de ramos originados em sua raiz.
A raiz é o primeiro nó na cadeia de ramos.
Cada ramo é composto com por um conjunto de nós que podem originar novas cadeias de ramos ou não.
Prefixos são strings que podem resultar em diferentes palavras / chaves.
Em uma árvore de prefixos, os ramos são prefixos que podem compor múltiplas chaves. As chaves são armazenadas como uma sequência de caracteres, ou sequência de nós, em forma de lista duplamente ligada.
O primeiro caractere da chave é um nó filho da raiz da árvore. Os caracteres sucessores são nós filhos de seus antecessessores, respectivamente.
O último caractere da palavra é conhecido como sendo o nó terminal de uma chave.
Dito isto, palavras que compartilham o mesmo prefixo, compartilham o mesmo ramo na árvore.

A string v está representada na árvore mas não é uma chave. vendas, vender e você são chaves que compartilham o mesmo prefixo.
A string vend está representada na árvore mas não é uma chave. vendas e vender são chaves que compartilham o mesmo prefixo.
// Nó
class No {
public valor: string | null;
public pai: No | null;
public ramos: Map<string, No>;
public terminal: boolean;
constructor(valor: string | null) {
this.valor = valor;
this.pai = null;
this.ramos = new Map();
this.terminal = false;
}
public pegarPalavra() {
let palavra = [];
let no: No | null = this;
while (no !== null) {
palavra.unshift(no.valor);
no = no.pai ? no.pai : null;
}
return palavra.join('');
};
};
export class ArvoredePrefixos {
public raiz: No;
constructor () {
this.raiz = new No(null)
}
// adicionar nova palavra na árvore
public adicionar(palavra: string): void {
// aponta ponteiro para o nó na raiz da árvore
let no = this.raiz;
// para cada posicao da palavra
for (let posicao = 0; posicao < palavra.length; posicao++) {
const caractere = palavra[posicao];
// caso o nó atual não possua um nó filho com a caractere atual
if(!no.ramos.has(caractere)) {
// criar um novo nó tendo o seu pai dado a raiz atual
const novoNo = new No(caractere);
// o pai do novo nó é a raiz atual
novoNo.pai = no;
// adiciona o novo nó como filho da raiz atual
no.ramos.set(caractere, novoNo);
}
// ponteiro para o próximo nó, que é o caractere filho da raíz atual
no = no.ramos.get(caractere) as unknown as No;
// se a posicao atual for a ultima posicao da palavra
if (posicao === palavra.length - 1) {
// definir o nó como terminal no ramo
no.terminal = true;
}
}
}
// checar se a árvore contem a palavra dada.
public busca (palavra: string) {
// ponteiro para o nó na raiz da árvore
let no = this.raiz;
// para cada posicao da palavra
for (let posicao = 0; posicao < palavra.length; posicao++) {
const caractere = palavra[posicao];
// caso o nó atual possua um nó filho com a caractere atual
const filho = no.ramos.get(caractere);
if(filho) {
// apontar ponteiro no para o no filho
no = filho;
} else {
// não contém
return false;
}
}
return no.terminal;
}
// buscar chaves dado um prefixo
public chavesComOPrefixo (prefixo: string = ''): string[] {
if (prefixo === '') {
return [];
}
let no = this.raiz;
const nos: No[] = [];
// para cada caractere do prefixo com comprimento X,
for(let posicao = 0; posicao < prefixo.length; posicao++) {
// encontrar o último nó em sua sub-árvore
const caractere = prefixo[posicao];
const filho = no.ramos.get(caractere);
if (filho) {
no = filho;
} else {
return [];
}
}
// palavras encontradas, inicialmente vazia pois só
// encontramos uma sub-ávore até o momento
const palavras: string[] = [];
// pegar todas as palavras dada uma sub-árvore
this.pegaPalavas(no, palavras);
return palavras;
}
// pega palavra em uma sub-árvore
private pegaPalavas (no: No, palavras: string[]): void {
// se o nó atual for o terminal nó de uma chave
if (no.terminal) {
palavras.push(no.pegarPalavra())
}
// itera sobre todas as sub-árvores
for(let noFilho of no.ramos.values()) {
this.pegaPalavas(noFilho, palavras);
}
}
public chavesComOCoringa (prefixo: string): string[] {
if (prefixo === '') {
return [];
}
let no = this.raiz;
const nos: No[] = [];
// para cada caractere do prefixo com comprimento X,
for(let posicao = 0; posicao < prefixo.length; posicao++) {
// encontrar o último nó em sua sub-árvore
const caractere = prefixo[posicao];
const filho = no.ramos.get(caractere);
if (filho) {
no = filho;
} else {
return [];
}
}
// palavras encontradas, inicialmente vazia pois só
// encontramos uma sub-ávore até o momento
const palavras: string[] = [];
// pegar todas as palavras dada uma sub-árvore
this.pegaPalavas(no, palavras);
return palavras;
}
// remove uma palavra da árvore
public remover (palavra: string): void {
// pega o primeiro nó do ramo, nesse caso a raiz
let no = this.raiz;
if (!palavra) {
return
}
this.removerChave(no, palavra);
}
// encontrar e remover palavra recursivamente
private removerChave (no: No, palavra: string): boolean {
// checa se o nó atual contem a palavra
if (no.terminal && no.pegarPalavra() === palavra) {
let temRamos = no.ramos.size > 0;
// se o nó conter ramos, então marcar o nó terminal como falso,
// dessa forma não removemos chaves que contém/inclui a palavra dada.
if (temRamos) {
no.terminal = false;
} else {
if(no.pai) no.pai.ramos = new Map<string, No>();
}
return true;
}
// remover palavras de todos os ramos recursivamente
for(let noFilho of no.ramos.values()) {
this.removerChave(noFilho, palavra);
}
return false;
}
}
const arvore = new ArvoredePrefixos();
arvore.adicionar('vendas');
arvore.adicionar('vender');
arvore.adicionar('vendido');
arvore.adicionar('você');
const palavrasEncontradas = arvore.chavesComOPrefixo('v');
console.log(palavrasEncontradas) // ['vendas', 'vender', 'vendido', 'você']
console.log(palavrasEncontradas.length) // 4
console.log(palavrasEncontradas[0]) // vendas
console.log(palavrasEncontradas[1]) // vender