Tries (árvores digitais)

Livro de Sedgewick e Wayne: primeira parte da sec.5.2, p.730.  Website do livro: resumo sec.5.2, slides.  Código fonte, documentação, e dados de teste de todos os programas do livro: veja algs4.cs.​princeton.edu/​code/.

Resumo:

Pré-requisitos:

Estrutura de uma trie

Busca (search)

Inserção (insert)

Exercícios 1

  1. Quanto vale get("")?  Quanto vale put("", val)?
  2. Quanto vale get(null)?  Quanto vale put(null, val)?
  3. Que acontece se o método get(Node x, String key, int d) for invocado com d estritamente maior que key.length()?
  4. De quantas maneiras diferentes pode terminar uma busca (bem- ou malsucedida) numa trie?
  5. (SW 5.2.5)  Escreva uma versão não recursiva do método get(). Diga quais são os invariantes do processo iterativo.
  6. (SW 5.2.5)  Escreva uma versão não recursiva do método put().
  7. As chaves de uma trie precisam ser comparáveis?
  8. Trie limpa?  Escreva um método limpa() (que fará parte da classe TrieST) para verificar se a trie é limpa.

Remoção (delete)

Exercícios 2

  1. Mostre que a trie produzida pelo método delete() é limpa (desde que a trie original seja limpa).

Implementação trie da TS de strings

Exercícios 3

  1. (SW 5.2.1)  Insira as chaves  no is th ti fo al go pe to co to th ai of th pa  numa trie inicialmente vazia. Faça um desenho da trie resultante. (Não desenhe os links de valor null.)
  2. (SW 5.2.3)  Insira as chaves  now is the time for all good people to come to the aid of  numa trie inicialmente vazia. Faça um desenho da trie resultante. (Não desenhe os links de valor null.)
  3. Size.  Escreva uma implementação (recursiva) preguiçosa do método size(). (É claro que método deve devolver o número de chaves e não o número de nós.)
  4. (SW 5.2.10) Size. Escreva uma implementação ansiosa do método size() que mantenha, em cada nó x, o número de nós na subtrie cuja raiz é x. (A classe TrieST deverá ser reescrita para acomodar essa implementação.)

Operações especiais: keysWithPrefix(), longestPrefixOf(), etc.

Exercícios 4

  1. Numa trie arbitrária, qual deve ser o valor de keysWithPrefix("a")?  Qual deve ser o valor de keysWithPrefix("")?
  2. Numa trie arbitrária, qual deve ser o valor de keysThatMatch(".")?  Qual deve ser o valor de keysThatMatch("")?
  3. Numa trie arbitrária, qual deve ser o valor de longestPrefixOf("a")?  Qual deve ser o valor de longestPrefixOf("")?

Implementação de keysWithPrefix() e collect()

Exercícios 5

  1. (SW 5.2.5)  Escreva uma versão não recursiva do método collect().
  2. Casamento com curinga.  Implemente o método keysThatMatch(). O método recebe uma string s e imprime todas as chaves que casam com s quando os caracteres '.' em s são interpretados como curingas, ou seja, casam com qualquer caractere. No nosso exemplo padrão, a resposta de keysThatMatch(.he) é shethe. A resposta de keysThatMatch(s..) é seashe.

Implementação de longestPrefixOf()

Exercícios 6

  1. Prove que os invariantes de longestPrefixOf() dados acima estão corretos. (Verifique que as propriedades valem no início da primeira e continuam valendo no início de cada iteração subsequente.)  Mostre que a validade dos invariantes no início da última iteração garante que longestPrefixOf() está correto.
  2. Critique os seguintes invariantes de longestPrefixOf():  (1) max é o tamanho do maior prefixo bom de s encontrado até agora e (2) depois que saímos do for, a variável max é o tamanho do maior prefixo bom de s encontrado até agora.
  3. Critique o seguinte código alternativo de longestPrefixOf():
    public String longestPrefixOf(String s) {
       int max = -1;
       Node x = root;
       for (int d = 0; x != null && d < s.length(); d++) {
          if (x.val != null) max = d;
          x = x.next[s.charAt(d)];
       }
       if (max == -1) return null;
       else return s.substring(0, max);
    }
    
  4. Escreva uma versão recursiva do método longestPrefixOf().
  5. O livro escreve o código de longestPrefixOf() em estilo recursivo e trata do caso em que nenhum prefixo de s é bom de maneira diferente da minha. O método longestPrefixOf() do livro devolve a string vazia se nenhum prefixo de s for bom e também se a string vazia for o único prefixo bom de s:
    public String longestPrefixOf(String s) {
       int len = search(root, s, 0, 0);
       return s.substring(0, len);
    }
    private int search(Node x, String s, int d, int max) {
       if (x == null) return max;
       if (x.val != null) max = d;
       if (d == s.length()) return max;
       char c = s.charAt(d);
       return search(x.next[c], s, d+1, max);
    }
    

    Analise o código.  Tente dizer o que, exatamente, o método search() faz.  (Na minha opinião, a versão do livro é um exemplo clássico de como não se deve escrever um método recursivo.)

