Você está na página 1de 40

Apostila de Estruturas de Dados

Organizada pelo Prof. Célio F. Silva

Estruturas de Dados – Estudo de Pilhas, Filas e Listas


encadeadas

As estruturas de dados têm larga aplicação na computação em geral.


Sistemas Operacionais e aplicativos as utilizam para várias atividades
importantíssimas, como gerenciamento de memória, execução de processos,
armazenamento e gerenciamento de dados no disco, etc. Ou seja, não faltam
motivos para um estudante da área ou qualquer
desenvolvedor/programador saberem a fundo e com fluência sobre o
assunto.

É necessário que você tenha conhecimento prévio e boa fluência com


ponteiros, passagem de parâmetros e comandos na linguagem C. Afinal, quem
está desenvolvendo uma aplicação e busca solução em estruturas de dados, supõe-se
que já tenha tal conhecimento sobre algoritmos “na ponta da língua”.

Pilhas

O funcionamento de uma pilha consiste numa estratégia chamada LIFO (last


in, first out – último a entrar, primeiro a sair). O único elemento que se pode
acessar na pilha é o elemento do topo da mesma, ou seja, o último a ser
empilhado.

Imagine uma pilha de pratos. Quando se vai lavá-los, você não começa
retirando o que está mais abaixo, e sim o que está no topo da pilha. É
basicamente assim que esse tipo de estrutura de dados funciona.

Implementando as seguintes funções para Pilha:

typedef struct pilha Pilha; //redefinição do tipo struct pilha para simplesmente Pilha

- Pilha* pilha_cria (void); //cria uma pilha


- void pilha_insere (Pilha* p, float v); // empilha um novo elemento
- float pilha_retira (Pilha* p); //retira um elemento (o último)
- int pilha_vazia (Pilha* p); //verifica se a pilha está vazia
- void pilha_libera (Pilha* p); //esvazia toda a estrutura alocada para a pilha,
liberando seus elementos

Pode-se implementar uma pilha utilizando vetores, aonde os elementos


serão armazenados, ocupando as primeiras posições do vetor. Dessa forma,
se temos n elementos armazenados na pilha, o elemento vet[n-1] representa
o do topo.

Código:
#define N 50
struct pilha {
int n;
float vet[N];
};

typedef struct pilha Pilha;

A função que cria a Pilha aloca dinamicamente essa estrutura e inicializa a


pilha como sendo vazia, ou seja, com o número de elementos igual a zero.
Código:
Pilha* pilha_cria (void) {
Pilha* p = (Pilha*)malloc(sizeof(Pilha));
p->n = 0; //inicializa com zero elementos
return p;
}
Como nossa pilha está implementada usando um vetor de tamanho fixo,
devemos sempre verificar se existe espaço para inserção de um novo
elemento.
Código:
void pilha_insere (Pilha* p, float v){
if (p->n == N) //verifica se a capacidade do vetor foi esgotada
{
printf (“Capacidade da pilha estourou!\n”);
exit (1);
}

/*insere o novo elemento na próxima posição livre*/

p->vet[p->n] = v;
p->n++;
}
A função de remoção retira um elemento (o que estiver no topo) e retorna
esse elemento, daí o retorno dessa função ser do tipo float, pois esse é o tipo
do dado que estamos armazenando na pilha. Se for um dado de outro tipo
(char, por exemplo), bastaria você alterar o tipo de retorno da função para
char.
Código:
float pilha_remove (Pilha* p)
{
float v;
if (pilha_vazia (p))
{
printf (“Pilha está vazia!\n”);
exit(1);
}

/*agora retira o elemento do topo da pilha*/

v = p->vet[p->(n-1)];
p->n--;
return v;
}
Pode-se implementar a função que verifica se a pilha está vazia da seguinte
maneira:
Código:
int pilha_vazia (Pilha* p)
{
return (p->n == 0); /*se isto for verdade, será retornado o conteúdo
dos parêntesis, caso contrário será retornado zero. */
}

E agora a função para liberar a memória alocada para a pilha:


Código:
void pilha_libera (Pilha* p)
{
free(p);
}

Filas

Basicamente o que diferencia a Fila da Pilha é a ordem de saída dos


elementos. Enquanto na pilha o elemento retirado é sempre o último a entrar
(o do topo da pilha), na fila sempre é retirado o primeiro elemento a entrar
na estrutura. Pode-se fazer uma analogia com uma fila de banco por
exemplo, onde a primeira pessoa a ser atendida é a que chega primeiro. À
medida que outras pessoas chegam na fila, deverão permanecer na fila
aguardando que sejam atendidas, seguindo este critério.

Implementaremos as seguintes funções para filas:

typedef struct fila Fila; //redefinição do tipo struct fila para simplesmente Fila

- Fila* fila_cria (void); //cria uma fila


- void fila_insere (Fila* p, float v); // enfileira um novo elemento
- float fila_retira (Fila* p); //retira um elemento (primeiro)
- int fila_vazia (Fila* p); //verifica se a fila está vazia
- void fila_libera (Fila* p); //esvazia toda a estrutura alocada para a fila, liberando
seus elementos

Mais uma vez, para simplificar, vamos implementar uma Fila a partir de um
vetor. Como um vetor em C tem uma quantidade fixa de elementos, digamos,
N elementos, observe que mais uma vez temos um limite na quantidade de
elementos a serem armazenados na fila.

De acordo com a idéia central da fila (FIFO - primeiro a entrar, primeiro a sair), ao
usarmos um vetor para implementação da fila, observe que, quando retiramos
elementos dela, é como se a fila “andasse” no vetor, pois ao se retirar elemento(s) da
fila, o índice do vetor que representa o topo da fila se altera. Por exemplo, imagine
um vetor de N = 6 posições. Se inserirmos 4 elementos, teremos as 4 primeiras
posições da fila ocupadas. Ao retirarmos 2 elementos (lembre-se que serão retirados
os 2 primeiros), temos agora a situação em que o 1º elemento da fila, ou seja, o topo
dela, será representado pelo 3º elemento inicialmente inserido, conforme ilustra a
figura:

Se retirarmos 2 elementos, teremos a seguinte situação:


A partir de agora, o 1º elemento da fila passa a ser o elemento 55, que foi o 3º
elemento inserido, presente no índice 2 do vetor. Observe que vetores em C sempre
iniciam com índice 0 e não 1.

