Baixe o app para aproveitar ainda mais
Prévia do material em texto
ESTRUTURA DE DADOS Sandra Rovena Frigeri APRESENTAÇÃO Algoritmos são desenvolvidos para resolver problemas, assim expressam soluções computacionais que atendem necessidades específicas. Desejável que os algoritmos façam um uso eficiênte dos recursos computacionais (memória, armazenamento e processamento), para isso, é necessário fazer escolhas e implementações adequadas das estruturas de dados. Isso é importante, pois a forma como os dados estão organizados durante o processamento fazem com que o sistema tenha uma melhor ou pior performance. Já, as estruturas de dados definem a organização dos dados e as funcionalidades de acesso e manipulação desses. Dessa forma, o principal objetivo deste material é apresentar as estruturas de dados mais utilizadas no desenvolvimento de programas, descrever sua organização de dados, sua lógica de funcionamento e operações de manipulação. 3ESTRUTURA DE DADOS SUMÁRIO CENTRO UNIVERSITÁRIO UNIFTEC Rua Gustavo Ramos Sehbe n.º 107. Caxias do Sul/ RS REITOR Claudino José Meneguzzi Júnior PRÓ-REITORA ACADÊMICA Débora Frizzo PRÓ-REITOR ADMINISTRATIVO Altair Ruzzarin DIRETORA DE EDUCAÇÃO A DISTÂNCIA (NEAD) Lígia Futterleib Desenvolvido pelo Núcleo de Educação a Distância (NEAD) Designer Instrucional Sabrina Maciel Diagramação, Ilustração e Alteração de Imagem Igor Zattera, Gabriel Olmiro de Castilhos Revisora Ana Clara Garcia APRESENTAÇÃO 2 INTRODUÇÃO 4 PILHAS (STACK) 9 Tipos Abstratos de Dados (TAD) 10 TAD Pilha 13 Implementação do TAD Pilha 17 FILAS (QUEUE) 26 Implementação do TAD fila com Encadeamento 29 LISTAS (LIST) 34 Tipos de Listas 35 Implementação do TAD com lista de encadeamento 36 ÁRVORES (TREE) 45 Árvores Binárias 47 GRAFOS (GRAPH) 61 Definições 63 Representação 66 Menor Caminho 67 MÉTODOS DE ORDENAÇÃO 72 Bubble Sort 73 REFERÊNCIAS 81 4ESTRUTURA DE DADOS INTRODUÇÃO “Imagination is more important than knowledge. For knowledge is limited, where an imagination embraces the entire world, stimulating progress, giving birth to evolution” (Albert Einstein) Você tem ideia de como são organizados os dados dos programas na memória? Você já se perguntou como se organizam as relações entre os dados? A estrutura básica de organização dos dados nos pro- gramas se efetiva através das variáveis, que são vinculadas aos tipos de dados (numérico, alfanumérico, booleano e seus derivados). Cada tipo de dado restringe os possíveis valores que as variáveis/constantes podem receber, além de determi- nar as operações que podem ser realizadas sobre esses dados. Vamos analisar juntos alguns exemplos: 1. Considere que você tem uma variável numérica (inteira) para armazenar a idade de uma pessoa. Você poderá fazer operações de soma, subtração, divisão, multiplicação com essa variável, pois são operações definidas para o tipo de dado inteiro. Além disso, essa variável poderá receber somente números inteiros (neste caso, valores reais – ponto-f lutuante – não poderão ser atribuídos à variável, nem dados alfanuméricos - caracteres). 5ESTRUTURA DE DADOS 2. Agora, se você tiver uma variável alfanumérica para guar- dar um endereço, as operações permitidas irão concatenar strings, buscar um substring, substituir elementos na string, enfim, operações definidas para variáveis deste tipo. Essa variável pode receber informação textual (letras, dígitos numéricos e alguns caracteres especiais). 3. E se você tiver uma variável do tipo booleano para testar se o usuário confirmou a opção de compra, esta variável poderá receber somente os valores Verdadeiro (true) ou Falso (false), bem como as operações, normalmente, res- tringem-se às operações booleanas (conjunção “e lógico”, disjunção “ou lógico”, negação “não lógico”, entre outras). A capacidade de armazenamento (tamanho), extensão dos valores permitidos e operações permitidas para cada tipo de dado podem variar de uma linguagem de programação para outra. Esses tipos de dados disponibilizados pela linguagem são denominados de tipos primitivos. Além desses, as linguagens de programação possuem estruturas para definir conjuntos de dados. Esses podem ser homogêneos, e são representados pelos vetores, os quais são conjuntos de dados em que cada elemento possui um índice de acesso e todos os elementos possuem o mesmo tipo. Desse modo, podemos ter vetores com elementos numéricos, alfanuméricos, booleanos e também podendo ser conjuntos. Por exemplo, po- demos ter vetores de temperaturas, vetores de idades, vetores de nomes, vetores de dados de pessoas. Os conjuntos de dados também podem ser heterogêneos, ou seja, formado por diversos dados e cada dado definido com um tipo específico, esses conjuntos são normalmente deno- minados registros (estruturas). Por exemplo, podemos ter um registro com os dados cadastrais de uma pessoa, contendo có- digo (numérico), nome (alfanumérico), idade (numérico), sexo (alfanumérico), entre outros dados. Nesta disciplina vamos estudar os tipos abstratos de dados, que são definidos pelo usuário (programador), utilizando os tipos primitivos e estruturas de conjuntos de dados disponibilizados pelas linguagens de programação. Vamos entender o conceito de estrutura de dados? Estruturas de dados (Data Structures) é a denominação da organização de dados e algoritmos. De modo a otimizar o 6ESTRUTURA DE DADOS seu uso, dependendo desses elementos, podemos solucionar de forma simples problemas extremamente complexos. Também, existem diversos modelos de estruturas de dados, e novos mo- delos são criados constantemente, pois acompanham a evolução dos algoritmos e das linguagens de programação. Você deve estar se perguntando em quais situações precisará utilizar estruturas de dados! Na verdade, todos os programas possuem estruturas de dados, mesmo que sejam variáveis simples! A seguir, vamos analisar alguns exemplos de situações que exigem estruturas de dados específicas! Considere que você precisa organizar um fichário com dados de clientes. A forma mais prática seria manter as fichas em ordem alfabética, pois facilitará a localização. As operações básicas que serão realizadas nesse fichário serão: incluir uma nova ficha (mantendo a ordenação), excluir fichas, consultar fichas, entre outras. Nesse caso, a estrutura de dados que vamos utilizar é denominada LISTA (LIST), que provê uma sequência de in- formações que se mantém ordenada por algum dos dados. Outra situação é organizar as pessoas que precisam ser atendidas em um guichê. A forma mais racional de resolver essa situação é organizar as pessoas em fila, sendo que novas pessoas ficam no fim da fila e será atendido primeiro quem estiver no início da fila. As operações básicas serão: incluir nova pessoa na fila, consultar posição da fila, excluir pessoa da fila (em caso de alguém desistir). A estrutura de dados que utilizaremos tem o mesmo nome, chama-se FILA (QUEUE), que provê uma sequência de informações que são ordenadas pela ordem de entrada das informações na FILA. O critério de ordenação de uma FILA denomina-se FIFO (First-In-First-Out, o primeiro a entrar é o primeiro a sair). 7ESTRUTURA DE DADOS Vamos analisar outra situação: em uma organização em- presarial deseja-se saber a estrutura dos cargos e as pessoas que ocupam cada um deles, considerando que cada cargo somente tem um superior direto. Nessa situação, constrói-se o organo- grama da empresa. Logo, as operações básicas serão a inclusão de novos cargos, a relação hierárquica de um cargo, a vinculação de uma pessoa a um cargo, a exclusão de um cargo, entre outras. Para atender a essa necessidade, utilizaremos uma estru- tura de dados hierárquica, denominada ÁRVORE (TREE), que possui uma origem (raiz, no exemplo o cargo mais alto da organização) e todos os outros elementos serão organizados, de forma hierárquica, “abaixo” deste. Agora, considere que você precisa organizar os pratos de um restaurante, a melhor forma é dispô-los em pilhas. Assim, o primeiro prato ficana base da pilha e o último é sempre colo- cado no topo da pilha. Para usar os pratos, pegaremos primeiro aquele que estiver no topo da pilha e assim, subsequentemente, visto que, se tentarmos puxar o prato da base, derrubaremos a pilha. As operações básicas serão colocar pratos na pilha, tirar pratos da pilha, consultar tamanho da pilha, entre outras. Utilizaremos aqui uma estrutura de dados que se chama PILHA (STACK), que é uma estrutura sequencial, a qual segue o critério LIFO de ordenação dos elementos (Last-In-First-Out, o último a entrar é o primeiro a sair). 8ESTRUTURA DE DADOS Agora, se tivermos que organizar um trajeto para per- correr todos os pontos turísticos de uma cidade. Teríamos que consultar um mapa e estabelecer as possíveis rotas e depois escolher a melhor delas. As operações possíveis seriam indicar os destinos, calcular os tempos e distâncias para ir de um ponto a outro, calcular rotas alternativas, etc. A representação de mapas e informações geográficas são normalmente construídas através de GRAFOS (GRAPH), que são estruturas genéricas que permitem organizar diversos elementos, estabelecendo relações entre eles, aos pares. Esses exemplos de aplicações referem-se às estruturas de dados básicas: Pilhas, Filas, Listas, Árvores e Grafos, que são o foco de estudo desta disciplina. Percebas que a maioria das estruturas de dados tem por princípio alguma forma de ordenação dos dados, principalmente porque isso facilita as consultas. Assim, também estudaremos diversos métodos de ordenação de dados. Temos certeza que você gostará de aprender o funciona- mento e o desenvolvimento computacional das estruturas de dados! Amplie seus conhecimentos! Pense (pesquise) outras aplicações que podem ser solucionadas com as estruturas de dados que estudaremos nesta disciplina: PILHAS, FILAS, LISTAS, ÁRVORES e GRAFOS. 9ESTRUTURA DE DADOS PILHAS (STACK) Você já se deu conta que muitos termos da informática foram tirados no mundo real e adaptados ao contexto computacional? Isso ocorre, porque a informática busca imitar, simular e replicar o mundo real. Entretanto, devido às limitações tecnológicas, o que se faz é uma abstração da nossa realidade para que possamos capturar o que existe de mais relevante em uma situação real, possibilitando a construção de mode- los que constituem soluções computacionais que possam ser implementadas. 10ESTRUTURA DE DADOS Uma abstração é uma representação que construímos de uma determinada realidade. É desejável que sejam construídos bons modelos, para tanto, é preciso conhecer bem os detalhes que formam a situação real e expressá-los através de uma es- trutura de dados adequada, além de implementar os algoritmos necessários para sua manipulação. Isso nos leva ao conceito de Tipo Abstrato de Dados (TAD), que é um agregado composto por um conjunto de dados e um conjunto de funcionalidades (operações) e operadores que podem ser utilizados para manipulação desses dados. Tudo isso, para atender uma necessidade específica. Tipos Abstratos de Dados (TAD) Vamos entender melhor esse conceito de TAD! Suponha que criamos uma variável do tipo ferramenta, ela poderia assumir os valores serrote, martelo, alicate, etc, e pode- ríamos fazer as seguintes operações com esta variável: comprar, afiar, utilizar, vender, etc. Este seria o nosso TAD ferramentas. Assim, ao definir um TAD, não levamos em conta de- talhes de implementação. Pode até ser que não seja possível implementar um TAD numa determinada linguagem, mas devemos saber para que serve, que informações contém e quais operações/operadores podem ser utilizados. 11ESTRUTURA DE DADOS Quando devemos identificar e projetar os Tipos Abstratos de Dados? De forma resumida, podemos dizer que o ciclo de vida de desenvolvimento de software engloba as etapas de análise, projeto, implementação, teste e manutenção. Na etapa de aná- lise, nós iremos determinar o que o sistema deverá fazer, qual seu foco, quais informações irá manipular, etc. Já na etapa de projeto, definiremos como o sistema será desenvolvido. Aqui, a ideia é especificar uma definição geral, mas não os detalhes de desenvolvimento. Nessa etapa, decidimos a arquitetura do software, organizamos seus principais módulos, assim, a partir disso, podemos organizar a equipe de programadores para sua implementação. As outras etapas são relativas à implementação: codificação em uma linguagem de programação; testes para garantir que o software funcione adequadamente; e manutenção, que se refere aos ajustes que podem ser necessários a tempo de uso. Assim, os TAD serão especificados (identificados e pro- jetados) na etapa de projeto do software, ou seja, quando cons- truímos uma visão geral de como o software deverá ser desen- volvido, sem nos preocuparmos com detalhes de implementação. Além disso, tenha em mente que o princípio básico do Tipo Abstrato de Dados (TAD) é permitir que o programador pos- sa separar o conceito (aquilo que o TAD deve ser e fazer) de sua implementação (detalhes de como deve ser desenvolvido, recursos, etc.). Essa separação possibilita que sejam realizadas mudanças na implementação e essas não alterarão o programa que utiliza o TDA. Agora reflita: por que devemos utilizar Tipos Abstratos de Dados? Podemos indicar algumas vantagens: • É mais fácil programar quando não é necessário se pre- ocupar com detalhes de implementação. • É mais fácil garantir a integridade dos dados, pois apenas as operações do TAD podem alterar os dados armaze- nados. 12ESTRUTURA DE DADOS A utilização de Tipos Abstratos de Dados (TAD) melhora a portabilidade e a reutilização do software, reduzindo os custos de desenvolvimento e manutenção. Um “programa” que não funciona, na verdade não é um programa! • Maior independência e portabilidade de código, pois alterações na implementação de um TAD não provocam alterações nas aplicações. • Maior potencial de reutilização de código, pois alterações na lógica de um programa não implicam na reconstrução das estruturas de armazenamento. Pilhas, Filas, Listas, Árvores e Grafos são tipos abs- tratos de dados! A utilização de TAD está relacionada com o desenvolvi- mento de bons programas! Mas o que significa desenvolver um bom programa? É suficiente “funcionar”? Mas para ser um bom programa não é suficiente apenas funcionar! Um bom programa deve ter algumas características que os diferenciarão dos programas ruins: • Um importante critério que caracteriza bons programas é sua eficiência, ou seja, programas que são rápidos e utilizam pouca memória, normalmente, são considerados melhores. Isso, porque muitas vezes temos restrições de espaço de armazenamento e requisitos de rapidez. • Outra característica relevante é a reusabilidade! Você sabe o que isso significa? De forma simplificada, reutilizar, ou seja, você desenvolve um programa com um objetivo e este pode ser reutilizado dentro de outro programa/sistema. Esta é a ideia básica das bibliotecas de funções. • Nós temos diversos tipos de computadores e diferentes sistemas operacionais, assim é desejável que os sistemas (programas) possam ser executados em diferentes plata- formas. Essa característica de um programa é denominada portabilidade de software e é muito relevante para avaliar um bom programa. Softwares que possuam portabilidade e reusabilidade têm custo mais baixo de desenvolvimento e manutenção. 13ESTRUTURA DE DADOS TAD Pilha O que é uma Pilha? Como uma pilha se transforma no TAD Pilha? Quando empilhamos objetos construímos uma pilha! Assim podemos ter pilhas de livros, pilhas de pratos, pilhas de cartas, pilhas de roupas, etc. Todas essas pilhas possuem um comportamento em comum: sempre que se deseja colocar um novo objeto na pilha, este é colocado no topo da mesma. Ele é empilhado, colocado em cima do último objeto que foi colocado na pilha. Para retirar objetos da pilha, retira-se, desempilha-se o objeto que está notopo, pois, de forma geral, se tentarmos tirar algum outro objeto acabaremos por derrubar a pilha! O tipo abstrato de dados PILHA (TAD PILHA) possui a mesma lógica de funcionamento das pilhas de objetos, desta maneira podemos caracterizá-lo como: • Conjunto ordenado de elementos, em que o critério de ordenação é a ordem de inclusão; • Novos elementos são sempre empilhados no topo da pilha; • O único elemento que pode ser retirado da pilha é o que está no topo; • Obedece ao critério LIFO (Last-In-First-Out), ou seja, o último a entrar é o primeiro a sair. Agora, podemos definir um conjunto de operações básicas do TAD PILHA: • Cria(P): função que cria a pilha na memória do compu- tador e faz a inicialização de dados, necessários ao seu adequado funcionamento. 14ESTRUTURA DE DADOS • Vazia(P): função que verifica se a pilha passada como parâmetro está vazia, ou seja, não possui elementos. • Empilha(P, X): função que empilha o elemento X na pilha P, ambos passados como parâmetros. Costumeiramente, essa função é nomeada em inglês como: PUSH(P, X) • Desempilha(P, X): função que desempilha o topo da pilha P e o atribui ao parâmetro X. Normalmente essa função é nomeada em inglês como: POP(P, X) • Libera(P): função que libera da memória as estruturas que foram utilizadas para a pilha, ou seja, destrói a pilha. Também podemos definir algumas operações complemen- tares, que podem fazer parte do conjunto de operações básicas do TAD PILHA: • Cheia(P): função que verifica se uma pilha está cheia, pois normalmente as pilhas no computador possuem um limite de capacidade de armazenamento de elementos e este pode ter sido atingido. • Topo(P, X): função que consulta o topo da pilha P e o atribui ao parâmetro X. Geralmente, essa função é no- meada em inglês como: TOP(P, X) • Tamanho(P, T): função que conta quantos elementos tem na pilha e atribui ao parâmetro T. Destacamos que esse conjunto de operações básicas pode variar! Utilizando essas operações básicas, diversas outras ope- rações podem ser construídas para trabalhar com pilhas. Por exemplo, suponha que temos duas pilhas P1 e P2, P1 tem um conjunto de elementos, P2 está vazia e desejamos transferir os elementos de P1 para P2. Assim, poderíamos descrever a operação Transfere (P1, P2) para atender a esse objetivo. Lem- bre-se que podemos utilizar somente as operações básicas de manipulação do TAD PILHA. A seguir, veja uma proposta de implementação dessa nova operação: Vamos entender o funcionamento desta função e o uso das operações básicas do TAD PILHA. Considere que a pilha P1 está definida da seguinte ma- neira: P1(3, 6, 2, 9), sendo que o número 3 foi o primeiro a ser //Função que transfere todos os elementos de P1 para P2 Transfere(parâmetros por referência P1, P2 do tipo PILHA){ Variável Elemento do tipo Dados_Pilha; Enquanto ( Vazia(P1) == Falso ) Faça { Desempilha(P1, Elemento); Empilha(P2, Elemento); } } 15ESTRUTURA DE DADOS colocado na pilha (base da pilha) e o número 9 foi o último a ser colocado, logo, ele está no topo da pilha. As próximas figuras demonstram o processo de execução da função Transfere (P1, P2), conforme a implementação apresentada. Perceba que a função possui uma repetição e enquanto a pilha P1 não estiver vazia, será repetido o processo que desempilha um elemento da pilha P1 e o empilha na pilha P2. Assim, na iteração 1, será desempilhado de P1 o elemento 9 e este será empilhado em P2. Na iteração 2, será desempilhado de P1 o elemento 2 e este será empilhado em P2. Na sequência, na iteração 3, será desempilhado de P1 o elemento 6 e este será empilhado em P2. Depois será desempilhado de P1 o elemento 3 e este será empilhado em P2. Agora, a pilha P1 está vazia, portanto a repetição termina e todos os elementos da pilha P1 foram transferidos para a pilha P2. Agora, vamos analisar como ficaram os elementos da pilha P2: Eles estão dispostos da mesma maneira que estavam na pilha P1? Verifique se os elementos estão invertidos! Ou seja, a pilha P2 tem os mesmos elementos que a pilha P1, porém eles não estão na mesma ordem! Mas se nós quiséssemos que, ao transferir os elementos da pilha P1 para a pilha P2, eles fossem mantidos na mesma ordem, como deveríamos fazer essa implementação? Vamos construir juntos essa solução? Você já percebeu que ao utilizar a função Transfere(P1, P2), ela transfere todos os elementos da pilha P1 para a pilha 16ESTRUTURA DE DADOS P2, porém os inverte. Agora, para construir uma função que transfira os elementos de P1 para P2, mas preservando a ordem, teríamos que chamar duas vezes a função transfere, a primeira para inverter e a segunda para “desinverter”, passando os dados por uma pilha auxiliar (AUX). Desta forma, a função Trans- fereIgual(P1, P2) pode ser implementada da seguinte maneira: Note, que ainda não nos preocupamos com a implementa- ção (codificação), na linguagem de programação, das operações básicas do TAD PILHA, mas sim com o funcionamento do TAD e utilização de suas operações básicas! Essa é a essência dos Tipos Abstratos de Dados, você tem uma estrutura de informações (no caso atual o tipo PILHA) e um conjunto de operações básicas, a partir desses elementos você constrói as aplicações que utilizam pilhas! Isso é semelhante ao uso do tipo primitivo inteiro das linguagens de programação, você não se preocupa em como são implementadas pelo compilador as operações aritméticas, apenas as utiliza quando precisa, certo? Essa é a mesma ideia do TAD. Ainda, precisamos conhecer dois conceitos relativos aos tipos abstratos de dados: operações primitivas e operações não primitivas. As operações primitivas de um TAD são aquelas que dependem da estrutura de armazenamento do TAD e dos recursos específicos da linguagem de programação para serem implementadas, são as operações básicas do TAD. Já as opera- ções não primitivas de um TAD são aquelas que não dependem da estrutura de armazenamento do TAD e são implementadas através de chamadas de operações primitivas (básicas) do TAD. Então, vamos analisar como são implementadas as ope- rações primitivas do TAD PILHA. //Função que transfere todos os elementos de P1 para P2, //preservando a ordem original TransfereIgual(parâmetros por referência P1, P2 do tipo PILHA){ Variável AUX do tipo PILHA; Transfere(P1, AUX); Transfere(AUX, P2); } Desafio! Construa uma nova função utilizando as operações básicas da TAD PILHA com o seguinte objetivo: Verificar se duas pilhas são exatamente iguais. TestaIgual(P1, P2) 17ESTRUTURA DE DADOS Implementação do TAD Pilha É possível implementar a TAD PILHA de diversas ma- neiras, e elas se distinguem pela natureza dos seus elementos, pela forma de armazenamento dos elementos e pelas operações de tratamento da pilha que são disponibilizadas. Assim, para implementar as operações primitivas do TAD PILHA, é ne- cessário definir a estrutura de armazenamento dos dados da pilha e a partir dessa desenvolver as funções de manipulação desta estrutura. Para nosso estudo consideraremos duas alternativas de implementação de pilha: a primeira com vetor e a outra com lista encadeada. Para tornar mais simples os exemplos, nós consideraremos que pilha armazena valores reais. Independente da alternativa de implementação, podemos definir a interface do TAD PILHA, que é composta pelas operações que serão disponibilizadas para manipular e acessar as informações da pilha. Para nossa implementação consideraremos as seguintes operações primitivas do TAD PILHA: Cria; Vazia; Push; Pop; Libera. Inicialmente, precisamos definir a interface do TAD PILHA, essa terá a estrutura de dados da pilha e a assinatura das funções de manipulação dessa estrutura (a definição do nome das funções, seus parâmetros e retornos). Se considerarmos a implementação na linguagem C, para a definição da interface deste TAD precisaremos criar um ar- quivo pilha.h, que terá o seguinte código: typedef struct pilha Pilha;//estruturado tipo pilha Pilha* cria (void);//função para criação da pilha void push (Pilha* p, float v);//função para empilhar v na pilha p float pop (Pilha* p);//função que retorna o valor do topo e o desempilha int vazia (Pilha* p);//função que testa se a pilha está vazia void libera (Pilha* p);//função que libera a memória 18ESTRUTURA DE DADOS Implementação do TAD pilha com vetor É possível implementar o TAD PILHA usando um sim- ples vetor? Sim! Muitas vezes, nas aplicações que utilizam pilhas, é possível conhecer o tamanho máximo dela, ou seja, sua quan- tidade máxima de elementos é previamente conhecida e nessa situação podemos utilizar um vetor para implementar o TAD PILHA. A implementação será muito simples e a estrutura da pilha deverá ter o vetor e a quantidade de elementos armazena- dos na mesma. Se a quantidade de elementos for igual a zero, a pilha estará vazia. Na linguagem C, o primeiro elemento do vetor possui o índice 0 (zero), dessa maneira, o índice do topo será a quantidade de elementos menos um. Assim, podemos definir a estrutura da pilha conforme mostrado a seguir, onde MÁXIMO define a quantidade máxima de elementos que a pilha poderá conter, tamanho armazenará a quantidade de elementos empilhados e o vetor conterá estes elementos. Vamos implementar agora as funções de manipulação do TAD PILHA. Para implementar a função que cria a pilha, deveremos alocar dinamicamente a pilha e inicializar seu tamanho com zero, indicando a pilha que está vazia. Veja a seguir a imple- mentação desta função na linguagem C: Para implementar a função que testa se a pilha está vazia, precisamos apenas testar o tamanho dela. A seguir você pode verificar essa implementação. #define MAXIMO 100 struct pilha { int tamanho; float vetor[MAXIMO]; }; Pilha* cria () { Pilha* p = (Pilha*) malloc(sizeof(Pilha)); p->tamanho = 0; /* inicializa com zero elementos */ return p; } 19ESTRUTURA DE DADOS A função para liberar a memória alocada pela pilha é bastante simples, basta liberar o ponteiro criado para armazenar a pilha, veja a seguir como fica esta implementação: Para empilhar um elemento na pilha, será usada a próxi- ma posição livre do vetor, se ainda houver espaço (considerar o limite máximo de elementos). A seguir, você poderá verificar a implementação desta função: Para desempilhar o elemento que está no topo da pilha, precisamos primeiro verificar se a mesma possui elementos, se não está vazia. Assim, logo você poderá analisar a implemen- tação da função POP: Tenho certeza que você achou bem simples essas imple- mentações, e se desejar fazer uma aplicação que utilize pilhas com tamanho máximo predefinido, poderá utilizar as funções dadas anteriormente para constituir a interface do TAD PILHA e, então, apenas precisará utilizar as operações primitivas sem ter que se preocupar com sua implementação, e elas já estarão prontas! int vazia (Pilha* p) { return (p->tamanho == 0); } void libera (Pilha* p) { free(p);//libera a memória alocada para a pilha } void push (Pilha* p, float v) { if (p->tamanho == MAXIMO){ /*esgotou tamanho da pilha */ printf(“Pilha atingiu limite máximo.\n”); } else { /* novo elemento é inserido depois do topo */ p->vetor[p->tamanho] = v;//guarda o valor no topo p->tamanho = p->tamanho + 1;//incrementa o tamanho } } float pop (Pilha* p) { float v; if (vazia(p)) { printf(“Não há elementos na pilha.\n”); } else { /* desempilha elemento do topo */ v = p->vetor[p->tamanho - 1];//consulta o valor do topo p->tamanho = p->tamanho – 1;//decrementa o tamanho return v; } } 20ESTRUTURA DE DADOS Implementação do TAD pilha com encadeamento Dados encadeados ficam semelhantes a uma corrente, co- nectados! Muitas vezes o número máximo de elementos que será armazenado na pilha não é conhecido, nessa situação, a melhor forma de fazer a implementação é utilizar alocação dinâmica (onde a memória é gerenciada sob demanda em tempo de exe- cução) através de encadeamento, elementos interligados. Parece complicado, mas não é! A ideia básica do encadeamento é fazer a ligação entre os elementos! Quando utilizamos alocação dinâmica é reservado um espaço na memória e para acessarmos esse dado, temos o endereço de onde foi alocado. Para fazer um encadeamento referenciamos o endereço de um elemento em outro elemento, dessa forma eles ficam relacionados (encadeados)! Nesse tipo de implementação, os elementos são organizados em posições não-contíguas, ficando dispersos ao longo da memória. Por essa razão, não é preciso estimar a priori o tamanho da estrutura, visto que o espaço é alocado conforme a necessidade. A base da construção de uma estrutura encadeada são os nodos (também conhecidos como “nós”), onde é armazenada a informação do elemento e o endereço do nodo relacionado. Dependendo das escolhas de estruturação do TAD, é possível ter mais de um endereço, por exemplo, para o próximo elemen- to e para o elemento anterior. Entretanto, veremos isso mais adiante, quando estudarmos o TAD LISTA. Na imagem a seguir, você poderá visualizar a representação gráfica do nodo (informação + endereço) e de uma estrutura encadeada, onde cada nodo conecta-se a outro nodo. Agora vamos representar uma pilha através da estrutura de nodos! Para isso, vamos considerar a seguinte pilha P com 5 elementos que possuem uma informação numérica: P (4, 2, 8, 5, 3), sendo 4 o valor que está na base da pilha e 3 que está no topo da pilha. Cada novo elemento que será adicionado na pilha aponta para o endereço do elemento que está no topo, e então, 21ESTRUTURA DE DADOS torna-se o novo topo da pilha. Dessa maneira, cada elemento aponta para o elemento anterior. O primeiro elemento da pilha, que fica na sua base, não referencia outro nodo, pois não há um nodo anterior a ele, por isso no campo de endereço desse nodo é colocado o valor NULL (que indica nulo, sem valor). Desse modo, fica estruturada uma pilha encadeada. Agora, podemos analisar a implementação do TAD PI- LHA com encadeamento. Assim, primeiro precisamos definir a estrutura do nodo, e a Pilha será um ponteiro para esta estrutura. A partir dessa definição, podemos fazer a implementação das operações primitivas de manipulação da pilha. A primeira operação é a de criação dessa, que apenas devolverá um ponteiro nulo, pois estará vazia. Para testar se a pilha está vazia, também é muito simples, basta testar se o seu valor é igual a NULL: Para empilhar um novo elemento, teremos que alocar memória (tomando o cuidado de verificar se há memória su- typedef struct nodo{ float elemento; nodo* endereco; }; typedef nodo* Pilha; Pilha Criar(){ return (NULL); } //Para usar a função: Pilha p = Criar(); int Vazia(Pilha p){ if (p == NULL) return(1); else return(0); } 22ESTRUTURA DE DADOS ficiente), bem como depois fazer com que esse novo elemento aponte para o endereço da pilha (que sempre estará apontando para o topo). Observe que se for o primeiro elemento, o ende- reço da pilha é NULL (pois foi recém criada). Logo, o primeiro elemento terá no endereço o valor NULL (conforme vimos na representação gráfica da pilha encadeada). Vejamos como fica a implementação da função PUSH (empilhar): A função para desempilhar (POP) deverá verificar se há elementos na pilha e se houver, consultar a informação do topo, alterar o topo da pilha e liberar a memória que era utilizada pelo topo anterior que será removido. A seguir, poderemos visualizar a implementação desta função. Pronto! Temos agora o conjunto de operações primitivas do TAD PILHA utilizando encadeamento! São essencialmente as mesmas funções que desenvolvemos com vetor, apenas não precisamos da função libera, pois a própria função pop já libera progressivamente a memória a cada elemento que é desempilha- do. Assim como, existem outras alternativas de implementaçãode pilhas, também há variações das implementações apresenta- das, você pode pesquisar e identificar as diferenças entre elas. void Push (Pilha p, float x) { nodo novo = (nodo) malloc (sizeof (nodo)); if (novo == NULL){ printf (“Memoria insuficiente!!”); } else { novo -> elemento = x; novo -> endereco = p;//o novo elemento aponta para o topo p = novo; //o topo da pilha passa a ser o novo endereço } } float pop(Pilha p) { nodo * remover; float x; if (vazia(p)){ printf (“Pilha vazia.”); } else { remover = p; x = remover->elemento; p = p->endereco; free(remover); return(x); } } Desafio! Você já ouviu falar das Torres de Hanoi? Pesquise sobre esse jogo e estruture a lógica para cumprir o objetivo das Torres de Hanoi. Dica, você precisará de 3 pilhas (p1, p2 e p3) para representar os pinos. A informação da pilha será um número inteiro que representa o tamanho de cada disco. 23ESTRUTURA DE DADOS Os Tipos Abstratos de Dados são uma importante ferra- menta para o desenvolvimento de sistemas, são versáteis e visam o desenvolvimento de bons programas que buscam a portabili- dade e reusabilidade. Conhecer os TAD clássicos, como o TAD PILHA, é fundamental para os programadores e projetistas de softwares. Pratique! Desenvolva programas usando Tipos Abstratos de Dados. Amplie seus conhecimentos! Pesquise sobre as aplicações do Tipo Abstrato de Dados PILHA para a computação! Principalmente, pesquise sobre o processo de passagem de parâmetros na chamada de funções e procedimentos, em que o sistema operacional utiliza pilha. Vamos lá! Verifique o que aprendeu nesta lição! 1. Por que utilizamos abstrações em computação? 2. Explique o que é e para que servem os Tipos Abstratos de Dados? 3. Explique quando devemos identificar e projetar os Tipos Abstratos de Dados para uma aplicação: 4. Explique por que devemos utilizar Tipos Abstratos de Dados: 5. Explique os critérios para determinar se um programa é bom, diferenciando-o de programas ruins: 6. Conceitue e descreva as características do TAD PILHA: 7. Enumere e explique as operações primitivas (básicas) do TAD PILHA: 8. Quando queremos utilizar o TAD PILHA para o desenvolvimento de uma aplicação, precisamos, necessariamente, implementar todas as funções da aplicação considerando a estrutura de armazenamento do TAD PILHA ,ou apenas utilizamos as funções (operações) primitivas? Por quê? 9. Existe somente uma forma de implementar o TAD PILHA? Por quê? 10. Quais os elementos que devem fazer parte da interface do TAD PILHA? O que é uma interface de um TAD? 11. Explique, de forma resumida, como o TAD PILHA pode ser implementado usando vetor. 12. Explique o que é alocação dinâmica e encadeamento. 13. Compare a implementação do TAD PILHA usando vetor e encadeamento. 14. Considere uma pilha que armazena em cada elemento um caracter (char), com a seguinte estrutura: Agora, considere que essa pilha será utilizada para armazenar os caracteres de uma palavra, assim, em cada nodo da pilha teremos uma letra da palavra. Agora o desafio é construir uma função que verifique se a palavra é um palíndromo, que são palavras ou expressões que podem ser lidas da esquerda para a direita ou da direita para a esquerda. Exemplo de palíndromos: asa, typedef struct nodo{ char elemento; nodo* endereco; }; typedef nodo* Pilha; arara, osso, radar, reviver, ovo, ana. A função a ser construída deve usar as operações primitivas da pilha (não manipulando a estrutura de dados diretamente) e ter a seguinte assinatura: int palindromo(Pilha p); Deverá retornar 1, se o conteúdo da pilha formar um palíndromo e, caso contrário, retornar 0 (zero). Dica, lembre das funções transfere e transfereigual, desenvolvidas ao longo do capítulo, elas podem ser úteis para atingir nosso objetivo. 26ESTRUTURA DE DADOS FILAS (QUEUE) Usamos filas no nosso dia a dia! Filas para caixas em supermercados e lojas, filas para atendimento em bancos, filas para atendimento em serviços públicos, entre outros. Além disso, usamos filas no computador, como exemplo, temos a fila de impressão! Filas são estruturas sequenciais, onde novos elementos são sempre incluídos no final da fila e o elemento que será utilizado (atendido) será aquele que estiver no início da fila, sendo, então, removido dela. As filas atendem a um critério de funcionamento denominado FIFO (First-In-First-Out), ou seja, o primeiro a entrar na fila será o primeiro a sair. Uma fila inicia vazia, considere uma fila de banco quando este está recém abrindo, quando chega a primeira pessoa essa se torna a primeira e a última pessoa da fila. Na sequência, novas pessoas irão para o fim da fila e quando o caixa do banco for atender, atenderá a primeira pessoa que está na fila, então ela sai da fila e a próxima se torna a primeira da fila. Portanto, precisamos saber quem está no início e quem está no fim da fila, esses são os dados básicos. Vamos compreender como funciona o Tipo Abstrato de Dados Fila! 27ESTRUTURA DE DADOS 0 1 2 3 4 tamanho=1 Ana Início Fim Inclusão do Primeiro Elemento 0 1 2 3 4 tamanho=0 Fila Vazia Assim como TAD PILHA, podemos implementar o TAD FILA utilizando vetores (que também pode ser chama- do de alocação estática) ou encadeamento (alocação dinâmica). Porém, há alguns inconvenientes na implementação da FILA com vetores. Usualmente, filas não tem um limite, portanto fica difícil definir o tamanho do vetor que deverá conter a fila. Diferente do TAD PILHA, em que a base da pilha está sempre na mesma posição e tanto as operações de empilhar como desempilhar são sempre realizadas no topo. No TAD FILA a operação de incluir um novo elemento sempre o faz no fim da fila, já a operação de remover um elemento da fila sempre se faz no início dela. Dessa maneira, sempre estaremos utilizando duas posições do vetor, uma indicando o início da fila e outro seu fim. E a prin- cipal característica a considerar é que o início da fila poderá passar a não coincidir com o início físico do vetor e isso torna esta implementação mais complexa. Além disso, o fim da fila, também acabará por não coincidir com o fim físico do vetor. Isso ocorre, porque vão sendo retirados elementos do início e estes espaços do vetor poderão ser aproveitados para inclusão de novos elementos. Para entender melhor essa situação, vejamos a sequência de imagens, a seguir, que apresentam a estruturação de uma fila utilizando vetores. Estamos utilizando um vetor de 5 posições, com os índices entre 0 e 4, e uma variável para conter o tamanho. Iniciamos com a fila vazia, tamanho = 0. Na sequência, adicionaremos a cada etapa um elemento novo, no final da fila. Assim, perceba que o tamanho será incrementado e que o fim da fila será deslocado, sempre apontando para o último elemento que foi incluído. Fa- zemos isso até encher a fila. Depois passamos a atender pessoas da fila e removê-las, nessa situação, a cada remoção o início da fila passa a ser alterado, bem como começam a existir espaços vagos no início físico do vetor. Por fim, demonstramos o que irá acontecer ao adicionar mais um elemento na fila, que será o último, mas acabará sendo incluído no início do vetor, pois é a próxima posição livre. Terminando o nosso exemplo, o início da fila estará na posição 3 do vetor e o fim da fila na posição 0. 28ESTRUTURA DE DADOS 0 1 2 3 4 tamanho=4 Ana Maria José Marco Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=5 Ana Maria José Marco Lúcia Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=4 Maria José Marco Lúcia Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=3 José Marco Lúcia Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=2 Ana Maria Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=3 Ana Maria José Início Fim Inclusão do Próximo Elemento 29ESTRUTURADE DADOS O vetor não é uma estrutura de dados f lexíveis, pois preci- samos definir a quantidade máxima de elementos no código do programa, que se tornará fixo nas execuções. Caso o número de elementos venha exceder o limite previsto para o vetor, teremos um problema. Por outro lado, se esse número de elementos for muito menor que o limite, teremos desperdício de espaço de memória reservado para o vetor. Normalmente, a intenção de utilizar vetores para im- plementação do TAD é oferecer simplicidade e facilidade de desenvolvimento, mas isso não ocorrerá com a implementação do TAD FILA com vetores. Portanto, optaremos por não tra- balhar a implementação dessa alternativa neste livro. Implementação do TAD fila com Encadeamento Veremos que a implementação do TAD FILA utilizan- do encadeamento é semelhante a implementação do TAD PILHA com encadeamento. A diferença está nas informações que são necessárias para gerenciar a fila, pois agora precisamos saber o endereço do início e fim da fila e também seu tamanho, isso é importante para que não tenhamos que varrer toda a fila para obter essa informa- ção. Esses dados formarão o que denominamos cabeçalho da fila (queue header, em inglês). Desta forma, podemos definir a estrutura deste cabeçalho na linguagem C conforme segue: 0 1 2 3 4 tamanho=2 Marco Lúcia Início Fim Inclusão do Próximo Elemento 0 1 2 3 4 tamanho=3 MarcoEduardo Lúcia InícioFim Inclusão do Próximo Elemento 30ESTRUTURA DE DADOS Já a estrutura do nodo da fila fica muito semelhante ao nodo da pilha, a diferença aqui é o ponteiro para o endereço, que neste caso denominamos próximo, porque irá apontar para o próximo elemento da fila. Por questões de simplicidade, es- tamos considerando que o conteúdo do elemento é apenas um valor real (f loat). Precisamos definir a interface de funções do TAD FILA que pode ser visualizado a seguir: Vamos analisar a implementação de cada uma dessas funções. A função para criação da fila deve alocar o cabeçalho da mesma e inicializar os seus campos, de maneira que, tamanho = 0 (zero elementos) e início e fim, recebem NULL (nulo), indicando que não apontam para nodos. A função que inclui elementos no final da fila deve alocar o novo nodo (verificando se há memória suficiente para este processo), depois o novo elemento pode ser encadeado. Para isso, é necessário que o último elemento da fila passe a apontar para esse novo elemento. Todavia, a exceção é quando a fila está vazia, pois neste caso não há último elemento, então o início e o fim apontarão para o novo elemento, que será o primeiro da fila. Em ambas as situações, o fim da fila passa ser o novo elemento. struct fila { int tamanho; nodo* inicio, fim; }; typedef struct fila Fila;//estrutura do tipo pilha typedef struct nodo{ float elemento; nodo* proximo; }; //função para criação da fila Fila* cria (){ Fila* f = (Fila) malloc (sizeof (Fila));//aloca cabeçalho f->tamanho = 0; f->inicio = NULL; f->fim = NULL; return(f); } Fila* cria ();//função para criação da fila void insere (Fila* f, float v);//função para incluir v na fila f float remove (Fila* f);//função que retorna o valor do início da fila f e o exclui int vazia (Fila* f);//função que testa se a fila está vazia void libera (Fila* f);//função que libera a memória int elementos(Fila* f);//função que retorna o tamanho da fila 31ESTRUTURA DE DADOS //função que testa se a fila está vazia int vazia (Fila* f){ if (f->inicio == NULL) return(1); else return(0); } A função que remove o elemento do início da fila precisa verificar se a fila possui elementos, ou seja, se não está vazia. Se tiver elementos é necessário guardar o ponteiro do primeiro elemento para que possa ser liberado da memória. Além disso, é necessário atualizar o início da fila, que será o próximo (o seguinte) da fila. Devemos ter o cuidado de verificar se a fila ficou vazia, ou seja, se ao remover o elemento não restaram elementos nela, nesse caso, será necessário atualizar o fim da fila. Por fim, liberamos o ponteiro do elemento removido e é retornada a sua informação. Veja sua implementação a seguir: A função que testa se a fila está vazia é simples, basta testar se o início da fila é NULL (nulo), indicando que não tem elementos. //função para incluir v na fila f void insere (Fila* f, float v){ nodo* anterior; nodo* novo = (nodo) malloc (sizeof (nodo)); if (novo == NULL){ printf (“Memoria insuficiente!!”); } else { novo -> elemento = v; novo -> proximo = NULL;//o novo elemento será o último da fila f->tamanho = f->tamanho + 1;//incrementa tamanho //agora temos que encadear a fila if (f->inicio == NULL){//fila está vazia f->inicio = novo; f->fim = novo; } else{ anterior = f->fim; anterior->próximo = novo;//o último elemento aponta para o novo f->fim = novo;//o novo elemento se torna o novo fim da fila } } } //função que retorna o valor do início da fila f e o exclui float remove (Fila* f){ nodo* primeiro; float v; if (vazia(f)){ printf (“Fila Vazia!!”); return(0); } else { primeiro = f->inicio; v = primeiro->elemento; //agora temos que atualizar o novo início da fila f->inicio = primeiro->proximo; f->tamanho = f->tamanho - 1;//decrementa tamanho if (f->inicio == NULL){//verifica se a fila ficou vazia f->fim = NULL; } free(primeiro); return(v); } } 32ESTRUTURA DE DADOS Por fim, temos a função que libera memória, que irá li- berar o espaço alocado pelo cabeçalho da fila. Para sabermos quantos elementos estão armazenados na pilha, temos a função elementos, que retorna à informação que está armazenada no campo tamanho do cabeçalho da pilha. Lembremo-nos que a função insere, incrementa esse campo e que a função remove, decrementa. Esse conjunto de operações primitivas do TAD FILA permite que diversas outras operações possam ser construídas utilizando-as como base. É importante que ao utilizar um TAD, isso sempre seja feito através de sua interface, ou seja, apenas utilizando as operações primitivas. Se necessário, podemos am- pliar a interface, adicionando novas operações. Quem utiliza o TAD não deve manipular a estrutura de armazenamento dele.//função que libera a memória void libera (Fila* f){ if (f != NULL) free(f); } //função que retorna o tamanho da fila int elementos(Fila* f){ if (f != NULL) return(f->tamanho); else return(0); } Amplie seus conhecimentos! Pesquise sobre o funcionamento da fila de impressão! Quais funcionalidades possui, quais informações são registradas de cada requisição de impressão. A partir disso, veja que podem existir filas com prioridades, então pesquise e reflita como poderia ser feito esse tipo de implementação! Vamos lá! Verifique o que aprendeu nesta lição: 1. Qual a lógica de funcionamento do TAD FILA? 2. O que é o critério FIFO? 3. Quais as diferenças entre o TAD FILA e o TAD PILHA? 4. Explique como poderíamos utilizar um vetor para implementar o TAD FILA. 5. Descreva quais os inconvenientes de utilizar vetor para implementar o TAD FILA. 6. Descreva a interface do TAD FILA. 7. Explique as operações primitivas do TAD FILA. 8. Você precisa construir uma função que esvazia a FILA, que pode conter diversos elementos, ou seja, enquanto a fila não estiver vazia você deve remover um elemento. Você somente pode utilizar as operações primitivas da fila. A assinatura desta função é: • void esvazia(Fila* f). 9. Construa, usando somente operações primitivas da fila, uma função que faça a cópia de uma fila f1 para uma fila f2. Utilize a seguinte assinatura para esta função: • void copia(Fila* f1, Fila* f2). 10. Se desejarmos procurar um elemento específico dentroda fila (por exemplo o elemento = 3), é possível fazer essa busca utilizando apenas as operações primitivas ou precisaríamos implementar uma nova operação primitiva? Por quê? 11. Considere que estamos usando um sistema para controlar as filas de atendimento de caixas de um supermercado, onde cada pessoa entra em uma fila específica e para nosso exemplo temos apenas 2 filas f1 e f2, porém ocorreu um problema em um dos caixas e apenas um se manteve funcionando. Assim, agora você deve construir uma função que irá transferir os elementos da fila f2, colocando-os no fim da fila f1, para isso você apenas pode utilizar as operações primitivas do TAD fila, sem manipular diretamente sua estrutura interna. A assinatura desta função é: • void transferir(Fila* f1, Fila*f2); 34ESTRUTURA DE DADOS LISTAS (LIST) Listas são um importante recurso que estamos acostumados a utilizar! Fazemos lista de compras, lista de convidados para uma festa, lista de alunos em uma disciplina, entre outros. Conjuntos de elementos podem ser intuitivamente re- presentados através de listas. Essas são estruturas lineares e f lexíveis, uma vez que as operações de inclusão e remoção de dados possam ser feitas em qualquer ponto da lista, conforme o critério de ordenação que for adotado. A ordem de uma lista pode ser simplesmente a ordem de inclusão, como ocorre com filas e pilhas, mas também pode ter algum campo(s) que determina(m) a relação de ordem dos elementos da lista. Por exemplo, se tivermos uma lista com dados de pessoas, esta poderia ser ordenada pelo nome. Assim como pilhas e filas, as listas podem conter todo tipo de informação em cada um de seus elementos. O ele- mento pode ser apenas um número ou caractere, mas também pode ser um conjunto heterogêneo de dados: um registro. Por exemplo, um registro com os dados de pessoas (código, nome, idade, telefone, etc.). Nosso objetivo agora é compreender e aprender a implementar o Tipo Abstrato de Dados Lista 35ESTRUTURA DE DADOS Listas são uma generalização das pilhas e filas. Todas são estruturas lineares, onde os elementos possuem relação de ordem entre eles. Essa relação pode ser estabelecida por contiguidade física (no caso dos vetores) ou por encadeamento (alocação dinâmica). Entretanto, pilhas e filas possuem uma lógica de funcionamento restrita, pois pilhas somente incluem e removem elementos no topo (“fim” da pilha), já as filas somente incluem elementos no fim da fila e somente removem elementos no início da mesma. Por outro lado, as listas permitem que a inserção de novos elementos seja feita no início, meio ou fim da lista e a remoção de elementos também pode ser realizada no início, meio ou fim da lista. Tipos de Listas Existem diversos tipos de listas, conforme a definição da estrutura de dados e lógica de operação. Assim, temos: Listas simplesmente encadeadas; Listas duplamente encadeadas; e Listas circulares. Nas listas, cada nodo (também chamado de nodo ou nó), possui suas informações e referência (endereço). Nas listas simplesmente encadeadas, a referência aponta para o próximo elemento da lista, que é um ponteiro para o endereço alocado para o nodo. Nesse tipo de lista a varredura é sempre realizada em um único sentido, a partir do início da lista para frente (em inglês, forward), passando pelos nodos e chegando ao fim da lista, que é indicado pelo nodo em que a referência para o próximo é nula (NULL). Observe a seguir a representação desse tipo de lista. Já nas listas duplamente encadeadas, é adicionado ao nodo mais um ponteiro (referência), que apontará para o elemento (nodo) anterior. Desse modo, caso possua mais f lexibilidade de varredura na lista, podemos varrer a mesma do início para o fim, bem como do fim para o início, também podemos chegar a qualquer extremidade a partir de qualquer nodo! Perceba que o próximo do último elemento é nulo (NULL), indicando o fim da lista e que o anterior do primeiro elemento também é nulo (NULL), indicando o início da lista. Você pode ver isso na figura a seguir: Agora, vamos ver o que muda quando utilizamos listas circulares! Início Fim NULL Informação Próximo Informação Próximo InformaçãoPróximoInformaçãoPróximo Fim InformaçãoPróximo Anterior Início InformaçãoPróximo Anterior Informação PróximoAnteriorInformação PróximoAnterior 36ESTRUTURA DE DADOS Início Fim Informação Próximo Informação Próximo InformaçãoPróximoInformaçãoPróximo Fim InformaçãoPróximo Anterior Início InformaçãoPróximo Anterior Informação PróximoAnteriorInformação PróximoAnterior A ideia das listas circulares é criar um anel, como se a lista não tivesse nem início e nem fim. Isso torna mais f lexível a varredura da lista. Logo, nas listas circulares, simplesmente encadeadas, a referência próxima do último nodo passa a apontar para o primeiro nodo, criando um círculo de encadeamento. Já no caso das listas circulares duplamente encadeadas, o ponteiro para o elemento anterior do primeiro nodo passa a apontar para o último. Assim, nesse tipo de lista, não temos delimitadores nos nodos (elementos) que indicam o início ou fim da lista (ponteiros próximo ou anterior com valores NULL), apenas as informações do cabeçalho da lista indicarão os limites dela. A figura seguinte apresenta a estrutura de uma lista circular simplesmente encadeada, onde é possível observarmos que o ponteiro próximo do último nodo da lista aponta (referencia) para o primeiro nodo da lista. E a seguir você pode visualizar uma lista circular dupla- mente encadeada: A escolha do tipo de lista é uma decisão de quem está implementando o TAD (Tipo Abstrato de Dados) LISTA, pois não interfere na interface do TAD, visto que não interfere na interface de operações primitivas, mas sim define alternativas para a estrutura de dados do TAD e, consequentemente, terá impacto na implementação, que em alguns casos poderá facilitar a realização de algumas operações e tornar outras mais complexas. Implementação do TAD com lista de encadeamento Para trabalharmos com uma lista encadeada, temos que ter em mente que para cada novo elemento que é inserido na estrutura, devemos alocar dinamicamente um espaço de memória 37ESTRUTURA DE DADOS A partir disso podemos definir a estrutura do nodo: Agora, faremos a definição do cabeçalho da lista - nós já utilizamos esse recurso na implementação das FILAS, e sabemos que tem por objetivo ter as informações da estrutura da lista. As informações básicas do cabeçalho são: início (ponteiro para o primeiro nodo da lista); fim (ponteiro para o último nodo da lista); tamanho (que indica quantos elementos constituem a lista, economizando o processo de varrer a lista para obter esse dado). Assim, podemos definir esta estrutura: struct dado { int codigo; char nome[40]; int idade; }; struct nodo { dado informacao; struct nodo* proximo; }; struct lista { int tamanho; struct nodo* inicio, fim; }; struct lista* LISTA; para armazená-lo. Dessa forma, o espaço de armazenamento na memória é proporcional à quantidade de elementos. No entanto, a alocação dinâmica não ocupa espaços contíguos de memória, portanto não é possível ter acesso direto aos elementos da lista. Assim, para constituir a lista precisamos encadear os elementos, ou seja, cada elemento precisa referenciar o próximo elemento da estrutura (e o anterior, no caso de listas duplamente encadeadas) e essa referência aponta para o endereço alocado de cada elemento, conforme a ordem pré-definida para a lista. Dessa forma, cada elemento possui sua informação (número, caractere, estrutura de informações, etc.) mais a informação de referência que é ponteiro para o próximo elemento e ponteiro para o anterior, conforme o tipo de lista. Vamos considerar a implementação de uma Lista Sim- plesmente Encadeada! Precisamos definir, usando a linguagem de programação, a estrutura de dados da LISTA, seu cabeçalho e as operações primitivas do TAD que constituemsua interface básica. Vamos considerar em nossos exemplos um registro simples de dados de pessoas, com as seguintes informações: código (que identifica cada pessoa); nome; idade. Você pode facilmente incluir mais informações nesse registro. Esses dados foram escolhidos pen- sando em diferentes possibilidades de ordenação da lista. Desse modo, a informação do nodo será uma estrutura conforme segue: 38ESTRUTURA DE DADOS A partir da definição da estrutura de armazenamento do TAD LISTA, podemos definir sua interface de funções, ou seja, as operações primitivas do TAD que manipulam sua estrutura. Na sequência, você poderá analisar a implementação de cada uma dessas funções. Podemos implementar a função de criação da lista de forma bem simples, pois consiste apenas em alocar o cabeçalho, inicializar o tamanho e atribuir NULL para o início e fim da lista, visto que estamos criando uma lista vazia (que ainda não possui elementos). Para verificar se uma lista está vazia, podemos testar se o tamanho é 0 (zero) ou se o início ou fim são nulos (NULL). Optaremos por testar o início da lista, se este for igual NULL, indica que não há elementos na lista e a função retornará o valor 1, caso contrário, retornará 0. Veja a seguir a implementação desta função: //função para criação da lista LISTA * cria (){ LISTA* p; p = (LISTA) malloc (sizeof (LISTA)); p->tamanho=0; p->inicio=NULL; p->fim=NULL; return(p); } //função que testa se a lista está vazia int vazia (LISTA * l){ if (l->inicio == NULL) return(1); else return(0); } //função para criação da lista LISTA* cria (); //função que testa se a lista está vazia int vazia (LISTA * l); //função que libera a memória void libera (LISTA * l); //função que retorna o tamanho da lista int elementos(LISTA * l); //função para incluir os dados v na lista l void insere (LISTA * l, dado v); //função que retorna o valor do início da lista l e o exclui int remove (LISTA * l, dado* v); 39ESTRUTURA DE DADOS A função que libera memória precisa liberar o espaço alocado para o cabeçalho. Em princípio, consideraremos que a lista está vazia e, portanto, não precisam ser liberados os nodos ocupados. Seria possível implementar uma função que esvazia uma lista, mais adiante faremos isso. A seguir, veja a imple- mentação da função libera: Para consultar a quantidade de elementos da lista, bas- ta consultar a informação de tamanho do cabeçalho da lista, conforme segue: Agora vamos analisar a implementação da função de inclusão de novos elementos. //função que retorna o tamanho da lista int elementos(LISTA * l){ return(l->tamanho); } //função que libera a memória void libera (LISTA * l){ free(l); } Um novo elemento pode ser incluído no fim da lista, nesse caso a lista passa a ter um comportamento semelhante ao de uma fila, onde o critério de ordenação é a ordem de inclusão. Por outro lado, a lista pode ter outro critério de ordenação de seus elementos, que pode ser a ordem crescente dos códigos de identificação das pessoas. Nesse caso, o novo elemento poderá ser incluído no início, no meio ou no fim da lista. Vamos analisar tal situação! Considere uma lista em que já foram incluídos 3 elementos (na figura, apenas estamos representando os códigos da infor- mação). Se formos adicionar um novo nodo em que o código da pessoa for 3, esse deverá ser incluído antes do atual início da lista, conseguintemente, passará a ser o novo início. Agora, se formos adicionar o código 11, este novo elemento deverá ser encadeado entre os nodos de código 9 e 15. Por outro lado, se formos adicionar o nodo com o código 20, este deverá ser encadeado no fim da lista. 40ESTRUTURA DE DADOS Verifique na figura a seguir a inclusão de um novo ele- mento antes do início da lista, onde o novo elemento com código 3 tem como próximo o atual início da lista e o início passa a apontar para o novo elemento. Agora, o diagrama demonstra a inclusão de um novo elemento no meio da lista, ajustando os ponteiros para manter o encadeamento conforme o critério de ordenação. Por fim, veja a representação da inclusão de um novo nodo no fim da lista: Como definir onde o novo nodo deve ser incluído? Para definir onde o novo nodo deve ser incluído, teremos que fazer uma varredura da lista, ou seja, a partir do início da lista comparar a chave de ordenação do novo nodo com a do nodo corrente, se a do novo elemento for menor, significa que 41ESTRUTURA DE DADOS ele será inserido antes do atual. Todavia, se chegarmos ao final da lista e não encontrarmos um elemento maior que o novo, ele se tornará o novo final da lista. Sendo assim, em todas as situações é necessário preservar o encadeamento. A implementação da função de inclusão de um novo nodo deve considerar todas essas situações, inclusive o fato de a lista estar vazia. Então, vamos ver essa implementação: Por fim, vamos implementar a função que remove um elemento da lista e retorna as suas informações! Em primeiro lugar, precisamos determinar qual será o nodo que será excluí- do! Optamos por receber o código do elemento a ser excluído, localizá-lo, consultar seus dados e, então, excluí-lo. Podemos identificar 4 situações a partir dessa busca: 1ª) O código não existe, portanto nada será excluído ou retornado; 2ª) O elemento a ser excluído é o primeiro da lista, portanto o seu início preci- sa ser ajustado; 3ª) O elemento a ser excluído está no meio da lista, assim precisamos ajustar os ponteiros; 4ª) O elemento é o último elemento da lista, portanto precisamos ajustar o seu fim. Sempre que um elemento for removido, precisamos atualizar o tamanho da lista, decrementando. Além disso, a função retorna 1 se teve êxito, caso contrário, será 0 (zero). //função que retorna um elemento da lista e o exclui int remove (LISTA * l, dado* v){ int código = *v.codigo; nodo* anterior, corrente; if (vazia(l)){ return(0);} else{ corrente = l->inicio; if (codigo == corrente->codigo){//achou no início! strcpy(*v.nome,corrente->nome); *v.idade=corrente.idade; l->inicio = corrente->proximo; free(corrente); l->tamanho = l->tamanho – 1; return(1); } else{ while((corrente != NULL) && (codigo != corrente->codigo)&& //função para incluir os dados v na lista l void insere (LISTA * l, dado v){ nodo* novo; nodo* anterior, corrente; novo = (nodo) malloc(sizeof(nodo));//alocação dinâmica do novo nodo //a seguir passamos as informações do registro v para o nodo nodo->codigo = v.codigo; strcpy(nodo->nome,v.nome); nodo->idade = v.idade; nodo->próximo=NULL; if (vazia(l)){ //o novo elemento será o início e fim da lista l->inicio = novo; l->fim = novo; } else{ //agora precisamos determinar a posição de inclusão do novo nodo corrente = l->inicio; if (novo->codigo < corrente->codigo){//inclusão no início novo->proximo = l->inicio; l->inicio = novo; } else {//varredura da lista while((corrente != NULL)&&(novo->código >= corrente->codigo)){ anterior = corrente; corrente = anterior->proximo; } anterior->proximo = novo; novo->proximo = corrente; if (corrente == NULL) l->fim = novo; } } l->tamanho = l->tamanho + 1; //incrementa tamanho } 42ESTRUTURA DE DADOS Podemos adicionar algumas operações à interface desse TAD, como por exemplo: procurar um elemento na lista, e caso encontre, os dados irão retornar e esvaziar a. Vamos implementar essas duas funcionalidades! (codigo > corrente->codigo)){ anterior = corrente; corrente = anterior->próximo; } if (corrente == NULL){//não encontrou return(0); }else { strcpy(*v.nome,corrente->nome); *v.idade=corrente.idade; anterior->próximo = corrente->próximo; if (l->fim == corrente){//é o último elemento l->fim = anterior; } l->tamanho = l->tamanho – 1; free(corrente); return(1); } } } } A função que esvazia a lista é muito simples, navegamos na lista e vamos liberando a memória! Importante: devemos atualizar o cabeçalho da lista! Pronto, agora já temos o ferramental básico para gerenciar o TAD LISTA! Sua f lexibilidade para as operações de inclusão e remoção de elementos o torna um pouco mais complexo que PILHAS e FILAS, mas temos certeza que você conseguiu entender como funciona. //função que procura um elemento na lista com base no seu código //se encontrar retorna o ponteiro para o nodo, caso contrário NULL nodo* procura(LISTA * l, int codigo){ nodo* corrente; if (vazia(l)) return(NULL); else { corrente = l->inicio; while ((corrente != NULL) && (n->codigo != corrente->codigo)&& (n->codigo > corrente->codigo)){ corrente = corrente->próximo;//ponteiro navega pela lista } if((corrente==NULL)|| (n->codigo != corrente->codigo)){ //não encontrou return(NULL); } else{ return(corrente); } } } //função que esvazia a lista void esvazia(LISTA * l){ nodo* corrente, libera; if (!vazia(l)){ corrente=l->inicio;//ponteiro auxiliar para navegar na lista while(corrente != NULL){ libera = corrente; corrente = libera->próximo; free(libera); } //atualizar o cabeçalho da lista que agora está vazia l->tamanho = 0; l->inicio = NULL; l->fim = NULL; } } 43ESTRUTURA DE DADOS Desafio! Altere as funções insere, remove e procura, considerando como critério de ordenação da lista o nome de uma pessoa! Devendo a lista ser em ordem crescente de nome! Temos certeza que você consegue! Amplie seus conhecimentos! Pesquise sobre a implementação de listas duplamente encadeadas. O que muda nas funções para inserir e remover elementos? Quais as vantagens do encadeamento duplo? Vamos lá! Verifique o que aprendeu nesta lição! 1. Construa uma tabela comparativa com os TAD PILHA, FILA e LISTA. Considere os seguintes aspectos de cada TAD: estrutura do nodo, lógica de funcionamento, operações primitivas, inclusão de novos elementos, exclusão de elementos. Ao final, analise a tabela que construiu e identifique as principais semelhanças e diferenças entre essas estruturas lineares. 2. Liste os tipos de listas encadeadas e descreva-os brevemente, diferenciando-os. 3. O que é o cabeçalho de uma lista e quais informações contêm? 4. Descreva a operação de inserir um novo elemento em uma lista. 5. Descreva a operação de remover um elemento de uma lista. 6. Considere a seguinte estrutura de informações de uma lista que gerencia coordenadas cartesianas (x,y). Além disso, os elementos (nodos) são mantidos em ordem de inclusão, ou seja, novos nodos são sempre incluídos no fim da lista. Para implementar o TAD LISTA para esta estrutura de dados, quais funções precisam ser alteradas e quais não necessitam de ajustes? 7. Temos um conjunto de dados E, que define a ordem de códigos (separados por vírgulas) que serão progressivamente incluídos em uma lista. E={7, 20, 11, 30, 9, 5}. Você deve fazer a representação gráfica (desenho) da lista, demonstrando sua situação após cada operação de inclusão de dados. Identificando início e fim da lista, tamanho e encadeamento entre os elementos. Tome por base a estrutura de funcionamento de uma lista simplesmente encadeada. struct dado { float x, y; }; 45ESTRUTURA DE DADOS ÁRVORES (TREE) Árvores na natureza possuem raiz, tronco, galhos e folhas. Usamos uma metáfora dessa estrutura para representar informações hierárquicas na construção de organogramas, árvores genealógicas e diretórios de arquivos, por exemplo. Vamos agora conhecer o Tipo Abstrato de Dados Árvore! 46ESTRUTURA DE DADOS As árvores são um importante recurso computacional, ideal para organizar informações que possuem relações hie- rárquicas. Vejamos um exemplo de estrutura hierárquica. O conjunto A contém os subconjuntos B, C e D. O subconjunto D possui os subconjuntos E, F e G. E o subconjunto G possui os subconjuntos H e I. A imagem a seguir apresenta essa relação entre os conjuntos: Esta estrutura de conjuntos pode ser representada da seguinte forma hierárquica: Assim, percebemos que hierarquias podem representar relações de pertinência (como no exemplo dos conjuntos), relação de subordinação (como no caso de um organograma) ou relação de ordem (como no caso de expressões matemáticas). A partir dessa compreensão, vamos analisar a definição computacional das árvores! Uma árvore é uma estrutura não linear, composta por um conjunto de nós. As árvores possuem um nó (nodo) especial, denominado raiz e este pode estar ligado às subárvores (zero ou mais). As subárvores de uma raiz são denominadas como seus filhos, logo, a raiz é o pai destas subárvores. Se considerarmos o exemplo dado, da estrutura hierárquica dos conjuntos, a raiz é A e suas subárvores são B, C e D. Assim, B, C e D são filhos de A. E A é pai de B, C e D. Toda subárvore é uma árvore, formando uma definição recursiva. No nosso exemplo, D é uma subárvore da raiz A, por outro lado, toda subárvore é uma árvore, portanto, D é uma raiz e tem suas subárvores E, F e G, que são seus filhos. Propriedade Fundamental das Árvores Somente existe um único caminho que liga cada nodo a raiz da árvore. Todo nodo somente tem um pai (ascendente). 47ESTRUTURA DE DADOS A quantidade de subárvores de um nodo define o seu grau. Desta forma, no nosso exemplo, o grau de A é 3, pois possui 3 subárvores (filhos), já o grau de G é dois, pois possui 2 subárvores, por outro lado é 0 (zero) o grau dos nós B, C, E, F, H e I, pois não possuem subárvores. Nós com grau 0 (zero) são denominados folhas. Analisando os graus de cada um dos nodos de uma árvore, podemos definir o grau da árvore, que é o maior grau de algum de seus nodos. No nosso exemplo, o grau da árvore é 3. Outra informação importante sobre as árvores é a altura, que é medida a partir da raiz (altura 0 – zero) e contanto 1 a cada nível que se desce na hierarquia até chegar a folha mais distante. Portanto a altura da árvore é o caminho mais longo que liga a raiz a uma de suas folhas. No nosso exemplo a árvore tem altura 3: A – D (+1), D – G (+1), G – H (+1), assim entre A – H temos 3 passos em níveis da hierarquia. Também precisamos conhecer o nível de um nodo, que é medido em relação a raiz. O nível da raiz é 0 (zero) e, partir disso, definimos os outros níveis. No nosso exemplo, o nível de A é 0 (zero), o nível de B, C e D é 1; o nível de E, F e G é 2; e o nível de H e I é 3. Essa é uma outra forma de considerar a altura de uma árvore, mensuramos o nível das folhas e o maior valor e teremos a altura da árvore. A quantidade máxima de subárvores (filhos) que um nodo raiz pode ter determina o tipo de árvore, desta forma temos: • Árvores Binárias: onde todos os nodos podem ter no máximo duas subárvores, filhos; • Árvores Genéricas: onde a quantidade de subárvores de cada nodo é ilimitada. O exemplo dado, o qual estamos analisando, representa uma árvore genérica! As árvores binárias, por terem um número limitado de subárvores para cada nodo, possibilitam uma implementação mais simples do que as árvores genéricas. Além disso, existem formas de mapear uma árvore genérica para uma estrutura de árvore binária – que veremos mais adiante. Por essas razões, o nosso estudo agora será focado nas árvores binárias! Árvores Binárias 48ESTRUTURA DE DADOS • Um nó raiz quepossui duas subárvores: subárvore esquerda (SAE) e subárvore direita (SAD); • Uma subárvore é uma árvore (definição recursiva, ou seja, uma árvore é composta por árvores). Esquematicamente, a árvore binária é: Para percorrer e utilizar os elementos de uma árvore bi- nária é possível utilizar três diferentes percursos, cada um irá tratar os elementos em uma ordem diferente. A denominação dos percursos está relacionada com o momento em que a raiz é tratada (utilizada). Os três tipos de percurso são: Você já pensou que podemos representar expressões ma- temáticas utilizando uma árvore binária? Analise os operadores matemáticos básicos: + (soma), - (subtração), * (multiplicação), / (divisão). São todos operadores binários, ou seja, trabalham com dois operandos para realizar a operação. Considere a seguinte expressão: (4 + 12) * (3 – 1) / (2 * 4) e a sua representação através de uma árvore binária: Outra aplicação que pode ser facilmente representada através de árvores binárias são os jogos de diversos campeonatos que envolvem dois times! Assim, podemos definir uma árvore da seguinte forma: • Uma árvore vazia; 49ESTRUTURA DE DADOS Vejamos como ficarão os elementos a partir de cada um dos percursos: • Percurso In-fixo: 4 + 12 * 3 – 1 / 2 * 4 • Percurso Pré-fixo: / * + 4 12 – 3 1 * 2 4 • Percurso Pós-fixo: 4 12 + 3 1 - * 2 4 * / Agora, vamos considerar um exemplo de uma árvore binária, onde cada nodo contém um número: • Percurso In-fixo, também denominado ‘em ordem’: nesse caso, é percorrida a subárvore esquerda, depois utiliza-se a raiz e depois é percorrida a subárvore à direita. (SAE, Raiz, SAD) • Percurso Pré-fixo, também denominado ‘pré-ordem’: nesse caso, utiliza-se primeiro a raiz, depois se percorre a subárvore esquerda e, por fim, a subárvore à direita. (Raiz, SAE, SAD) • Percurso Pós-fixo, também denominado ‘pós-ordem’: aqui, primeiro, percorremos a subárvore esquerda, depois a subárvore direita e, por fim, utilizamos a raiz. (SAE, SAD, Raiz) Vamos retomar o exemplo da expressão aritmética para entender cada um dos tipos de percursos: 50ESTRUTURA DE DADOS Vejamos como ficarão os números a partir de cada um dos percursos: • Percurso In-fixo: 4, 15, 18, 25, 27, 30, 35, 40, 70, 80, 90 (raiz no meio) • Percurso Pré-fixo: 40, 25, 15, 4, 18, 30, 27, 35, 80, 70, 90 (raiz primeiro) • Percurso Pós-fixo: 4, 18, 15, 27, 35, 30, 25, 70, 90, 80, 40 (raiz no fim) Árvore Binária de Busca Uma caso especial das árvores binárias são as árvores binárias de busca, que são construídas seguindo um critério de ordenação, logo, considerando a raiz, todos os elementos da subárvore esquerda (SAE) possuem valores menores que a raiz e todos os elementos da subárvore direita (SAD) possuem valores maiores que a raiz. Esse critério determina a forma que os elementos serão incluídos na árvore e deve ser respeitado, quando elementos forem excluídos. Assim, considerando a seguinte sequência de valores V a serem incluídos em uma árvore de pesquisa binária V = {40, 3, 50, 10, 5, 7, 14, 45, 60}. O primeiro elemento (40) será a raiz da árvore e, a partir desse os outros elementos serão incluídos, (3) é menor que (40), portanto, será incluído na SAE, já (50) é maior que (40), consequentemente, será incluído na SAD. De- pois, (10) é menor que (40), logo, será incluído na SAE, onde já temos o valor (3), e (10) é maior que (3), portanto será incluído na sua SAD. Assim, subsequentemente, veja como ficará a ár- vore binária de busca para esse conjunto de entrada de dados: Perceba que para manter a visualização da árvore binária mais equilibrada, foram colocados nodos sem valor (/). 51ESTRUTURA DE DADOS Agora, vejamos um exemplo de uma árvore binária, o qual parece uma árvore binária de busca, mas não segue estritamente o critério deste tipo de árvore: Esta árvore parece seguir o critério da árvore binária de busca, pois se analisarmos isoladamente, cada raiz e seus filhos diretos respeitam a regra. No entanto, a mesma refere-se a toda a SAE, que precisa ter valores menores que a raiz e toda a SAD precisa ter elementos maiores que raiz. Veja que o valor (25) está na SAE da raiz (20), mas não poderia estar, pois é maior que a raiz. Também note que o valor (19) está na SAD da raiz (20), mas não poderia estar, visto que é menor que a raiz. Dessa forma, essa é uma árvore binária, mas não é uma árvore binária de busca. Outra característica da árvore binária de busca é que ao utilizar um percurso infixo (em ordem) obtemos uma lista or- denada dos elementos. Implementação da árvore binária de busca Como acontece com os outros tipos abstratos de dados, precisamos definir a estrutura de armazenamento da árvore e as operações de manipulação desta estrutura. Comecemos pela estrutura, que na linguagem C fica: typedef struct nodo{ int info; struct nodo * esquerda; struct nodo * direita; }nodo; 52ESTRUTURA DE DADOS Agora vamos definir o conjunto de operações primitivas do TAD Árvore, sua interface: A seguir, veremos a implementação destas funções. Você perceberá que a maioria delas utiliza recursividade! Isto é práti- co, uma vez que a estrutura da árvore também é recursiva (uma árvore possui subárvores, que são árvores)! Iniciamos pela implementação da função que inclui ele- mentos no TAD ARVORE BINÁRIA DE BUSCA. Nessa, verificamos se o ponteiro para a árvore é NULL, se for ali, será adicionado o novo elemento, do contrário, comparamos o valor do novo elemento com aquele que está no nodo corrente. Já, se for menor, chamamos recursivamente a função para incluir o novo nodo à esquerda, caso contrário, ou seja, se for maior, chamamos recursivamente a função para incluir o novo nodo à direita. Veja na página a seguir essa implementação: //Inclusão de um nodo na árvore a partir da raiz void insere_folha(nodo ** arv, int info); //Caminhamento pre-ordem void pre_ordem(nodo * arv); //Caminhamento em-ordem void em_ordem(nodo * arv); //Caminhamento pos-ordem void pos_ordem(nodo * arv); //Pesquisar um nodo void pesquisar_nodo(nodo * arv, char pesq); //retornar o nível de um nodo int nivel_do_nodo(nodo * arv, char info); //liberar a memória da árvore void libera(nodo *arv); //remover um nodo da árvore nodo* retira (nodo* arv, int v); O que é Recursividade? A recursividade é a definição de uma rotina (função ou método) que pode chamar a si mesma. Em geral, uma definição recursiva é definida por casos: um número limitado de casos base (que finalizam o processo recursivo); e casos recursivos (que chamam a própria rotina, normalmente modificando os parâmetros). De maneira simplificada: pense que temos um processo de subir uma escada, onde o parâmetro é um ponteiro para a escada, neste verificamos se chegamos ao fim da escada (esse é nosso caso base), se sim, termina, caso contrário, subimos um degrau e chamamos, recursivamente, o processo de subir a escada para o resto da mesma. Pesquise mais sobre a implementação de funções recursivas e analise alguns exemplos clássicos: Fibonacci recursivo e Fatorial recursivo. 53ESTRUTURA DE DADOS Depois de construir a árvore, temos que definir as fun- ções para realização dos percursos (caminhamento), ou seja, as funções que irão apresentar (mostrar) ou fazer outro uso de todos os nodos da árvore. Assim, temos, na sequência, funções que implementam os percursos: em pré-ordem, em-ordem e em pós-ordem. Cada uma se diferencia pelo momento em que a raiz é mostrada (ou utilizada). Se desejarmos mostrar os elementos da Árvore Binária de Busca, devemos utilizar o percurso em-or- dem (infixo). As três funções a seguir têm por objetivo mostrar todos os elementos conforme o percurso escolhido. A seguir temos a função que faz o percurso infixo (em- -ordem), que apresentará os elementos em ordem crescente! //Inclusão de um nodo na árvore a partir da raiz void insere_folha(nodo ** arv, int info){ if (*arv == NULL){ *arv = cria_elemento(info); }else{
Compartilhar