You are on page 1of 6

Algoritmos e Estruturas de Dados III

Documentação do Trabalho Prático 0 - TP0


Evaldo Martins de Souza 1

Departamento de Ciência da Computação


1

Universidade Federal de Minas Gerais (DCC/UFMG)

Resumo. O presente trabalho consiste em implementar um programa que gere


uma árvore trie. Uma árvore trie é uma estrutura de dados do tipo árvore
ordenada, que pode ser usada para armazenar um arranjo associativo em que
as chaves são normalmente cadeias de caracteres. Edward Fredkin1 , o inventor,
usa o termo trie (do inglês "retrieval", que significa recuperação), porque essa
estrutura é basicamente usada na recuperação de dados.

1. Introdução
Este trabalho consiste na elaboração de programa que consiga implementar uma
árvore trie a partir de um conjunto de palavras que consistirão no dicionário do programa.
Em seguida, dada uma certa quantidade de palavras que formarão o texto, o pro-
grama deverá verificar se as referidas palavras se encontram na presentes na árvore.
Além disso, o programa deverá, também, contar a frequência que cada palavra do
dicionário aparece no texto, devendo essa contagem ser interna à estrutura da árvore.
Em uma árvore trie, cada vértice armazena um caracter da palavra e um termi-
nador nulo define o fim da palavra. Cada vértice pode ser um prefixo para outra palavra
que possua início com a letra armazenada naquele vértice.

2. Modelagem
O trabalho foi modelado utilizando uma estrutura que possui três variáveis em sua
composição:
• node: um arranjo de ponteiros do tamanho do alfabeto e do mesmo tipo da estru-
tura;
• index: um ponteiro do tipo inteiro que será usado para contar os casamentos das
palavras;
• cont: um inteiro que será usado para fazer contagens durante a passagem pelos
nós da árvore.
• leaf: um inteiro que servirá de flag para sinalizar quando um nó é uma folha;
Estrutura. A estrutura foi nomeada para nodeTrie (renomeada nodeT) e é alo-
cada dinâmicamente.
Além disso, o código possui três funções que executarão as operações básicas:
criar, inserir e buscar as palavras na árvore. A contagem de palavras em comum no texto
e no dicionário deverá ocorrer na árvore.
1
Edward Fredkin (Los Angeles, 1 de janeiro de 1934) é um físico estadunidense.
3. Solução
3.1. Explicação
A solução encontrada, foi utilizar a estrutura citada na Seção 2 para realizar as
operações de criar a árvore , inserir as palavras do dicionário na árvore , buscar as palavras
do texto na árvore e contar as palavras em comum ao texto e dicionário.
Para tanto, lê-se de um arquivo de entrada para o programa. Cada arquivo tem
quatro linhas que trazem as seguintes informações:
• a primeira linha traz um valor N, N < 105 , que informa o número de palavras do
dicionário.
• a segunda linha contém N palavras que são todas as palavras do dicionário. Es-
sas palavras fazem parte do alfabeto latino, padrão ISO e contém as 26 letras do
alfabeto, de a à z, todas minúsculas e com no máximo 15 caracteres.
• a terceira linha tem um número M, M < 1016 , que informa a quantidade de
palavras do texto.
• a quarta linha possui M palavras, todas pertencentes ao texto, que seguem os
mesmos padrões das palavras do dicionário.

3.2. Funcionamento do programa