Análise

Exercícios 7

  1. Min. Implemente a operação min() na classe TrieST. Esse método deve devolver a menor chave (no sentido lexicográfico) da trie. Você pode supor que a trie é limpa. Escreva uma versão recursiva e uma iterativa do método. Escreva os invariantes da versão iterativa.
  2. Max. Implemente a operação max() na classe TrieST. Esse método deve devolver a maior chave (no sentido lexicográfico) da trie. Você pode supor que a trie é limpa. Escreva uma versão recursiva e uma iterativa. Escreva os invariantes da versão iterativa.
  3. (SW 5.2.8) Operações que envolvem ordem. Implemente as operações floor(), ceiling(), rank() e select() na classe TrieST.
  4. (SW 5.2.14) Contagem de substrings de comprimento L. Escreva um cliente de TrieST que leia um texto da entrada padrão e calculate o número de substrings de comprimento L distintas que o texto contém.  Por exemplo, o texto cgcgggcgcg tem cinco substrings de comprimento 3 distintas:  cgc, cgg, gcg, ggc e ggg.  Dica: Insira cada substring de comprimento L em uma tabela de símbolos.
  5. (SW 5.2.15) Substrings distintas. Escreva um cliente de TrieST que leia um texto da entrada padrão e calcule o número de substrings distintas de qualquer comprimento. (Isso pode ser feito muito eficientemente com a árvore de sufixos. Veja suffix arrays na página 875 do livro.)
  6. (SW 5.2.16) Semelhança de documentos. A L-semelhança de dois arquivos de texto é a distância euclidiana entre os vetores de frequência definidos pelo número de ocorrências de cada L-grama (sequência de L caracteres consecutivos) dividido pelo número total de L-gramas.  Escreva um cliente de TrieST com um método estático que receba, pela linha de comando, um inteiro L e os nomes de dois arquivos de texto e calcule a L-semelhança dos dois arquivos. Escreva também um método estático main() que receba um inteiro L pela linha de comando e uma lista de arquivos texto pela entrada padrão e imprima uma matriz que mostre a L-semelhança de todos os pares de arquivos.
  7. (SW 5.2.21) Substring matches. Suponha dada uma lista L de palavras (separadas por espaços em branco) na entrada padrão. Dada uma string s (na linha de comando), queremos todas as palavras em L que contêm s (como substring). Projete uma API para essa tarefa e desenvolva um clente de TrieST que implemente a API. Dica: Insira todos os sufixos de cada palavra da lista na tabela de símbolos.
  8. (SW 5.2.22) Macacos digitadores. Suponha que um macaco digita palavras aleatórias anexando cada uma de 26 possíveis letras, com probabilidade p, ao final da palavra corrente e termina a palavra com probabilidade 1 − 26p. Escreva um programa para estimar a distribuição de frequência dos comprimentos de palavras produzidas. Se abc, por exemplo, é produzida mais de uma vez, conte-a somente uma vez.
  9. Qual era mesmo o título daquele filme?  Escreva um programa que receba uma string s e devolva os títulos de filmes que se aproximam de s.  Mais precisamente, o programa deve devolver (1) os títulos dos filmes que tenham s como prefixo, (2) o mais longo prefixo de s que seja um título de filme, e (3) os títulos dos filmes que casam com s quando os caracteres '.' em s são interpretados como curingas.  Detalhes ténicos:  Extraia os títulos de filmes do arquivo movies.txt.  Não queremos nos atrapalhar com letras maiúsculas, letras acentuadas, pontuação, etc. Assim, todos os títulos devem ser convertidos para o alfabeto   a b … y z ? 0 1 … 8 9   mais o espaço em branco (total de 38 caracteres).  O alfabeto de consulta tem um caractere adicional, o ponto, que funciona como curinga.   Converta cada um dos 256 caracteres do ASCII estendido no correspondente caractere do nosso alfabeto.  Por exemplo, as letras A e ä serão levadas em a; a letra Ç será levada em c; etc. Todos os caracteres sem um correspondente natural devem ser levados em ?.  Armazene tudo numa trie para responder consultas rapidamente.  As consultas devem ser feitas pela linha de comando e a string de consulta deve ser embrulhada em aspas simples (caracteres ') para que a string possa conter espaços em branco. (Portanto a string de consulta não pode conter aspas simples.)  Tanto quanto possível, use as classes Java do livro sem alteração.  Aproveite trechos de código do livro quando apropriado.  Faça testes.  No fim do seu programa, coloque (em forma de comentário) o resultado de testes interessantes (o que você digitou no terminal e qual foi a resposta do programa).

O lado negativo

Exercícios 8

  1. (SW p.741)  Escreva uma generalização de TrieST que trabalhe com um alfabeto especificado.  Dicas: Implemente um constructor que receba um objeto Alphabet como argumento. Use o método toIndex() de Alphabet para converter caracteres em índices no conjunto 0..R-1. Use o método toChar() de Alphabet para converter índices do conjunto 0..R-1 em carateres.

Perguntas e respostas