author | title | subtitle | date | transition | fontsize | header-includes | ||
---|---|---|---|---|---|---|---|---|
Igor Machado Coelho |
Estruturas de Dados I |
Árvores |
05/10/2020 - 26/04/2023 |
cube |
10 |
|
São requisitos para essa aula:
- Introdução/Fundamentos de Programação (em alguma linguagem de programação)
- Interesse em aprender C/C++
- Noções de recursividade
- Noções de tipos de dados
- Noções de listas e encadeamento
Agradecimentos especiais ao prof. Fabiano Oliveira e prof. Fábio Protti, cujos conteúdos didáticos formam a base desses slides
A Árvore (do inglês Tree) é um Tipo Abstrato de Dado (TAD) que pode assumir duas formas:
- árvore
$T$ vazia, denotada por$T = \emptyset$ - árvore
$T$ composta por:- um nó
$R$ chamado de nó raiz -
$0$ ou mais árvores disjuntas$T_1$ ,$T_2$ , ..., associadas a$R$ ; tais árvores são chamadas de subárvores
- um nó
Um conjunto de árvores é chamado floresta
Se T é uma árvore com raiz R, então:
- os nós de T são todas as raízes de subárvores de R, além da raiz de T
- um nó com 0 filhos é chamado de folha (do inglês leaf)
- se um nó F é um filho de um nó P, denominamos P como pai de F
- a raiz é um nó ancestral de todos nós da árvore
- todos os nós da árvore são descendentes do nó raiz
Um caminho em uma árvore é uma sequência de nós com relação filho de ou pai de:
Exemplos:
- E,C,A
- D,G,I
- C,A,D,G
O tamanho de um caminho consiste no número de nós. O nível de um nó é o tamanho de seu caminho até a raiz:
Nível de: A=1; C=2; F=3; H=4.
Desafio: em cursos de Teoria dos Grafos é provado que existe um único caminho conectando dois nós na árvore. Utilize sua intuição para verificar esta afirmação!
A altura de nó X é o tamanho do maior caminho que conecta X a uma folha descendente. Denotamos a altura de
Alturas:
A altura da árvore é a altura de sua raiz!
No exemplo,
Uma árvore é dita ordenada se há uma ordem associada aos filhos de cada nó.
Uma árvore é dita
A árvore acima é ternária (podendo também ser
Em árvores binárias ordenadas de raiz R, a primeira subárvore de cada nó é denominada subárvore à esquerda de R (cuja raiz se chama filho esquerdo), e a segunda é a subárvore à direita de R (cuja raiz se chama filho direito).
Exemplo: B é filho esquerdo e C é filho direito de A
Uma árvore estritamente $m$-ária é aquela na qual cada nó possui exatamente
Exemplo: Considere a inclusão de um filho à esquerda de C.
Observação: Chamada pelo NIST de full binary tree, embora também seja preferivelmente chamada de própria (ou proper).
Uma árvore $m$-ária cheia (ou perfeita) é aquela na qual todo nó com alguma subárvore vazia está no último nível.
Observação: Chamada pelo NIST de perfect binary tree (ou perfect k-ary tree), embora também seja chamada de full ou, preferencialmente perfect.
Uma árvore $m$-ária completa é aquela na qual todo nó com alguma subárvore vazia está no último ou penúltimo níveis, estando os nós do último nível completamente preenchidos da esquerda para a direita.
Observação: Chamada pelo NIST de complete binary tree. Note que alguns autores consideram essa mesma definição para árvores cheias ou perfeitas. O ponto fundamental é a facilidade de implementação em vetores (vide próximos slides). [Knuth97]1
-
Qual a altura máxima de uma árvore binária com
$n$ nós? -
Qual a altura máxima de uma árvore estritamente binária com n nós?
-
Qual a altura mínima de uma árvore binária com n nós?
-
Numa árvore binária cheia com n nós, qual o número de nós no último nível?
. . .
Solução:
Apresentaremos dois tipos de implementação para o TAD Árvore: Sequencial e Encadeada.
Note que, nesse momento, não apresentaremos operações sobre o TAD Árvore, focando somente em sua representação interna. A razão é que existem diversos tipos específicos de árvores, que apresentam operações distintas no TAD, de acordo com seu propósito.
Consideramos uma implementação de árvore
constexpr int M = 3; // aridade M=3 (ternária)
class NoEnc1
{
public:
char chave // dado armazenado
NoEnc1* nosFilhos[M]; // ponteiros para filhos
};
class ArvoreEnc1
{
public:
NoEnc1* raiz; // raiz da árvore
};
Consideramos uma implementação de árvore
constexpr int M = 3; // aridade M=3 (ternária)
class NoEnc2
{
public:
char chave; // dado armazenado
NoEnc2* prox; // proximo elemento
NoEnc2* nosFilhos; // ponteiro único para filhos
};
class ArvoreEnc2
{
public:
NoEnc2* raiz; // raiz da árvore
};
Consideramos uma implementação de árvore
Note que podemos reescrever os ponteiros de NoEnc2
com os termos esq
e dir
(nó esquerdo e nó direito).
class NoEnc3
{
public:
char chave; // dado armazenado
NoEnc3* esq; // filho esquerdo
NoEnc3* dir; // filho direito
};
class ArvoreEnc3
{
public:
NoEnc3* raiz; // raiz da árvore
};
Consideramos uma implementação de árvore binária, com alocação encadeada de nós.
Note que podemos reescrever os ponteiros de NoEnc3
utilizando unique_ptr
, para maior segurança:
class NoEnc4
{
public:
char chave; // dado armazenado
std::unique_ptr<NoEnc4> esq; // filho esquerdo
std::unique_ptr<NoEnc4> dir; // filho direito
};
class ArvoreEnc4
{
public:
std::unique_ptr<NoEnc4> raiz; // raiz da árvore
};
Observamos pelas implementações NoEnc2
e NoEnc3
que uma árvore
As Árvores com Implementação Sequencial utilizam um array para armazenar os dados. Assim, os dados sempre estarão em um espaço contíguo de memória.
Desafio: quanto espaço é necessário para armazenar uma árvore qualquer com altura
Consideraremos uma árvore sequencial com, no máximo, MAX_N
elementos do tipo caractere.
constexpr int MAX_N = 50; // capacidade máxima da árvore
class ArvoreSeq1
{
public:
char elem [MAX_N]; // elementos na fila
};
Desafio: Quantos níveis cabem nessa árvore?
Note que, para esse fim, somente as árvores completas terão maior eficiência, utilizando uma representação por níveis.
Desafio: onde fica o primeiro elemento de cada nível da árvore
Dado um nó
- o pai de
$V$ ? - os filhos de
$V$ ?
. . .
Resposta: considerando contagem 1..MAX_N, estarão respectivamente nas posições
Desafio: considere a contagem 0..MAX_N-1 e refaça o cálculo.
. . .
Solução: posições
Fim parte de implementações.
Como "imprimir" uma árvore?
Estruturas lineares tem uma intuição mais direta para o conceito de impressão, mas para estruturas arbóreas isso já não é tão direto. Além da impressão, muitas vezes é desejável efetuar outras operações ou visitas em nós de uma árvore.
Operações de Percursos em Árvore (do inglês, tree traversals) apresentam uma solução para isso:
- Percurso de pré-ordem (do inglês, preorder)
- Percurso de pós-ordem (do inglês, postorder)
- Percurso em-ordem ou ordem simétrica (do inglês, inorder)
No percurso de pré-ordem, o nó é visitado primeiro, depois os filhos esquerdos, e finalmente, são visitados os filhos direitos.
- Aplicação: impressão da ordem de visita (pilha de execução) para algoritmos recursivos em árvore.
No percurso de pós-ordem, os filhos esquerdos são visitados primeiro, depois os filhos direitos, e finalmente o nó é visitado.
- Aplicação: calcular altura de um nó (note que a altura de um nó depende da altura de seus filhos).
No percurso em-ordem, os filhos esquerdos são visitados primeiro, depois o nó é visitado, e finalmente os filhos direitos são visitados.
- Aplicação: impressão "visual" da árvore como caracteres na tela (desafio!). Visita ordenada em árvores com propriedades de busca e mapas (próxima aula).
void preordem(auto* no) {
if(no) {
printf("%c\n", no->chave); // operação ou "visita"
preordem(no->esq);
preordem(no->dir);
}
}
Apresente o percurso de pré-ordem para as árvores abaixo:
. . .
Solução: 1. ABCDFGE 2. ABCD 3. ABCD 4. ABDECFG
void posordem(auto* no) {
if(no) {
posordem(no->esq);
posordem(no->dir);
printf("%c\n", no->chave); // operação ou "visita"
}
}
Apresente o percurso de pós-ordem para as árvores abaixo:
. . .
Solução: 1. BFGDECA 2. DCBA 3. DCBA 4.DEBFGCA
void emordem(auto* no) {
if(no) {
emordem(no->esq);
printf("%c\n", no->chave); // operação ou "visita"
emordem(no->dir);
}
}
Apresente o percurso de ordem simétrica para as árvores abaixo:
. . .
Solução: 1. BAFDGCE 2. DCBA 3. ABCD 4. DBEAFCG
Fim parte de percursos.
Além da bibliografia do curso, recomendamos para esse tópico:
- Szwarcfiter, J.L; Markenzon, L. Estruturas de Dados e seus Algoritmos. Rio de Janeiro, LTC, 1994. Bibliografia Adicional:
- Cerqueira, R.; Celes, W.; Rangel, J.L. Introdução a estruturas de dados: com técnicas de programação em C. Editora, 2004.
- Cormen, T.H.; Leiserson, C.E.; Rivest, R.L.; Stein Algoritmos: Teoria e Prática. Ed. Campus, 2002.
- Cormen, T.H.; Leiserson, C.E.; Rivest, R.L.; Stein, C. Introduction to Algorithms, 3rd ed.. The MIT Press, 2009.
- Preiss, B.R. Estruturas de Dados e Algoritmos Ed. Campus, 2000;
- Knuth, D.E. The Art of Computer Programming - Vols I e III. 2nd Edition. Addison Wesley, 1973.
- Graham, R.L., Knuth, D.E., Patashnik, O. Matemática Concreta. Segunda Edição, Rio de Janeiro, LTC, 1995.
- Livro "The C++ Programming Language" de Bjarne Stroustrup
- Dicas e normas C++: https://github.com/isocpp/CppCoreGuidelines
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
- pandoc
- LaTeX
- GNU/Linux
- git
- markdown-preview-enhanced (github)
- visual studio code
- atom
- revealjs
- groomit-mpx (screen drawing tool)
- xournal (screen drawing tool)
- ...
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
- github
- gitlab
- microsoft
- ...
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho
Footnotes
-
[Knuth97] Donald E. Knuth, The Art of Computer Programming, Addison-Wesley, volumes 1 and 2, 2nd edition, 1997. ↩