Por causa desse fato, precisamos fazer uma alteração nos índices do vetor a cada vez
que realizarmos uma remoção de quaisquer elementos da fila. Essa reordenação é
feita de forma “circular” no vetor, afim de aproveitar as posições liberadas quando se
retira elementos. No exemplo citado, teríamos a seguinte situação ilustrativa, após
essa reordenação:

Essa reordenação de índices pode ser feita utilizando uma função específica:
Código:
Static int incr (int i)
{
return (i+1)%N;
}

Pode-se dispensar o uso desta função utilizando diretamente apenas o incremento


circular, já que essa função seria sistematicamente chamada a cada vez que se
retirar elementos da fila, e isso é um processo computacionalmente custoso. O
incremento circular ficaria assim:

Código:
i = (i+1)%N;

Para a interface da fila, Pode-se utilizar uma estrutura contando 3 campos: um vetor
de tamanho N, um inteiro n para representar o número de elementos armazenados
na fila e um índice ini para o início da fila.

Tendo o índice para o início da fila, Pode-se calcular o índice para o final da fila, que
chamaremos de fim da seguinte maneira:

Código:
fim = (ini+n)%N;

A estrutura da fila fica então:

Código:
#define N 100
struct fila
{
int n;
int ini;
float vet[N];
}

A função que cria a fila:

Código:
Fila* fila_cria (void)
{
Fila* f = (Fila*) malloc(sizeof(Fila));
f->n = 0; //inicializa a fila
vazia;
f->ini = 0; //escolhe uma posição
inicial;

return f;
}

A função que insere elementos na fila deve verificar se existe espaço disponível para
esta inserção, e inserir o elemento no final da fila, utilizando o índice fim, conforme já
falamos anteriormente:

Código:
void fila_insere (Fila* f, float v)
{
int fim;
if (f-> == N)
{
printf (“Fila não tem espaço disponível”);
exit (1);
}

fim = (f->ini + f->n)%N; //cálculo do índice do último elemento


f->vet[fim] = v;
f->n++;
}

A função para retirar um elemento da fila verifica se a fila já está ou não vazia:

Código:
float fila_retira (Fila* f)
{
float v;
if (fila_vazia(f))
{
printf (“Fila vazia\n”);
exit (1);
}

v = f->vet[f->ini];
f->ini = (f->ini + 1) % N;
f->n--;

return v;

Função para verificar se a fila está vazia:


Código:
int fila_vazia (Fila* f)
{
return (f->n == 0);
}

Função para liberar toda a memória alocada para a fila:

Código:
void fila_libera (Fila* f)
{
free (f);
}

Listas encadeadas

Listas são estruturas de dados que contém um conjunto de blocos de


memória que armazenam dados. Esses blocos são encadeados (ligados) por
ponteiros, formando uma espécie de “corrente”, onde as peças dessa
corrente estão ligadas umas as outras. O encadeamento de listas pode ser de
dois tipos: simplesmente encadeada e duplamente encadeada.

Listas simplesmente encadeadas

Listas simplesmente encadeadas possuem um único ponteiro, que apontará para o


próximo elemento da lista.
Código:
struct lista {

int info;
struct lista* prox;
}

typedef struct lista Lista;

A função que cria uma lista simplesmente encadeada retornará um NULL do tipo da
lista, ou seja, uma lista totalmente vazia:

Código:
Lista* lst_cria (void)
{
return NULL;
}

A função de inserção insere novos elementos no início da lista encadeada, pois esta é
a maneira mais simples de se inserir elementos numa lista, seja ela simplesmente ou
duplamente encadeada. É importante lembrar que uma lista é representada pelo
ponteiro para o primeiro elemento dela todos os demais elementos estão encadeados
um a um sucessivamente, a partir do primeiro. Ou seja, para se acessar qualquer
elemento da lista, basta fornecer o ponteiro para o primeiro elemento e ir
percorrendo elemento a elemento até se chegar no elemento desejado.
Código:
Lista* lst_insere (Lista* l, int i)
{
Lista* novo = (Lista*) malloc (sizeof(Lista));
novo->info = i;
novo->prox = l;

return novo;
}

Observe que o novo elemento é encadeado aos demais já existentes na lista (caso ela
já tenha outros elementos) e a nova lista é representada pelo novo elemento
inserido, que passa a ser o 1º da lista.

Função que imprime na tela os elementos da lista:

Código:
void lst_imprime (Lista* l)
{
Lista*p;

for (p = l; p != NULL; p = p->prox)


{
printf (“info = %d\n”, p->info);
}

Função para verificar se a lista está vazia:

Código:
int lst_vazia (Lista* l)
{
return (l == NULL);
}

Para criarmos uma função que busque um determinado elemento, vamos


implementá-la de forma a retornar um ponteiro para o elemento buscado. Como
sempre, você pode alterar a maneira de retorno de quaisquer das funções aqui
mostradas, de forma a se enquadrar melhor ao seu caso.

Código:
Lista* lst_retira (Lista* l, int v)
{
Lista* ant = NULL;
Lista* p = l;

while (p != NULL && p->info != v)


{
ant = p;
p = p->prox;
}

if (p == NULL)
return l;

if (ant == NULL)
{
l = p->prox;
}

else
{
ant->prox = p->prox;
}

free (p);
return l;

Função para liberar os elementos da lista:

Código:
void lst_libera (Libera* l)
{
Lista* p= l;
while (p != NULL)
{
Lista* t = p->prox;
free (p);
p = t;
}
}

Listas duplamente encadeadas

Uma lista duplamente encadeada possui 2 ponteiros em cada Nó, um para o próximo
elemento e outro para o anterior (ant e prox respectivamente). Isso possibilita
“andarmos” para “frente” e para “trás” ao longo da lista.

Logicamente, a estrutura desse tipo de lista é diferente das listas simplesmente


encadeadas:

Código:
struct lista2
{
int info;
struct lista2* ant;
struct lista2* prox;
};

typedef struct lista2 Lista2;

Função de inserção no início:


Código:
Lista2* lst2_insere (Lista2* l, int v)
{
Lista2* novo = (Lista2*)malloc(sizeof(Lista2));
novo->info = v;
novo->prox = l;
novo->ant = NULL;

if (l != NULL)
l->ant = novo;
return novo;
}

Função de busca:

Código:
Lista2* lst2_busca (Lista2* l, int v)
{
Lista2* p;
for (p = l; p != NULL; p = p->prox)

if (p->info == v)
return p;

return NULL; //não encontrou o elemento

Função para retirar um elemento:

Código:
Lista2* lst2_retira (Lista* l, int v)
{
Lista2* p = busca(l, v); //procura o elemento

if (p == NULL) //testa se é o 1º elemento


return l;

if (l == p)
l = p->prox;
else
p->ant->prox = p->ant;

if (p->prox != NULL) //verifica se é o último elemento


p->prox->ant = p->ant;

free (p);

return l;
}
Além dessas estruturas, é possível criar muitas outras até mesmo juntando
estruturas diferentes para se formar uma. Por exemplo, você pode implementar uma
pilha ou fila utilizando uma lista encadeada ao invés de um vetor, como fizemos. O
retorno das funções podem ser diferentes, de acordo com a necessidade, e assim por
diante. As estruturas de dados são “maleáveis” e você pode utilizá-las como quiser,
desde que preserve os conceitos fundamentais de cada uma.
Nada do que foi explanado aqui foi copiado de alguém, apenas os códigos das funções
que utilizei de um livro de estruturas de dados, chamado “Introdução a Estruturas de
Dados”, de Waldemar Celes.

Sintam-se a vontade para fazer sugestões e críticas à respeito do texto, no intuito de


melhorar o fácil entendimento do mesmo a todos, principalmente iniciantes. Este
tópico é apenas para ensinar e tirar dúvidas a respeito das estruturas de dados em C,
portanto peço que o utilizem com bom censo.

__________________

"O importante é ganhar. Tudo e sempre. Essa história que o importante é competir
não passa de demagogia." Senna

Observação (1)

 A alteração de uma estrutura de dados em um programa


lento pode mudar sensivelmente seu tempo de execução.
 Esta alteração não muda a corretude do programa.
 É importante projetar os programas tendo as estruturas
de dados como centro.
 A construção de algoritmos em torno de estruturas de
dados tais como dicionários e Filas de prioridades leva a
estruturas limpas e bons desempenhos.
 A escolha errada de uma estrutura de dados pode ser
desastroso para o desempenho.
 A escolha da melhor estrutura de dados não é tão crítica,
pois podem existir outras escolhas que se comportam
similarmente.
Observação (2)

 É importante construir os programas de forma que


implementações alternativas possam ser experimentadas.
 É importante separar os componentes da estrutura de
dados de sua interface.
 Esta abstração de dados é importante para a limpeza,
leitura e modificação dos programas.
 A ordenação é uma das partes princiPais de um algoritmo.
Deveria ser a primeira coisa a ser feita para buscar
eficiência.
 A ordenação pode ser usada para ilustrar muitos
paradigmas de projeto de algoritmos. Técnicas de
estruturas de dados, divisão e conquista, randomização e

Tipos de dados fundamentais

 Um tipo de dado abstrato é uma coleção de operações


bem definidas que podem ser feitas em uma estrutura
particular.
 São estas operações que definem o que a estrutura faz,
mas não como ela funciona.
Containers

 É uma estrutura de dados que permite o armazenamento


e a recuperação de dados independentemente de seu
conteúdo.
 As operações fundamentais são:
 Put(C,x) – insere o dado x no container C.
 Get(C) – recupera o próximo item do container C.
Tipos diferentes de containers admitem diferentes
tipos de recuperação, baseado na ordem ou posição
de inserção.
Containers (2)

 A utilidade de containers é maior quando a quantidade de


dados é limitada e a ordem de recuperação é pré-definida
ou irrelevante.
 Tipos de containers:
 Pilhas
 Filas
 Tabelas

 A ordenação é uma das partes princiPais de um algoritmo.


Deveria ser a primeira coisa a ser feita para buscar
eficiência.
 A ordenação pode ser usada para ilustrar muitos
paradigmas de projeto de algoritmos. Técnicas de
estruturas de dados, divisão e conquista, randomização e
construção incremental levam a algoritmos de ordenação.
Estrutura de Dados - Árvores
Tree - Árvores

Uma árvore não ordenada simples, conforme este diagrama - o Nó rotulado 7 tem dois Filhos, o
2 e o 6, e um dos Pais, com o 2. O Nó raiz, no topo, não tem Pai.

Em ciência da computação , uma árvore é uma estrutura amplamente utilizada. Esta estrutura
de dados que emula um sistema hierárquico (estrutura de árvore) com um conjunto de Nós
associados.

Matematicamente, é uma árvore , mais especificamente uma arborescência : um acíclico


conectado grafo onde cada Nó tem Filhos ou mais Nós de zero e em um Nó Pai. Além disso, os
Filhos de cada Nó tem uma ordem específica.

Terminologia
Um Nó é uma estrutura que pode conter um valor, uma condição, ou representar uma estrutura
de dados separados (o que poderia ser uma árvore de seu próprio). Cada Nó de uma árvore ou
mais Nós Filho zero, que estão abaixo dele na árvore (por convenção, as árvores são extraídas
cresce para baixo). Um Nó que tem um Filho é chamado Filho do Pai do Nó (ou nodo Pai, ou
superior ). Um Nó tem no máximo um Pai.

Nós que não têm Filhos são chamados Nós folhas . Eles também são chamados de Nós
terminais.

Uma árvore livre é uma árvore que não tem raízes.

A altura de um Nó é o comprimento do caminho mais longo para baixo de uma folha desse Nó.
A altura da raiz é a altura da árvore. A profundidade de um Nó é o comprimento do caminho
para a raiz (ou seja, o caminho de raiz). Isto é comumente necessária na manipulação dos
diferentes árvores auto balanceamento, Árvores AVL , em particular. Convencionalmente, o
valor -1 corresponde a um sub sem Nós, ao passo que zero corresponde a uma subárvore com
um Nó.

O Nó mais alto em uma árvore é chamado Nó raiz. Sendo o Nó superior, o Nó raiz não têm Pais.
É o Nó em que as operações sobre a árvore comumente começar (apesar de alguns algoritmos
de começar com os Nós folha e trabalhar até terminar na raiz). Todos os outros Nós podem ser
alcançados a partir dele, seguindo as bordas ou links. (Na definição formal, cada caminho como
também é único). Nos diagramas, é tipicamente desenhado no topo. Em algumas árvores, tais
como pilhas , o Nó raiz tem propriedades especiais. Cada Nó em uma árvore pode ser visto
como o Nó raiz da subárvore enraizada naquele Nó.
Um Nó interno ou Nó interno é qualquer Nó de uma árvore que Nós Filho , não sendo,
portanto, um Nó folha .

A subárvore de uma árvore T é uma árvore que consiste em um Nó em T e todos os seus


descendentes em T. (Isto é diferente da definição formal de sub utilizados na teoria dos grafos.
[1]
) A sub-árvore correspondente ao Nó raiz é a árvore inteira, a subárvore correspondente a
qualquer outro Nó é chamado de sub bom (em analogia ao termo apropriado subconjunto ).

Tree - Representações
Há muitas maneiras diferentes para representar árvores, representações comuns representam a
Nós como registros alocado na heap (para não ser confundida com a estrutura de dados heap )
com ponteiros para seus Filhos, seus Pais, ou ambos, ou de itens em um array , com as relações
entre eles determinada pela sua posição na matriz (por exemplo, heap binário ).

Árvores e gráficos
A árvore de estrutura de dados pode ser generalizada para representar dirigido gráficos ,
eliminando as restrições que um Nó pode ter, no máximo, um Pai, e que não são permitidos
ciclos. As bordas são ainda considerados abstratamente como pares de Nós, no entanto, a mãe
ea criança são termos usualmente substituída pela terminologia diferente (por exemplo, de
origem e destino). Diferentes estratégias de implementação existem, por exemplo, listas de
adjacência .

Relação com as árvores em teoria dos grafos

Em teoria dos grafos , uma árvore é conectado acíclico gráfico , salvo indicação em contrário, as
árvores e os gráficos são sem direção. Não existe uma correspondência um-para-um entre as
árvores e as árvores, tais como estrutura de dados. Pode-se ter uma árvore sem direção
arbitrária, arbitrariamente, escolher um de seus vértices como a raiz, fazer todas as suas arestas,
tornando-os pontos de distância do Nó raiz - produzindo uma arborescência - e atribuir uma
ordem para todos Nós. O resultado corresponde a uma estrutura de dados árvore. Escolher
uma raiz diferente ou ordenação produz um diferente.

MétodoTraversal
Ver artigo principal: o percurso da árvore

Percorrendo os elementos de uma árvore, por meio das ligações entre Pais e Filhos, é chamado
de pé da árvore, ea ação é um pé de árvore. Muitas vezes, uma operação pode ser realizada
quando um ponteiro chega a um Nó particular. Uma caminhada em que cada Nó Pai é
atravessado antes dos seus Filhos é chamada de pré-encomenda a pé, uma caminhada em que
as crianças são percorridos antes de seus respectivos Pais são percorridos é chamado de pós-
ordem a pé, uma caminhada em que deixou subárvore de um Nó , então o próprio Nó, e
finalmente a sua subárvore direita são percorridos é chamado de passagem de fim-in. (Este
último cenário, referindo-se exatamente duas subárvores, uma subárvore esquerda e uma
subárvore direita, assume, especificamente, uma árvore binária ).

Operações comuns
• Enumerando todos os itens
• Enumerando uma seção de uma árvore
• Pesquisando um item
• Adicionando um novo item em uma determinada posição na árvore
• Excluindo um item
• Remover uma seção inteira de uma árvore (chamada poda )
• Adicionando uma seção inteira de uma árvore (chamada de enxertia )
• Encontrando a raiz para qualquer Nó

Usos comuns

• Manipular hierárquica de dados


• Tornar as informações fáceis de pesquisa (veja o percurso da árvore )
• Manipular as listas ordenadas de dados
• Como um fluxo de trabalho para composição de imagens digitais para efeitos visuais
• algoritmos Router
• Forma de multi-estágio de tomada de decisão (ver xadrez negócio )

Estude também

• Árvore (teoria dos grafos)


• Árvore (teoria dos conjuntos)
• Estrutura em árvore
• Hierarquia (matemática)

Outras árvores

• algoritmo DSW
• Tiro
• Árvore binária Esquerda-direita criança irmão
Árvore binária

Uma árvore binária simples de tamanho 9 e 3 de altura, com um Nó raiz cujo valor é de 2. A
árvore acima não é nem classificado nem uma árvore binária balanceada

Em ciência da computação , uma árvore binária é uma árvore de estrutura de dados em que
cada Nó tem no máximo dois Filhos . Normalmente, o primeiro Nó é conhecido como o Pai e os
Nós Filhos são chamados de esquerda e direita.

Na teoria dos tipos , uma árvore binária com os Nós do tipo A é definida indutivamente como T =
A μα. 1 + A × α × α. Árvores binárias são comumente usadas para implementar as árvores de
busca binária e heaps binários .

Definições para árvores com raízes

• Uma aresta direcionada refere-se à ligação da mãe para o Filho (setas na imagem da
árvore).
• O Nó raiz de uma árvore é o Nó sem Pais. Há mais de um Nó raiz de uma árvore
enraizada.
• Um Nó folha não tem Filhos.
• A profundidade de um Nó n é o comprimento do caminho desde a raiz até o Nó. O
conjunto de todos os Nós em uma determinada profundidade é às vezes chamado de um
nível da árvore. O Nó raiz é zero em profundidade (ou uma [1] ).
• A altura de uma árvore é o comprimento do caminho desde a raiz até o mais profundo
Nó na árvore. A raiz) da árvore (com apenas um Nó (a raiz) tem uma altura de zero (ou
um [2] ).
• Os irmãos são os Nós que compartilham o mesmo Nó Pai.
• Se existe um caminho de Nó a Nó p, q, onde p Nó está mais próximo do Nó de raiz do
que q, então p é um antepassado de Q e Q é um descendente de p.
• O tamanho de um Nó é o número de descendentes que tem inclusive em si.
• Em grau de um Nó é o número de arestas que chegam a esse Nó.
• Fora grau de um Nó é o número de arestas que saem do Nó.
• Raiz é o único Nó na árvore com In-degree = 0.

Tipos de árvores binárias

• Um binário é uma árvore enraizada enraizado árvore em que cada Nó tem no máximo
dois Filhos.
• Uma árvore binária completa (às vezes boa árvore binária ou 2-árvore ou árvore
estritamente binária) é uma árvore na qual todos os outros Nós do que as folhas tem
dois Filhos.
• Uma árvore binária perfeita é uma árvore binária completa em todas as folhas que
estão na mesma profundidade ou mesmo nível. [3] (Esta é ambígua também chamada de
árvore binária completa.)
• Uma árvore binária completa é uma árvore binária na qual todos os níveis, exceto
possivelmente o último, está completamente cheio, e todos os Nós são os mais à
esquerda possível. [4]
• Uma árvore binária completa infinito é uma árvore com níveis, onde para cada nível
d o número de Nós existentes a nível d é igual a 2 d. O número cardinal do conjunto de
todos Nós é . O número cardinal do conjunto de todos os caminhos é . O
binário completo árvore infinita essencialmente descreve a estrutura do conjunto de
Cantor ; o intervalo de unidade sobre o eixo real (de cardinalidade ) É a imagem
contínua do conjunto de Cantor, esta árvore é chamado às vezes o espaço de Cantor .
• Uma árvore binária balanceada é onde a profundidade de todas as folhas difere por no
máximo, 1. Balanced as árvores têm uma profundidade previsível (quantos Nós são
percorridos desde a raiz até uma folha, raiz contando como Nó 0 e posterior como 1, 2,
..., profundidade). Esta profundidade é igual à parte inteira do l o g 2 (n) onde n é o
número de Nós na árvore equilibrada. Exemplo 1: árvore balanceada com um Nó, o l g 2
(1) = 0 (profundidade = 0). Exemplo 2: árvore balanceada com 3 Nós, o g l 2 (3) = 1,59
(profundidade = 1). Exemplo 3: árvore balanceada, com 5 Nós, o l g 2 (5) = 2,32
(profundidade da árvore é de 2 Nós).
• Uma árvore enraizada binário completo pode ser identificada com um magma livre .
• Um degenerado árvore é uma árvore onde para cada Nó Pai, há apenas um associado
Nó Filho. Isto significa que em uma avaliação de desempenho, a árvore se comportar
como uma estrutura de dados lista ligada.

Uma árvore de raízes tem um Nó de topo como root.

Propriedades de árvores binárias


• O número de Nós n em uma árvore binária perfeita pode ser encontrada usando a
seguinte fórmula: n = 2 h + 1-1, onde h é a altura da árvore.
• O número de Nós n em uma árvore binária completa é mínimo: n = 2 horas e máximo: n =
h + 1-1,
2 onde h é a altura da árvore.
• O número de Nós n em um binário árvore perfeita também podem ser encontradas
usando esta fórmula: n = 2 L - 1, onde L é o número de Nós folhas da árvore.
• O número de n Nós folha em uma árvore binária perfeita pode ser encontrada usando a
seguinte fórmula: n = 2 h, onde h é a altura da árvore.
• O número de links NULL em uma árvore binária completa do Nó n é (n +1).
• O número de Nó de folha de uma árvore binária completa do Nó n é U p p e B r u o n d (n
/ 2).
• Para qualquer vazio árvore não binária com os Nós folha n 0 e n 2 Nós de grau 2, n 0 n = 2
+ 1. [5]
• n = n 0 + 1 n n n + 2 + 4 n + 3 n + 5 + .... B n-1 + n + B
• B = n - 1, n = 1 + 1 * n 1 + n 2 * 2 + 3 * n 3 + 4 * 4 + n ... B + B * n, para não incluir n 0

- Note que essa terminologia varia frequentemente na literatura, especialmente no que diz
respeito ao significado "completo" e "cheio".
Prova
prova: n0 = 1 n2

Seja n = número total de Nós


B = os ramos de T
n0, n1, n2 representam os Nós sem Filhos, Filho único, e duas crianças, respectivamente.
B=n-1
B = 2 * n1 + n2
n = n1 * n2 + 2 + 1
n = n0 + n1 + n2
n1 + n2 * 2 + 1 = n0 + n1 + n2 ==> n0 = n2 + 1

Definição em teoria dos grafos


Teoria dos Gráficos usa a seguinte definição: Uma árvore binária é ligada grafo acíclico de tal
forma que o grau de cada vértice não é mais do que três. Pode ser mostrado que, em qualquer
árvore binária de dois ou mais Nós, há exatamente dois Nós mais de um grau do que há de grau
três, mas não pode haver qualquer número de Nós de grau dois. Uma árvore binária enraizada
é tal um gráfico que tem um de seus vértices de grau não superior a dois apontada como a raiz.

Com a raiz, assim escolhidos, cada vértice terá um Pai exclusivamente definido, e até dois Filhos,
no entanto, até agora não há informação suficiente para distinguir um Filho esquerdo ou direito.
Se deixar cair a exigência de conectividade, permitindo que vários componentes ligados no
gráfico, que chamamos de uma estrutura de uma floresta.

Outra maneira de definir árvores binárias é uma definição recursiva em grafos dirigidos. Uma
árvore binária é:

• Um único vértice.
• Um gráfico formado por tomar duas árvores binárias, adicionando um vértice, e
adicionando uma borda dirigiu a partir do novo vértice para a raiz da cada árvore binária.

Isso também não estabelecer a ordem dos Filhos, mas não fixa um Nó raiz específica.

Combinatória

Os agrupamentos de pares de Nós em uma árvore pode ser representada como pares de letras,
cercada por parênteses. Assim, (ab) denota a árvore binária cuja subárvore esquerda é um
direito e cuja subárvore é b. Seqüências de pares de parênteses equilibrada pode ser utilizada
para designar árvores binárias em geral. O conjunto de todas as seqüências possíveis consiste
inteiramente de parênteses equilibrada é conhecida como a linguagem Dyck .

Dado n Nós, o número total de maneiras em que estes Nós podem ser organizados em uma
árvore binária é dada pelo número de Catalão n C. Por exemplo, C 2 = 2 declara que (a 0) e 0 (a)
são as únicas árvores binárias possível que Nós dois, e C 3 = 5 declara que ((a 0) 0), ((0 a 0) ), (0
(um 0)), (0 (0 a)) e (ab) são os únicos cinco árvores binárias possível que três Nós. Aqui 0
representa uma subárvore que não está presente.

A capacidade de representar árvores binárias como seqüências de símbolos e parênteses


implica que árvores binárias pode representar os elementos de um magma . Por outro lado, o
conjunto de todas as árvores binárias possível, juntamente com a operação natural de unir
árvores para um outro, formam um magma, o magma livre .

Dada uma seqüência que representa uma árvore binária, o operador para obter a subárvore
esquerda e direita são muitas vezes referidos como carro e cdr .

Métodos para o armazenamento de árvores binárias


Árvores binárias podem ser construídas a partir de linguagem de programação primitivas de
várias maneiras.

Nós e as referências

Em uma linguagem com os registros e referências , árvores binárias são geralmente construídos
por ter uma estrutura de Nó de árvore que contém alguns dados e referências a seu Filho e
deixou seu Filho direito. Às vezes, ele também contém uma referência a seu Pai único. Se um
Nó que tem menos de dois Filhos, alguns dos ponteiros Filho pode ser definido como um valor
nulo especial, ou para um especial do linfonodo sentinela .

Em línguas com sindicatos marcou como ML , um Nó de árvore é muitas vezes uma união
etiquetada de dois tipos de Nós, um dos quais é a-tupla de dados, deixou três Filhos, eo Filho a
direita, e os outros de que é uma folha " "Nó, que não contém dados e funções bem como o
valor nulo em uma linguagem com ponteiros.

Lista Ahnentafel

Árvores binárias podem também ser armazenados como uma estrutura de dados implícitos em
arrays , e se a árvore é uma árvore binária completa, este método não desperdiça espaço. Neste
arranjo compacto, se um Nó possui um índice i, seus Filhos são encontrados em dois índices i + 1
(para a esquerda da criança) e 2 i + 2 (para a direita), enquanto a sua mãe (se houver) é

encontrada em índice (Supondo que a raiz tem índice zero). Isto beneficia método de
armazenamento compacto mais e melhor localização de referência , especialmente durante um
percurso pré-venda. No entanto, é caro para crescer e desperdiça espaço proporcional a 2 h - n
para uma árvore de altura h com n Nós.

A árvore binária pode também ser representada na forma de matriz, bem como adjacência lista
ligada. No caso da matriz, cada Nó (raiz, esquerda, direita) é simplesmente colocado no índice, e
não há nenhuma conexão mencionado sobre a relação entre Pais e Filhos. Mas, em
representação lista ligada, Pode-se encontrar a relação entre Pais e Filhos. Em representação de
matriz os Nós são acessados por meio do cálculo do índice. Este método é usado em linguagens
como FORTRAN, que não tem alocação dinâmica de memória. Nós não Pode-se inserir um novo
Nó na árvore binária matriz implementada com facilidade, mas isso é facilmente feito quando se
utiliza uma árvore binária implementada como lista ligada.
Métodos de iteração sobre árvores binárias
Muitas vezes, uma vontade de visitar cada um de Nós em uma árvore e analisar o valor lá.
Existem várias ordens comum, na qual os Nós podem ser visitados, e cada um tem propriedades
úteis que são explorados em algoritmos baseados em árvores binárias.

Essas ordens são as seguintes:

Pre-Order: Root em primeiro lugar, as crianças após o pós-ordem: crianças em primeiro lugar,
depois de raiz em ordem: à esquerda da criança, de raiz, o Filho da direita.

Codificação

A estrutura de dados sucinta é aquele que toma o mínimo possível de espaço absoluto, tal
como estabelecido por informações teóricas limites inferiores. O número de diferentes
árvores binárias em Nós n é C n, n º número da Catalunha (assumindo que vemos árvores com
estrutura idêntica idênticas). Para n grande, isto é cerca de 4 n, portanto precisamos de pelo
menos cerca de log 2 4 n = 2 n bits para codificá-lo. Uma árvore binária sucinta, pois, que
ocupam apenas 2 bits por Nó.

Uma representação simples que atende a esse limite é de visitar os nodos da árvore em pré-
venda, saída 1 "para um Nó interno e" 0 "para uma folha. [1] Se a árvore contém dados, Pode-se
simplesmente armazená-lo em simultâneo uma matriz consecutivos preorder. Esta função
realiza o seguinte:

função EncodeSuccinct (n Nó, bitstring estrutura, os dados array) (


se n = nil então
acréscimo de 0 a estrutura;
outro
acréscimo de 1 a estrutura;
anexar n.data de dados;
EncodeSuccinct (n.left, estrutura de dados);
EncodeSuccinct (n.right, estrutura de dados);
)

A estrutura da cadeia só tem 2 n + 1 bits, no final, onde n é o número de (interno) Nós, Nós nem
sequer temos a loja de seu comprimento. Para mostrar que nenhuma informação é perdida,
Pode-se converter a saída de volta para a árvore original como este:

função DecodeSuccinct (estrutura bitstring, dados array) (


remover primeiro bit de estrutura e colocá-lo em b
se b = 1, então
criar um novo Nó n
remover o primeiro elemento de dados e colocá-lo em n.data
n.left = DecodeSuccinct estrutura (os dados)
n.right = DecodeSuccinct estrutura (os dados)
n retorno
outro
nil retorno
)
Mais sofisticadas representações sucinta permitir não somente o armazenamento compacto de
árvores, mas até mesmo operações úteis sobre as árvores diretamente enquanto eles ainda
estão na sua forma sucinta.

Codificação de árvores em geral como árvores binárias

Existe um-para-um mapeamento entre um general ordenou árvores e árvores binárias, que em
particular é usado por Lisp para representar as árvores geral ordenou como árvores binárias.
Para converter uma árvore geral ordenou a árvore binária, só precisamos para representar a
árvore de maneira geral, criança-irmão esquerda. O resultado desta representação será
automaticamente árvore binária, se visto de uma perspectiva diferente. Cada Nó N da árvore
ordenada corresponde a um Nó N 'na árvore binária, deixou o Filho de N' é o Nó que
corresponde ao primeiro Filho do N, eo direito da criança N 'é o Nó correspondente ao' s N
próximo irmão --- isto é, o próximo Nó na ordem entre os Filhos do Pai de N. Esta representação
de árvore binária de uma árvore de ordem geral, é por vezes também referida como uma criança
Esquerda-direita da árvore binária irmão (árvore LCRS), ou uma árvore duplamente encadeadas ,
ou uma cadeia de Filial-Herdeiro .

Uma maneira de pensar sobre isso é que cada Nó crianças estão em uma lista ligada ,
encadeadas com os seus campos a direita eo Nó só tem um ponteiro para o início ou cabeça
dessa lista, através do seu campo de esquerda.

Por exemplo, na árvore à esquerda, um tem os 6 Filhos (B, C, D, E, F, G). Ela pode ser convertida
em árvore binária, à direita.

A árvore binária pode ser pensada como a árvore original inclinado lateralmente, com as bordas
pretas à esquerda representa o primeiro Filho e as bordas direita azul representando próximo
irmão. As folhas da árvore à esquerda seria escrita em Lisp como:

(((NO) IJ) CD ((P) (Q)) F (M))

que seria aplicado na memória como a árvore binária à direita, sem qualquer correspondência
sobre os Nós que tem um Filho esquerdo.
Estruturas de Dados Árvores - Tree

S. Sudarshan

Baseado parcialmente em material de Fawzi & Chau-Wen Tseng Emad

• Árvore
o Nodes
o Cada nó pode ter 0 ou mais filhos
o Um nó pode ter no máximo um pai
• Árvore binária
o Árvore com 0-2 filhos por nó

Árvore

Binary Tree

Árvores

• Terminologia
o ⇒ Root nenhum pai
o Folha ⇒ nenhuma criança
o ⇒ Interior não-folha
o Altura ⇒ distância da raiz até a última folha

nó raiz

nós folha

nós Interior

Altura

Árvores binárias de pesquisa

• propriedade Key
o Valor em nó
 Os menores valores em subárvore esquerda
 Valores maiores de subárvore direita
o Exemplo
 X> Y
 X <Z

Árvores binárias de pesquisa

• Exemplos

Árvores binárias de pesquisa

Não é uma árvore de busca binária

10

30

25

45

10

45

25

30

5
10

30

25

45

Implementação árvore binária

Nó da classe (

dados int / / Pode ser int, uma classe, etc

Nó * esquerda, direita *; / null se vazio

void inserir (int dados) (...)

void excluir (int dados) (...)

Nó * find (dados int) (...)

...

Iterativo Pesquisa de árvore binária

Nó * Encontrar (n * Node, chave int) (

while (n! = NULL) (

if (n-> dados == chave) / / Achei

n retorno;

if (> dados n> chaves) / / Na subárvore esquerda

n = n-> esquerda;

mais / / Em subárvore direita


n = n> direita;

return null;

Nó * n = Pesquisar (root, 5);

Procura recursiva de árvore binária

Nó * Encontrar (n * Node, chave int) (

if (n == NULL) / / não encontrado

return (n);

outro if (n-> dados == chave) / / Achei

return (n);

else if (> dados n> chaves) / / Na subárvore esquerda

Pesquisar retorno (> esquerda n, chave);

else / / Na subárvore direita

Pesquisar retorno (> direita n, chave);

Nó * n = Pesquisar (root, 5);

Exemplo binário procurados

• Pesquisar (raiz de 2)

10

30

2
25

45

10

30

25

45

10> 2, à esquerda

5> 2, à esquerda

2 = 2, encontrado

5> 2, à esquerda

2 = 2, encontrado

raiz

Exemplo binário procurados

• Pesquisar (raiz, 25)

10

30

25

45
5

10

30

25

45

10 <25, a direita

30> 25, deixou

25 = 25, encontrado

5 <25, direita

45> 25, deixou

30> 25, deixou

<10 25, à direita

25 = 25, encontrado

Tipos de árvores binárias

• Degenerate - apenas uma criança


• Complete - sempre duas crianças
• Balanced - "quase" dois filhos
o definições mais formal existe, acima são idéias intuitivas

árvore binária degenerada

Balanced árvore binária

árvore binária completa

Binary Propriedades Árvores


• Degenerar
o Height = O (n) para n nós
o Semelhante a lista ligada

• Equilibrado
o Height = O (log (n)) para nós n
o Útil para pesquisas

árvore binária degenerada

Balanced árvore binária

Binary Propriedades Search

• Tempo de busca
o Proporcional à altura da árvore
o Balanced árvore binária
 O (log (n)) tempo
o árvore degenerada
 O (n) o tempo
 Como pesquisar lista ligada / array unsorted

Binary Search Tree Construção

• Como construir e manter árvores binárias?


o Inserção
o Eliminação
• Manter a propriedade de chave (invariantes)
o Os menores valores em subárvore esquerda
o Os maiores valores na subárvore direita

Árvore de busca binária - Inserção

• Algoritmo
o Executar a busca para o valor X
o Pesquisa vai terminar no nó Y (se X não na árvore)
o Se X <Y, X inserir novas folhas como subárvore esquerda de novo para
Y
o Se X> Y, inserir novas folhas X como nova subárvore direita de Y
• Observações
o O (log (n)) para operação de árvore balanceada
o Inserções árvore desequilíbrio pode

Exemplo de Inserção

• Insert (20)

10

30

25

45

10 <20, direita

30> 20, deixou

25> 20, deixou

Insira 20 no lado esquerdo

20

Árvore de busca binária - Supressão

• Algoritmo
o Executar a busca para o valor X
o Se X é uma folha, exclua X
o Else / / deve excluir nó interno
a) Substituir o maior valor de Y na subárvore esquerda

Ou menor valor de Z na subárvore direita

b) valor de reposição Delete (Y ou Z) de subárvore

• Observação
o O (log (n)) para operação de árvore balanceada
o Supressões árvore desequilíbrio pode

Exemplo de exclusão (Folha)

• Delete (25)

10

30

25

45

10 <25, a direita

30> 25, deixou

25 = 25, excluir

10

30

45
Exemplo de exclusão (nó interno)

• Apagar (10)

10

30

25

45

30

25

45

30

25

45

Substituição de 10 com o maior valor na subárvore esquerda

Substituindo 5 com maior valor em subárvore esquerda


Excluindo folha

Exemplo de exclusão (nó interno)

• Delete (10)

10

30

25

45

25

30

25

45

25

30

45

Substituição de 10 com o menor valor na subárvore direita

Excluindo folha

árvore resultante
Balanced Trees Search

• Tipos de árvores de busca binária balanceada


o altura equilibrado contra equilibrada de peso
o "Rotações Tree", utilizado para manter o equilíbrio em inserir /
excluir
• Non-Árvores binárias de pesquisa
o / 3 árvores de duas
 cada nó interno tem 2 ou 3 filhos
 todas as folhas na mesma profundidade (altura equilibrada)
o B-árvores
 de 03/02 árvores Generalização
 Cada nó interno tem entre k / 2 e k crianças
 Cada nó tem uma matriz de ponteiros para as crianças
 Amplamente utilizado em bancos de dados

(Não) Procura-outras árvores

• Árvores sintáticas
o Converter a partir de representação textual para a representação da
árvore
o programa Textual em árvore
 Usados extensivamente em compiladores
o Árvore de representação de dados
 HTML por exemplo, dados podem ser representados como
uma árvore
 chamada DOM (Document Object Model) da árvore
 XML
 Assim como o HTML, mas usada para representar dados
 Tree estruturado
Parse Trees

• Expressões, programas, etc pode ser representado por estruturas de árvore


o Por exemplo, árvore de expressão aritmética
o A-(C / 5 * 2) + (D * 5% 4)

-%

A*4*

/2D5

C5

Tree Traversal

• Objetivo: visitar cada nó de uma árvore


• em ordem de passagem

void Node: inorder () (

if (esquerda! = NULL) (

cout <<"(";> inorder (esquerda); cout <<")";


)

<<Dados cout <<endl;

if (direita! = NULL)> inorder (à direita)

Saída: A - C / 5 * 2 + D *% 5 4

Para disambiguate: suportes de impressão

+
-%

A*4*

/2D5

C5

Tree Traversal (cont.)

• pré-ordem e pós-ordem:

void Node: preorder () (

<<Dados cout <<endl;

if (esquerda! = NULL) deixou-> preorder ();

if (direita! = NULL)> preorder (direita);

void Node: postorder () (

if (esquerda! = NULL) deixou-> preorder ();

if (direita! = NULL)> preorder (direita);

<<Tribunal de dados <<endl;

Saída: + - * A / C 5 2% * D 5 4

Saída: A C 02/05 * - * D 5 + 4%

-%

A*4*
/2D5

C5

XML

• Representação de Dados
o Por exemplo,
<dependency>
<object> sample1.o </ object>
<Sample1.cpp <depends> / depende>
<depends> <sample1.h / depende>
<Rule> g + + sample1.cpp <c-regra> /
</> Dependência
o representação da árvore

dependência

objeto

depende

sample1.o

sample1.cpp

depende

sample1.h

regra

g + +-c ...
Estruturas de Dados Gráfico

• Por exemplo: redes aéreas, redes rodoviárias, circuitos elétricos


• Nós e arestas
• Por exemplo: representação da classe Node
 Lojas de nome
 armazena ponteiros para todos os nós adjacentes
 i, e. == Ponteiro borda
 Para armazenar os ponteiros múltiplos: matriz ou lista
ligada utilização

Ahm'bad

Délhi

Mumbai

Calcutá

Chennai

Madurai

Fim do Capítulo
Lista Estática Seqüencial
Uma lista estática sequencial é um arranjo de registros onde estão estabelecidos
regras de precedência entre seus elementos ou é uma coleção ordenada de
componentes do mesmo tipo.
O sucessor de um elemento ocupa posição física subsequente.
Ex: lista telefônica, lista de alunos

A implementação de operações pode ser feita utilizando array e record, onde o


vetor associa o elemento a(i) com o índice i (mapeamento sequencial).

Características de Lista Estática Sequencial

• elementos na lista estão ordenados;


• armazenados fisicamente em posições consecutivas;
• inserção de um elemento na posição a(i) causa o deslocamento a direita do
elemento de a(i) ao último;
• eliminação do elemento a(i) requer o deslocamento à esquerda do a(i+1) ao
último;

Mas, absolutamente, uma lista estática sequencial ou é vazia ou pode ser escrita
como ( a(1), a(2), a(3), ... a(n) ) onde a(i) são átomos de um mesmo conjunto S.

Além disso, a(1) é o primeiro elemento, a(i) precede a(i+1), e a(n) é o último
elemento.

Assim as propriedades estruturadas da lista permitem responder a questões como:

• qual é o primeiro elemento da lista


• qual é o último elemento da lista
• quais elementos sucedem um determinado elemento
• quantos elementos existem na lista
• inserir um elemento na lista
• eliminar um elemento da lista

Consequência: As quatros primeiras operações são feitas em tempo constante.


Mas, as operações de inserção e remoção requererão mais cuidados.

Veja algumas operações básicas relacionadas com lista estática sequencial.

Vantagem:

• acesso direto indexado a qualquer elemento da lista


• tempo constante para acessar o elemento i - dependerá somente do índice.
Desvantagem:

• movimentação quando eliminado/inserido elemento


• tamanho máximo pré-estimado

Quando usar:

• listas pequenas
• inserção/remoção no fim da lista
• tamanho máximo bem definido

Vamos tentar evitar as desvantagens anteriores e usar endereços não consecutivos


(Lista Encadeada Estaticamente).

Referências

• Eric W. Weisstein "Subtree." De MathWorld-A Wolfram Web Resource.


http://mathworld.wolfram.com/Subtree.html
• Donald Knuth . A Arte da Programação de Computadores: Algoritmos Fundamentais
Third Edition. Addison-Wesley, 1997. ISBN 0-201-89683-4 . Seção 2.3: Árvores, pp. 308-
423.
• Thomas H. Cormen , Charles E. Leiserson , Ronald L. Rivest e Clifford Stein . Introdução
aos Algoritmos Second Edition. MIT Press e-McGraw Hill, 2001. ISBN 0-262-03293-7 .
Secção 10,4: Representando árvores enraizadas, pp. 214-217. Estruturas capítulos 12-14
(árvores de busca binária, árvores rubro-negras, aumentando de Dados), pp. 253-320.
• www.wikipedia.com.br Wikipédia, a enciclopédia livre