Quando o programa é executado, a árvore inicial é criada com a função trieCre-
ate(), que inicializa as variáveis e retorna uma árvore vazia. Em seguida, faz a leitura da
quantidade de palavras do dicionário. As palavras do dicionário são lidas e alocadas nas
posições adequadas do arranjo dentro da estrutura da árvore.
Quando todas as palavras do dicionário são lidas, elas são inseridas no dicioná-
rio pela função trieInsert(), que recebe o dicionário (árvore vazia criada anteriormente)
e a palavra lida do arquivo de entrada e o índice do dicionário será inserida, como parâ-
metros.
Seguindo o fluxo de execução, o programa, um ponteiro do inteiro e que recebrá
contagem final de frequência das palavras recebe o resultado da função trieSearch() que
recebe o dicionário como parâmetro.
Na função trieSearch(), que tem o dicionário como parâmetro , é efetuada a
leitura da quantidade de palavras do texto e, em seguida, as palavras do texto são lidas.
Cada palavra lida é comparada com o dicionário e, caso haja o casamento das palavras, o
ponteiro do tipo inteiro utilizado para guardar as ocorrências de casamento é incrementado
e atualizado.
O retorno da função é um vetor de frequências atualizado, com a quantidade exata
de casamentos de cada palavra do dicionário com o texto.
Ao final, uma linha de texto contendo a quantidade de ocorrências de cada palavra
do dicionário no texto será mostrada.
A liberação dos espaços de memória alocados para inicialização e uso de algumas
variáveis é feita com a chamada da função trieDestroy(), que recebe a árvore como
parâmetro e verifica se o nó é uma folha ou não. A liberação da memória é feita nó a nó.
Depois executa-se a liberação do index e, finalmente, da árvore.
4. Análise
A complexidade pode ser medida analisando , primeiramente, as funções que
compõem o programa separadamente. Dessa forma, temos:
A função trieCreate(): recebe a quantidade de palavras do dicionário como parâ-
metro e cria a árvore que será utilizada no programa. Sempre que for necessário crior um
nó ou uma subárvore , a função é chamada. A complexidade de criar a árvore é O(N),
onde N é a quantidade de palavras. A complexidade de espaço é O(MAX), onde MAX
é o tamanho do alfabeto.
Algorithm 1 Algoritmo para criar a árvore
1: raiz ← NULL
2: se raiz então
3: i
4: raiz->folha ← 0
5: raiz->contador ← 0
6: raiz->index ← memória alocada dinâmicamente
7: para i ← 0 até tamanho da palavra faça
8: raiz->node[i] ← NULL
9: fim para
10: fim se
11: retorne raiz

A função trieInsert(): recebe as palavras a serem inseridas na árvore, sendo que


receb como parâmetro o dicionário, a palavra a ser inserida e o índice. A complexidade
da função é O(M), onde M é o tamanho da palavra a ser inserida.

Algorithm 2 Algoritmo para inserir a palavra na árvore


1: index
2: tamanho da palavra
3: raiz ← dicionário
4: para i ← 0 até tamanho da palavra faça
5: index ← palavra[i] - ’a’
6: se dicionário->node[index] = NULL então
7: dicionário->node[index] ← trieCreate(raiz->contador)
8: dicionário← dicionário->node[index]
9: senão
10: dicionário← dicionário->node[index]
11: fim se
12: dicionário->folha ← 1
13: fim para
14: dicionário->index[dicionário->contador]← indice
15: dicionário->contador ++
16: retorne a raiz

A função trieSearch(): recebe as palavras do texto e as compara com as palavras


do dicionário. A complexidade é a mesma da função trieInsert().
Algorithm 3 Algoritmo para buscar a palavra na árvore
1: quantidade palavras do texto
2: i, j
3: flag ← 1
4: *frequência ← memória alocada dinâmicamente
5: string[]
6: raiz ← dicionário
7: Leia: quantidade de palavras do texto
8: para i ← 0 até quantidade de palavras do texto - 1 faça
9: ok ← 1
10: dicionáriogets raiz
11: Leia: string
12: para j ← i + 1 até tamanho da palavra da string faça
13: index ← string[j] - ’a’
14: se dicionário->node[index] == NULL então
15: ok ← 0
16: j ← tamanho da string + 1
17: senão
18: dicionário← dicionário->node[index]
19: fim se
20: fim para
21: k
22: se ok == 1 E dicionário->folha == 1 E j <= tamanho da string então
23: para k ← i + 1 até menor que dicionário->contador faça
24: frequência[dicionário->index[k]]++
25: fim para
26: fim se
27: fim para
28: retorne frequência

