Prévia do material em texto
ESTRUTURA DE DADOS ESTRUTURA DE DADOS Copyright © UVA 2019 Nenhuma parte desta publicação pode ser reproduzida por qualquer meio sem a prévia autorização desta instituição. Texto de acordo com as normas do Novo Acordo Ortográfico da Língua Portuguesa. AUTORIA DO CONTEÚDO Alfredo Nazareno Pereira Boente REVISÃO Clarissa Penna Theo Cavalcanti PROJETO GRÁFICO UVA DIAGRAMAÇÃO UVA SUMÁRIO Apresentação Autor 6 7 Estruturas lineares encadeadas: listas 28 • Lista simplesmente encadeada • Lista duplamente encadeada • Lista circular UNIDADE 2 8 • Estrutura de dados e seus tipos • Vetores e matrizes de dados • Ponteiros ou apontadores Introdução às estruturas de dados UNIDADE 1 SUMÁRIO Grafos e árvores 70 • Grafos e árvores • Árvores binária e AVL • Buscas em árvore UNIDADE 4 52 • Trabalhando com filas • Trabalhando com pilhas • Filas circulares Estruturas lineares encadeadas: filas e pilhas UNIDADE 3 6 A estrutura de dados tem por objetivo permitir ao estudante conhecer os conceitos bási- cos de estruturas e seus tipos, implementando soluções computacionais com as princi- pais estruturas de dados: vetores, matrizes, ponteiros/apontadores, lista, fila, pilha, grafos e árvores. A disciplina está dividida logicamente em quatro unidades, distribuídas da seguinte forma: • Na Unidade 1, serão abordadas as estruturas de dados e seus tipos, implementando soluções com vetores, matrizes e ponteiros. • Na Unidade 2, será apresentada a estrutura lista nos formatos linear e circular, vi- sando à implementação de soluções computacionais com essa estrutura de dados. • Na Unidade 3, serão implementadas soluções algorítmicas com as estruturas de- dados fila e pilha. • Na Unidade 4, serão estudados os conceitos de grafos e árvores, buscando imple- mentar programas com árvores de dados, árvores binárias e árvores AVL. APRESENTAÇÃO 7 ALFREDO NAZARENO PEREIRA BOENTE Pós-doutorado em Neurociência e Sistemas Lógicos aplicados a Gestão Empresarial, Educação e Pesquisa Experimental pelo Programa de Pós-Graduação em História das Ciências e das Técnicas e Epistemologia da Universidade Federal do Rio de Janeiro – HCTE/UFRJ (2014), doutorado em Ciências da Engenharia de Produção pelo Instituto Alberto Luiz Coimbra de Pós-Graduação e Pesquisa de Engenharia da UFRJ – Coppe/ UFRJ (2013), doutorado em Processamento de Dados pela American World University – AWU (EUA) (2006), mestrado em Administração e Desenvolvimento Empresarial pela Universidade Estácio de Sá – Unesa (2009), mestrado em Ciências da Computação pela Universidad de La Habana (Cuba, 2003), mestrado em Engenharia de Software pela AWU (EUA, 2002), especialização em Análise de Sistemas pela Unesa (2000), especialização em Docência Superior pela Universidade Candido Mendes – Ucam (2001), especializa- ção em Tecnologia em Banco de Dados pela Unesa (2001), especialização em Ciências da Computação pela Universidad de La Habana (Cuba, 2002), especialização em Ad- ministração Escolar pela Unesa (2003), especialização em Logística Empresarial pela Faculdade Internacional Signorelli (2016), graduação em Processamento de Dados com ênfase em Projeto e Análise de Sistemas pelas Faculdades Reunidas Professor Nuno Lisboa – FRNL (1996), graduação em Processos Gerenciais – Gestão Empresarial pela Universidade Veiga de Almeida – UVA (2018), graduação em Engenharia de Produção pela UVA (2019). AUTOR Introdução às estruturas de dados UNIDADE 1 9 Esta unidade é importante para o estudante porque apresenta as principais estruturas de dados e seus tipos, além de implementar soluções com o uso das estruturas vetores, matrizes e ponteiros/apontadores — alocação dinâmica. INTRODUÇÃO Nesta unidade, você será capaz de: • Analisar as estruturas de dados, implementando soluções algorítmicas com vetores, matrizes e ponteiros — alocação dinâmica. OBJETIVO 10 Estrutura de dados e seus tipos No que tange ao estudo das estruturas de dados, segundo Forbellone e Eberspächer (2005),a partir dos tipos de dados primitivos, serão compostos os tipos de dados construídos, ora denominados estrutura de dados. Cada estrutura de dados tem um tipo peculiar de organização e forma de manipulação dos dados, o que, teoricamente, constituirá certa estrutura de dados especificamente. Então, é verdadeiro afirmar que uma estrutura de dados se refere a uma forma única e peculiar de armazenamento e organização de dados que se mostra eficiente quando é adequada a diferentes tipos de aplicações, com suas tarefas específicas. Decerto, algoritmos e estruturas de dados são considerados temas fundamentais para o estudo da computação. Em linhas gerais, um problema pode ser resolvido facilmente por uma estrutura de dados específica, embora todas as estruturas de dados possam ser utilizadas para resolver o mesmo tipo de problema. Pugga e Rissetti (2009) afirmam que a escolha da estrutura de dados mais adequada para certo estudo inclui os padrões de acesso, os mecanismos de busca e os requisitos de armazenamento de dados. As estruturas de dados podem ser apresentadas como estruturas homogêneas ou heterogêneas. Elas são ditas homogêneas quando o conjunto de dados apresentado por essa estrutura de dados é um dado primitivo do mesmo tipo. Já as estruturas de dados hetero- gêneas apresentam um conjunto de dados formado por dados primi- tivos de tipos diferentes. As estruturas de dados são utilizadas para armazenar dados considerados de média complexidade, visando ao atendimento das necessidades inerentes de um programa, a fim de solucionar certo problema. Na literatura existem diferentes estruturas de dados, cada qual com suas características e especificidades. 11 Tipos de estruturas de dados Diversas estruturas de dados podem ser utilizadas a fim de buscar a solução de um problema computacionalmente. Serão apresentadas as seguintes estruturas de dados: vetores, matrizes, ponteiros, listas, filas, pilhas, grafos e árvores. Mantenha o foco e vamos conhecê-las! Vetor (array) Uma estrutura indexada é um conjunto de elementos de dados do mesmo tipo acessa- dos por meio de índices, equivalentes à sua posição na estrutura (PUGGA; RISSETTI, 2009). Por analogia, podemos imaginar algo semelhante a um texto ou livro, em que os títulos estão especificados no sumário, com a designação da respectiva página. Dessa forma, para que certo assunto seja encontrado, basta localizar no índice a página para acessar seu conteúdo de forma direta e objetiva. Um vetor é uma estrutura unidimensional de dados indexada com dados do mesmo tipo, também conhecida como array, que necessita apenas de um índice para identificar e localizar um determinado elemento nela armazenado. Um vetor pode ser representado por uma linha de contêiner ou espaços predefinidos e identificados por índice, conforme ilustrado na Figura 1. Dessa forma, tem-se o vetor como uma coleção de variáveis do mesmo tipo que com- partilham o mesmo nome e o mesmo endereço de memória em posições consecutivas. Figura 1: Representação de um vetor. Fonte: Pugga e Rissetti (2009). índice: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] nome: temp 18 17 20 26 32 29 15 12 16 21 12 Matriz Uma matriz nada mais é que um vetor multidimensional caracterizado pelo uso de,pelo menos, duas dimensões do mesmo endereço de memória com dados do mesmo tipo. Apresenta conceito equivalente ao do estudo de matrizes da disciplina Álgebra Linear, ou seja, todas as propriedades das matrizes existentes na matemática podem ser ado- tadas em programas que manipulam essa estrutura de dados em busca de solução de problemas. As estruturas indexadas, que necessitam de mais de um índice para identificar um de seus elementos, são chamadas de matriz de dimensão n, sendo que n representa o nú- mero de índices requeridos (PUGGA; RISSETTI, 2009). Então, um elemento da matriz pode ser identificado por um número de linha e coluna dessa matriz, conforme ilustrado na Figura 2. Ponteiro De acordocom Mizrahi (2008), o nome de uma variável indica o que será armazenado nela. A isso dá-se o nome de variável significativa. O endereço de uma variável é um pon- teiro. Seu valor indica em que parte da memória do computador a variável está alocada. Uma variável ponteiro, também conhecida tecnicamente como variável apontadora (ver Figura 3), é aquela que, no lugar de armazenar conteúdo de dado, guarda o espaço, o endereço de memória de outra variável, para posterior manipulação. Figura 2: Representação de uma matriz. Fonte: Pugga e Rissetti (2009). Quantidade Va lo r ( R$ ) n ... 5 4 Região X 3 2 1 1 2 3 4 5 ... n 13 Ponteiros ou apontadores proporcionam um método de acesso à variável sem que haja a necessidade de a referenciar diretamente, ou seja, realiza o modo indireto de acesso. Lista Uma lista é uma estrutura de dados linear que pode ser caracterizada como encadeada ou circular, composta por nós indicativos do próximo elemento da lista, do primeiro ao penúltimo, pois o último elemento da lista não aponta para ninguém (NULL). A Figura 4, conforme explica Forbellone e Eberspächer (2005), ilustra um exemplo clás- sico de lista de tarefas a serem cumpridas no centro da cidade do Rio de Janeiro. Ini- cialmente, cada atividade é relacionada conforme vai surgindo na memória, até que se esgotem. Figura 3: Representação de ponteiro. Fonte: Elaborada pelo autor (2019). Índices 112 113 114 115 116 117 118 119 120 121 Conteúdos 0 0 1 0 2 1 113 0 1 0 Figura 4: Exemplo de uma lista de tarefas. Fonte: Forbellone e Eberspächer (2005). Lista de tarefas 1. Pagar as contas no banco. 2. Comprar os livros na livraria. 3. Deixar o carro no estacionamento. 4. Pegar algumas fitas na videolocadora. 5. Enviar correspondências pelo correio. 6. Buscar as fotos reveladas. 7. Autenticar documentos no cartório. 8. Passar na banca de jornal. 14 Subentende-se, portanto, que a primeira tarefa da lista a ser realizada é a de número 1 — pagar as contas no banco. Em seguida, a tarefa 2 — comprar os livros na livraria — é a próxima, e assim por diante, até que a tarefa número 8 — passar na banca de jornal — seja executada, terminando, assim, todas as tarefas da lista. Note que, depois da tarefa 8, não existem outras a serem executadas. Fila As listas encadeadas, filas e pilhas são, na verdade, listas de dados cuja diferença principal está na forma de acesso à inclusão e à remoção de seus elementos (PUGGA; RISSETTI, 2009). O conceito de fila de programação é o mesmo de quando esperamos para sermos atendidos em determinada ordem: o primeiro elemento a entrar na fila será o primeiro a sair dela. Esse conceito é tecnicamente conhecido como Fifo (first in, first out), ou seja, o primeiro que entra na fila é o primeiro a sair dela. Em português, também é conhecido como Peps (primeiro a entrar, primeiro a sair), conforme ilustra a Figura 5. Figura 5: Estrutura de dados fila. Fonte: Pugga e Rissetti (2009). Pilha As pilhas, de acordo com Pugga e Rissetti (2009), são estruturas de dados conhecidas como listas Lifo (last in, first out), ou seja, o último elemento a entrar será o primeiro a sair. Em português, também é conhecida como Ueps (último a entrar, primeiro a sair). Novos elementos são armazenados no fim da fila O primeiro elemento a entrar será o primeiro a sair (Peps) Elementos armazenados na fila 15 Na verdade, trata-se de uma estrutura do tipo lista linear, em que todas as operações feitas — inclusão e remoção — são realizadas por um único extremo, pelo topo da pilha, conforme ilustra a Figura 6. Figura 7: Representação de grafo. Fonte: Simões-Pereira (2014). Grafo No estudo da teoria dos grafos, um grafo representa um conjunto de pontos, denomina- dos vértices, ligados por retas, denominadas arestas, que, dependendo do tipo de aplica- ção, poderá apresentar setas ou não. Segundo Simões-Pereira (2014), matematicamente, dizemos que um grafo é representado da seguinte forma: G = (V, E), que se refere a um sistema formado por um conjunto V de elementos denominados vértices e um conjunto E de arestas, conforme ilustra a Figura 7. Figura 6: Estrutura de dados pilha. Fonte: Elaborada pelo autor (2019). n Tamanho da pilha (...) 5 5 B 4 Topo D 3 A 2 1 C 1 Base Itens Pilha a u x v b 16 Quando um grafo tem arestas que apresentam setas nas extremidades para representar o sentido, ou seja, a direção do fluxo, ele é chamado tecnicamente de grafo dirigido, ou dígrafo. Saiba mais Árvore A estrutura de dados árvore é uma lista na qual cada elemento possui dois ou mais su- cessores, porém todos os elementos possuem um, e apenas um, antecessor, conforme ilustra a Figura 8 (FORBELLONE; EBERSPÄCHER, 2005). MIDIATECA Acesse a midiateca da Unidade 1 e veja o conteúdo complementar indicado pelo professor sobre linguagem C. Figura 8: Exemplo de árvore. Fonte: Forbellone e Eberspächer (2005). A B C G M F E D H I J L 17 Para ler uma abordagem mais aprofundada sobre vetores e matrizes de dados, com exercícios propostos para fixação de conteúdo, sugerimos o capítulo 7 (p.181-193) do seguinte livro, disponível na Biblioteca Virtual: MIZRAHI, V. V. Treinamento em linguagem C. 2. ed. São Paulo: Pearson Education do Brasil, 2008. Dica 18 Vetores e matrizes de dados Uma estrutura vetor representa um endereço de memória em que serão armazenados diversos dados de acordo com o dimensionamento dado a ele, na própria definição, no- momento da criação da variável vetor (PUGGA; RISSETTI, 2009). Imagine você escrever um programa para ler três notas de alunos de uma turma para calcular sua média aritmética sem o uso de vetor. A solução poderia ser algo assim: /* Comentario: Calculo de media de 3 notas SEM vetor */ # include “stdio.h” float nota1, nota2, nota3, media; void main( ) { printf (“Digite a nota 1: “); scanf (“%f”, nota1); printf (“Digite a nota 2: “); scanf (“%f”, nota2); printf (“Digite a nota 3: “); scanf (“%f”, nota3); media = (nota1 + nota2 + nota3) / 3; printf (“Media = %.2f\n”, media); } /* Comentario: Calculo de media de 3 notas COM vetor */ # include “stdio.h” float notas[3], media; int i; void main( ) { for (i=1; i<=3; i++) { printf (“Digite a nota %d: “, i); scanf (“%f”, notas[i]); Agora vamos escrever um programa para solucionar o mesmo problema, porém com o uso de vetor unidimensional: 19 media += notas[i]; } media /= 3; printf (“Media = %.2f\n”, media); } /* programa exemplo de vetor */ #define DIM 5 #include <stdio.h> /* Corpo principal do programa */ void main ( ) { int i, vetor1[DIM], vetor2[DIM]; for ( i=0; i<DIM; i++ ) { printf (“vetor1[%d]=“, i); scanf (“%d”, &vetor1[ i ]); } for ( i=0; i<DIM; i++ ) { printf (“vetor2[%d]=“, i); scanf (“%d”, &vetor2[ i ]); } for ( i=0; i<DIM; i++ ) printf (“vetor1[%d] + vetor2[%d] = %d\n”, i, i, vetor1[ i ] + vetor2[ i ]); } Vejamos outro exemplo prático do uso de vetor unidimensional: Por definição, uma matriz é uma coleção de variáveis do mesmo tipo que compartilham o mesmo nome (MIZRAHI, 2008). Dessa forma, quando temos vetores com mais de uma dimensão, dizemos estar manipulando vetores multidimensionais ou, simplesmente, ma- trizes. Veja o exemplo a seguir: 20 /* Comentario: Exemplo de MATRIZ */ #include <stdio.h> #define LIN 2 #define COL 2 void main ( ) { int mat [LIN][COL], i, j; for ( i=1; i<3; i++ ) for ( j=1; j<3; j++ ) { printf (“\nEntre com o elemento[%d][%d]”, i, j); scanf (“%d”, &mat[ i ][ j ]); } for ( i=1; i<3; i++ ) for ( j=1; j<3; j++ ) if ( i == j ) printf (“\n%d”, mat[ i ][ j ]); } O programa exemplo apresentado permite criar uma matriz quadrada de ordem 2, ou seja, uma matriz 2x2 cujos elementos são valores inteiros. Ao final do processamento serão exibidos todos os elementos que façam parte da diagonal principal. MIDIATECA Acesse a midiateca da Unidade 1 e veja o conteúdo complementar indicado pelo professor sobre matrizes. 21 Ponteiros ou apontadores Como já estudamos anteriormente,um ponteiro, ou apontador, é uma variável que arma- zena no seu espaço de memória um endereço de memória referente a outra variável no lugar de armazenar um conteúdo de dado. A alocação dinâmica de memória em tempo de execução é realizada em linguagem C por meio de ponteiro, como podemos analisar no código de programa abaixo: /* Comentario: alocacao de memoria dinamica */ #include <stdio.h> #include <malloc.h> #include <dos.h> void main ( ) { // Declaracao do ponteiro int *ptr; ptr = ( int * ) malloc( sizeof( int )); *ptr = 3; system(“CLS”); printf (“Conteudo do ponteiro: %d\n\n”, *ptr); system(“PAUSE”); } Note que, no programa apresentado, atribuímos à variável ptr um valor retornado por uma função chamada malloc( ), a qual é declarada na biblioteca padrão malloc.h. Para que possamos entender a instrução ptr = ( int * ) malloc( sizeof( int )), vamos dividi-la em partes: - O operador sizeof( ) devolve o tamanho, em bytes, do tipo ou da expressão entre parên- teses. - A função malloc( ) tem o objetivo de retornar um ponteiro para uma área livre de memó- ria a ser identificada por ela. 22 Dessa forma, a instrução ptr = ( int * ) malloc( sizeof( int )) cria dinamicamente uma variá- vel inteira referenciada por *ptr. Podemos, ao mesmo tempo, trabalhar com muitas variá- veis do tipo ponteiro na memória do computador. A determinação referente à quantidade necessária para utilização em um determinado programa depende exclusivamente de a que o programa se propõe. Veja a saída produzida pelo programa: Vamos analisar outro exemplo de ponteiro/apontador: /* Comentario: Outro exemplo de ponteiro/apontador */ #include <stdio.h> #include <conio.h> #include <dos.h> int main(void) { int num = 13; // Declaracao do ponteiro int *ptr; 23 // Atribuicao do endereco da variavel num ao ponteiro ptr ptr = # system(“CLS”); printf (“\nUtilizando ponteiros”); printf (“\n\nConteudo da variavel num: %d”, num); printf (“\nEndereco da variavel num: %x”, &num); printf (“\nConteudo da variavel ponteiro ptr: %x”, ptr); system(“PAUSE”); return(0); } Veja a saída produzida pelo programa: Toda variável ponteiro, ou apontador, depois da especificação de seu tipo, apre- senta um asterisco à esquerda, identificando, portanto, que se trata, efetiva- mente, de uma variável ponteiro. Saiba mais 24 MIDIATECA Acesse a midiateca da Unidade 1 e veja o conteúdo complementar indicado pelo professor sobre ponteiros. NA PRÁTICA Você é programador da empresa Renalf Mega Data e seu gerente de projetos de sistemas solicitou que você fizesse um programa de computador que imple- mentasse um vetor de 50 posições para armazenamento de dados, fornecido via teclado, para cada uma das seguintes variáveis: código, mercadoria, quanti- dade e preço unitário. Deve-se armazenar também o preço total, que deverá ser calculado automaticamente pelo sistema (quantidade versus preço unitário). No final do processamento, imprimir todos os códigos das mercadorias e seus respectivos preços totais para todos os registros cuja quantidade seja igual ou superior a 100 unidades. /* Comentario: Uma possivel solucao */ #include <stdio.h> int codigo[50], quantidade[50]; char mercadoria[10][50]; float preco_unitario[50], preco_total[50]; int i; void main( ) { /* Entrada de Dados */ for(i=1; i<=50; i++) { printf(“\nCodigo ? “); scalf(“%d”, &codigo[ i ]); printf(“\nMercadoria ? “); scalf(“%s”, &mercadoria[ i ]); printf(“\nQuantidade ? “); scalf(“%d”, &quantidade[ i ]); printf(“\nPreco Unitario ? “); scalf(“%f”, &preco_unitario[ i ]); preco_total[ i ] = quantidade[ i ] * preco_unitario[ i ]; } 25 /* Saida de Informacoes */ for(i=1; i<=50; i++) if(quantidade[ i ] >= 100) printf(“\nCodigo: %d Preco Total: %.2f “, codigo[ i ], preco_total[ i ]); } 26 Resumo da Unidade 1 Nesta unidade, você conheceu as principais estruturas de dados e seus tipos, implemen- tou soluções computacionais que envolvessem o uso de vetores e matrizes de dados, assim como a estrutura ponteiro/apontador — alocação dinâmica. Para complementar seus estudos, sugerimos que leia os livros indicados nas referências. Nesta unidade, destacaram-se o conhecimento das estruturas de dados e seus tipos, a manipulação dos programas de computador com o uso de vetores e matrizes de dados e o desenvolvimento de algoritmos computacionais de alo- cação dinâmica com ponteiros e apontadores. CONCEITO 27 Referências FORBELLONE, A. L. V.; EBERSPÄCHER, H. F. Lógica de programação: a construção de algoritmos e estruturas de dados. 3. ed. São Paulo: Pearson Prentice Hall, 2005. Bibliote- ca Virtual. MIZRAHI, V. V. Treinamento em linguagem C. 2. ed. São Paulo: Pearson Education do Brasil, 2008. Biblioteca Virtual. PUGGA, S.; RISSETTI, G. Lógica de programação e estrutura de dados com aplicações em Java. 3. ed. São Paulo: Pearson Prentice Hall, 2009. Biblioteca Virtual. SIMÕES-PEREIRA, J. M. S. Grafos e redes: teoria e algoritmos básicos. Rio de Janeiro: Interciência, 2014. Biblioteca Virtual. Estruturas lineares encadeadas: listas UNIDADE 2 29 Esta unidade é importante para que o estudante compreenda os processos de manipula- ção da estrutura de dados lista, com suas possíveis variações de encadeamento — sim- ples e duplamente encadeada —, assim como operações elementares com lista circular. INTRODUÇÃO OBJETIVO Nesta unidade, você será capaz de: • Compreender a estrutura linear encadeada por meio de lista simplesmente encadeada. • Implementar algoritmo computacional com lista duplamente encadeada. • Desenvolver soluções computacionais com a estrutura de dados lista circular. 30 Lista simplesmente encadeada De acordo com Forbellone e Eberspächer (2005), denomina-se lista, ou lista encadea- da, um conjunto de elementos individualizados em que cada elemento referencia outro como sucessor. As listas são estruturas de dados complexas que podem ser trabalhadas sob três confi- gurações distintas: • Lista simplesmente encadeada, que será estudada neste tópico. • Lista duplamente encadeada, que será estudada no segundo tópico. • Lista circular, que será estudada no terceiro tópico. A lista simplesmente encadeada refere-se a uma sequência de nós em que cada nó possui armazenado certo dado e também o endereço do seguinte nó da lista, conforme ilustra a Figura 1. Lista refere-se a um ponteiro para o primeiro nó efetivo da lista, que contém, neste caso, o valor “1”, assim como um ponteiro para o próximo nó dessa lista. Observe que o último nó aponta para NULL, ou seja, indica o final da lista. Note que o sentido da lista é linearmente descrito da esquerda para a direita. Então, o início da lista se dá à esquerda, enquanto o final da lista simplesmente encadeada termina à direita. Curiosidade Figura 1: Representação de uma lista simplesmente encadeada. Fonte: Elaborada pelo autor (2019). 1 2 3Lista 31 Vejamos, em seguida, um programa exemplo de lista simplesmente encadeada: /* Comentario: Lista Simplesmente Encadeada */ #include <stdio.h> #include <stdlib.h> #include <locale.h> /* Comentario: Declaracao da Estrutura Nodo */ typedef struct sNodo { int info; struct sNodo *prox; } Nodo; /* Comentario: Declaracao da Estrutura Lista Simplesmente Encadeada */ typedef struct sListaSimplesEnc { Nodo *prim; } ListaSimplesEnc; /* Comentario: Criar Lista Vazia */ void criarLista(ListaSimplesEnc *pList) { pList->prim = NULL; } /* Comentario: Mostrar Elementos da Lista */ void mostrarLista(ListaSimplesEnc *pList) { Nodo *p; printf(“Lista: “); for (p = pList->prim; p != NULL; p = p->prox) { printf(“%d -> “, p->info); } printf(“NULL\n”); } /* Comentario: Inserir no Inicio da Lista */ void inserirIni(ListaSimplesEnc *pList, int v) { Nodo *novo; novo = (Nodo*)malloc(sizeof(Nodo)); if (novo != NULL) { 32 novo->info = v; novo->prox = pList->prim; pList->prim = novo; } else { printf(“Memória Insuficiente\n”); } } /* Comentario: Remover no Inicio da Lista */ void removerIni(ListaSimplesEnc*pList) { Nodo *pAux = pList->prim; if (pAux != NULL) { pList->prim = pList->prim->prox; free(pAux); printf(“Valor Removido\n”); } else { printf(“Lista Vazia\n”); } } /* Comentario: Inserir Elemento em Ordem na Lista */ void inserirOrd(ListaSimplesEnc *pList, int v) { Nodo *novo; novo = (Nodo*)malloc(sizeof(Nodo)); if (novo != NULL) { novo->info = v; Nodo *pAtu, *pAnt; pAnt = NULL; pAtu = pList->prim; while (pAtu != NULL && pAtu->info < v) { pAnt = pAtu; pAtu = pAtu->prox; } novo->prox = pAtu; 33 if (pAnt != NULL) { pAnt->prox = novo; } else { pList->prim = novo; } } else { printf(“Memória Insuficiente\n”); } } /* Comentario: Remover um Elemento Especifico da Lista */ void removerEle(ListaSimplesEnc *pList, int v) { Nodo *pAtu, *pAnt; pAnt = NULL; pAtu = pList->prim; while (pAtu != NULL && pAtu->info != v) { pAnt = pAtu; pAtu = pAtu->prox; } if (pAnt != NULL) { if (pAtu != NULL) { pAnt->prox = pAtu->prox; free(pAtu); printf(“Valor Removido\n”); } else { printf(“Valor não encontrado\n”); } } else { printf(“Lista Vazia\n”); } } /* Comentario: Remover Todos os Elementos da Lista */ void removerTudo(ListaSimplesEnc *pList) { Nodo *pAux = pList->prim; 34 if (pAux != NULL) { while (pAux != NULL) { pList->prim = pAux->prox; free(pAux); pAux = pList->prim; } printf(“Todos os elementos foram removidos!\n”); } else { printf(“Lista Vazia\n”); } } /* Comentario: Alterar Elemento da Lista */ void alterarEle(ListaSimplesEnc *pList, int v1, int v2) { Nodo *pAtu, *pAnt; pAnt = NULL; pAtu = pList->prim; while (pAtu != NULL && pAtu->info != v1) { pAnt = pAtu; pAtu = pAtu->prox; } if (pAnt != NULL) { if (pAtu != NULL) { pAtu->info = v2; printf(“Valor alterado\n”); } else { printf(“Valor não encontrado\n”); } } else { printf(“Lista Vazia\n”); } } /* Comentario: Lista Vazia */ int estaVazia(ListaSimplesEnc *pList) { return(pList->prim == NULL); } 35 /* Comentario: Programa Principal */ void main() { setlocale(LC_ALL, “portuguese”); ListaSimplesEnc minhaLista; int valor, op, valorAlt; criarLista(&minhaLista); printf(“Escolha uma opção:\n”); while (1) { printf(“\n(1) Inserir elemento no início da Lista\n”); printf(“(2) Inserir elemento em ordem (verifique se a lista está orde- nada)\n”); printf(“(3) Remover elemento no início da Lista\n”); printf(“(4) Remover elemento específico da Lista\n”); printf(“(5) Mostrar Lista\n”); printf(“(6) Apagar todos os elementos da Lista\n”); printf(“(7) Alterar elemento da Lista\n”); printf(“(0) Sair\n”); printf(“ ? “); scanf(“%d”, &op); system(“cls”); switch (op) { case 1: // inserir elemento no inicio printf(“Valor? “); scanf(“%d”, &valor); inserirIni(&minhaLista, valor); break; case 2: // inserir elemento ordenado printf(“Valor? “); scanf(“%d”, &valor); inserirOrd(&minhaLista, valor); break; case 3: // remover o primeiro removerIni(&minhaLista); break; case 4: // remover determinado elemento printf(“Valor? “); scanf(“%d”, &valor); removerEle(&minhaLista, valor); break; case 5: // mostrar lista if (estaVazia(&minhaLista)) { printf(“Lista vazia\n”); } else { mostrarLista(&minhaLista); 36 } break; case 6: // apagar todos os elementos da Lista removerTudo(&minhaLista); break; case 7: // alterar um elemento printf(“Valor a ser alterado? “); scanf(“%d”, &valor); printf(“Novo valor? “); scanf(“%d”, &valorAlt); alterarEle(&minhaLista, valor, valorAlt); break; case 0: // abandonar o programa removerTudo(&minhaLista); exit(0); default: printf(“Opção inexistente!\n”); } } } MIDIATECA Acesse a midiateca da Unidade 2 e veja o conteúdo complementar indicado pelo professor sobre lista encadeada. 37 Lista duplamente encadeada Conforme afirmam Pugga e Rissetti (2009), o encadeamento duplo de lista, ou lista du- plamente encadeada, apresenta como característica a possibilidade de ligação com du- plo sentido — horário e anti-horário —, ou seja, pode varrer os elementos da lista tanto da esquerda para a direita como da direita para a esquerda, conforme ilustra a Figura 2. Em uma lista duplamente encadeada, cada nó tem um ponteiro para o próximo elemento e um para o elemento anterior. Assim, ao acessar um elemento da lista duplamente en- cadeada, acessam-se também os elementos adjacentes, ou seja, o próximo e o anterior. Vejamos, em seguida, um programa exemplo de lista duplamente encadeada: /* Comentario: Lista Duplamente Encadeada */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <locale.h> enum OpcoesMenu{SAIR=0,CRIAR_LISTA,INSERIR_AMIGO,IMPRIMIR_LISTA,REMOV- ER_AMIGO,VERIFICAR_LISTA_VAZIA,LIBERAR_MEMORIA}; #define TAMMAX_AMIGO 40 /* Comentario: Define Estrutura da lista */ struct no { char amigo[TAMMAX_AMIGO]; struct no * anterior; struct no * proximo; }; Figura 2: Representação de uma lista duplamente encadeada. Fonte: Elaborada pelo autor (2019). 1 2 3Lista 38 /* Comentario: Lista Duplamente Encadeada */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <locale.h> enum OpcoesMenu{SAIR=0,CRIAR_LISTA,INSERIR_AMIGO,IMPRIMIR_LISTA,REMOV- ER_AMIGO,VERIFICAR_LISTA_VAZIA,LIBERAR_MEMORIA}; #define TAMMAX_AMIGO 40 /* Comentario: Define Estrutura da lista */ struct no { char amigo[TAMMAX_AMIGO]; struct no * anterior; struct no * proximo; }; /* Comentario: Criar Lista Duplamente Encadeada */ No * criar_elemento() { No * e = (No*) malloc(sizeof(No)); e->anterior = e->proximo = NULL; e->amigo[0] = ‘\0’; return e; } /* Comentario: Incluir Amigo no Final da Lista */ void inserir(No * lista, char * nome_amigo) { if (is_vazia(lista)) { strncpy(lista->amigo, (const char *) nome_amigo, strlen(nome_amigo)); } else { No * tmp = lista; while(tmp->proximo != NULL) { tmp = tmp->proximo; } No * e = criar_elemento(); strncpy(e->amigo, (const char *) nome_amigo, strlen(nome_amigo)); e->anterior = tmp; e->proximo = NULL; tmp->proximo = e; } } 39 /* Comentario: Pesquisar Amigo */ No * pesquisar (No * inicio_lista, char * nome_amigo) { No * tmp; if (is_vazia(inicio_lista)) { tmp = NULL; } else { tmp = inicio_lista; while(tmp != NULL) { if (strcasecmp(tmp->amigo, nome_amigo) == 0) return tmp; tmp = tmp->proximo; } } return tmp; } /* Comentario: Imprimir Lista de Amigos */ void imprimir(No * lista) { if (is_vazia(lista)) { puts(“\nSua lista de amigos está vazia.\n”); } else { No * tmp = lista; printf(“\n* * * Lista de Amigos * * *\n”); while(tmp != NULL) { printf(“%s\n”, tmp->amigo); tmp = tmp->proximo; } } } /* Comentario: Remover Amigo da Lista */ No * remover(No * lista, No * elemento) { No * tmp_inicio; if (elemento == lista) { 40 tmp_inicio = lista; tmp_inicio = elemento->proximo; tmp_inicio->anterior = NULL; } else { tmp_inicio = elemento->anterior; tmp_inicio->proximo = elemento->proximo; tmp_inicio = lista; } free(elemento); return tmp_inicio; } /* Comentario: Apagar Lista Duplamente Encadeada */ void liberar_memoria(No * lista) { if (lista->proximo == NULL) { free(lista); } else { return liberar_memoria(lista->proximo); } } /* Comentario: Menu de Escolhas */ void menu_escolha() { printf(“\n* * * M E N U * * *\n\n”); printf(“%d) Sair\n”, SAIR); printf(“%d) Criar lista\n”, CRIAR_LISTA); printf(“%d) Inserir amigo\n”, INSERIR_AMIGO); printf(“%d) Imprimir lista de amigos\n”, IMPRIMIR_LISTA); printf(“%d) Remover amigo\n”, REMOVER_AMIGO); printf(“%d) Verificar se lista está vazia\n”, VERIFICAR_LISTA_VAZIA); printf(“%d) Apagar lista\n”, LIBERAR_MEMORIA); } /* Comentario: Leitura do Nome do Amigo */ char * ler_amigo() { char nome[TAMMAX_AMIGO]; printf(“\nQual o nome do Amigo: “); fgets(nome, TAMMAX_AMIGO, stdin); (*strrchr(nome, ‘\n’))= ‘\0’; return strdup(nome); } 41 /* Comentario: Programa Principal */ int main() { int opcao; char * nome_amigo; No * lista = NULL; setlocale(LC_ALL, “portuguese”); while(1) { system(“CLS”); menu_escolha(); printf(“\nOpção: “); scanf(“%d%*c”, &opcao); if (opcao == SAIR) break; if (lista == NULL && opcao != CRIAR_LISTA) { puts(“\nA lista não foi criada.\n”); } else { switch(opcao) { case CRIAR_LISTA: lista = criar_elemento(); printf(“\nLista criada com sucesso.\n”); break; case INSERIR_AMIGO: puts(“\n* * * INCLUSÃO * * *”); nome_amigo = ler_amigo(); if (pesquisar(lista, nome_amigo) == NULL) { inserir(lista, nome_amigo); } else { puts(“\nAmigo já consta na lista.\n”); } break; case IMPRIMIR_LISTA: imprimir(lista); break; 42 case REMOVER_AMIGO: puts(“\n* * * REMOÇÃO * * *”); nome_amigo = ler_amigo(); No * registro = pesquisar(lista, nome_amigo); if (registro == NULL) { puts(“\nAmigo não encontrado.\n”); } else { lista = remover(lista, registro); } break; case VERIFICAR_LISTA_VAZIA: printf(“\nA lista%sestá vazia\n”, is_vazia(lista)? “ “ : “ não “); break; case LIBERAR_MEMORIA: liberar_memoria(lista); lista = NULL; printf(“\nLista apagada.\n”); break; default: printf(“\nDigite uma opção válida.\n”); } } system(“PAUSE”); } /* Comentario: Liberar a Lista Duplamente Encadeada da Memória */ if (lista != NULL) liberar_memoria(lista); return 0; } MIDIATECA Acesse a midiateca da Unidade 2 e veja o conteúdo complementar indicado pelo professor sobre lista duplamente encadeada. 43 Lista circular A lista circular, conforme ilustra a Figura 3, nada mais é que uma lista ligada, ou lista sim- plesmente encadeada, cujo último elemento aponta diretamente para o primeiro dessa lista (FORBELLONE; EBERSPÄCHER, 2005). Vejamos em seguida um programa exemplo de lista circular: /* Comentario: Lista Circular */ #include <stdio.h> #include <stdlib.h> #include <locale.h> /* Comentario: Declaração da Estrutura da Lista Circular */ struct no { int info; struct no *prox; }; /* Comentario: Descrição das Funções */ void addinicio(struct no *head, int valor); void addfim(struct no *head, int valor); void remover(struct no *head, int valor); void buscar(struct no *head, int valor); void imprimir(struct no *head); void menu(); /* Comentario: Inserir Elemento no Início da Lista Circular */ void addinicio(struct no *head, int valor) { Figura 3: Representação de uma lista circular. Fonte: Elaborada pelo autor (2019). 1 2 3Lista 44 struct no *p; printf(“\nValor do Elemento: “); scanf(“%d”,&valor); if((p = malloc(sizeof(struct no))) == NULL) { printf(“\nErro de Memória\n”); } else { p->info = valor; p->prox = head->prox; head->prox = p; } } /* Comentario: Inserir no Final da Lista Circular */ void addfim(struct no *head, int valor) { struct no *pNow, *pNext; printf(“\nValor do elemento: “); scanf(“%d”,&valor); if((pNow = malloc(sizeof(struct no))) == NULL) { printf(“\nErro de Memória\n”); } else { pNow->info = valor; pNext = head->prox; while(pNext->prox != head) { pNext = pNext->prox; } pNow->prox = pNext->prox; pNext->prox = pNow; } } /* Comentario: Remover Elemento da Lista Circular */ void remover(struct no *head, int valor) { struct no *p, *aux; printf(“\nValor a Remover: “); scanf(“%d”,&valor); p = head->prox; aux = head; head->info = valor; 45 while(p->info != valor) { aux = p; p = p->prox; } if(p == head) { printf(“\nElemento não encontrado\n”); } else { aux->prox = p->prox; free(p); printf(“\nElemento removido com Sucesso!\n”); } } /* Comentario: Imprimir Elementos da Lista Circular */ void imprimir(struct no *head) { struct no *p; int okay = 0; p = head->prox; while(p!=head) { printf(“\n%d”,p->info); p = p->prox; okay = 1; } if(okay == 0) { printf(“\nLista Vazia\n”); } } /* Comentario: Pesquisar Elemento na Lista Circular */ void buscar(struct no *head, int valor) { struct no *p; printf(“\nBuscar elemento: “); scanf(“%d”,&valor); p = head->prox; head->info = valor; while(p->info != valor) { p = p->prox; } 46 if(p == head) { printf(“\nElemento não encontrado!\n”); } else { printf(“\nElemento encontrado com sucesso!\n”); } } /* Comentario: Menu de Escolhas */ void menu() { printf(“\n1) Inserir no Início”); printf(“\n2) Inserir no Final”); printf(“\n3) Buscar Elemento”); printf(“\n4) Remover Elemento”); printf(“\n5) Imprimir Lista”); printf(“\n\n0) Sair: “); } /* Comentario: Programa Principal */ int main(int argc, char *argv) { struct no *head; int op, valor; setlocale(LC_ALL, “portuguese”); if((head = malloc(sizeof (struct no))) == NULL) { printf(“\nErro de Memória\n”); } else { head->prox = head; do { system(“CLS”); menu(); scanf(“%d”,&op); switch(op) { case 0: break; case 1: addinicio(head, valor); break; 47 case 2: addfim(head, valor); break; case 3: buscar(head, valor); break; case 4: remover(head, valor); break; case 5: imprimir(head); break; default: printf(“\nOpçção Inválida!\n”); break; } printf(“\n”); system(“PAUSE”); } while(op!=0); return 0; } } Existe também a possibilidade de implementação de soluções computacionais com listas duplamente circulares. Curiosidade 48 MIDIATECA Acesse a midiateca da Unidade 2 e veja o conteúdo complementar indicado pelo professor sobre listas circulares. NA PRÁTICA A empresa Data Systems tem a necessidade de implementar uma solução computacional em linguagem C que tenha por objetivo o registro de uma senha alfabética como elemento de controle de certo aplicativo web cuja estratégia é ter desenvolvido um programa com lista duplamente encadeada para habilitar (incluir) e desabilitar (remover) a senha alfabética dessa lista. Então, sua mis- são é descrever essas rotinas em linguagem C. /* Comentario: Solução Rotinas Lista Duplamente Encadeada */ void inserir(No * lista, char * senha_alfa) // Habilitar senha na lista duplamente encadeada { if (is_vazia(lista)) { MIDIATECA Acesse a midiateca da Unidade 2 e veja o conteúdo complementar indicado pelo professor sobre lista duplamente circular. 49 strncpy(lista->senha, (const char *) nome_senha, strlen(nome_senha)); } else { No * tmp = lista; while(tmp->proximo != NULL) { tmp = tmp->proximo; } No * e = criar_elemento(); strncpy(e->senha, (const char *) nome_senha, strlen(nome_senha)); e->anterior = tmp; e->proximo = NULL; tmp->proximo = e; } } No * remover(No * lista, No * elemento) // Desabilitar senha na lista duplamente encadeada { No * tmp_inicio; if (elemento == lista) { tmp_inicio = lista; tmp_inicio = elemento->proximo; tmp_inicio->anterior = NULL; } else { tmp_inicio = elemento->anterior; tmp_inicio->proximo = elemento->proximo; tmp_inicio = lista; } free(elemento); return tmp_inicio; } 50 Resumo da Unidade 2 Nesta unidade, você conheceu as estruturas lineares encadeadas (listas), nas variações de lista simplesmente encadeada, lista duplamente encadeada e lista circular, por meio da implementação de programas de computador. CONCEITO Nesta unidade, destacam-se o conhecimento e a implementação de solução computacional por meio das estruturas de dados lista simplesmente encadea- da, lista duplamente encadeada e lista circular. 51 Referências FORBELLONE, A. L. V.; EBERSPÄCHER, H. F. Lógica de programação: a construção de algoritmos e estrutura de dados. 3. ed. São Paulo: Pearson Prentice Hall, 2005. Biblioteca Virtual. Estruturas lineares encadeadas: filas e pilhas UNIDADE 3 53 Nesta unidade, o estudante conhecerá os procedimentos necessários para a manipula- ção de filas e pilhas lineares encadeadas, assim como das filas circulares, como opção de estrutura de dados complexa. INTRODUÇÃO Nestaunidade, você será capaz de: • Entender as estruturas de dados fila e pilha. • Implementar solução com fila linear. • Implementar solução com pilha linear. • Desenvolver programa de computador com a estrutura fila circular. OBJETIVO 54 Trabalhando com filas A fila é uma estrutura de dados dinâmica que admite a inserção e a remoção de ele- mentos a partir da técnica conhecida como Fifo (first in, first out), ou seja, o primeiro elemento a entrar na fila será o primeiro a sair dela, conforme ilustrado na Figura 1 (MIZRAHI, 2008). Figura 1: Estrutura de dados fila linear. Fonte: Elaborada pelo autor (2019). Em português, a técnica Fifo é conhecida como Peps, ou seja, o “primeiro a entrar é o primeiro a sair”. Vejamos um exemplo de programa com a estrutura de dados fila: /* Comentario: Trabalhando com a Estrutura de Dados Fila Linear */ #include <stdio.h> #include <stdlib.h> #include <locale.h> /* Comentario: Declaração da Estrutura Fila */ struct Fila { int capacidade; int *dados; int primeiro; int ultimo; int nItens; }; /* Comentario: Criar Fila */ void criarFila( struct Fila *f, int c ) { 1 3 n2 ... n + 1 Remoção Final da fila Início da fila Inserção 55 f->capacidade = c; f->dados = (int*) malloc (f->capacidade * sizeof(int)); f->primeiro = 0; f->ultimo = -1; f->nItens = 0; } /* Comentario: Inserir Elemento na Fila */ void inserir(struct Fila *f, int v) { if(f->ultimo == f->capacidade-1) f->ultimo = -1; f->ultimo++; f->dados[f->ultimo] = v; f->nItens++; } /* Comentario: Remover Elemento da Lista */ int remover( struct Fila *f ) { int temp = f->dados[f->primeiro++]; if(f->primeiro == f->capacidade) f->primeiro = 0; f->nItens--; return temp; } /* Comentario: Fila Vazia */ int estaVazia( struct Fila *f ) { return (f->nItens==0); } /* Comentario: Fila Cheia */ int estaCheia( struct Fila *f ) { return (f->nItens == f->capacidade); } /* Comentario: Mostrar Elementos da Fila */ void mostrarFila(struct Fila *f) { int cont, i; for ( cont=0, i= f->primeiro; cont < f->nItens; cont++) { printf(“%d “,f->dados[i++]); 56 if (i == f->capacidade) i=0; } printf(“\n”); } /* Comentario: Programa Principal */ void main () { int opcao, capa; int valor; struct Fila umaFila; setlocale(LC_ALL, “portuguese”); system(“CLS”); printf(“Capacidade da Fila?”); scanf(“%d”,&capa); criarFila(&umaFila, capa); while( 1 ) { system(“CLS”); printf(“ * * * FAÇA SUA ESCOLHA * * * \n\n”); printf(“ 1- Inserir elemento na Fila \n”); printf(“ 2- Remover elemento da Fila \n”); printf(“ 3- Mostrar elementos da Fila \n”); printf(“ 0- Sair \n\n”); printf(“Opção? “); scanf(“%d”, &opcao); switch(opcao){ case 0: exit(0); case 1: if (estaCheia(&umaFila)) { printf(“\nA Fila está Cheia.\n”); system(“PAUSE”); } else { printf(“\nElemento a ser inserido?”); scanf(“%d”, &valor); inserir(&umaFila,valor); } 57 break; case 2: if (estaVazia(&umaFila)) { printf(“\nA Fila está Vazia.\n”); system(“PAUSE”); } else { valor = remover(&umaFila); printf(“\nO Elemento %d foi removido com sucesso.\n”, valor) ; system(“PAUSE”); } break; case 3: if (estaVazia(&umaFila)) { printf(“\nA Fila está Vazia.\n”); system(“PAUSE”); } else { printf(“\nElementos da Fila: “); mostrarFila(&umaFila); printf(“\n”); system(“PAUSE”); } break; default: printf(“\nOpção Inválida.\n”); system(“PAUSE”); } } } 58 Acesse a midiateca da Unidade 3 e veja o conteúdo complementar indicado pelo professor sobre programação em linguagem C. MIDIATECA 59 Trabalhando com pilhas As pilhas são uma lista na qual é aplicada a disciplina de acesso antagônica denomi- nada Ueps (o último a entrar é o primeiro a sair), ou, em inglês, o termo Lifo (last in, first out), ou seja, qualquer elemento que entrar na pilha somente sairá quando todos os que entraram depois dele saírem (PUGGA; RISSETTI, 2009). A Figura 2 ilustra a estrutura de dados pilha linear. Figura 2: Estrutura de dados pilha linear. Fonte: Elaborada pelo autor (2019). Vejamos um exemplo de programa com a estrutura de dados pilha: /* Comentario: Trabalhando com a Estrutura de Dados Pilha Linear */ #include <stdio.h> #include <stdlib.h> #include <locale.h> /* Comentario: Declaração da Estrutura de Dados Pilha */ struct Pilha { int topo; int capa; int *pElem; }; int capacidade; Empilha elemento (Push) Pilha Base Topo Desempilha elemento (Pop) n + 1 ... 2 n 3 1 60 /* Comentario: Criar Pilha */ void criarpilha( struct Pilha *p, int c ) { p->topo = -1; p->capa = c; p->pElem = (int*) malloc (c * sizeof(int)); } /* Comentario: Pilha Vazia */ int estavazia ( struct Pilha *p ) { if( p-> topo == -1 ) return 1; else return 0; } /* Comentario: Pilha Cheia */ int estacheia ( struct Pilha *p ) { if (p->topo == p->capa - 1) return 1; else return 0; } /* Comentario: Inserir Elemento na Pilha - Empilhar */ void empilhar ( struct Pilha *p, int v) { p->topo++; p->pElem [p->topo] = v; } /* Comentario: Remover Elemento da Pilha - Desempilhar */ float desempilhar ( struct Pilha *p ) { int aux = p->pElem [p->topo]; p->topo--; return aux; } /* Comentario: Direciona ao Topo da Pilha */ int retornatopo ( struct Pilha *p ) { return p->pElem [p->topo]; } /* Comentario: Programa Principal */ int main() { struct Pilha minhapilha; int op; int valor; 61 setlocale(LC_ALL, “portuguese”); system(“CLS”); printf( “Capacidade da Pilha?” ); scanf( “%d”, &capacidade ); criarpilha (&minhapilha, capacidade); while( 1 ) { system(“CLS”); printf(“ * * * FAÇA SUA ESCOLHA * * * \n\n”); printf(“ 1- Inserir Elemento na Pilha [Empilhar] \n”); printf(“ 2- Remover Elemento da Pilha [Desempilhar] \n”); printf(“ 3- Mostrar Elemento do Topo da Pilha \n”); printf(“ 0- Sair \n\n”); printf(“Opção?”); scanf(“%d”, &op); switch (op){ case 0: exit(0); break; case 1: if( estacheia( &minhapilha ) == 1 ) printf(“\nA Pilha está Cheia. \n”); else { printf(“\nValor ? “); scanf(“%d”, &valor); empilhar (&minhapilha, valor); } break; case 2: if ( estavazia(&minhapilha) == 1 ) printf( “\nA Pilha está Vazia. \n” ); else { valor = desempilhar (&minhapilha); printf ( “\nO Elemento %d foi Desempilha- do. \n”, valor ); } break; 62 Acesse a midiateca da Unidade 3 e veja o conteúdo complementar indicado pelo professor sobre pilha dinâmica. MIDIATECA case 3: if ( estavazia (&minhapilha) == 1 ) printf( “\nA Pilha está Vazia. \n” ); else { valor = retornatopo (&minhapilha); printf( “\nElemento do Topo da Pilha: %d\n”, valor ); } break; default: printf( “\nOpção Inválida. \n” ); } system(“PAUSE”); } } 63 Filas circulares De acordo com Forbellone e Eberspächer (2005), uma fila circular é um vetor dinâmi- co cujo término se encontra com seu início. No entanto, os elementos da fila circular podem não estar ocupando fisicamente todas as posições desse vetor, identificando esse final, portanto, como NULL. Vejamos a ilustração de uma fila circular na Figura 3. Figura 3: Estrutura de dados fila circular. Fonte: Elaborada pelo autor (2019). A fila circular ilustrada anteriormente é composta por 16 espaços de memória (MÁX. = 16). No entanto, apenas seis dessas posições estão com elementos fisicamente pre- sentes. Vejamos um exemplo prático de fila circular: /* Comentario: Estrutura de Dados Fila Circular */ #include<stdio.h> #include<stdlib.h> #include <locale.h> #define MAX 5 /* Comentario: Declaração da Estrutura Fila Circular */ struct circular { int com; int fim; int total; Início Final 1 2 3 4 5 6 64 int memo[MAX]; }; typedef struct circularcircular; void enfileirar(struct circular *F, int x); int desenfileirar(struct circular *F); void listar(struct circular A); int busca(struct circular B, int x); /* Comentario: Inserir Elemento na Fila Circular */ void enfileirar(struct circular *F, int x) { F->fim++; if(F->fim == MAX) F->fim = 0; F->memo[F->fim] = x; F->total++; } /* Comentario: Remover Elemento da Fila Circular */ int desenfileirar(struct circular *F) { int aux; aux = F->memo[F->com]; F->com++; if(F->com == MAX) { F->com = 0; } F->total --; return aux; } /* Comentario: Listar os Elementos da Fila Circular */ void listar(struct circular A) { printf(“\nElementos da Fila Circular \n\n”); while(A.total!=0) { printf(“%d “, desenfileirar(&A)); } printf(“\n”); } /* Comentario: Buscar um Elemento na Fila Circular */ int busca(struct circular B, int x) { int achou; 65 while(B.total!=0) { if(x == desenfileirar(&B)) { achou = 1; } } return achou; } /* Comentario: Menu de Opções */ int menu() { int opc; system(“CLS”); printf(“* * * FAÇA SUA ESCOLHA * * *\n\n”); printf(“ 1- Enfileirar Elemento \n”); printf(“ 2- Desenfileirar Elemento \n”); printf(“ 3- Listar Elementos \n”); printf(“ 4- Buscar Elemento na Fila \n”); printf(“ 0- Sair \n\n”); printf(“ Opção: “); scanf(“%d”, &opc); return opc; } /* Comentario: Programa Principal */ int main () { struct circular F; F.com = 0; F.total = 0; F.fim = -1; int opc,x,pos,num; setlocale(LC_ALL, “portuguese”); do { opc = menu(); switch(opc) { case 0: exit (0); break; case 1: if(F.fim==MAX-1) { printf(“\nA Fila está Cheia. “); } 66 else { printf(“\n Elemento (número) a ser inserido na Fila: “); scanf(“%d”, &x); if(busca(F,x) == 1) { printf(“Não é possível inserir elementos repetidos “); } else { enfileirar(&F, x); } } break; case 2: if(F.total == 0) printf(“\nA Fila está Vazia. “); else printf(“\nElemento retirado da Fila Circular => %d”, desenfileirar(&F)); break; case 3: if(F.fim == -1) printf(“\nA Fila está Vazia. “); else listar(F); break; case 4: printf(“\nQual é o elemento que deseja localizar?”); scanf(“%d”, &x); if((busca(F,x)==1)) printf(“\nO elemento %d está na Fila. “, x); else printf(“\nO elemento %d não está na Fila. “, x); break; default: printf(“\nOpção Inválida. \n”); } printf(“\n”); system(“PAUSE”); }while(opc!=0); } 67 Acesse a midiateca da Unidade 3 e veja o conteúdo complementar indicado pelo professor sobre fila dinâmica. MIDIATECA A empresa Renalf S/A precisa desenvolver uma rotina (procedimento) de pro- gramação, escrita em linguagem C, com o objetivo de realizar as operações de empilhamento e desempilhamento de elementos (código de acesso secreto) a partir da estrutura de dados pilha considerando a estrutura de uma pilha. /* Comentario: Declaração da Estrutura Pilha */ struct Pilha { int topo; int capa; int *pElem; }; /* Comentario: Procedimento de Empilhamento */ void empilhar ( struct Pilha *p, int v) { p->topo++; p->pElem [p->topo] = v; } /* Comentario: Procedimento de Desempilhamento */ float desempilhar ( struct Pilha *p ) { int aux = p->pElem [p->topo]; p->topo--; return aux; } NA PRÁTICA 68 Resumo da Unidade 3 Nesta unidade, você conheceu e implementou as estruturas de dados fila e pilha lineares. Também foi desenvolvido programa de computador com a estrutura fila circular com fins de aprofundamento de estudo. O estudo das estruturas de dados, além dos vetores, das matrizes, dos pon- teiros/apontadores e das listas, também apresenta o recurso de filas e pilhas. Nesse contexto, quando utilizamos a estrutura de dados fila, aplicamos o méto- do conhecido como Fifo (first in, first out), ou seja, o primeiro elemento a entrar na fila será, sempre, o primeiro elemento a ser retirado dela. De forma análoga, ao utilizarmos a estrutura de dados pilha, aplicamos o método conhecido como Lifo (last in, first out), ou seja, o último elemento a entrar na pilha (empilhamen- to) será o primeiro a ser retirado dela (desempilhamento). CONCEITO 69 Referências FORBELLONE, A. L. V.; EBERSPÄCHER, H. F. Lógica de programação: a construção de algoritmos e estruturas de dados. 3. ed. São Paulo: Pearson Prentice Hall, 2005. Bibliote- ca Virtual. MIZRAHI, V. V. Treinamento em linguagem C. 2. ed. São Paulo: Pearson Education do Brasil, 2008. Biblioteca Virtual. PUGGA, S.; RISSETTI, G. Lógica de programação e estrutura de dados com aplicações em Java. 3. ed. São Paulo: Pearson Prentice Hall, 2009. Biblioteca Virtual. SIMÕES-PEREIRA, J. M. S. Grafos e redes: teoria e algoritmos básicos. Rio de Janeiro: Interciência, 2014. Biblioteca Virtual. Grafos e árvores UNIDADE 4 71 Nesta unidade, o estudante conhecerá a teoria dos grafos e a estrutura de dados árvore — sua variação como árvore binária e árvore AVL —, explorando também os percursos em árvore e os algoritmos de busca. INTRODUÇÃO Nesta unidade, você será capaz de: • Entender o funcionamento de programas executáveis a partir da visão da ar- quitetura de computadores, assim como a diferença entre a linguagem interpre- tada e a linguagem montadora. OBJETIVO 72 Grafos e árvores Grafo O conceito de grafo, definido por Simões-Pereira (2014), apresenta o grafo G = (V, E) como um sistema formado por: • Um conjunto V de elementos chamados vértices, pontos ou nodos. • Um conjunto E de pares não ordenados de vértices denominados arestas ou linhas. Figura 1: Representação de um grafo. Fonte: Simões-Pereira (2014). Dígrafo ou grafo dirigido Um dígrafo D = (V, A) é definido de modo semelhante, em que A representa um conjunto de pares ordenados que chamamos de arcos, conforme ilustra a Figura 2. Figura 2: Representação de um dígrafo. Fonte: Simões-Pereira (2014). 1 4 2 3 1 2 3 73 As figuras 1 e 2 podem ser representadas, respectivamente, da seguinte forma algébrica: G = (V ’ , E) V ’ = { 1, 2, 3, 4 } E = { [1, 2], [2, 3], [3, 1], [1, 4] } D = (V ’’, A) V ’’ = { 1, 2, 3 } A = { (1, 2), (2, 1), (1, 3), (2, 3) } Árvore Segundo Pugga e Rissetti (2009), a árvore é uma estrutura de dados bidirecional, não linear, constituída de nós que representam um modelo hierárquico, uma vez que armaze- nam dados com base em relações de dependência. As estruturas de dados do tipo árvore permitem a implementação de algoritmos que: • Realizam suas tarefas de forma mais eficiente que vetores, listas e conjuntos. • Oferecem uma organização mais natural para os dados. O sentido do fluxo de dados em um grafo tradicional sempre é bidirecional, enquanto o fluxo de dados em um dígrafo é unidirecional, determinado pela direção da seta. Importante 74 Figura 3: Representação hierárquica da estrutura árvore. Fonte: Pugga e Rissetti (2009). Vamos entender melhor o esquema de representação hierárquica da estrutura árvore? • O nó A é considerado o nó raiz ou o pai da árvore (A). • Todos os nós da árvore que contêm filhos são denominados nós intermediários (B, E, F, D e G). • Todo nó da árvore que não apresenta nenhum filho é chamado, tecnicamente, de nó folha ou nó terminal (C, I, J, K, H e L). O nível da árvore indica a posição hierárquica em relação à raiz: • O nó A tem nível 0. • Os nós B, C e D têm nível 1. • Os nós E, F, G e H têm nível 2. • Os nós I, J, K e L têm nível 3. A altura ou profundidade é definida pelo nível máximo de nós: • A árvore T tem altura igual a 3. • A subárvore da esquerda(B) tem altura igual a 2. O grau de um nó é indicado por seu número de filhos: • O nó B tem grau 2. • O nó F tem grau 1. • O nó C tem grau 0. A B E G I K C F H J L D 75 Subárvore Quando existe a representação de uma árvore dentro de outra, dá-se, tecnicamente, o nome de subárvore. Figura 4: Representação de subárvore. Fonte: Pugga e Rissetti (2009). Nesse caso, podemos visualizar as subárvores da esquerda e da direita da árvore A. Representação por parênteses aninhados Cada nó é associado a seus respectivos descendentes ou filhos, conforme ilustra a Figura 5. Figura 5: Representação por parênteses aninhados de uma árvore. T = ( A ( B ( E ( I ) ( J ) ) ( F ( K ) ) ) ( C ) ( D ( G ( L ) ) ( H ) ) ) Fonte: Elaborada pelo autor (2019). A B E G I K C F H J L D T1 T2 Além da forma hierárquica, uma árvore pode ser representada por: • Parênteses aninhados. • Inclusão. • Identação. Curiosidade 76 Representação por inclusão Cada nó é simbolizado por uma figura geométrica, e seus descendentes são inseridos ou incluídos nessa figura, de modo que o nó principal englobe os demais. Figura 6: Representação por inclusão de uma árvore. Fonte: Pugga e Rissetti (2009). Representação por identação Esse tipo de representação coloca os nós identados, de acordo com a prioridade, acerca do nó que está classificado como raiz, pai, filho ou folha. Figura 7: Representação por identação de uma árvore. Fonte: Elaborada pelo autor (2019). A B E I J F K C D G H L A B E I J F K C G L D H 77 Árvores binária e AVL Árvore binária Uma árvore binária, de acordo com Mizrahi (2008), é um conjunto de registros ou nós em forma de estrutura de dados que satisfaz determinadas condições e é caracterizado por: • Um nó especial denominado raiz. • Nenhum nó possuir grau superior a dois, isto é, nenhum nó possui mais de dois descendentes ou filhos. • Uma distinção entre uma subárvore esquerda e uma subárvore direita. Figura 8: Representação de uma árvore binária. Fonte: Elaborada pelo autor (2019). O atravessamento, ou caminhamento, de árvore é a passagem de forma siste- mática por cada um de seus nós. Saiba mais 78 A árvore binária apresenta como particularidade o posicionamento de seus nós, confor- me afirmam Pugga e Rissetti (2009): • Os nós da direita sempre possuem valor superior ao do nó pai. • Os nós da esquerda sempre possuem valor inferior ao do nó pai. Figura 9: Árvore binária. Fonte: Pugga e Rissetti (2009). Uma árvore binária pode também ser classificada como completa ou cheia, conforme ilustra a Figura 10. Figura 10: Árvore binária cheia. Fonte: Adaptado de Forbellone e Eberspächer (2005). Assim como qualquer estrutura de dados, a árvore também é representada em forma de vetor. 57 45 77 13 84 78 60 609 79 Figura 11: Representação de uma árvore em forma de vetor de dados. Fonte: Forbellone e Eberspächer (2005). Árvore AVL Segundo Mizrahi (2008), a árvore AVL é uma árvore binária de busca balanceada, confor- me pudemos observar na Figura 10, também conhecida como Adelson-Velsky e Landis, em homenagem a seus criadores. Uma árvore balanceada é aquela que busca minimizar o número de comparações rea- lizadas, no pior caso, para uma busca com chaves de probabilidades de ocorrências idênticas. 0 1 2 3 4 5 6 80 Buscas em árvore As buscas em árvores são estabelecidas pelos percursos que seguimos para percorrer os nós dessa árvore. De acordo com Forbellone e Eberspächer (2005), o percurso em árvores refere-se à forma como os vértices são visitados durante o processo de varredura (leitura) da árvore. Os percursos caracterizam as chamadas árvores de busca, que poderão utilizar dois métodos: • Busca em profundidade (DFS – depth-first search). • Busca em largura (BFS – breadth-first search). Busca em profundidade A busca em profundidade pode adotar três técnicas distintas para explorar esses percursos: • Pré-ordem. • Em ordem. • Pós-ordem. Na técnica pré-ordem, o percurso primeiro visita o nó raiz, em seguida percorre o nó da esquerda e, por último, trilha o nó da direita. RED: raiz esquerda direita Já na técnica em ordem, o percurso visita o nó da esquerda, percorre a raiz e, por fim, trilha o nó da direita. ERD: esquerda raiz direita Finalmente, a técnica pós-ordem apresenta o percurso visitando o nó da esquerda, em seguida percorrendo o nó da direita e, por fim, o nó raiz. EDR: esquerda direita raiz 81 Tomando como base a árvore binária representada a seguir, na Figura 12 temos: • Percurso pré-ordem (RED): A, B, D, E, C e F. • Percurso em ordem (ERD): D, B, E, A, C e F. • Percurso pós-ordem (EDR): D, E, B, F, C e A. Figura 12: Busca em profundidade em árvore binária. Fonte: Elaborada pelo autor (2019). Busca em largura Na busca em largura, intuitivamente começa-se pelo nó raiz e se exploram (visita) todos os vértices vizinhos. Então, para cada um dos vértices mais próximos, exploramos seus vértices vizinhos inexplorados, e assim por diante, até que ele encontre o alvo da busca ou termine os nós visitados na árvore. Figura 13: Busca em largura em árvore binária. Fonte: Elaborada pelo autor (2019). A B D E F C A B D E F C 82 Note, na ilustração da Figura 13, que a varredura começa sempre de cima para baixo e da esquerda para a direita a partir do nó raiz. Ao aplicar a técnica de busca em largura, com base na árvore apresentada, a seguinte saída é obtida: A, B, C, D, E e F. Importante Vejamos um exemplo prático do uso de operações com árvores. /* Comentario: Manipulação com Árvore Binária */ #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <locale.h> /* Comentario: Declaração da Estrutura do Registro */ struct carro { char nome[30]; char marca[30]; int ano; float preco; }; /* Comentario: Declaração da Estrutura da Árvore */ struct No { int numero; struct carro x; struct No *esquerda; struct No *direita; }; typedef struct No No; /* Comentario: Criar Árvore */ void criarArvore(No **pRaiz) { *pRaiz = NULL; } /* Comentario: Inserir Elemento */ void inserir(No **pRaiz, int numero, struct carro x) { if(*pRaiz == NULL) { *pRaiz = (No *) malloc(sizeof(No)); (*pRaiz)->esquerda = NULL; (*pRaiz)->direita = NULL; 83 (*pRaiz)->numero = numero; (*pRaiz)->x = x; } else { if(numero < (*pRaiz)->numero) inserir(&(*pRaiz)->esquerda, numero, x); if(numero > (*pRaiz)->numero) inserir(&(*pRaiz)->direita, numero, x); } } /* Comentario: Teste Nó Maior Direita */ No *MaiorDireita(No **no) { if((*no)->direita != NULL) return MaiorDireita(&(*no)->direita); else { No *aux = *no; if((*no)->esquerda != NULL) *no = (*no)->esquerda; else *no = NULL; return aux; } } /* Comentario: Teste Nó Maior Esquerda */ No *MenorEsquerda(No **no) { if((*no)->esquerda != NULL) return MenorEsquerda(&(*no)->esquerda); else { No *aux = *no; if((*no)->direita != NULL) *no = (*no)->direita; else *no = NULL; return aux; } } /* Comentario: Remover Elemento */ void remover(No **pRaiz, int numero) { if(*pRaiz == NULL) { printf(“\nNúmero não existe na árvore!\n”); return; } if(numero < (*pRaiz)->numero) remover(&(*pRaiz)->esquerda, numero); else 84 if(numero > (*pRaiz)->numero) remover(&(*pRaiz)->direita, numero); else { No *pAux = *pRaiz; if (((*pRaiz)->esquerda == NULL) && ((*pRaiz)->direita == NULL)) { free(pAux); printf(“\nRemovido com Sucesso! \n”); (*pRaiz) = NULL; } else { if ((*pRaiz)->esquerda == NULL) { (*pRaiz) = (*pRaiz)->direita; pAux->direita = NULL; free(pAux); pAux = NULL; printf(“\nRemovido com Sucesso! \n”); } else { if ((*pRaiz)->direita == NULL) { (*pRaiz) = (*pRaiz)->esquerda; pAux->esquerda = NULL; free(pAux); pAux = NULL; printf(“\nRemovido com Sucesso! \n”); } else { pAux = MaiorDireita(&(*pRaiz)->esquerda); pAux->esquerda = (*pRaiz)->esquerda; pAux->direita = (*pRaiz)->direita; (*pRaiz)->esquerda = (*pRaiz)->direita = NULL; free((*pRaiz)); *pRaiz = pAux; pAux = NULL; printf(“\nRemovido com Sucesso! \n”); } } } } } /* Comentario: Percurso Pre Ordem */ voidexibirPreOrdem(No **pRaiz) { if((*pRaiz) != NULL) { printf(“%i\n”, (*pRaiz)->numero); exibirPreOrdem(&(*pRaiz)->esquerda); exibirPreOrdem(&(*pRaiz)->direita); } } 85 /* Comentario: Percurso Em Ordem */ void exibirEmOrdem(No **pRaiz) { if((*pRaiz) != NULL) { exibirEmOrdem(&(*pRaiz)->esquerda); printf(“%i\n”, (*pRaiz)->numero); exibirEmOrdem(&(*pRaiz)->direita); } } /* Comentario: Percurso Pós-Ordem */ void exibirPosOrdem(No **pRaiz) { if((*pRaiz) != NULL) { exibirPosOrdem(&(*pRaiz)->esquerda); exibirPosOrdem(&(*pRaiz)->direita); printf(“%i\n”, (*pRaiz)->numero); } } /* Comentario: Verifica Quem é o Maior */ int maior(int a, int b){ if(a > b) return a; else return b; } /* Comentario: Imprimir Árvore */ int imprimir(No **pRaiz) { if((*pRaiz) != NULL) { if((*pRaiz) != NULL) printf(“\nPai %i\n”,(*pRaiz)->numero); if((*pRaiz)->esquerda != NULL) printf(“Esq: %i\t”,(*pRaiz)->esquerda->numero); else printf(“Esq: NULL\t”); if((*pRaiz)->direita != NULL) printf(“Dir: %i\t”,(*pRaiz)->direita->numero); else printf(“Dir: NULL\t”); if((*pRaiz)->esquerda != NULL) imprimir(&(*pRaiz)->esquerda); if((*pRaiz)->direita != NULL) imprimir(&(*pRaiz)->direita); } 86 else printf(“A árvore está vazia! \n”); } /* Programa Principal */ int main () { struct carro ca; int c; No *pRaiz; criarArvore(&pRaiz); setlocale(LC_ALL, “portuguese”); int op; do{ system(“CLS”); printf(“* * * FAÇA SUA ESCOLHA * * *\n\n”); printf(“1. Inserir Veículo: \n”); printf(“2. Remover Veículo: \n”); printf(“3. Mostrar PRÉ-ORDEM: \n”); printf(“4. Mostrar EM ORDEM: \n”); printf(“5. Mostrar PÓS-ORDEM: \n”); printf(“6. Imprimir Árvore \n”); printf(“\nOpção [0 para Sair]: “); scanf(“%d”, &op); switch(op) { case 1: system(“CLS”); printf(“\nCarro: “); scanf(“%s”, &ca.nome); printf(“\nMarca: “); scanf(“%s”,&ca.marca); printf(“\nAno de Fabricação: “); scanf(“%d”, &ca.ano); printf(“\nPreço do Veículo: “); scanf(“%f”, &ca.preco); printf(“\nDigite um Número (Referência na Árvore): “); scanf(“%d”,&c); inserir(&pRaiz,c,ca); system(“PAUSE”); break; case 2: system(“CLS”); printf(“\nDigite um Número: “); scanf(“%d”,&c); remover(&pRaiz,c); system(“PAUSE”); break; 87 case 3: system(“CLS”); exibirPreOrdem(&pRaiz); system(“PAUSE”); break; case 4: system(“CLS”); exibirEmOrdem(&pRaiz); system(“PAUSE”); break; case 5: system(“CLS”); exibirPosOrdem(&pRaiz); system(“PAUSE”); break; case 6: system(“CLS”); imprimir(&pRaiz); printf(“\n”); system(“PAUSE”); break; case 0: break; default: printf(“\n\nOpção Inválida. \n”); system(“PAUSE”); break; } }while(op!=0); return 0; } 88 Considerando a estrutura de dados árvore binária e o percurso sendo executa- do por meio de busca em profundidade, apresente as rotinas de programação em linguagem C que correspondem aos percursos: pré-ordem, em ordem e pós-ordem. void exibirPreOrdem(No **pRaiz) { if((*pRaiz) != NULL) { printf(“%i\n”, (*pRaiz)->numero); exibirPreOrdem(&(*pRaiz)->esquerda); exibirPreOrdem(&(*pRaiz)->direita); } } void exibirEmOrdem(No **pRaiz) { if((*pRaiz) != NULL) { exibirEmOrdem(&(*pRaiz)->esquerda); printf(“%i\n”, (*pRaiz)->numero); exibirEmOrdem(&(*pRaiz)->direita); } } void exibirPosOrdem(No **pRaiz) { if((*pRaiz) != NULL) { exibirPosOrdem(&(*pRaiz)->esquerda); exibirPosOrdem(&(*pRaiz)->direita); printf(“%i\n”, (*pRaiz)->numero); } NA PRÁTICA 89 Resumo da Unidade 4 Nesta unidade, você conheceu os conceitos e os fundamentos das estruturas de dados grafos e árvores. Compreendeu, também, o funcionamento de uma árvore binária e sua variação de árvore AVL, e implementou solução que manipulasse árvores binárias em linguagem C. Nesta unidade, foram apresentados os conceitos de grafos e de árvores como estruturas de dados dinâmicas para a solução de problemas complexos. Nesse viés, foram apresentadas as árvores binárias de buscas, explorando a busca em profundidade e a busca em largura, com suas possíveis variações. CONCEITO 90 Referências FORBELLONE, A. L. V.; EBERSPÄCHER, H. F. Lógica de programação: a construção de algoritmos e estruturas de dados. 3. ed. São Paulo: Pearson Prentice Hall, 2005. Bibliote- ca Virtual. MIZRAHI, V. V. Treinamento em linguagem C. 2. ed. São Paulo: Pearson Education do Brasil, 2008. Biblioteca Virtual. PUGGA, S.; RISSETTI, G. Lógica de programação e estrutura de dados com aplicações em Java. 3. ed. São Paulo: Pearson Prentice Hall, 2009. Biblioteca Virtual. SIMÕES-PEREIRA, J. M. S. Grafos e redes: teoria e algoritmos básicos. Rio de Janeiro: Interciência, 2014. Biblioteca Virtual.