Isso devido ao fato de que a função trieSearch() também executa um loop levando
em consideração o tamanho da palavra e posteriormente executa operações com custos
lineares. O segundo loop For dentro da função.
A função trieDestroy(): recebe a árvore como parâmetro e tem complexidade de
tempo O(M), M como tamanho da palavra, e a complexidade de espaço é O(N), onde
N é a altura (níveis) da árvore.
Algorithm 4 Algoritmo para liberar a árvore
1: i
2: se raiz->folha ← 0 então
3: para i ← 0 até tamanho do alfabeto faça
4: se raiz->node[i] então
5: trieDestroy(raiz->node[i])
6: raiz->node[i] ← NULL
7: Libera: raiz->node[i]
8: fim se
9: fim para
10: Libera: raiz->index
11: Libera: raiz
12: fim se
13: retorne raiz

A árvore trie tem uma complexidade de tempo geral de , enquanto que sua com-
plexidade de espaço é de O(MAX ∗ M ∗ N), onde MAX é o tamanho do alfabeto, M é
o tamanho da palavra buscada e/ou inserida e N é a quantidade de palavras na árvore.

5. Experimentos
5.1. Configuração da máquina
O computador usado para os teste possui as seguintes configurações:

Tabela 1. Configuração do computador utlizados nos testes

MacBook Air (13-inch, Early 2015)


Sistema Operacional OS X El Capitan V. 10.11. 6
Processador 1,6 GHz Intel Core i5
Memória 4 GB 1600 MHz DDR3
Gráficos Intel HD Graphics 6000 1536 MB

O programa foi testado em uma máquina virtual (instalada no computador supra-


citado) com as seguintes configurações:

Tabela 2. Configuração da máquina virtual

MacBook Air (13-inch, Early 2015)


Software VirtualBox Versão 5.0.16
Sistema Operacional Ubuntu 16.04 LTS Virtual 64 bits

5.2. Experimentos e testes


Os experimentos foram realizados utiliazando entradas variadas com as seguintes
caracterÃsticas:
Tabela 3. Testes realizados

Descrição dos testes


Testes Tamanho do dicionário Tamanho do texto Tempo (ms)
1 20 30 3
2 20 30 2.8
3 20 30 3.2
4 1 100 3.8
5 10 100 3.1
6 100 100 2.3
7 1 100 1.2
8 10 100 1.3
9 100 100 3.3
10 200 100 3.6

Os experimentos mostram que, tanto o tamanho do dicionário, quanto o tamanho


do texto e a quantidade de vezes que a palavra do dicionário aparecem no texto podem
influenciar na execução do programa.
Dentre as entradas com maior tempo de execução, estão justamente as que as pa-
lavras do dicionário mais ocorrem no texto. São também as entradas com textos grandes.
Outro aspecto relevante é que a quantidade de vezes em que a palavra do dici-
onário ocorre no texto influencia mais na execução do programa do que a quantidade
de palavras do dicionário em si. Obviamente, um dicionário com muitas palavras que
ocorrem muitas vezes em um texto deverá ter um tempo execução maior.

6. Conclusão
Com os experimentos citados na Seção 5 fica evidente que a quantidade de ocor-
rências do dicionário no texto influencia no tempo de execução do programa. Como a
alocação de memória ocorre de forma dinâmica, quanto maior o dicionário maior também
será o espaço necessário para armazená-lo.
Por fim, o programa encontra os casamentos entre dicionário e texto, conforme
proposto na especificação, cumprindo o que se espera.

Referências
Cormen, T. H. (2002). Algoritmos: Teoria e Prática. Elsevier.
Ziviani, N. et al. (2004). Projeto de Algoritmos: com Implementações em Pascal e C,
volume 2. Thomson.

You might also like