Você está na página 1de 416

SISTEMAS DE INFORMAÇÃO Estrutura de Dados

www.esab.edu.br
Estrutura de Dados

Vila Velha (ES)


2014
Escola Superior Aberta do Brasil

Diretor Geral
Nildo Ferreira
Diretora Acadêmica
Beatriz Christo Gobbi
Coordenadora do Núcleo de Educação a Distância
Beatriz Christo Gobbi
Coordenadora do Curso de Administração EAD
Rosemary Riguetti
Coordenador do Curso de Pedagogia EAD
Claudio David Cari
Coordenador do Curso de Sistemas de Informação EAD
David Gomes Barboza

Produção do Material Didático-Pedagógico


Delinea Tecnologia Educacional / Escola Superior Aberta do Brasil

Diretoria Executiva Designer Educacional


Charlie Anderson Olsen Aline Batista
Larissa Kleis Pereira Revisão Gramatical
Margarete Lazzaris Kleis Bárbara Seger Zeni
Conteudista Érica Valduga
Marcelo Medeiros Laís Gonçalves Natalino
Coordenação de Projeto Designer Gráfico
Andreza Regina Lopes da Silva Laura Rodrigues
Supervisão de conteúdo Neri Gonçalves Ribeiro
Renata Oltramari Diagramação
Liderança Técnica Design Gráfico Dilsonir José Martins Junior
Fernando Andrade Karina Silveira

Liderança Técnica Revisão Gramatical Equipe Acadêmica da ESAB


Tiago Costa Pereira Coordenadores dos Cursos
Laís Gonçalves Natalino Docentes dos Cursos

Copyright © Todos os direitos desta obra são da Escola Superior Aberta do Brasil.
www.esab.edu.br
Av. Santa Leopoldina, nº 840
Coqueiral de Itaparica - Vila Velha, ES
CEP 29102-040
Apresentação
Caro estudante,

Seja bem-vindo à disciplina de Estrutura de Dados, um complemento aos


conceitos fundamentais de programação vistos nas disciplinas que envolvem
lógica de programação, aliado à discussão e apresentação de novas técnicas para o
desenvolvimento de programas de computadores mais eficientes no uso de memória
principal e tempo de processamento.

Adotaremos como base para validação das técnicas apresentadas a linguagem de


programação C, porém, os conceitos e técnicas desenvolvidas nesta disciplina podem
ser codificados em qualquer linguagem de programação atual, como Java.

Hoje é fundamental para o profissional que deseja ser um especialista em Tecnologia


da Informação ter habilidades e competências que lhe permitam maximizar o uso dos
recursos do computador.

E você, como nosso aluno, deve estar interessado nessa área, em saber como planejar
e desenvolver rotinas computacionais que aperfeiçoem os resultados dos algoritmos,
evitando o desperdício de memória principal criando rotinas de alocação, pesquisa e
classificação dos dados.

Nesta disciplina, tendo como base os autores Forbellone et al.(2005), Cerqueira, Celes
e Rangel Netto (2004), Ascencio e Araújo (2010) e Guimarães (1994), você conhecerá
os conceitos e as técnicas de como implementar os tipos de alocação de memória, os
algoritmos de classificação e pesquisa de dados, as estruturas de dados avançadas,
como: Filas, Pilhas, Listas e Árvores.

Assim, esta disciplina possibilitará que você entenda, projete, desenvolva e valide
o uso de estruturas de dados mais eficientes e que possibilitem o aproveitamento
máximo dos recursos do computador.

Bom estudo!
Objetivo
Analisar, avaliar e implementar as principais técnicas de representação e
manipulação de dados, possibilitando aos alunos a capacidade de escolher as
estruturas de dados mais adequadas aos problemas que envolvam a ordenação e
recuperação de informações por meio de algoritmos de organização e busca.

Habilidades e competências
• Analisar e implementar estruturas de dados para solução de problemas
computacionais.
• Projetar estruturas de dados para resolução de problemas complexos.
• Avaliar as melhores estruturas de dados para cada tipo de problema
computacional.
• Codificar estruturas de dados.
• Analisar as diferentes estruturas de dados.
• Aplicar adequadamente estruturas de dados.

Ementa
Conceitos básicos e apontadores. Representação de estruturas simples e
encadeadas (listas lineares, pilhas, filas, árvores). Procedimentos e funções de
acesso, remoção e inserção de nós nas estruturas. Métodos de classificação e
ordenação de dados. Pesquisa de dados.
Sumário
1. Introduzir o aluno aos conceitos de Estruturas de Dados..................................................7
2. Funções – parte I...........................................................................................................15
3. Funções – parte II..........................................................................................................22
4. Variáveis compostas homogêneas: vetores....................................................................33
5. Alocação dinâmica de memória.....................................................................................40
6. Alocação dinâmica em C................................................................................................49
7. Tipos de passagens de parâmetros.................................................................................58
8. Tipos estruturados – parte I...........................................................................................68
9. Tipos estruturados – parte II..........................................................................................80
10. Matrizes – conceituação................................................................................................92
11. Matrizes – codificação.................................................................................................101
12. Matrizes – exercícios....................................................................................................109
13. Tipos abstratos de dados – parte I...............................................................................120
14. Tipos abstratos de dados – parte II..............................................................................126
15. Listas encadeadas........................................................................................................133
16. Listas encadeadas simples – conceituação..................................................................141
17. Listas encadeadas simples – codificação.....................................................................154
18. Lista encadeada simples – exercícios...........................................................................166
19. Listas circulares – conceituação...................................................................................175
20. Listas circulares – codificação......................................................................................182
21. Listas circulares – exercícios.........................................................................................190
22. Listas duplamente encadeadas – conceituação...........................................................197
23. Listas duplamente encadeadas – codificação..............................................................206
24. Listas circulares duplamente encadeadas – conceituação............................................214
25. Listas circulares duplamente encadeadas − codificação..............................................226
26. Pilhas – conceituação..................................................................................................237
27. Pilhas – codificação – parte I.......................................................................................244
28. Pilhas – codificação – parte II......................................................................................253
29. Filas – conceituação.....................................................................................................258
30. Filas – codificação........................................................................................................264
31. Filas – exercícios..........................................................................................................271
32. Fila dupla.....................................................................................................................277
33. Árvores – parte I..........................................................................................................284
34. Árvores – parte II.........................................................................................................292
35. Árvores – parte III........................................................................................................300
36. Árvores – parte IV........................................................................................................308
37. Árvores – parte V.........................................................................................................316
38. Métodos de pesquisa e ordenação: conceituação.........................................................324
39. Métodos de ordenação: bubble sort – parte 1.............................................................331
40. Métodos de ordenação: bubble sort – parte 2.............................................................338
41. Métodos de ordenação: quick sort – parte 1................................................................345
42. Métodos de ordenação: quick sort – parte 2................................................................352
43. Métodos de ordenação: merge sort..............................................................................361
44. Métodos de ordenação: merge sort..............................................................................368
45. Métodos de pesquisa: busca em vetor..........................................................................375
46. Métodos de pesquisa: árvore binária de pesquisa........................................................382
47. Métodos de pesquisa: árvore balanceada.....................................................................388
48. Exercícios de fixação.....................................................................................................395
Glossário.............................................................................................................................405
Referências.........................................................................................................................414
Introduzir o aluno aos conceitos de
1 Estruturas de Dados
Objetivo
Introduzir o aluno aos conceitos de estruturas de dados.

Nesta unidade serão apresentados os conceitos fundamentais da estrutura


de dados: o que é estrutura de dados, a diferença entre informações e
dados e as áreas de aplicação da estrutura de dados e seus benefícios.
Inicialmente podemos entender a estrutura de dados como qualquer
forma de armazenamento e manipulação de dados no computador, como
variáveis, vetores, matrizes e arquivos. Tal pensamento não está errado,
porém, nesta unidade, você estudará que a estrutura de dados é muito
mais do que armazenamento de informações, há um gerenciamento de
memória associado ao processo de criação e destruição dessas estruturas,
assim como normas e procedimentos para seu uso.

Para elaboração desta unidade foram utilizadas as referências de


Guimarães (1994) e Ascencio e Araújo (2010).

1.1 O que é estrutura de dados?


A definição de estrutura de dados é um conceito amplo e requer um
conjunto de conhecimentos que a fundamentam. Vamos começar pela
compreensão da abstração de dados, que consiste na análise de um
problema identificando os processos finitos que devem ser realizados para
se chegar ao resultado desejado.

Por exemplo, no desenvolvimento de um sistema acadêmico durante


a etapa de análise do problema pode-se chegar à conclusão que as
informações a serem armazenadas para cada aluno são: matrícula, o
nome e o sexo. Já em outro contexto, um sistema acadêmico durante a
mesma etapa de análise do problema pode especificar que cada aluno

www.esab.edu.br 7
será identificado pela matrícula, nome, idade, sexo, data de nascimento,
nome do pai e da mãe. Nos dois casos os requisitos avaliados foram os
dados cadastrais de um aluno, porém no processo de abstração do que é
um “aluno”, os dois sistemas a serem desenvolvidos possuem definições
diferentes, mesmo se tratando de um aluno, e cabe ao analista fazer o
levantamento de acordo com o cenário que lhe é apresentado. Assim
como os dados cadastrais, as funcionalidades de cada sistema, as regras de
negócio e uso do sistema também dependerão da abstração de cada um
dos contextos apresentados.

A Figura 1 representa a abstração de dados.

Entrada Processamento Saída

Figura 1 – Abstração de Dados.


Fonte: Elaborada pelo autor (2013).

A entrada refere-se aos dados que são colhidos no mundo real, ou seja,
externo ao computador, e a saída à sua transformação na informação
desejada. O processamento representa uma sequência finita de operações
a serem realizadas a partir dos dados de entrada, a fim de transformá-
los em informações. A abstração de dados se constitui pela análise dos
valores manipulados, os métodos e os procedimentos que devem ser
aplicados aos dados de entrada com a finalidade de resolver determinados
problemas, representados pela saída desejada.

Por exemplo, um sistema que deve converter uma medida em metros para
centímetros requer uma sequência ordenada de procedimentos e operações
que possibilitam ao computador realizar essa tarefa. Para tanto, é necessário
avaliar o problema e abstrair as regras importantes a serem transferidas
para o algoritmo, como a definição dos valores de entrada e saída como
números reais, já que as medidas manipuladas pelo sistema (metros e
centímetros) são números que podem conter casas decimais. Para conversão
da medida, será necessária a operação de divisão da medida em metros por
100, já que cada um metro corresponde a 100 centímetros. Por fim, será
necessária a utilização de procedimentos para interagir com o usuário para
que ele informe os dados que serão processados (entrada de dados) e para a
apresentação do resultado final da operação de conversão (saída de dados).

www.esab.edu.br 8
Para sua reflexão
A definição de quais métodos e procedimentos
serão utilizados para resolução de um problema
na forma de um sistema computacional não é
imediata, é construída a partir de testes e validação
dos resultados, por meio de uma técnica de
análise e refinamento dos processos. Requerem
também técnicas de representação do problema,
como fluxogramas, diagramas, algoritmos, todos
como uma documentação formal e roteiros de
validação da solução, aliados à expertise de cada
executor. Por isso, deve-se ter em mente que há
diversas estratégias para se chegar à solução de um
determinado problema, apresentando vantagens
e desvantagens em relação a cada uma delas, mas
todas devem garantir a resolução do problema.

Note que mesmo que seja apresentado um problema, como o problema


anterior de conversão de metros para centímetros para um conjunto de
programadores e analistas de sistemas, cada um apresentará uma lógica
e estratégia que pode não ser a mesma para todos já que a abstração
do problema e de como resolvê-la é uma visão subjetiva de cada um,
mas todos devem garantir que o resultado da conversão será o mesmo
independentemente da solução, já que somente essa condição garante
que o sistema funcionará de acordo com o esperado.

O executor ou analista deve ser capaz de escolher aquela estratégia que


melhor se adéqua para resolução do problema, levando em consideração
a eficiência da solução, as necessidades de armazenamento dos dados e o
tempo de processamento.

Note que as características de armazenamento de dados, de


processamento e de eficiência da solução determinam o processo de
informatização da solução, que é a transferência para o computador de
uma tarefa que era realizada no mundo real, a partir de uma sequência

www.esab.edu.br 9
finita de processamentos. Estes, por sua vez, são fundamentados nas
definições de como o sistema deverá funcionar, requisitos, testes e
validações especificadas pelos especialistas em tecnologia da informação.

Por exemplo, no desenvolvimento de um sistema computacional que


deverá controlar a temperatura de uma caldeira, é fundamental que o
processamento das informações aconteça de forma rápida e eficiente.
Pois, além de monitorar os dados, é preciso que a resposta em caso de
temperaturas elevadas ou situações de riscos seja rápida, e o sistema possa
garantir em tempo hábil a tomada de decisões por parte dos envolvidos.
Esses requisitos e especificações fazem parte do processo de abstração dos
dados realizados pelos especialistas em tecnologia da informação.

A elaboração de um algoritmo, que representa a solução de um


problema, a partir de uma linguagem de programação chama-se
implementação. Sua finalidade é transformar uma abstração de dados
em um processo concreto, formal, com a execução de métodos e
procedimentos já projetados na forma de algoritmos.

Para Ascencio e Araújo (2010, p. 1),

Durante o processo de computação de sua saída, um algoritmo manipula dados


obtidos de sua entrada. Quando dados são dispostos e manipulados de forma
homogênea, constituem um tipo abstrato de dados.

Nesse processo de abstração de dados, quando uma série finita de


instruções é manipulada de forma homogênea, a sua formatação em
algoritmos e a sua posterior execução pelos computadores representa a
aplicação da estrutura de dados e da sua conceituação.

Podemos entender a estrutura de dados como um recurso que vai


além do armazenamento de dados, pois exige a identificação e
desenvolvimento de procedimentos, métodos e técnicas que permitem a
solução do problema, utilizando o espaço de armazenamento de forma
otimizada, com processamento eficiente e garantia de integridade das
informações. Ou seja, mais do que armazenar os dados em memória, o

www.esab.edu.br 10
acesso e processamento dos dados devem ocorrer de forma a atender às
necessidades da solução em tempo hábil e com garantia que os resultados
processados estão corretos.

Portanto, uma estrutura permite que os dados se mantenham


organizados por alguma lógica. Além disso, o usuário pode manipular os
dados disponibilizados pelas operações.

Ou seja, a estrutura de dados, por meio de sequências finitas de operações,


gerencia os dados que podemos entender por informações armazenadas.

Por exemplo, no desenvolvimento de um sistema que realize a busca


dos dados de um funcionário, por meio de um vetor pelo valor da
sua matrícula (que é única, dois funcionários não podem ter a mesma
matrícula), é importante que quando acontecer a localização dessa
matrícula, o sistema encerre a busca, pois não haverá outro funcionário
com essa matrícula. Caso isso não seja definido como uma regra da
aplicação, o sistema continuará a pesquisa desnecessariamente. Ao
final o resultado será o mesmo, a matrícula será localizada, porém o
tempo de processamento será maior devido à pesquisa em toda a tabela
de matrículas. Portanto, a estrutura de dados não se limita apenas ao
armazenamento de dados, mas permite também otimizar os processos de
armazenamento , organização e acesso aos dados.

Para Guimarães (1994, p. 2), as estruturas de dados “[...] são usadas nos
algoritmos para representar as informações do problema a ser resolvido”.

Ainda, para Ascencio e Araújo (2010, p. 2),

[...] na representação do tipo abstrato de dados, emprega-se uma estrutura de dados.


[...] uma estrutura de dados é um meio para armazenar e organizar dados com o
objetivo de facilitar o acesso às modificações.” Diz ainda que “[...] as estruturas de dados
diferem-se uma das outras pela disposição ou manipulação de seus dados, de modo que
não se pode separar as estruturas de dados e os algoritmos associados a elas.

Segundo Guimarães (1994, p. 2), “[...] o processo de construção de


programas, a formulação do algoritmo e a definição das estruturas de
dados a serem usadas estão intimamente ligadas”.

www.esab.edu.br 11
Observe que a definição de estrutura de dados está associada
ao armazenamento dos dados, mas não apenas isso. A forma de
armazenamento dos dados, como serão manipulados e sua organização
estão relacionados com as atividades e problemas do mundo real.
Portanto, levá-la com suas regras de negócios e formas de manipulação
para o computador requer do especialista a habilidade de avaliar,
compreender e modelar todo esse universo por meio da estrutura de
dados mais adequada.

Então podemos entender a estrutura de dados como a forma pela qual os


dados serão armazenados, organizados e manipulados no computador;
associada às operações, pode ser executada com esses dados, visando
sempre evitar o desperdício de memória alocada e otimizando o
processamento dos dados.

1.2 Dados versus Informação


Observe que na seção anterior em alguns momentos os conceitos de
dados e informação se confundem, e essa é uma dúvida comum, mas
fácil de ser resolvida.

De forma geral, podem-se entender dados como a informação pura, não


processada, no seu estado inicial, que pode representar vários significados
e isoladamente não transmite conhecimento.

Já a informação pode ser compreendida como o dado já processado, útil,


com alguma finalidade.

Por exemplo, o conjunto de letras a seguir pode ser entendido como


dados, pois ele não transmite conhecimento ou informação útil: BESA.
Mas, se processarmos esse conjunto de dados de forma a organizá-los,
obtemos a informação: ESAB.

www.esab.edu.br 12
1.3 Utilização e benefícios
Como visto nas seções anteriores, a conceituação de estrutura de dados
está diretamente relacionada ao desenvolvimento de algoritmos e sua
posterior implementação. Para criar e utilizar um algoritmo, é necessário
avaliar seu desempenho e a utilização dos recursos disponibilizados,
como alocação de espaços na memória principal ou secundária, e tempo
gasto para execução de determinada rotina.

Nesse sentido, a estrutura de dados pode ser aplicada em algoritmos


simples, de entrada, processamento e saída de dados, ou mais complexos,
como algoritmos de busca e classificação de dados.

Em muitos casos as estruturas de dados devem ser utilizadas para a construção


de conjunto de dados, ou coleções, na forma de vetores estáticos ou dinâmicos.
Os vetores estáticos são utilizados para o armazenamento de dados em que se
conhece a quantidade de dados a serem processados. Já os vetores dinâmicos
não possuem uma quantidade de elementos predefinidos, alocando memória
à medida que cada elemento é inserido na estrutura de dados, e liberando
memória, à medida que um elemento é removido, tornado o uso mais eficiente.

Para sua reflexão


Para o desenvolvimento de programas de
computadores é muito commum que o
programador não conheça o número de dados
que serão manipulados pelo sistema, sendo uma
informação dinâmica e muito variável.Por isso a
importância em conhecer as estruturas de dados.

Vamos refletir sobre, por exemplo, um sistema que precisa gerar um relatório
de produtos comercializados num determinado período. Os dados da
pesquisa precisam ser armazenados em uma estrutura de dados para depois
serem apresentados ao usuário. Porém, não se sabe previamente a quantidade
de elementos que serão armazenados, o tamanho da estrutura para esse
armazenamento, e que se pode variar de período para período. Uma solução

www.esab.edu.br 13
seria criar uma estrutura de dados com um tamanho absurdamente grande
que garantiria o armazenamento dos dados, mas haveria um desperdício de
memória do computador caso a quantidade de elementos do relatório fosse
muito baixa. Dessa forma, a melhor solução é a criação de uma tabela com a
quantidade exata de elementos que a pesquisa retornou, mas isto só poderá
ocorrer em tempo de execução do programa, pois somente no momento em
que a rotina que realiza a pesquisa dos produtos é executada é que se terá a
quantidade de elementos da tabela.

Em alguns casos as estruturas de dados são utilizadas para solução de problemas


bem específicos como a implementação de filas de atendimento em bancos,
nas quais cada cliente é identificado por uma senha, e que devem garantir que
o indivíduo que chegou mais cedo tenha prioridades nos atendimentos. Ou
para elaboração de um programa de computadores que possa calcular e mostrar
o menor caminho entre dois pontos de uma cidade, a fim de diminuir o custo
de combustível e o tempo para realizar o deslocamento.

A escolha da estrutura de dados mais adequada deve analisar algumas


características do problema a ser solucionado. Conforme Guimarães
(1994), essas características são:

• as propriedades relevantes do objeto real que queremos representar;


• as operações a serem efetuadas sobre elas;
• os resultados de acordo com os fenômenos do mundo real.
Em resumo, as estruturas de dados devem ser definidas de forma a
atender as operações que serão realizadas e das características (abstração)
do mundo real que ela deverá representar.

Nesta unidade você pôde conhecer o conceito de estrutura de dados e


sua relação com a abstração de dados, passando pela contextualização
de tipos abstratos de dados. Vimos também a diferenciação entre dados
e informações. Além desses tópicos iniciais, foi discutida a forte relação
entre algoritmos e estrutura de dados. Por fim, foram apresentadas
algumas aplicações para a estrutura de dados na elaboração de métodos
de pesquisa, ordenação e construção de estruturas específicas.

Na próxima unidade serão apresentadas aplicações da estrutura de


dados, começando pela construção de funções, suas propriedades e
características de funcionamento.
www.esab.edu.br 14
2 Funções – parte I
Objetivo
Apresentar a definição de funções, funcionamento da pilha de
execução e visibilidade de variáveis.

Na unidade anterior você pôde se familiarizar com o conceito de


estrutura de dados e abstração de dados e as vantagens e benefícios da
sua aplicação. Nesta unidade será apresentada a técnica de refinamentos
sucessivos na forma de módulos ou funções, destacando seu papel na
redução da complexidade dos algoritmos e a consequente facilidade
da manutenção e validação. Para tanto, serão abordados temas como a
decomposição de problemas, a construção e parametrização de módulos
e a aplicação da estrutura de dados responsável pelo gerenciamento da
execução dos módulos em um programa de computador.

Esta unidade está fundamentada nos trabalhos de Guimarães (1994) e


Forbellone et al. (2005).

A resolução de um problema na forma de algoritmos com uso das


estruturas de dados é uma tarefa complexa, que exige a análise do
problema, das possíveis soluções e dos requisitos que precisam ser
atendidos, a fim de garantir a plenitude da solução. Diante disso, uma
técnica muito eficiente para estruturação de um algoritmo é dividir o
problema complexo em problemas menores (FORBELLONE et al.,
2005). Essa estratégia possibilita uma visão mais específica de cada rotina
a ser construída e validada. Já Guimarães (1994, p. 155), sobre o mesmo
tema, afirma que “[...] a metodologia de refinamentos sucessivos parte
do princípio de que resolver um problema complexo é mais fácil se não
precisarmos considerar todos os aspectos do problema simultaneamente”.
Portanto, a decomposição de um problema, a partir de um refinamento
sucessivo, em problemas menores e mais simples, resulta na redução da
sua complexidade. Essa ideia vem ao encontro de Forbellone et al. (2005,
p. 127) quando nos diz que

www.esab.edu.br 15
[...] a decomposição de um problema é fator determinante para a redução da
complexidade. [...] quando decompomos um problema em subproblemas,
estamos invariavelmente dividindo também a complexidade e, por consequência,
simplificando a resolução.

Na prática, a decomposição ou modularização se dá na codificação e


no uso de funções. Forbellone et al. (2005, p.128) citam a relação de
modularização e algoritmos com o seguinte discurso: “[...] depois de
decompor um problema complexo em subproblemas, podemos construir
subalgoritmos ou módulo para cada subproblema”. Esses subalgoritmos
são conhecidos como funções ou métodos.

2.1 Definição
As funções são subprogramas que decompõem as grandes tarefas em
subtarefas. Um programa de computador geralmente consiste de várias
funções. A criação de funções visa à redução do código e à consequente
simplificação dos processos de validação e testes, uma vez que rotinas que
se repetem no código devem ser transformadas em funções que, então,
serão chamadas diversas vezes.

As funções uniformizam “[...] determinado conjunto de ações afins, que


obedecem à mesma estruturação de um algoritmo, com o objetivo de
representar um bloco lógico em especial” (FORBELLONE et al., 2005,
p. 130-131).

As funções são executadas quando chamadas no programa ou em outras


funções, resultando em um desvio na execução do programa. Para que esse
procedimento seja executado de forma organizada, é necessária uma estrutura
de dados que gerencie as chamadas e encerramentos de cada função em
execução. Essa estrutura de dados é conhecida como pilha de execução.

www.esab.edu.br 16
2.2 Pilha de execução
Como estudado na seção anterior, a modularização transforma um problema
em um conjunto de soluções, então, o próximo passo é compreender como
esses módulos são interligados, manipulados e executados.

A execução ou ativação de um módulo ocorre quando em algum ponto


do algoritmo há uma chamada para a função, pelo nome definido na
declaração da função. A execução do módulo se dá por um desvio no fluxo
de execução do algoritmo para a função chamada, retornando somente
após a execução de todas as instruções da função, sempre para a próxima
linha após a chamada. A Figura 2 representa um algoritmo fazendo a
chamada para duas funções hipotéticas, Função A e Função B, observe:

Linhas Algoritmo Linhas Função A


1 1
2 A 2
3 3
4
5 Linhas Função B
6 B 1
7 2
3
4
5

Figura 2 – Ativação de Funções.


Fonte: Elaborada pelo autor (2013).

O algoritmo principal possui 7 linhas com comandos quaisquer, e sua


execução começa na linha 1 e se encerra na linha 7. Durante a execução,
como na linha 2, há uma chamada para a função “Função A”, por meio da
instrução “A”, que é o nome da função, haverá um desvio para a primeira
linha dessa função, que será executada da linha 1 até a linha 3, que encerra
a função. Após o encerramento da função, o fluxo retorna para a próxima
linha após a chamada, linha 3 do algoritmo principal, e segue a execução
normalmente. Como na linha 6 do algoritmo há uma chamada para a

www.esab.edu.br 17
“Função B”, novamente haverá um desvio, executando todas as instruções
da linha 1 a 5 da função B, e retornando para o algoritmo na linha 7,
dando sequência na execução até o seu encerramento.

Todo esse processo é possível devido a uma estrutura de dados que


gerencia todos os desvios entre o algoritmo e as funções, chamada de
pilha de execução.

A cada execução de um método ou função, a sua chamada é inserida


no topo de uma estrutura de dados chamada de pilha, que possui duas
operações, a inserção no topo e a remoção do topo, de forma que o
último elemento inserido será o primeiro a ser removido. A Figura 3
representa a pilha de execução para o exemplo anterior em que um
algoritmo chama as funções A e B, observe:

Algoritmo Função A Função A Execução


inserido em execução. terminou. retorna
no topo Método Método para o
da linha. inserido removido algoritmo.
na pilha. da pilha.
A A
Algo Algo Algo Algo
1 2 3 4 5
Antes da Algoritmo
execução em execução

Método B Função B Execução Fim da


inserido terminou. retorna execução do
no topo Método é para o algoritmo.
da pilha. removido algoritmo.
da pilha.
B B
Algo Algo Algo
6 7 8 9
Função B é
chamada e Variáveis e
entra em
execução.
parâmetros
Figura 3 – Pilha de execução.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 18
Observe que no passo 1 a pilha estava vazia, não havia função em
execução. No passo 2 o algoritmo entrou em execução, sendo inserido na
pilha de execução. Assim que a “função A” foi chamada no algoritmo, ela
foi inserida no topo da pilha (passo 3) de execuções, mantendo-se ali até
que a função se encerre. No passo 4 a “Função A” foi encerrada, sendo
retirada da pilha e devolvendo a execução para o algoritmo (passo 5).
No momento em que a “Função B” é chamada e entra em execução, esta
é inserida no topo da pilha (passo 6), mantendo-se até que sua execução
se encerre. No encerramento da “Função B”, ela é retirada da pilha
(passo 7), devolvendo a execução para o algoritmo (passo 8) até que ele
termine sua execução e seja retirado da pilha (passo 9).

Durante a execução das funções, as variáveis declaradas e utilizadas


nelas passam a ser criadas à medida que a função entra em execução e
destruídas quando a função se encerra. O acesso às variáveis a partir do
seu local de declaração é chamado de Visibilidade de Variáveis.

2.3 Visibilidade de variáveis


Observando a pilha de execução pode-se notar que apenas uma função é
executada por vez, sendo que a função anterior só retorna à sua execução
quando a função que está no topo da pilha é encerrada (apenas a função
do topo está em execução naquele instante), e consequentemente,
retirada da pilha. Portanto, a cada execução a função é inserida no
topo da pilha. Se já existe alguma função em execução, esta não será
mais executada até que a função que está no topo da pilha se encerre,
retornando o processamento para a função que estava logo abaixo, e
assim sucessivamente para todas as funções empilhadas, inclusive o
algoritmo que chamou as funções.

Nesse contexto, quando são declaradas variáveis dentro dessas funções, chamadas de
variáveis locais (FORBELLONE et al., 2005), elas passam a existir somente no momento
em que a função é executada.

www.esab.edu.br 19
Por isso essas variáveis não podem ser acessadas por outras funções, uma
vez que a execução das outras funções só terá sequência quando a função
no topo da pilha se encerrar. No momento em que isso ocorre, as variáveis
locais também saem da pilha, assim como a função, e deixam de existir.

Quando uma função ou procedimento é chamado, as variáveis locais e


parâmetros são copiados para a pilha com a função. Quando a função
termina, as variáveis e parâmetros são desalocados da memória, no
mesmo momento em que a função se encerra. Por essa razão, as variáveis
locais fora das funções não podem ser acessadas por outras funções ou
pelo programa principal (algoritmo).

Guimarães (1994, p. 133) destaca: “[...] uma variável declarada dentro


de um bloco só é conhecida dentro deste bloco”. Para que os dados
armazenados no algoritmo possam ser acessados por qualquer método
ou função, esta deve ser declarada no algoritmo, por isso chamada de
variáveis globais (FORBELLONE et al., 2005).

No caso da linguagem de programação C, as variáveis globais são


declaradas fora das funções, inclusive da função main() que é obrigatória
e responsável pela execução do programa, é a primeira função executada
por um programa em C. Caso uma variável seja declarada na função
main() ela não estará acessível as outras funções.

A Figura 4 apresenta um programa escrito em C com duas variáveis, observe:

www.esab.edu.br 20
Figura 4 – Declaração de variáveis em C.
Fonte: Elaborada pelo autor (2013).

A variável sexo foi declarada fora das funções teste() e main(). Sendo assim,
se trata de uma variável global, acessível às duas funções. Já a variável idade
foi declarada dentro da função main(), dessa forma sendo visível apenas a
essa função. Na função teste(), na linha 9, foi indicado um erro (quadrado
em vermelho), indicando que a variável idade não existe. Todas as
instruções, da linha 1 a linha 18 correspondem ao algoritmo em C, assim
temos nesse algoritmo as bibliotecas stdio e stdlib, a variável global sexo, as
funções teste() e main(), que possui a variável local idade.

O escopo de abrangência de uma variável é chamado de visibilidade da


variável. Forbellone et al. (2005, p. 136) destaca que

[...] em alguns casos, uma determinada variável é utilizada apenas por um módulo
específico, o que não justifica uma definição global, pois somente se fazem necessários o
conhecimento e a utilização dessa variável dentro dos limites deste bloco lógico.

As variáveis locais devem ser utilizadas para a definição de dados que


serão manipulados somente naquele método, garantindo que o espaço
de memória seja alocado dinamicamente, pois ocorre durante a execução
da função, e após a sua execução seja liberado, otimizando o uso de

www.esab.edu.br 21
memória principal para criação e armazenamento de variáveis. Por outro
lado, caso os dados devam ser acessados pelas funções desenvolvidas
pelo programador e pelo método main(), que é o método principal e
obrigatório em todo programa desenvolvido em C, as variáveis devem ser
declaradas fora das funções, tornando-se assim globais e visíveis a todas as
estruturas de dados do algoritmo. Porém, mesmo que não sejam utilizadas,
ocupam espaço de memória enquanto o programa estiver em execução.

Estudo complementar
Para saber mais sobre os conceitos e exemplos de
funções em C, acesse aqui.

Nesta unidade estudamos sobre a modularização de algoritmos com o uso


de funções e como se dá o funcionamento e gerenciamento dos blocos
de instruções quando chamados pelo algoritmo, por meio da pilha de
execução que interfere diretamente na visibilidade e escopo das variáveis.

Na próxima unidade daremos sequência ao estudo das funções com foco


na recursividade e endereçamento de variáveis.

www.esab.edu.br 22
3 Funções – parte II
Objetivo
Apresentar a definição de ponteiros e conhecer a técnica de
recursividade e como utilizar variáveis estáticas dentro das funções.

Caro aluno, na unidade anterior você pôde estudar sobre a pilha de


execução das funções e a sua relação com a visibilidade das variáveis.
Nesta unidade serão apresentados os conceitos de endereçamento de
memória na forma de ponteiros, o uso da recursividade, que consiste em
uma função que faz uma chamada para ela mesma e o uso de variáveis
estáticas, que utilizam um único endereçamento no seu armazenamento
na memória principal. Para abordar esses assuntos, foram utilizadas as
referências de Guimarães (1994) e Forbellone et al. (2005).

Nos estudos anteriores você conheceu o conceito e forma de


funcionamento da pilha de execução que armazena as chamadas das
funções e suas variáveis. Esse armazenamento se dá na memória principal
do computador, em endereços específicos, conhecidos como ponteiros.

3.1 Ponteiro de variáveis


Pode-se entender a variável como uma caixa que armazenará algum
conteúdo definido pelo programador, no momento de sua inicialização
ou em uma atribuição. A variável possui quatro importantes
propriedades: o nome, também conhecido como identificador; o tipo
de dados; o valor armazenado; e a sua localização na memória principal,
chamado de endereçamento. Para Guimarães (1994, p. 20), “[...]
podemos imaginar uma variável como o nome de um local onde se
pode colocar qualquer valor do conjunto de valores possíveis do tipo
básico associado”. O nome da variável é utilizado pelo programador
para identificar no algoritmo a variável que será manipulada, por isso
chamada de identificador. O valor da variável pode ser alterado durante
a execução do programa, sempre respeitando a definição do tipo de

www.esab.edu.br 23
dados que podem ser: números (inteiros e reais), caracteres (símbolo ou
conjunto de símbolos) e lógicos (verdadeiro ou falso). A criação de uma
variável se dá pela instrução de declaração da variável, que na linguagem
de programa C/C++ possui a semântica apresentada na Figura 5, observe:

Tipo de Dado Identificador ;

Figura 5 – Declaração de Variável em C/C++.


Fonte: Elaborada pelo autor (2013).

Observe que a variável em C pode ter apenas um tipo de dados, e mais


de uma variável podem ser declaradas para o mesmo tipo, desde que os
nomes (identificadores) sejam separados por vírgula. São exemplos de
declaração de variáveis em C.

• int idade;
• float peso, altura, media;
• char nome[30], cidade[30];
A declaração tem como finalidade criar as variáveis que serão utilizadas
no programa de computador, mas na prática o resultado é a alocação de
espaço na memória principal, na área chamada de Heap. Cada variável
declarada será alocada em um bloco de memória com endereço único.
Mesmo que a variável não seja utilizada, esse espaço não será liberado até
que o programa se encerre. O código a seguir apresenta a declaração de
variáveis em C/C++, observe:

Algoritmo 1 – Exemplo de declaração de variáveis em C


01 int idade;
02 float media;
03 char sexo;

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 24
No algoritmo declaram-se três variáveis: idade como inteiro, na linha
1, media como float, na linha 2, e sexo como char na linha 3. Antes da
execução do programa, essas instruções solicitam ao sistema operacional
um espaço de memória para cada variável. Essa ação é chamada de
alocação de memória e resulta na criação física da variável e da associação
a um endereço. A Figura 6 representa a alocação das variáveis do
algoritmo 1 na memória principal, veja:

Memória Principal
Memória
Heap
idade
Emq1
media identificador
Emq2 endereço valor
sexo
Emq3

Figura 6 – Alocação de variáveis do algoritmo 1.


Fonte: Elaborada pelo autor (2013).

Observe que na memória Heap, um espaço da memória principal, foram


alocados espaços para cada variável. Além de alocar o espaço, associa-
se um endereço à variável, representado pelo termo Emq (endereço
de memória qualquer), sendo que cada variável possui o seu endereço
exclusivo, ou seja, duas variáveis não podem utilizar o mesmo endereço.

Note que cada variável é representada pelo seu nome, um valor e um


endereçamento de memória. Assim, na Figura 6, Emq1, Emq2 e Emq3
são os endereços de memória de cada variável.

Na Figura 7 está representada uma atribuição à variável idade, observe.

www.esab.edu.br 25
Memória Principal
Memória
Heap
idade
Emq1 43
idade = 43;
media
Emq2 Emq1 43
sexo
Emq3

Figura 7 – Atribuição de valor à variável.


Fonte: Elaborada pelo autor (2013).

Note que foi realizada uma atribuição utilizando o identificador da


variável “idade”, usando a sintaxe: idade = 43; Para a linguagem de
programação a variável idade corresponde ao ponteiro Emq1, que é o
seu endereço na memória Heap, e para que a atribuição ocorra há um
desvio para esse endereço, localizando a variável na memória e gravando
o valor 43 nesse endereço. Caso o identificador referenciado não exista
na memória Heap, a linguagem de programação indicará um erro de
compilação informando que o identificador não foi declarado.

Observe que de acordo com o que foi estudado até aqui, toda variável
possui um identificador, um valor, um endereço de memória e um tipo
de dado. Mas há um tipo diferenciado de variável que é chamado de
ponteiro cuja finalidade é armazenar o endereço de uma variável na
memória do computador. A Figura 8 representa uma variável do tipo
ponteiro, observe:

idade nome
endXYZ

Endereço:endXYZ
Figura 8 – Variável do tipo ponteiro.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 26
A variável nome possui um endereço hipotético endXYZ, que por sua
vez está sendo atribuído à variável ponteiro que o armazenará. Caso
seja executada a instrução para imprimir o valor da variável ponteiro,
o resultado será a impressão do endereço armazenado, porém, caso seja
solicitada a impressão do valor nesse endereço, o resultado impresso será
o mesmo valor que está armazenado na variável nome.

Para Cerqueira, Celes e Rangel (2004, p. 46), “[...] os ponteiros servem


para armazenamento e manipulação de valores de endereços de memória”.

Assim como a Heap gerencia a criação de cada variável declarada no programa,


as chamadas das funções também são monitoradas por essa estrutura,
armazenando o endereço de cada função chamada, das variáveis locais e
globais. Mas há um tipo específico de chamada de funções na qual uma função
faz uma chamada a ela mesma no código, e a Heap precisa gerenciar cada
endereço dessas chamadas, garantindo a execução correta do programa.

3.2 Recursividade
A recursividade consiste em uma função fazer a chamada dela mesma
em seu código, e a sua finalidade é tornar algoritmos mais complexos em
rotinas mais simples e com menos linhas de codificação. Para Guimarães
(1994, p. 141): “[...] existem casos em que um procedimento ou função
chama a si próprio. Diz-se então que o procedimento ou função é
recursivo”. O código do Algoritmo 2 apresenta uma função recursiva de
pesquisa de um determinado valor inteiro em um vetor.

Algoritmo 2 – Pesquisa recursiva


01 int pesquisar(int chave, int posicao,int
tamanho){
02 if(posicao == tamanho) return 0; //não existe
03 else{
04 if(dados[posicao] == chave) return 1;//achou
05 else{
06 posicao++;
07 return pesquisar(chave,posicao,tamanho);
08 }
09 }
}

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 27
As regras de funcionamento do processo recursivo são: se a posição
visitada do vetor for igual ao tamanho do vetor, instrução da linha 2,
então todas as posições já foram visitadas e o valor não existe no vetor,
não há mais posições a serem pesquisadas e o método retornará um 0
(falso). Na regra da linha 4, compara-se o vetor na posição de pesquisa
com a chave, que é o valor procurado, e se for igual a função retornará
o valor 1 (verdadeiro), indicando que o valor existe. Caso o vetor não
tenha todas as posições visitadas e o valor não tenha sido achado, na
linha 7 a função é chamada recursivamente recebendo como parâmetro
o valor a ser pesquisado (chave), a nova posição de pesquisa, pois esta foi
incrementada na linha 6, e o tamanho do vetor.

O código a seguir implementa a pesquisa no vetor com valores


predefinidos.

Algoritmo 3 – Pesquisa recursiva no vetor


01 #include <stdio.h>
02 #include <stdlib.h>
03 int dados[] = {1,3,15,70,19,101,2,20,23,4,127,88};
04 int pesquisar(int chave, int posicao,int tamanho){
05 if(posicao == tamanho) return 0; //não existe
06 else{
07 if(dados[posicao] == chave) return 1;//achou
08 else{
09 posicao++;
10 return pesquisar(chave,posicao,tamanho);
11 }
12 }
13 }
14 int main (void){
15 int valor,resultado;
16 puts("Informe um Valor para Pesquisa:");
17 scanf("%d",&valor);
18 resultado = pesquisar(valor,0,12);
19 if(resultado == 1) puts("Valor existe no vetor");
20 else puts("Valor não existe no vetor");
21 system("pause");
22 return 0;
23 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 28
O algoritmo solicita ao usuário um valor a ser pesquisado. Como o vetor
possui 12 elementos, na linha 18 é chamada a função pesquisar com os
parâmetros valor (valor a ser localizado no vetor), 0 (primeira posição de
pesquisa) e 12 (tamanho do vetor). A cada interação da função, caso o
valor não seja localizado e o vetor não tenha sido todo visitado, a função
pesquisar é reexecutada com o mesmo valor de pesquisa, o mesmo
tamanho de vetor, porém com a próxima posição de pesquisa.

O Quadro 1 apresenta a sequência de pesquisa do valor 70 no vetor:

Número de Valor do
Linhas Executadas
Chamadas Valor Posição Tamanho vetor Dados
do Método
do Método [posição]
Primeira - 1 70 0 12 1 6, 7, 8, 9, 10, 11,12
2 70 1 12 3 6, 7, 8, 9, 10, 11,12
3 70 2 12 15 6, 7, 8, 9, 10, 11,12
4 70 3 12 70 6, 7, 8,9 retorna 1

Quadro 1 – Simulação da Função Pesquisar.


Fonte: Elaborado pelo autor (2013).

Observe que as três primeiras chamadas executam do início da função até


a linha 10 quando a função é reexecutada com o mesmo valor de pesquisa,
mesmo tamanho e a próxima posição do vetor que foi incrementada na
linha 9. Na quarta execução da função, na linha 7, o valor do vetor será
igual ao valor pesquisado e, portanto, a função executará o comando
return 1, e se encerrará, já que o comando return termina a execução de
uma função. A Figura 9 apresenta a sequência de execução de cada uma
das chamadas função pesquisar buscando o valor 70.

4 3 2 1

Figura 9 – Sequência de execução do método pesquisar buscando o valor 70.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 29
Observe que a chamada número 1 da função fará uma nova chamada
para a mesma função, assim como a chamada número 2, chamada
número 3 e chamada número 4. Todas as chamadas recursivas serão
executadas no comando return pesquisar (chave, posição, tamanho).

Cada chamada é inserida na pilha de execução e fica aguardando para


retornar à execução normal quando a chamada acima encerrar. Dessa
forma, a chamada 1 aguarda a chamada 2, que por sua vez aguarda a
chamada 3, que aguarda a chamada 4. Como a chamada 4 retornará 1,
pois a condição if(vetor[posicao] == chave) será verdadeira, essa chamada
será encerrada, dando a vez da execução da chamada 3, que parou na
instrução return pesquisar (). Assim, a chamada 3 retornará o valor 1
devolvido pela chamada 4, e será encerrada, transferindo a execução para
chamada 2, que também aguarda o return pesquisar(). Devolverá então o
valor 1 retornado pela chamada 3 e portanto se encerrará, passando a vez
para chamada 1 que está aguardando no return pesquisar(), que retornará
1 da chamada 2 e se encerrará.

Saiba mais
Para se aprofundar no conhecimento de métodos
recursivos, você pode acessar um conjunto de
endereços com blogs que tratam do assunto
clicando aqui.

Bem, até agora estudamos as funções, as chamadas recursivas e a


visibilidade das variáveis. Em princípio as variáveis locais têm seus dados
perdidos com o encerramento da função na qual foram declaradas.
Digo em princípio, pois é possível implementar variáveis locais que não
perdem os dados após a execução das funções. Essa técnica é chamada de
variáveis estáticas.

www.esab.edu.br 30
3.3 Variáveis estáticas
O uso das variáveis estáticas está associado ao contexto de visibilidade
das variáveis, que podem ser locais e globais. Como estudado na unidade
anterior na seção de visibilidade das variáveis, as variáveis locais só podem
ser acessadas durante a execução do método, sendo visível somente nesse
método e que no seu encerramento todas as variáveis são desalocadas e
os dados são perdidos. Já as variáveis globais podem ser acessadas por
qualquer método do programa e é destruída somente ao final da sua
execução. O papel das variáveis estáticas declaradas nas funções é garantir
que o valor da variável seja mantido mesmo após o encerramento da
execução do método.

O código a seguir apresenta o método chamado contar que declara e


usa a variável contador de forma estática. O método main() faz duas
chamadas a esse método, observe:

Algoritmo 4 – Função com variável estática


01 #include <stdio.h>
02 #include <stdlib.h>
03 void contar(){
04 static int contador =0; //declara uma variável
como estática
05 contador++; //incrementa a variável contador em 1
06 printf("Contador vale:%d",contador); //imprime a
variável na tela
07 }
08 int main()
09 {
10 system("cls");
11 printf("\nExecutando a Funcao Contar\n");
12 contar();
13 printf("\nExecutando a Funcao Contar\n");
14 contar();
15 system(“pause”);
16 return 0;
17 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 31
Note que a cláusula static, utilizada na linha 4, é que define uma variável
como estática, e deve ser usada na sua declaração. Cada vez que o
método contar() é executado, a variável contador é incrementada em 1
e impressa na tela. No método main() a função contar é chamada duas
vezes, a primeira chamada na linha 12 e outra na linha 14. Na chamada
da linha 12, a variável contador começará em 0, será incrementada em 1,
portanto, seu valor atual será 1. Mesmo após o encerramento do método,
o valor da variável não será perdido, e na segunda chamada do método,
na linha 14, a variável contador, por ser estática, iniciará de 1, não mais
de 0, depois será incrementada em 1, mudando seu valor atual para 2 e
posteriormente será impresso na tela.

A Figura 10 apresenta o resultado do programa após a sua execução:

Figura 10 – Resultado da execução do Algoritmo 4.


Fonte: Elaborada pelo autor (2013).

Observe que a variável contador não perdeu seus dados após a primeira
execução, isso só é possível porque as variáveis estáticas são armazenadas
na memória principal em blocos de memória diferentes do espaço
utilizado pelas funções, e no encerramento da função todas as variáveis
são desalocadas com o método. Mas as variáveis estáticas não, pois estão
em outro endereçamento de memória.

A Figura 11 apresenta a alocação de funções com variáveis locais e


variáveis estáticas, veja:

www.esab.edu.br 32
Função / Método Função / Método

variáveis variáveis
locais estáticas

Figura 11 – Alocação de variáveis locais e variáveis estáticas.


Fonte: Elaborada pelo autor (2013).

Note que a Figura 11 representa que as variáveis locais ficam no mesmo


bloco de memória da função que as cria, por isso, quando uma função é
encerrada, as variáveis são perdidas. Já as variáveis estáticas são alocadas
em outro bloco de memória, por isso quando a função se encerra, a
variável se mantém alocada.

Nesta unidade foi apresentado o conceito de ponteiros, que é o endereço


da variável na memória principal, na área chamada de Heap. Esse
endereçamento é resultado da instrução de declaração da variável. Duas
variáveis não podem ter o mesmo endereço, pois se trata da forma pela
qual a linguagem identifica a variável que está sendo manipulada no
programa. Vimos também que a recursividade consiste na técnica de
criar funções que chamam a si própria. Por fim, você pôde estudar o uso
de variáveis estáticas declaradas em funções e como possibilitam o acesso
aos seus dados mesmo após o encerramento da função.

Na próxima unidade estudaremos as variáveis compostas homogêneas,


conhecidas como vetores.

www.esab.edu.br 33
Variáveis compostas homogêneas:
4 vetores
Objetivo
Apresentar o conceito, a aplicação e a manipulação de vetores.

Nesta unidade será apresentada a estrutura de dados vetor com ênfase no


seu funcionamento e armazenamento na memória principal. Para isso,
vamos utilizar as principais ideias dos trabalhos de Cerqueira, Celes e
Rangel Netto (2004).

Na unidade anterior você pôde estudar a declaração de variáveis e pôde


verificar que a variável é formada por um nome ou identificador, valor,
endereço de memória e o tipo de dados. Mas existem casos em que há
a necessidade de um armazenamento de vários dados do mesmo tipo,
porém, a declaração de inúmeras variáveis torna a solução do problema
mais complexa do que realmente é. Por exemplo, uma empresa necessita
cadastrar a média de vendas de um determinado produto nos doze
meses do ano. Uma solução seria criar 12 variáveis do tipo real para
armazenamento dessas médias e posterior processamento no algoritmo.
Note que a solução é bem simples, entretanto, utilizar 12 variáveis torna o
processo mais complexo, pois gerenciar 12 variáveis de forma correta não
é tarefa fácil, o que pode gerar inconsistências na solução do problema.

A estrutura de dados que pode ser utilizada para facilitar a solução do


problema é o uso de vetores.

4.1 Conceito de vetores


O vetor permite criar uma variável dividida em inúmeras posições, todas do
mesmo tipo de dados. Para Cerqueira, Celes e Rangel Netto (2004, p. 59),
“[...] o vetor é a forma mais simples de estruturar um conjunto de dados”.

www.esab.edu.br 34
Para contextualizar, vamos analisar o problema de armazenamento das
médias de vendas de um determinado produto nos 12 meses do ano. Em
vez de declarar 12 variáveis, é possível a criação de uma variável com 12
posições. A declaração de vetor em C é realizada utilizando-se a seguinte
sintaxe: tipo de dado identificador[n], em que n se refere à quantidade de
posições do vetor, começando em 0 e terminando na posição n-1.

A instrução a seguir apresenta a declaração da variável “vendas”, como


um vetor de reais com 12 posições, veja: float vendas[12]. A instrução
criará a variável “vendas”, com um único endereço de memória, porém
com 12 posições para serem manipuladas. Observe na Figura 12 a
estrutura do vetor vendas:
0 1 2 3 4 5 6 7 8 9 10 11
vendas
Emq1
Figura 12 – Vetor de Vendas.
Fonte: Elaborada pelo autor (2013).

Note que o vetor “vendas” possui um único endereço de memória,


representado pela expressão (Emq1 – endereço de memória qualquer),
e um conjunto de 12 posições para armazenamento e manipulação dos
dados, começando na posição 0 e terminando na posição 11. Como o
tipo de dados float ocupa 4 bytes, e o vetor possui 12 posições, serão
alocados na memória 48 bytes contíguos para esse vetor, conforme
representado na Figura 13 a seguir, observe:
12 X 4 = 48 bytes

vendas
Emq1
Figura 13 – Alocação de Memória para o vetor de vendas.
Fonte: Elaborada pelo autor (2013).

Analisando a Figura 13 pode-se notar que o bloco de memória não


possui as posições ou a divisão. E isso é verdade: na memória será alocado
um bloco único de 48 bytes, resultante do cálculo da quantidade de
elementos do vetor multiplicado pelo espaço ocupado pelo tipo de dados

www.esab.edu.br 35
do vetor. Porém no uso do vetor para armazenamento ou leitura dos
dados, o programador deverá informar qual a posição a ser utilizada. Mas
como isso é possível?

Como a variável possui um único endereço de memória, e o vetor armazena


o tipo float (real), e cada real ocupa 4 bytes, para que seja possível gravar na
posição indexada no vetor, basta multiplicar a posição por 4.

Por exemplo, para atribuir o valor 128.99 na posição 3 do vetor, a


instrução em C é: vendas [3] = 128.99, mas na execução do programa,
como será realizada essa operação pelo sistema operacional de forma que
na posição 3 seja realmente armazenado o valor 128.99?

A Figura 14 representa o processo para se chegar à posição 3 do vetor:

Posições deslocadas
Bloco de memória ocupado pelo tipo de dados
0 1 2 3 4 5 6 7 8 9 10 11
vendas 128.99

Emq1
ponteiro
Deslocamento: quantidade de posições X 4 bytes
Ponto de partida
Endereço da variável
Figura 14 – Localizando a posição no vetor.
Fonte: Elaborada pelo autor (2013).

Como a variável possui um endereço de memória, esse ponto representa


o início do bloco que foi alocado contiguamente, ou seja, cada bloco
de 4 bytes do tipo float foi alocado um ao lado do outro. Para chegar à
posição desejada é preciso calcular em quanto será o deslocamento na
estrutura, cujo resultado é multiplicar o tamanho do bloco pelo número
de posições. Nesse caso, a posição 3 resulta em um deslocamento de 12
bytes a partir do endereço inicial do vetor. Esse deslocamento posicionará
o ponteiro do vetor 12 bytes à frente do endereço de memória de todo
o bloco. Como cada bloco possui 4 bytes, os próximos quatro bytes a
partir do ponteiro se refere à posição indexada pela instrução vendas[3] =
128.99. Nesse bloco será gravado o valor informado.

www.esab.edu.br 36
Saiba mais
Para aprofundar os estudos sobre os tipos de
dados em C e o espaço ocupado por cada um deles,
acesse o endereço clicando aqui. Agora que vimos
que os vetores nos permitem criar uma variável
dividida em inúmeras posições, veremos a seguir
como os vetores são aplicados e manipulados.

4.2 Aplicação e manipulação


A aplicação de vetores está muito associada à necessidade de
armazenamento de vários dados na forma de uma tabela. Para tanto,
na sua declaração é necessário que seja explicitamente informada a
quantidade de elementos dessa tabela, que é formada por uma única
linha e “N” colunas.

Para inserir, alterar ou consultar um determinado valor do vetor


é necessária a referência da posição do vetor que se deseja acessar,
utilizando a seguinte sintaxe: identificador[p], em que “p” se refere à
posição que varia de 0 ao tamanho do vetor -1. Caso a posição informada
seja maior ou igual ao tamanho do vetor, ocorrerá um erro fatal que
encerrará a execução do algoritmo, já que a posição não pertence ao
bloco previamente alocado.

A Figura 15 representa o vetor de vendas com 12 posições e o resultado


da instrução que intenciona gravar na posição 12 do vetor, observe:

0 1 2 3 4 5 6 7 8 9 10 11
12
vendas
12 X 4 = 48 bytes
Figura 15 – Manipulando a posição fora do vetor.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 37
Note que a posição 12 do vetor é um bloco de memória que não lhe
pertence, por isso quando acessada pelo programa de computador resultará
em um erro fatal, grave, que resultará no encerramento da aplicação.

Para atribuição de um valor no vetor, a sintaxe é: identificador[p] = valor.


Caso nessa posição já exista um valor, este será sobrescrito, uma vez que
em cada posição do vetor somente um valor pode ser armazenado. Dessa
forma essa instrução possibilita tanto a inserção de um valor no vetor
quanto a sua alteração. Para consulta de um valor, basta referenciar o
vetor e a posição de acesso por meio da sintaxe: identificador[p].

O algoritmo a seguir apresenta as instruções de inserção, alteração e


consulta no vetor.

Algoritmo 5 – Manipulação de vetores


01 #include <stdio.h>
02 #include <stdlib.h>
03 float vendas[12];
04 int main(){
05 puts("Informe a media de vendas do Primeiro Mes:");
06 scanf("%f",&vendas[0]); //inserção de dados
07 printf(" A venda do primeiro mes
foi:%.2f",vendas[0]);//consulta
08 puts("\nInforme NOVAMENTE a media de vendas do
Primeiro Mes:");
09 scanf("%f",&vendas[0]); //alteração de dados
10 printf(" A venda do primeiro mes
foi:%.2f",vendas[0]);//consulta
11 system(“pause”);
12 return 0;
13 }

Fonte: Elaborado pelo autor (2013).

Observe no algoritmo que na linha 4 será gravado um valor informado


pelo usuário na primeira posição do vetor. Na linha 5 será mostrado o valor
informado na entrada de dados da instrução da linha 4. Na linha 7 será
cadastrado um novo valor na primeira posição do vetor, alterando o antigo
valor para esse novo valor que será informado pelo usuário do sistema. Na
linha 8 será mostrado o valor atual da primeira posição do vetor.

www.esab.edu.br 38
Uma característica importante do vetor é que uma vez definida a sua
dimensão (quantidade de elementos) na declaração do vetor, durante
a execução do programa o bloco de memória alocado não poderá ser
gerenciado, independentemente da inserção ou não de itens no vetor.
Cerqueira, Celes e Rangel Netto (2004) destacam o uso da alocação
dinâmica para os casos em que a dimensão do vetor é desconhecida.
Quando sabemos a dimensão, é preferível o uso de vetores.

Mesmo que dados não sejam armazenados no vetor, o espaço é pré-


alocado a fim de garantir que o bloco de memória esteja disponível
durante a execução do programa, sendo liberado após o seu
encerramento. Tal contexto pode representar um grande problema no
uso dessa estrutura de dados, uma vez que havendo necessidade de
mais ou menos posições, não há como modificá-lo, pois se trata de uma
alocação estática, aquela que ocorre antes da execução do programa.

No caso do vetor de vendas, por exemplo, caso o usuário deseje cadastrar


as vendas de um período superior a 12 meses, não será possível, pois o
vetor durante a execução do programa permitirá somente 12 posições de
armazenamento. Caso o usuário do sistema cadastre apenas as vendas dos
seis primeiros meses, o vetor ocupará o espaço referente às 12 posições,
independentemente de quantas já foram cadastradas, sendo que as
demais posições serão preenchidas com zero.

Dessa forma, o uso de vetores deve estar associado a casos em que se


conhece exatamente quantos dados serão cadastrados, sem a possibilidade
de variações nesse número de informações a serem gerenciadas.

Nesta unidade estudamos sobre a estrutura de dados vetor que possui


como principal característica o uso de índices ou posições para
referenciar o local de inserção ou busca dos dados. Por se tratar de uma
estrutura de dados de alocação estática, que não permite a otimização e
gerenciamento da sua dimensão, é recomendado para os casos em que
o número de dados a serem gerenciados é conhecido e não se modifica
durante a execução do programa.

Na próxima unidade você estudará o conceito de alocação dinâmica, uma


técnica que possibilita a criação de estruturas de dados com tamanhos
alterados durante a execução do programa.

www.esab.edu.br 39
5 Alocação dinâmica de memória

Objetivo
Apresentar o conceito de alocação dinâmica de memória.

Vimos na unidade anterior o conceito de vetores, também como


são aplicados e manipulados. Já nas unidades anteriores, em especial
a unidade 3 estudamos sobre a definição de ponteiros, o uso da
recursividade e variáveis estáticas. Nesta unidade, vamos apresentar a
alocação dinâmica de memória, por meio do aporte teórico de Cerqueira,
Celes e Rangel Netto (2004) que se fundamenta nos conceitos de
vetores, recursividade e variáveis estáticas.

Conforme estudamos na unidade 3, as variáveis são formadas por


um nome, um tipo de dado, os valores que são armazenados e um
endereço de memória. O ciclo de vida de uma variável começa no
momento em que é declarada. Sem a sua declaração uma variável não
pode ser reconhecida pelo compilador em uso e, consequentemente,
não pode ser alocada na memória principal. Entretanto, a instrução
de declaração da variável não representa a sua criação. A alocação de
memória para a variável pode ocorrer antes da execução do programa,
quando o programa é carregado na memória do computador, na Heap,
de forma que o espaço de memória para variável é alocado e não pode
ser gerenciado. Mesmo que não seja atribuído valor algum à variável, esse
espaço de memória se manterá reservado até que o programa se encerre.
Esse processo de alocação de memória no carregamento do programa
ocorre, por exemplo, na declaração das variáveis globais, aquelas visíveis a
todas as funções implementadas no programa. Esse processo de alocação
de memória no carregamento do programa é chamado de alocação
estática. Cerqueira, Celes e Rangel Netto (2004, p. 12), destacam:
“[...] a declaração de uma variável reserva um espaço na memória para
armazenar um dado do tipo da variável e associa o nome da variável a
esse espaço de memória”.

www.esab.edu.br 40
O Quadro 2 a seguir apresenta as vantagens e desvantagens da alocação
estática, observe:

Vantagens Desvantagens
O gerenciamento da memória é realizado O espaço de memória alocado não pode ser
exclusivamente pelo sistema operacional alterado durante a execução do programa.
que é responsável por alocar e desalocar o Caso a definição do espaço solicitado não
espaço de memória solicitado. atenda às necessidades do programa de
computador, não há como gerenciá-lo de
forma a corrigir o problema.
A definição de alocação de memória Caso o espaço solicitado na declaração
é simples, realizada pela instrução de das variáveis seja maior que o espaço
declaração de variável. disponibilizado pelo sistema operacional, o
programa de computador não será iniciado,
indicando um problema de Overflow.
A sua implementação é simples e atende É inviável para solução de problemas
aos problemas triviais de armazenamento em que não se conhece a quantidade de
de dados que exigem pouco espaço de dados que serão armazenados ou o espaço
armazenamento, principalmente quando a ser alocado é muito maior do que o
se conhece a quantidade de dados a serem disponibilizado pelo sistema operacional.
armazenados e manipulados.

Quadro 2 – Vantagens e desvantagens da alocação estática.


Fonte: Elaborado pelo autor (2013).

Note que um grande problema da alocação estática está na definição de


quanto de espaço de memória será alocado e reservado para a execução
do programa no que se refere às variáveis. Essa é uma realidade no
desenvolvimento de softwares no dia a dia. Por exemplo, uma escola que
possui um cadastro de alunos e deseja gerar um relatório com os que
moram em determinado bairro: quantos alunos atendem a essa condição?
Haverá necessidade de criação de uma estrutura de dados para armazenar
e listar esses dados: como dimensioná-la?

Para resolver esse problema da alocação estática, em que o espaço de


memória é alocado sem a possibilidade de gerenciamento por parte
do programa, por exemplo, aumentando ou diminuindo esse espaço
à medida que se faz necessário, com criação de novas posições num
vetor, ou diminuição de posições do vetor com a remoção de dados não
serão mais utilizadas é que a maioria das linguagens de programação
disponibiliza a alocação dinâmica.

www.esab.edu.br 41
5.1 Definição
Na alocação dinâmica as variáveis são declaradas, porém, o espaço de
memória será alocado durante a execução do programa por meio de
instruções específicas por parte do programador, garantindo que ele possa
alocar e liberar espaço de memória a qualquer momento. Isso possibilita
ao programador a construção de estruturas de dados que tenham
tamanhos variados, de acordo com a necessidade de momento, evitando-
se o desperdício de memória.

Um exemplo clássico de desperdício de memória é quando declaramos


variáveis globais no programa que não são utilizadas. Esse procedimento
não gerará erro algum de compilação e o programa será executado sem
restrições. Entretanto, todas as variáveis globais ocupam um espaço
de memória no carregamento do programa, e mesmo que não sejam
utilizadas, o espaço alocado não é liberado até que o programa se encerre.
Por isso é muito importante uma revisão do programa verificando se
variáveis globais foram declaras e não foram utilizadas.

Outra técnica muito comum é a criação de vetores com quantidade de


elementos muito alta, pois não se conhece previamente essa quantidade
em tempo de execução do programa. Por exemplo, para armazenar
os nomes dos alunos de uma sala é criado um vetor com 60 posições,
estimando-se que uma turma possa ter no máximo essa quantidade de
alunos, entretanto, caso a turma tenha 20 alunos, as outras 40 posições
foram previamente alocadas e não usadas, representando um desperdício
de memória considerável. Se cada nome no vetor for definido com 35
caracteres, estamos desperdiçando 1.400 bytes de memória (40 posições
vezes 35 bytes de cada nome).

Para Cerqueira, Celes e Rangel Netto (2004, p. 64), “[...] a alocação


dinâmica é um meio de requisitar espaços de memória em tempo de
execução. [...] a principal vantagem da alocação dinâmica é fazer a
alocação sem desperdício de memória”.

www.esab.edu.br 42
O Quadro 3 apresenta as vantagens e desvantagens da alocação dinâmica,
observe:

Vantagens Desvantagens
O espaço de memória é definido em tempo Como se trata de um bloco de memória
de execução sem desperdiço de memória, já criado em tempo de execução, toda a
que o espaço pode ser alocado e liberado em responsabilidade em alocar e liberar o
tempo de execução. espaço de memória é do programador.
Pode ser aplicado em soluções triviais ou Caso ele libere um espaço de memória em
em problemas complexos que exigem um um momento inadequado, todos os dados
espaço de memória considerável. serão perdidos. Cabe ao programador saber
É implementado a partir de ponteiros, o momento de executar a instrução que
que são variáveis que apontam para um aloca o espaço de memória, bem como o
endereço de memória. momento em que o espaço é liberado.

Quadro 3 – Vantagens e desvantagens da alocação dinâmica.


Fonte: Elaborado pelo autor (2013).

Note que no caso da alocação dinâmica, a principal vantagem é alocar


e liberar espaço de memória a qualquer momento da execução do
programa, porém, cabe ao programador saber o momento exato em que
essas operações devem ser realizadas. Enquanto a alocação estática era
gerenciada pelo sistema operacional e o programador “usava” as variáveis,
na alocação dinâmica cabe ao programador declarar, alocar e liberar as
variáveis, aumentando a complexidade dos algoritmos.

A alocação dinâmica é a base para implementação de estruturas de dados


mais avançadas como Pilhas, Filas, Listas e Árvores, que são basicamente
estruturas de armazenamento em que não se conhece previamente o
número de elementos que serão manipulados.

Para sua reflexão


O espaço de memória alocado para um programa
de computador ou funções é gerenciado pelo
sistema operacional do computador, porém, não
podemos imaginar que todo o espaço de memória do
computador está disponível. Uma grande parte dessa
memória é utilizada pelo próprio sistema operacional
para execução e gerenciamento de suas tarefas.

www.esab.edu.br 43
5.2 Uso da memória
Basicamente o uso da memória pode ocorrer de forma estática ou
dinâmica. A forma mais comum de definição do espaço de memória se
dá pela declaração de variáveis globais, sendo uma alocação estática, e o
espaço alocado se mantém o mesmo durante toda execução do programa.
As variáveis globais são declaradas no programa e acessíveis a todos os
demais blocos de programas, sejam eles funções ou procedimentos. Outra
forma muito utilizada são as variáveis locais, declaradas dentro dos blocos
de memória e visíveis somente a esses blocos. Nesse caso, as demais funções
e procedimentos, bem como o próprio programa de computador, não
têm acesso a essas variáveis já que são criadas durante a execução do bloco
e liberadas após seu encerramento. Como essas variáveis são criadas em
tempo de execução do método, são alocações dinâmicas.

Uma nova forma de alocação e uso de memória é definida pelo


próprio programador por meio de instruções específicas que alocam ou
liberam um espaço de memória, existente na maioria das linguagens de
programação. Assim como as variáveis locais e globais usam do espaço
de memória, todos os comandos, funções, procedimentos, ou seja,
as estruturas de dados utilizadas no programa também ocupam um
espaço da memória principal do computador. Quando um programa de
computador é executado, um espaço de memória é alocado para o código
do programa, para as variáveis globais e variáveis que serão alocadas
dinamicamente. Caso o programa necessite de mais espaço do que há
disponível pelo sistema operacional, o programa é encerrado.

A Figura 16 representa a distribuição de memória principal por parte do


sistema operacional para que seja possível a execução do programa, observe:

www.esab.edu.br 44
Variáveis
alocadas
dinamicamente

Variáveis globais

Código do programa

Figura 16 – Uso da memória.


Fonte: Elaborada pelo autor (2013).

É possível notar na Figura 16 que o espaço de memória destinado às


variáveis locais e código do programa é fixo, uma vez alocado, não é mais
disponibilizado até que o programa se encerre. Já o espaço de memória
para alocações dinâmicas é flexível, seu espaço pode aumentar ou diminuir
em função das variáveis que são criadas ou destruídas durante a execução
do programa. A estrutura de dados representada na Figura 16 representa
uma pilha, as variáveis são inseridas e removidas pelo topo, de forma que o
último elemento inserido é sempre o primeiro a ser removido.

A alocação da memória para armazenamento do código do programa e


variáveis globais é estática, o tamanho não varia durante a execução do
programa, já o espaço ocupado pelas variáveis globais é flexível. Depende
das operações de alocação e liberação de memória que serão executadas
durante o programa, na chamada de funções e procedimentos com
variáveis locais e procedimentos ou nas instruções explícitas definidas
pelo programador requisitando ou liberando espaços de memória.

Observe que as variáveis estáticas são alocadas antes da execução do programa


durante a carga do programa na memória principal, por isso sua alocação
é mais simples, em blocos de memória contíguos e únicos. Já as variáveis
dinâmicas são alocadas em tempo de execução à medida que o programa
requisita mais espaço ou as desaloca. Quando um espaço de memória é
requisitado dinamicamente, o sistema operacional procura por um espaço
disponível no tamanho do bloco requisitado em toda a memória.

www.esab.edu.br 45
5.3 Alocação contígua e alocação dispersa
Quando uma variável estática é alocada na memória pelo sistema
operacional os blocos são alocados contiguamente, ou seja, um bloco
do lado do outro, de forma organizada. Um vetor com oito posições,
por exemplo, terá os oito blocos de memória que o constituem alocados
um ao lado do outro, facilitando dessa forma o acesso e manipulação da
informação. A Figura 17 apresenta a alocação do vetor de dez posições,
sendo uma alocação estática, veja:

Memória

vendas

Figura 17 – Alocação estática de vetor.


Fonte: Elaborada pelo autor (2013).

Isso é possível, pois o sistema operacional pode alocar o espaço todo antes
mesmo da execução do programa, já que cada item do vetor possui o
mesmo espaço de memória. Por exemplo, se for um vetor de “char”, cada
posição do vetor ocupa 1 byte, resultando em um bloco total de 8 bytes.

Na alocação dinâmica, o espaço só é conhecido em tempo de execução


quando, explicitamente ou por uso de variáveis locais, as variáveis são
criadas e liberadas, e cada bloco é alocado dispersamente, como pode ser
observado na Figura 18.

www.esab.edu.br 46
Memória

Figura 18 – Alocação dinâmica de vetor.


Fonte: Elaborada pelo autor (2013).

Observe que cada posição do vetor está sendo alocada à medida que
uma nova posição se faz necessária. Caso se desejasse alocar todo o vetor
dinamicamente, ou seja, alocar todas as posições no mesmo momento,
o resultado seria o mesmo da Figura 17, um único bloco de memória
alocado durante a execução do programa, com a vantagem que a
quantidade de posições é conhecida em tempo de execução.

Como se trata de uma alocação dinâmica, de cada posição do vetor, e


não da estrutura toda, o vetor alocou quatro elementos, podendo ter
mais ou menos elementos em tempo de execução. Como a alocação não
é contígua, exige que cada elemento alocado saiba qual o seu próximo
elemento, pelo seu endereço na memória, observe na Figura 19.
Memória

e1
e10
e5

e15

Figura 19 – Alocação Dinâmica de Estruturas de Dados com encadeamento.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 47
Cada elemento possui um endereço de alocação na memória, além
disso, é armazenado para cada elemento o endereço do próximo
elemento, assim, o elemento do endereço e1 aponta para o elemento
e10, e o elemento do endereço e10 aponta para o endereço e5, assim
sucessivamente até o elemento e15, gerando uma estrutura de dados com
os elementos dos endereços e1, e10, e5 e e15.

Nesta unidade estudamos o conceito de alocação dinâmica e a sua


importância para a definição de variáveis que podem ter seu tamanho
definido em tempo de execução, evitando o desperdício de memória
alocada para os casos em que não se conhece previamente o número
de elementos que serão manipulados. Normalmente é definida uma
quantidade de elementos muito alta que possa garantir o armazenamento
dos dados independentemente da quantidade que realmente será
utilizada. A alocação dinâmica é utilizada para implementação de
estruturas de dados mais avançadas, como Pilhas, Filas e Árvores.

Em relação ao uso da memória, vimos que as variáveis globais são


declaradas no programa e acessíveis a todos os demais blocos de
programas, sejam eles funções ou procedimentos. Outra forma muito
utilizada são as variáveis locais, declaradas dentro dos blocos de memória
e visíveis somente a esses blocos. As demais funções e procedimentos,
bem como o próprio programa de computador, não têm acesso a essas
variáveis já que são criadas durante a execução do bloco e liberadas após o
seu encerramento. Como essas variáveis são criadas em tempo de execução
do método, são alocações dinâmicas. Uma nova forma muito eficiente de
alocação e uso de memória é definida pelo próprio programador por meio
de instruções específicas que alocam ou liberam um espaço de memória,
existente na maioria das linguagens de programação. As variáveis estáticas
são alocadas contiguamente, já as variáveis dinâmicas podem ser alocadas
de forma dispersa ou contígua na memória, o que exige que cada
elemento tenha uma referência do próximo elemento que está ligada a ele,
por meio do seu endereço de memória.

Na próxima unidade serão apresentados os comandos da linguagem de


programação C que permitem a alocação dinâmica de variáveis.

www.esab.edu.br 48
6 Alocação dinâmica em C

Objetivo
Apresentar as funções de alocação dinâmica de memória em C.

Para elaboração desta unidade foi usada como referência Cerqueira, Celes
e Rangel Netto (2004).

Na unidade anterior você teve a oportunidade de estudar o conceito


da alocação dinâmica, que pode ocorrer no uso de variáveis locais ou
no uso de instruções específicas por parte do programador. Bem, nesta
unidade vamos focar na declaração e uso de variáveis globais utilizando
a linguagem de programação C, que são declaradas explicitamente pelo
programador, deixando de lado o uso de variáveis locais.

O primeiro passo para utilização de variáveis dinâmicas é a declaração da


variável, com uma diferença: em vez de ser declarado um tipo de dados,
será declarado um ponteiro (endereço de memória) para um tipo de dado.

6.1 Definição de ponteiros


A definição, ou declaração de um ponteiro, tem como finalidade
informar para a linguagem de programação que a aquela estrutura de
dados guardará o endereço de uma variável e não o seu valor. Cerqueira,
Celes e Rangel Netto (2004, p. 49) conceituam ponteiro como “[...]
meios de se alterar valores de variáveis aos acessá-las indiretamente
através do endereço de memória”.

Porém, sabendo o endereço da variável, é possível “apontar” para ela e


efetuar operações de gravação, alteração e consulta aos dados.

www.esab.edu.br 49
Para declaração de uma variável ponteiro em C é utilizado o símbolo
de asterisco (*) antes do nome da variável, veja o exemplo a seguir no
Algoritmo 6:

Algoritmo 6 – Declaração de ponteiros em C


01 int *ponteiro_inteiro;
02 char *ponteiro_char;
03 float *ponteiro_real;

Fonte: Elaborado pelo autor (2013).

As instruções acima declaram três variáveis do tipo ponteiro, sendo que a


primeira variável, chamada ponteiro_inteiro, armazenará o endereço do
espaço de memória que tem como finalidade guardar números inteiros.
A variável ponteiro_char tem como objetivo armazenar o endereço de
memória do espaço destinado para caracteres e a variável ponteiro_real o
endereço de memória dos números reais.

Celles, Cerqueira e Rangel Netto (2004) destacam que para cada tipo
de dados da linguagem de programação C há um tipo ponteiro capaz de
armazenar endereços de memória.

Estudo complementar
Para complementar seus estudos sobre os
conceitos de ponteiros em C, acesse o link e veja
alguns exemplos.

Agora que já sabemos como declarar um ponteiro, o próximo passo é


saber como alocar um espaço de memória para essas variáveis ponteiros.

www.esab.edu.br 50
6.2 Comandos para alocação de memória em C
A linguagem C disponibiliza por meio da biblioteca “stdlib” a função malloc
(memory allocation) para alocação de um bloco contíguo de memória do
computador, retornando o endereço desse bloco. Segundo Cerqueira, Celes
e Rangel Netto (2004, p. 66), “[...] a função básica para alocar memória é
malloc. Ela recebe como parâmetro o número de bytes que se deseja alocar e
retorna o endereço inicial da área de memória alocada”.

O Algoritmo 7 apresenta a declaração e alocação de memória de 1 byte


para um ponteiro de caractere (char), observe:

Algoritmo 7 – Uso da função malloc


01 #include <stdlib.h>
02 int main(){
03 char *ponteiro_char;
04 ponteiro_char = (char *) malloc(1);
05 system(“pause”);
06 return 0;
07 }

Fonte: Elaborado pelo autor (2013).

Note que na linha 1 foi incluída a importação da biblioteca “stdlib.h”


para que seja possível o uso da função malloc. Na linha 3 foi declarada
uma variável do tipo ponteiro para caractere (char) e na linha 4 foi
alocado 1 byte de memória para a variável.

Após a declaração e alocação de espaço de memória para variável, esta


pode ser utilizada para entrada e saída de dados, normalmente, como
pode ser visto no código do Algoritmo 8:

www.esab.edu.br 51
Algoritmo 8 – Armazenando e consultando dados do ponteiro
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(){
04 char *ponteiro_char;
05 ponteiro_char = (char *) malloc(1);
06 puts("Informe um Caractere qualquer:\n");
07 scanf("%c",ponteiro_char);
08 printf("O ponteiro de CHAR contem o valor:
%c",*ponteiro_char);
09 system(“pause”);
10 return 0;
11 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 7 foi realizada uma entrada de dados via teclado
para a variável ponteiro_char, que armazenará o valor informado pelo
usuário. Já na linha 8 é mostrado na tela o caractere armazenado no
endereço de memória da variável ponteiro_char, que deve ser o mesmo
valor digitado na linha 7.

No Algoritmo 9 haverá um complemento na rotina de forma a verificar


se o comando de alocação de dados aconteceu com sucesso, observe:

Algoritmo 9 – Verificando o resultado da alocação dinâmica


01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(){
04 int *ponteiro_inteiro;
05 ponteiro_inteiro = (int*) malloc(4);
06 if (ponteiro_inteiro!=NULL){
07 printf ("Erro:Memoria Insuficiente para
alocação do Inteiro");
08 }else{
09 printf ("Memoria Alocada com Sucesso!");
10 }
11 system(“pause”);
12 return 0;
13 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 52
Para a variável ponteiro_inteiro serão requisitados 4 bytes de memória,
por meio da instrução da linha 5 (malloc(4)). Na linha 6 do algoritmo
é verificado se o valor da variável ponteiro_inteiro após a execução da
função malloc() vale NULL, uma vez que, se houve algum problema na
alocação de memória para a variável, esta não guardará o endereço de
memória e valerá NULL. A palavra reservada NULL indica que a variável
ponteiro ainda não foi alocada, portanto, não pode ser manipulada,
tornando-se uma boa prática de programação sempre antes de usar a
variável ponteiro verificar se ela possui um endereço de memória (é
diferente de NULL).

Cada tipo de dados em C possui um tamanho predefinido de espaço a


ser alocado na memória. O quadro a seguir apresenta os tipos de dados
e o espaço ocupado quando alocado em memória para um sistema
operacional de 32 bits, veja:

Tipo Bytes Tipo Bytes Tipo Bytes


char 1 signed short int 2 unsigned int 4
unsigned char 1 long int 4 signed int 4
signed char 1 signed long int 4 short int 2
unsigned short
int 4 unsigned long int 4 2
int
float 4 Double 8 long double 10

Quadro 4 – Tipos de dados em C e espaço alocado


Fonte: Elaborado pelo autor (2013).

Não há necessidade de decorar quanto ocupa de espaço de memória


cada tipo de dados da linguagem C, pois utilizando a função sizeof()
da biblioteca “stdlib” é possível saber quanto de espaço ocupa o tipo de
dados utilizado. Observe o Algoritmo 10.

www.esab.edu.br 53
Algoritmo 10 – Alocação de dados a partir do tipo de dados
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(){
04 int *ponteiro_inteiro;
05 ponteiro_inteiro = (int*)
malloc(sizeof(int));
06 if (ponteiro_inteiro == NULL){
07 printf ("Erro:Memória Insuficiente para
alocação do Inteiro");
08 }else{
09 printf ("Memória Alocada com Sucesso!\n");
10 printf ("Foram alocados: %d bytes!\
n",sizeof(int));
11 }
system(“pause”);
return 0;
12 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 5 não foi definido explicitamente o tamanho do


bloco a ser alocado, esse espaço dependerá do tipo de dados que está
sendo informado para a função sizeof(). No caso do algoritmo. o tipo
“int” foi informado, e de acordo com o Quadro 4, que apresenta cada
tipo de dados da linguagem C, serão alocados 4 bytes para esse ponteiro.

Agora que você já conhece o comando para alocação dinâmica de memória


em C, o próximo passo é saber como liberar esse espaço que foi alocado.

6.3 Comandos da linguagem C para liberação de


memória
Para liberar o espaço de memória alocado por um ponteiro, e esse é o grande
diferencial no uso da alocação dinâmica, desalocar o espaço de memória
ocupado por variáveis que não são mais utilizadas no programa, a biblioteca
“stdlib. h” disponibiliza a função free(). Segundo Cerqueira, Celes e Rangel
Netto (2004, p. 67), “[...] para liberar um espaço de memória alocado
dinamicamente, usamos a função free. Essa função recebe como parâmetro
o ponteiro de memória a ser liberada”. O Algoritmo 11 apresenta a sintaxe
para desalocar uma variável do tipo ponteiro, observe:

www.esab.edu.br 54
Algoritmo 11 – Desalocando memória
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(){
04 int *ponteiro_inteiro;
05 ponteiro_inteiro = (int*)
malloc(sizeof(int));
06 if (ponteiro_inteiro == NULL){
07 printf ("Erro:Memoria Insuficiente para
alocação do Inteiro");
08 }else{
09 printf ("Memoria Alocada com Sucesso!\n");
10 printf ("Liberando Memória Alocada!\n");
11 free(ponteiro_inteiro);
12 }
13 system(“pause”);
14 return 0;
15 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 5 a variável ponteiro_inteiro foi alocada por meio


da função malloc() e na linha 11, a mesma variável foi desalocada,
liberando todo espaço de memória que havia sido requisitado e alocado.
É importante saber que a função free só pode ser utilizado para variáveis
alocadas dinamicamente.

Nesta unidade estudamos os comandos de alocação dinâmica


disponibilizados pela linguagem C por meio da biblioteca “stdlib.h”. O
comando que aloca um espaço de memória é chamado de malloc(), e
retorna o endereço da variável na memória principal ou armazenará um
NULL em caso de erro no processo de requisição e alocação da memória.
Vimos também a função sizeof(), que retorna o espaço de memória
ocupado por um tipo de dados ou variável em C.

Na próxima unidade você estudará o uso de ponteiros na passagem de


parâmetros nas funções e procedimentos.

www.esab.edu.br 55
Fórum
Caro estudante, dirija-se ao Ambiente Virtual de
Aprendizagem da instituição e participe do nosso
Fórum de discussão. Lá você poderá interagir com
seus colegas e com seu tutor de forma a ampliar,
por meio da interação, a construção do seu
conhecimento. Vamos lá?

www.esab.edu.br 56
Resumo

Na primeira unidade estudamos os conceitos fundamentais da estrutura


de dados, a suas aplicações e a diferença entre dados e informações,
diferença fundamentada na informação pura (dados) e na informação
processada. Analisamos os conceitos de abstração de dados e a sua
relação com as estruturas de dados, que se refere basicamente à análise do
mundo real e como trazê-lo na forma de um programa de computador e
armazenar os dados nas estruturas mais adequadas.

Na unidade 2 estudamos a técnica de refinamentos sucessivos que se


materializa na forma de funções, destacando seu papel na redução da
complexidade dos algoritmos e a consequente facilidade da manutenção
e validação. Nessa unidade foram abordados temas como a decomposição
de problemas, a construção e parametrização de módulos e a estrutura
de dados responsável pelo gerenciamento da execução dos módulos,
chamado de pilha de execução.

Na unidade 3 foi apresentado o conceito de ponteiros, os quais


constituem o endereço da variável na memória principal, que é uma
das quatro propriedades que definem uma variável. As demais são:
identificador, valor e tipo de dados. Estudamos ainda a recursividade e
sua aplicação, que consiste em uma função fazer a chamada dela mesma.

Na unidade 4 revisamos os conceitos de vetor com foco na sua forma


de alocação de memória que é estática, o que inviabiliza o seu uso para
problemas em que não se conhece o número exato de dados que serão
armazenados e manipulados.

Na unidade 5 abordamos os conceitos de alocação dinâmica, que


ocorrem em tempo de execução e na alocação estática, que requisita o
espaço de memória antes da execução do programa.

www.esab.edu.br 57
Por fim, na unidade 6, vimos os comandos da biblioteca “stdlib.h” para
alocação de memória (malloc), liberação de memória (free) e espaço de
armazenamento das variáveis ou tipo de dados (sizeof ).

www.esab.edu.br 58
7 Tipos de passagens de parâmetros
Objetivo
Apresentar o funcionamento da passagem de parâmetro nos
métodos.

Na unidade anterior, você estudou sobre os comandos de alocação e


liberação de memória em C.

Nesta unidade estudaremos a passagem de parâmetros presentes na


chamada da função, que corresponde à forma pela qual um valor externo
é enviado para a função. Esses parâmetros podem ser constantes ou
variáveis, e o valor transferido para a função é chamado de argumento.

O material desta unidade foi desenvolvido com base em Forbellone et al.


(2005), Ascencio e Araújo (2010) e Celes, Cerqueira e Rangel (2004).

7.1 Declaração de função


Para Forbellone et al. (2005, p. 141), o procedimento de refinamentos
sucessivos funciona como um quebra-cabeça em que cada peça possui
apenas uma possibilidade de encaixe com outras peças.

De forma geral, a declaração de uma função, segundo Celes, Cerqueira e


Rangel (2004, p. 39) é feita da seguinte forma:

tipo_retorno nome_da_função, (lista de parâmetros){


corpo da função
}

www.esab.edu.br 59
Segundo Celes, Cerqueira e Rangel (2004, p. 40),

[...] os parâmetros devem ser listados, com os respectivos tipos, entre os parênteses
que seguem o nome da função. Além de receber parâmetros, uma função pode ter um
valor de retorno associado.

No exemplo mostrado no Algoritmo 12, o código do algoritmo em


linguagem C implementa uma função chamada calcularIMC (o IMC
é o índice de massa corporal, calculado a partir da altura e do peso),
que recebe como parâmetro dois argumentos do tipo float (real) que
representam o peso e altura de um indivíduo. Acompanhe:

Algoritmo 12 – Cálculo do Índice de Massa Corporal – IMC


1 float calcularIMC(float peso, float altura){
2 return peso / (altura * altura);
3 }

Fonte: Elaborado pelo autor (2013).

Essa função, como pode ser visto na linha 1, possui dois parâmetros:
peso e altura do tipo float. A função retorna um float, o que representa
o resultado do peso dividido pela altura ao quadrado e que é a regra de
negócio de cálculo do índice de massa corporal.

Essa função possui dois parâmetros, chamados peso e altura.

No exemplo a seguir, o código do Algoritmo 13 em linguagem C


implementa a chamada da função, passando como argumentos os valores
de peso e altura informados pelo usuário e armazenados nas variáveis
valorpeso e valoraltura.

Algoritmo 13 – Uso da função de IMC


1 int main(){
2 float valorpeso,valoraltura;
3 float imc;
4 printf(“Informe o seu PESO:\n”);
5 scanf(“%f”,&valorpeso);
6 printf(“Informe a sua ALTURA:\n”);
7 scanf(“%f”,&valoraltura);

www.esab.edu.br 60
8 imc = calcularIMC(valorpeso,valoraltura);
9 printf(“Seu IMC vale:%f”,imc);
10 system(“pause”);
11 return 0;
12 }

Fonte: Elaborado pelo autor (2013).

As variáveis valorpeso e valoraltura, declaradas na linha 2, têm a


finalidade de armazenar os dados informados pelo usuário do programa.
Por sua vez, essas variáveis são utilizadas na chamada da função
calcularIMC, na linha 8, representando os argumentos (valores externos)
que serão transferidos para essa função. Na execução da função, os
valores de argumento são atribuídos aos parâmetros na mesma ordem
da declaração do método, assim, o dado do argumento valorpeso
será atribuído ao parâmetro chamado peso, e o dado do argumento
valoraltura para o parâmetro chamado altura. A partir da atribuição dos
dados, a função executa o cálculo utilizando esses valores de argumentos
e os devolve com um número real, que no programa principal, na linha
8, é atribuído à variável chamada imc.

Segundo Forbellone et al. (2005, p. 143):

[...] a passagem de parâmetros ocorre a partir da correspondência argumento-


parâmetro. Os argumentos, que podem ser constantes ou variáveis, presentes na
chamada da função, serão correspondidos pelos parâmetros do módulo na mesma
ordem, ou seja, ao primeiro argumento corresponde o primeiro parâmetro, ao
segundo argumento, o segundo parâmetro e assim por diante.

www.esab.edu.br 61
A Figura 19 apresenta a identificação de cada item utilizado no
processamento do cálculo de índice de massa corporal por meio da
função calcularIMC(), observe:

Figura 19 – Passagem de parâmetros para o cálculo de IMC.


Fonte: Elaborada pelo autor (2013).

Observe que as variáveis ao serem utilizadas na chamada do método


são identificadas como sendo argumentos e repassam o valor para os
parâmetros, na ordem em que foram declarados na função.

Saiba mais
O Algoritmo 13 pode ser baixado da internet
neste endereço, que é um repositório gratuito de
códigos-fontes e que está sendo utilizado para
a publicação dos algoritmos desenvolvidos pelo
autor desta disciplina.

A forma pela qual os parâmetros podem ser passados para uma função
pode ser por valor ou referência e afetam de que forma os valores do
argumento serão manipulados.

www.esab.edu.br 62
7.2 Tipos de passagem de parâmetro

Ascencio e Araújo (2010, p. 253) destacam que “[...] o parâmetro pode ser
passado por valor ou referência e a forma de declaração deste tipo depende
da sintaxe de cada linguagem de programação”. Ou seja, o funcionamento
independe da linguagem de programação, mas a forma pela qual é
declarada a estrutura do parâmetro depende de cada linguagem.

Para sua reflexão


Cada linguagem de programação possui uma
sintaxe própria para declaração de funções e seus
parâmetros. Porém, as regras de funcionamento
das formas de passagem de parâmetros (com
alteração ou não do valor), quando manipuladas
internamente no subprograma, mantêm-se –
independentemente da linguagem utilizada.
Assim, procure dominar os conceitos sobre os
temas abordados com relação à lógica e ao
funcionamento das estruturas de dados. Isso será
fundamental para que depois você possa dominar
as regras de escrita de cada linguagem.
A resposta a essa reflexão forma parte de sua
aprendizagem e é individual, não precisando ser
comunicada ou enviada aos tutores.

7.2.1 Passagem de parâmetro por valor

Na passagem de parâmetros por valor, a alteração no conteúdo de um


parâmetro, dentro de uma função, não será refletida no argumento que
transferiu o valor para a função. Segundo Ascencio e Araújo (2010, p.
256), a:

[...] passagem de parâmetro por valor fornece valores para a função como se fossem
variáveis locais, de forma que os parâmetros são manipulados dentro da função, mas
após o seu encerramento estes valores são liberados e não influenciam nas variáveis
que foram usadas como argumentos.

www.esab.edu.br 63
No Algoritmo 14 e em linguagem C, é declarada a função multiplicar
com passagem de parâmetros por valor. Observe:

Algoritmo 14 – Passagem de parâmetro de valor


1 #include <stdio.h>
2 #include <stdlib.h>
3 void multiplicar(int num){
4 num = num * 10;
5 printf(“NUM vale: %d\n”,num);
6 }
7 int main(){
8 int x = 10;
9 multiplicar(x);
10 printf(“X vale:%d\n”,x);
11 system(“pause”);
12 return 0;
13 }

Fonte: Elaborado pelo autor (2013).

A função multiplicar recebe como parâmetro um valor inteiro


representado pelo parâmetro chamado num. A passagem de parâmetro
por valor em linguagem C tem como sintaxe a definição do tipo de
variável e o nome da variável, como é demonstrado na linha 3, como:
int num. No programa principal, a variável x é declarada como inteiro,
e o valor 10 é atribuído. Ao ser chamada a função multiplicar(x), o
argumento 10 será atribuído ao parâmetro num, que multiplicará o valor
de num por 10, ou seja, 10 vezes 10, armazenando na própria variável
num, de forma que o atributo num comece valendo 10, mude para 100 e
seja mostrado na tela para o usuário.

A Figura 20 apresenta o resultado da execução do algoritmo anterior,


observe:

Figura 20 – Passagem de parâmetros para por valor – algoritmo de multiplicação.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 64
O valor do parâmetro num da função multiplicar mudou do valor 10
para 100 durante a execução do método, mas o valor do argumento
representado pela variável x continua valendo 10. Ou seja, a alteração do
parâmetro num não alterou o valor do argumento x, as duas estruturas
de dados são manipuladas separadamente.

7.2.2 Passagem de parâmetro por referência

Segundo Ascencio e Araújo (2010, p. 26), na passagem de parâmetro


por referência os parâmetros passados para a função correspondem ao
endereço de memória ocupado pelas variáveis. Dessa maneira, toda vez
que for necessário acessar determinado valor, isso será realizado por meio
de referência, ou seja, apontamento do seu endereço.

Em linguagem de programação em C, a definição de um parâmetro por


referência é realizada pelo uso do asterisco ao lado esquerdo do nome do
atributo.

O código do Algoritmo 15 em C, apresentado a seguir, possui a função


chamada somar, que utiliza um parâmetro por referência chamado valor,
observe:

Algoritmo 15 – Passagem de parâmetro de referência


1 void somar(int *pnum){
2 *pnum = *pnum + *pnum;
3 printf(“Valor vale: %d\n”,*pnum);
4 }
5 int main(){
6 int numero = 10;
7 printf(“Numero vale: %d\n”,numero);
8 somar(&numero);
9 printf(“Numero vale: %d\n”,numero);
10 system(“pause”);
11 return 0;
12 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 65
O parâmetro pnum é declarado na linha 1 do algoritmo com um
símbolo de asterisco (*) ao lado da definição de seu nome, *pnum.
Essa sintaxe é utilizada para determinar que o parâmetro receba como
argumento o endereço de uma variável. Dessa forma, na utilização do
método, a variável do parâmetro deverá ser informada, precedida por
um símbolo de &, como podemos observar na linha 8 do código, com
somar(&numero). Podemos, portanto, entender que a função aguarda
o endereço da variável, por isso o asterisco ao lado do nome da variável,
e na passagem do argumento para função, devemos colocar um & (e
comercial) ao lado da variável para que o endereço desta seja enviado
para o parâmetro.

Na implementação da função, o uso do parâmetro pnum é sempre


associado ao símbolo de asterisco (*) ao lado do nome do parâmetro,
como pode ser observado nas linhas 2 e 3 do algoritmo, de forma que as
operações sejam realizadas sobre o endereço da variável, atualizando o valor
armazenado nessa variável. Na linha 2, a instrução *pnum = *pnum +
*pnum, que é uma atribuição, podemos observar que a declaração *pnum
antes do sinal de atribuição (=) representa o endereço no qual o resultado
da operação será armazenado. As instruções à direita do sinal de atribuição
representam o endereço em que será buscado o valor da variável.

A Figura 21 apresenta o resultado da execução do algoritmo, veja:

Figura 21 – Passagem de parâmetros por referência – algoritmo de soma.


Fonte: Elaborada pelo autor (2013).

Após a execução do método, a variável numero, utilizada como


argumento para a função somar, terá o mesmo valor da variável pnum.
Ou seja, a variável número começou valendo 10, porém como o
parâmetro pnum recebe o endereço da variável numero, as operações de
pnum refletem em alterações na variável numero, e como pnum resulta
em 20, a variável numero também mudou para 20.

www.esab.edu.br 66
Isso só é possível devido ao fato de que na passagem de parâmetro por
referência, o parâmetro recebe o endereço do número e não o seu valor.
Dessa forma, tanto o argumento quanto o parâmetro apontam para o
mesmo lugar ou local da memória, mas com nomes diferentes, de forma
que alterações no parâmetro refletem no argumento.

A Figura 22 ilustra esse processo, observe:

numero numero *pnum


numero=10; somar(&numero) *pnum=*pnum+*pnum *pnum *pnum *pnum
10 em01 em01 em01 em01
endereço:em01 em01 (10) (10) (10) (10)

*pnum
numero em01
20 (20)
em01

Figura 22 – Passagem de parâmetros por referência.


Fonte: Elaborada pelo autor (2013).

Observe que inicialmente a variável numero, armazenada no endereço


fictício de memória em01, recebe o valor 10. Na chamada da função
somar, o parâmetro pnum recebe o endereço da variável numero (em01),
do programa principal. Como nesse endereço está atribuído o valor 10, o
parâmetro pnum também valerá 10. Na atribuição ao parâmetro pnum,
que começa valendo 10, já que no endereço em01 já foi armazenado
o valor 10, pnum será atualizado para a soma de 10 + 10, ou seja,
20. Como o endereço de pnum é o mesmo da variável numero, ao
mudar o valor da variável pnum, simultaneamente o valor da variável
número também é alterado para 20. Por isso, nesse momento, a variável
numero também valerá 20. Quando a função somar é encerrada, o valor
atualizado é mantido na variável numero.

O Algoritmo 15 pode ser baixado da internet neste endereço. Procure


baixá-lo e testá-lo no computador e compilador de sua preferência.

www.esab.edu.br 67
Estudo complementar
Acessando este endereço, você poderá
complementar os estudos sobre as formas de
passagem de parâmetro.

Nesta unidade você pôde estudar os tipos de passagens de parâmetro,


que podem ser por valor ou referência. A principal diferença desses
dois métodos está na alteração ou não do valor de entrada da função
(argumento-parâmetro). Na passagem de parâmetro por valor, as
alterações do valor dentro da função não alteram o valor do argumento.
Na passagem por referência, as alterações do parâmetro alteram também
o valor do argumento. Na linguagem C, a passagem de parâmetro por
valor é definida pelo tipo de dado e o nome do parâmetro. Já a passagem
por referência é definida pelo tipo de dado, um asterisco e nome do
parâmetro.

Na próxima unidade você iniciará os estudos dos tipos de dados


estruturados.

Até lá!

www.esab.edu.br 68
8 Tipos estruturados – parte I
Objetivo
Apresentar a definição e o funcionamento dos tipos de variáveis
estruturadas.

Na unidade anterior você estudou sobre os tipos de passagens de


parâmetros, que podem ser por valor ou por referência. Nesta unidade
iniciaremos os estudos sobre as variáveis estruturadas, um tipo de variável
que é composta por vários outros tipos de dados – por isso também
conhecido como tipo heterogêneo.

Para elaboração desta unidade, foram utilizados Ascencio e Araújo


(2010), Celes, Cerqueira e Rangel (2004) e Forbellone et al. (2005).

8.1 O tipo heterogêneo


Segundo Celes, Cerqueira e Rangel (2004, p. 98), “[...] um tipo
estruturado é um tipo de dado formado por vários valores de tipos mais
simples”. Sendo esses tipos de dados mais simples, os tipos primitivos de
uma linguagem de programação, como inteiro, real, caractere e lógico.

Já Ascencio e Araújo (2010, p. 333), fazem a referência aos tipos


estruturados chamando-os de “[...] registro, sendo uma estrutura de
dados que consegue agregar vários dados acerca de uma mesma entidade.
Onde cada dado contido num tipo estruturado é chamado de campo”.

Para Forbellone et al. (2005, p. 85),

[...] os registros, ou tipos estruturados, são uma das principais estruturas de dados,
sendo uma variável composta que engloba um conjunto de dados primitivos, por isso
é conhecida como tipo estruturado heterogêneo”.

www.esab.edu.br 69
A Figura 23 representa uma ficha cadastral de alunos para uma escola.
Observe:

Matrícula: Data:
Nome:
Sexo: M [ ] F [ ]
Data de nascimento: / / Idade:
Nome do Pai:
Nome da Mãe:
Telefone: ( )
Celular: ( )
E-mail:

Figura 23 – Ficha cadastral de aluno.


Fonte: Elaborada pelo autor (2013).

Note que a ficha do aluno é formada por vários dados que podemos
chamar de campos, alguns para armazenamento de tipos inteiro (como
matrícula e idade), outros para cadastro de tipos alfanuméricos (como
nome, nome do pai e da mãe). No campo sexo será cadastrado um
caractere, M ou F. Como a ficha é constituída por campos de diversos
tipos de dados, para implementar essa ficha utilizando uma linguagem
de programação de computadores será necessária uma estrutura de dados
que também seja heterogênea.

Agora, como criar essa estrutura heterogênea na linguagem de


programação C? É o que veremos a seguir.

Os tipos estruturados em C são definidos por meio da palavra reservada


struct, conforme apresentado a seguir:

www.esab.edu.br 70
struct nome_da_estrutura{
tipo de dado campo1;
tipo de dado campo2;
tipo de dados campoN;
};

Os campos representam os itens que compõem um tipo estruturado e


possuem uma sintaxe de definição muito semelhante à declaração de
variáveis, assim, podemos representar cada campo como uma variável,
conforme apresentado no Algoritmo 16, nas linhas 2 a 12.

Segundo Ascencio e Araújo (2010, p. 338):

[...] a partir da definição desta estrutura, o programa poderá utilizá-la na forma


de um novo tipo de dados, e este novo tipo de dado é capaz de armazenar várias
informações cujos tipos podem ser diferentes, chamados de membros da estrutura.

O Algoritmo 16, implementado em linguagem C, apresenta a sintaxe


para definição do tipo estruturado para armazenamento da ficha de aluno
ilustrada na Figura 23.

Algoritmo 16 – Declaração do tipo estruturado – ficha cadastral de aluno


1 struct fichaaluno{
2 int matricula;
3 char data[10];
4 char nome[50];
5 char sexo;
6 char datanascimento[10];
7 int idade;
8 char nomepai[50];
9 char nomemae[50];
10 char telefone[14];
11 char celular[14];
12 char email[30];
13 };

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 71
Observe que o tipo estruturado está sendo declarado com o nome
fichaaluno, na linha 1 e formado por um conjunto de campos, que
possuem a declaração na forma de variáveis, apresentados nas linhas 2 a
12, que representam as informações contidas na ficha cadastral de aluno.
A ordem dos campos não precisa ser a mesma ordem da ficha cadastral.

Para que um programa utilize o tipo struct, fichaaluno, que foi criado
anteriormente, é necessária a definição de uma variável com esse tipo, da
seguinte forma:

nome_da_estrutra nome_da_variável;

O Algoritmo 17, em C, declara uma variável chamada aluno do tipo


“fichaaluno”. Observe:

Algoritmo 17 – Declaração do tipo aluno


1 struct fichaaluno aluno;

Fonte: Elaborado pelo autor (2013).

Para utilizar as variáveis definidas por um tipo estruturado, faz-se da


mesma forma que as variáveis simples. É preciso que uma variável
seja declarada com o tipo estruturado criado. Assim, as operações de
atribuição e recuperação dos dados armazenados na variável podem
ser realizadas, porém é preciso que seja informado qual campo do tipo
estruturado se deseja acessar. A sintaxe para acessar um campo do tipo
estruturado, para gravação ou leitura, é preciso informar o nome da
variável, seguido de um sinal de ponto (.) e o nome do campo:variável.
nome_campo.

Para exemplificar, no Algoritmo 18, em linguagem C, serão criadas


rotinas para o cadastro e listagem dos dados dos alunos utilizando o tipo
estruturado fichaaluno. Veja:

www.esab.edu.br 72
Algoritmo 18 – Manipulação do tipo estruturado
1 #include <stdio.h>
2 #include <stdlib.h>
3 struct fichaaluno{
4 int matricula;
5 char data[10];
6 char nome[50];
7 char sexo;
8 char datanascimento[10];
9 int idade;
10 char nomepai[50];
11 char nomemae[50];
12 char telefone[14];
13 char celular[14];
14 char email[30];
15 };
16
17 struct fichaaluno aluno;
18
19 void cadastrar(){
20 system(“cls”);
21 printf(“Matricula:\n”);
22 scanf(“%d”,&aluno.matricula);
23 getchar();
24 printf(“Data:\n”);
25 gets(aluno.data);
26 printf(“Nome:\n”);
27 gets(aluno.nome);
28 printf(“Sexo:\n”);
29 aluno.sexo = getchar();
30 getchar();
31 printf(“Data de Nascimento:\n”);
32 gets(aluno.datanascimento);
33 printf(“Idade:\n”);
34 scanf(“%d”,&aluno.idade);
35 getchar();
36 printf(“Nome do PAI:\n”);
37 gets(aluno.nomepai);
38 printf(“Nome da Mae:\n”);
39 gets(aluno.nomemae);
40 printf(“Telefone:\n”);
41 gets(aluno.telefone);
42 printf(“Celular:\n”);
43 gets(aluno.celular);
44 printf(“E-mail:\n”);
45 gets(aluno.email);

www.esab.edu.br 73
46 }
47
48 void listar(){
49 system(“cls”);
50 printf(“Matricula:%d\n”,aluno.matricula);
51 printf(“Data do Cadastro:%s\n”,aluno.data);
52 printf(“Nome:%s\n”,aluno.nome);
53 printf(“Sexo:%c\n”,aluno.sexo);
54 printf(“Data de Nascimento:%s\n”,aluno.datanascimento);
55 printf(“Idade:%d\n”,aluno.idade);
56 printf(“Nome do PAI:%s\n”,aluno.nomepai);
57 printf(“Nome da MAE:%s\n”,aluno.nomemae);
58 printf(“Telefone:%s\n”,aluno.telefone);
59 printf(“Telefone Celular:%s\n”,aluno.celular);
60 printf(“E-mail:%s\n”,aluno.email);
61 }
62 int main(){
63 cadastrar();
64 listar();
65 system(“pause”);
66 return 0;
67 }

Fonte: Elaborado pelo autor (2013).

Alguns pontos merecem ser destacados nesse algoritmo. A manipulação


dos dados do tipo estruturado, tanto na entrada de dados ou na saída dos
dados, segue a seguinte sintaxe: nome da variável do tipo estruturado.
nome do campo. Portanto, o uso de um tipo estruturado é possível
por meio do acesso individual de cada um dos campos, seja para gravar
ou para recuperar uma informação. Ou seja, no momento em que se
deseja manipular uma variável do tipo estruturado, é preciso que nos
questionemos sobre qual campo desejamos ler ou gravar, já que a variável
é composta por um conjunto de campos. A atribuição ou leitura de uma
variável estruturada sem que seja especificado qual, ou quais campos,
gerará um erro de compilação. Pois a própria linguagem alerta que o uso
de uma variável composta exige a especificação de qual campo, e não
precisa se desesperar, basta se lembrar de informar qual o campo a ser
acessado, usando a sintaxe variável.nome_campo.

www.esab.edu.br 74
A gravação dos dados é realizada na função chamada cadastrar, que limpa
a tela pelo comando cls na linha 20 e solicita que o usuário informe cada
um dos membros da variável aluno, do tipo “tipo_aluno”. A listagem
dos dados é realizada na função listar, que imprime na tela cada um dos
membros cadastrados na função cadastrar(). Como essas duas funções
(cadastrar e listar) manipulam a variável aluno, ela foi declarada de forma
global, na linha 17, na instrução: struct tipo_aluno aluno;

Assim como as variáveis de tipos primitivos podem ser declaradas


como do tipo ponteiro, como por exemplo: int *p, as variáveis do tipo
estruturado também podem ser transformadas em ponteiros. É esse
assunto que veremos a seguir.

8.2 Ponteiro para estruturas e passagens de


estruturas para funções
A linguagem de programação C permite a declaração de ponteiros para
um tipo estruturado. Para tanto, basta, na declaração da variável que
será do tipo estruturado, a utilização do asterisco (*). Celes, Cerqueira e
Rangel (2004, p. 100) destacam que “[...] o símbolo de asterisco significa
o operador ‘conteúdo-de’”. Estamos, de certa forma, informando que
para acessar ou gravar na variável é preciso se dirigir ao endereço dessa
variável. Por isso usamos o contexto de “conteúdo-de”, que aponta para o
endereço da variável na memória.

Para acessar o ponteiro para tipo estruturado, o operador é composto


por um traço seguido de um sinal de maior, formando a simbologia ->.
O ponteiro para a variável do tipo estruturado aponta para um único
endereço de memória, que por sua vez é composto pelos campos do
tipo estruturado. Ou seja, o único endereço de memória armazena um
conjunto de campos, e o acesso se dá pela sintaxe: variável_ponteiro-
>nome_do_campo.

www.esab.edu.br 75
O Algoritmo 19, em linguagem C, demonstra a declaração, atribuição e
o acesso aos dados de um ponteiro para tipo estruturado. Observe:

Algoritmo 19 – Uso de ponteiro para tipos estruturados


1 #include <stdio.h>
2 #include <stdlib.h>
3 struct dadoscarro{
4 char placa[8];
5 char marca[30];
6 int anofabricacao;
7 };
8 struct dadoscarro carro;
9 struct dadoscarro *pontCarro = &carro;
10 void cadastrar(){
11 system(“cls”);
12 printf(“Informe a Placa:\n”);
13 gets(pontCarro->placa);
14 printf(“Informe a Marca:\n”);
15 gets(pontCarro->marca);
16 printf(“Informe o ANO DE FABRICAO:\n”);
17 scanf(“%d”,&pontCarro->anofabricacao);
18 }
19 void listar(){
20 system(“cls”);
21 printf(“PLACA:%s\n”,pontCarro->placa);
22 printf(“MARCA:%s\n”,pontCarro->marca);
23 printf(“ANO DE FABRICACAO:%d\n”,pontCarro->anofabricacao);
24 }
25 int main(){
26 cadastrar();
27 listar();
28 system(“pause”);
29 return 0;
30 }

Fonte: Elaborado pelo autor (2013).

Nas instruções das linhas 3 a 7 do algoritmo, é declarado o tipo


estruturado dadoscarro. Na linha 8, é declarada a variável carro do tipo
“dadoscarro”. Já na linha 9 do algoritmo, é declarado que a variável
pontCarro é um ponteiro para um tipo estruturado dadoscarro na
instrução struct dadoscarro *pontCarro e essa variável é inicializada
com o endereço de memória da variável carro, declarada na linha 9, na
instrução “= &carro;”. Ou seja, a variável pontCarro contém o endereço
de memória da variável carro.

www.esab.edu.br 76
Nas funções cadastrar() e listar(), os dados são solicitados por meio da
funções entrada de dados gets, para campos do tipo literal, e scanf para
os demais tipos de dados (numéricos e caractere) e apresentados por
meio da função de saída de dados printf, ao usuário e seguem a sintaxe:
variável ponteiro -> nome do campo.

Além da possibilidade de declaração de variáveis ponteiros para um tipo


estruturado, é possível a passagem dessas estruturas como parâmetro para
as funções. Ou seja, uma função pode receber como parâmetro um tipo
estruturado, como veremos nos próximos tópicos.

Na passagem de parâmetros de tipos estruturados, o processamento é


semelhante à passagem de parâmetros simples, podendo ser por valor ou
por referência.

Para exemplificar, no Algoritmo 20, haverá uma alteração no código


do Algoritmo 19, com a inclusão de uma função para listar os dados
do veículo cadastrado recebendo como parâmetro um tipo estruturado.
Observe:

Algoritmo 20 – Listar dados do veículo


1 void listar(struct dadoscarro item){
2 system(“cls”);
3 printf(“PLACA:%s\n”,item.placa);
4 printf(“MARCA:%s\n”,item.marca);
5 printf(“ANO DE FABRICACAO:%d\n”,item.anofabricacao);
6 }
7 int main(){
8 cadastrar();
9 listar(carro);
10 system(“pause”);
11 return 0;
12 }

Fonte: Elaborado pelo autor (2013).

A função listar recebe como parâmetro um valor do tipo dadoscarro,


chamado item. Na execução da função, o valor do argumento do tipo
dadoscarro é enviado para o atributo item e cada campo é exibido
na tela. A chamada desse método, listar, é apresentada na linha 9 do
Algoritmo 20.

www.esab.edu.br 77
O código completo do Algoritmo 20 pode ser baixado neste endereço.
Procure baixá-lo e executá-lo para verificação dos conteúdos apresentados
sobre o tema.

Para que não haja desperdício de memória na criação dos tipos


estruturados, é possível determinar o espaço de memória que será
alocado pela estrutura e, mais do que isso, há a possibilidade de liberação
da memória alocada – caso o tipo estruturado não seja mais necessário.
Portanto, durante a execução do programa é possível determinar
quanto de espaço de memória será alocado para um tipo estruturado,
de forma que seja utilizado somente o espaço de memória necessário,
sem desperdícios. E caso a variável não seja mais utilizada, é possível
que o espaço previamente alocado seja liberado, para que também não
haja desperdício de memória. Você pode criar e remover uma variável
da memória do computador durante a execução do programa. Com isso
teremos um algoritmo mais eficiente no gerenciamento de memória.

Um tipo estruturado por ser composto normalmente por vários campos,


tende a ocupar bastante espaço de memória. Por isso, é fundamental a
alocação apenas do espaço que realmente será utilizado e a liberação da
memória quando a variável que manipula esse tipo de dados não é mais
necessária.

Saiba mais
Para saber mais sobre os tipos de dados
estruturados em C, acesse este endereço e
complemente seus estudos.

8.3 Alocação dinâmica de estruturas


Como estudado na unidade 6, a linguagem C disponibiliza as funções
malloc() e free() para alocação e liberação de memória. Essas funções
podem ser utilizadas para os tipos estruturados, como pode ser verificado
no exemplo do Algoritmo 21:

www.esab.edu.br 78
Algoritmo 21 – Alocação dinâmica de tipos estruturados
1 #include <stdio.h>
2 #include <stdlib.h>
3 struct estruturadata{
4 int dia;
5 int mes;
6 int ano;
7 };
8 int main(){
9 struct estruturadata data;
10 struct tipo_data *pontData;
11 pontData = (struct tipo_data*) malloc(sizeof(struct tipo_
data));
12 pontData->dia = 01;
13 pontData->mes = 07;
14 pontData->ano = 1980;
15 system(“cls”);
16 puts(“Resultado\n”);
17 printf(“%d\n%d\n%d”,pontData->dia,pontData->mes,pontData-
>ano);
18 free(pontData);
19 system(“pause”);
20 return 0;
21 }

Fonte: Elaborado pelo autor (2013).

Nas linhas 3 a 6 do algoritmo é declarado o tipo estruturado


estruturadata com os campos dia, mês e ano, todos do tipo inteiro (int).
Na linha 9 é declarada a variável data do tipo estruturadata. Na linha 10
é declarada a variável pontData, que é um ponteiro para a variável data.
Na linha 11 a variável pontData, por meio da função maloc(), recebe
o endereço do espaço de memória alocado, um espaço de 12 bytes, já
que seus campos são do tipo inteiro e cada um deles ocupa 4 bytes. A
instrução malloc() faz a alocação dos dados, a instrução sizeof() retorna
o espaço de memória do tipo estruturado estruturadata. Note que esta
operação se dá em dois passos, primeiramente a função sizeof() retorna
o tamanho em bytes dos campos que compõem o tipo estruturado
estruturadata. Depois desse tamanho, é informada a função malloc()
que então aloca esse espaço solicitado, que é o tamanho máximo em
bytes para o tipo estruturadata. Dessa forma, não haverá desperdícios de
memória na alocação do espaço.

www.esab.edu.br 79
Na linha 12, o espaço de memória alocado para a variável pontData
é liberado por meio da instrução free(). Essa instrução garante que o
espaço seja liberado já que a variável não será mais utilizada, evitando o
desperdício de memória.

Nesta unidade você estudou os tipos estruturados, também conhecidos


como tipos heterogêneos ou registros. Um tipo estruturado é formado
por membros ou campos, que podem ser de tipos de dados diferentes.
Para manipular variáveis do tipo estruturado, é necessário informar o
nome da variável e o campo que se deseja manipular, usando a sintaxe:
variável.nome_campo.

Também foi possível verificar que os tipos de dados estruturados podem


ser manipulados na forma de ponteiros, usando o símbolo asterisco (*)
para indicar que a variável aponta para um tipo estruturado. Para utilizar
uma variável ponteiro de tipo estruturado, é necessário informar o nome
do ponteiro e o campo que se deseja acessar, usando a sintaxe: variável_
ponteiro->nome_campo.

Por fim, você estudou como utilizar os comandos malloc () e free () para
alocar e liberar espaço em tempo de execução para tipos estruturados.

Na próxima unidade, continuaremos estudando os tipos estruturados e


veremos como utilizá-los para definição de novos tipos e como manipulá-
los na forma de um vetor.

Tarefa dissertativa
Caro estudante, convidamos você a acessar o
Ambiente Virtual de Aprendizagem e realizar a
tarefa dissertativa.

www.esab.edu.br 80
9 Tipos estruturados – parte II
Objetivo
Apresentar a definição e o funcionamento dos tipos de variáveis
estruturadas.

Na unidade anterior você teve o primeiro contato com os tipos


estruturados, que podem ser utilizados de forma estática ou por meio de
um ponteiro, e também como passagem de parâmetro de funções.

Nesta unidade, nosso assunto continua sendo os tipos estruturados, mas


com um foco na definição de novos tipos de dados, na criação de vetores
de tipos estruturados e como utilizar o tipo enumeração.

O conteúdo desta unidade foi desenvolvido com base em Ascencio e


Araújo (2010) e Celes, Cerqueira e Rangel (2004).

9.1 Definição de novos tipos


A maioria das linguagens permite a criação de novos tipos de dados. No
caso da linguagem C, essa funcionalidade é realizada por meio da palavra
reservada typedef (definição de tipo). Celes, Cerqueira e Rangel (2004,
p. 102) destacam que a “[...] definição de novos tipos de dados visa à
utilização em nossos programas. Esse novo tipo de dado representa um
mnemônico que pode ser usado na declaração de variáveis”.

Para sua reflexão


A maioria das linguagens de programação
permitem a ciração de tipos heterogenos, que
podem ser chamados de registros ou classes, como
no caso das linguagens de programação Pascal e
Java, respectivamente.

www.esab.edu.br 81
A sintaxe para declaração e novos tipos em C é: typedef tipo nome_
novo_tipo;

O Algoritmo 22, em C, exemplifica a definição de tipos. Observe:

Algoritmo 22 – Definição de novos tipos


1 #include <stdio.h>
2 #include <stdlib. h>
3 typedef char literal[30];
4 typedef int inteiro;
5 typedef float moeda;
6 int main(){
7 literal nome;
8 inteiro idade;
9 moeda salario;
10 system(“cls”);
11 printf(“Infome o seu Nome:\n”);
12 gets(nome);
13 printf(“Infome a sua Idade:\n”);
14 scanf(“%d”,&idade);
15 printf(“Infome a sua Renda:\n”);
16 scanf(“%f”,&salario);
17 printf(“\nNome:%s\nIdade:%d\
nSalario:%f”,nome,idade,salario);
18 system(“pause”);
19 return 0;
20 }

Fonte: Elaborado pelo autor (2013).

Essa técnica de criação de novos tipos de dados visa deixar mais claro
o tipo de informação que será manipulado, podendo ser utilizado para
apelidar um tipo já existente, como no Algoritmo 22, na linha 4, em que
o tipo int foi apelidado de inteiro, por meio da instrução typedef. Outra
aplicação é na definição de um tipo estruturado. Por exemplo, suponha
que um programa tem como finalidade realizar o cadastro dos dados de
uma agenda, com o local do compromisso, data e hora, representado por
um tipo estruturado “fichaagenda”. Usando a definição de um novo tipo,
chamado o tipo estruturado de agenda, por meio da instrução typedef, ficará
mais claro qual o tipo de dados que está sendo manipulado. Consequente
facilitará a compreensão e manutenção no sistema, principalmente se a
pessoa responsável pela manutenção não for a que implementou o programa.
Portanto, trata-se de uma boa prática de programação.

www.esab.edu.br 82
Na linha 3 do Algoritmo 22, foi criado o tipo de dados chamado literal,
que representa uma cadeia de 30 caracteres. Na linha 2 foi criado o
tipo de dados inteiro e na linha 3 o tipo de dados moeda, um float.
Na linha 7 foi declarada a variável nome como sendo do tipo literal. A
variável idade, declarada na linha 8, é do tipo inteiro e a variável salário,
declarada na linha 9, do tipo moeda.

As operações de entrada e saída de dados das variáveis não mudam,


seguem o padrão dos tipos primitivos. Por isso, nas linhas 11 a 17 é
realizada a entrada de dados para as variáveis, e na linha 17 é mostrado
o valor de cada uma delas. A instrução system(“cls”), na linha 10, é para
limpar a tela do programa.

A cláusula typedef pode ser utilizada para definição de tipos estruturados


de dados, facilitando a sua declaração, como pode ser analisado no
Algoritmo 23.

Algoritmo 23 – Definição de tipo – typedef


1 #include <stdlib.h>
2 #include <stdio.h>
3 struct registro{
4 int codigo;
5 char descricao[40];
6 float preco;
7 };
8 typedef struct registro tipo_produto;
9 int main(){
10 tipo_produto produto;
11 system(“cls”);
12 printf(“Infome o CODIGO:\n”);
13 scanf(“%d”,&produto.codigo);
14 getchar();
15 printf(“Infome a DESCRICAO:\n”);
16 gets(produto.descricao);
17 printf(“Infome o PRECO:\n”);
18 scanf(“%f”,&produto.preco);
19 printf(“\nCODIGO:%d”,produto.codigo);
20 printf(“\nDESCRICAO:%s”,produto.descricao);
21 printf(“\nPRECO:%f”,produto.preco);
22 system(“pause”);
23 return 0;
24 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 83
Na linha 3, foi criado o tipo estruturado chamado registro com os
campos código, descrição e preço. Na linha 8, foi declarado o novo tipo
de dados chamado tipo_produto, que, assim como na linha 3, também é
do tipo estruturado registro. Na linha 10, a variável produto foi declarada
como sendo do tipo tipo_produto. Note que a partir da definição de um
novo tipo para um tipo struct, como na linha 8: typedef struct registro
tipo_produto, não será mais necessário que variáveis declaradas com este
novo tipo tenham que utilizar a cláusula struct informando que se trata
de um tipo estruturado. Isso, pois essa informação já está na definição do
novo tipo (implícito), assim a declaração se torna mais simples, como na
linha 10: tipo_produto produto.

Dessa forma, a variável produto passou a contar com os campos código,


descrição e preço.

Dica
A possibilidade de definição de novos tipos de
dados deve ser utilizada para criar nomes que
facilitem a compreensão dos dados que serão
manipulados, por isso, procure utilizar sinônimos
na declaração dos novos tipos. Por exemplo, para
representar um tipo de dados estruturado que
representa um endereço composto por logradouro,
número, bairro, CEP etc., a boa prática é declarar o
novo tipo como sendo “endereço”.

Da mesma forma que uma variável pode ser declarada como sendo de
um tipo estruturado, um vetor também pode ser declarado da mesma
forma, facilitando o desenvolvimento do programa.

Por exemplo, para o cadastro do nome, idade e sexo dos clientes


de uma loja, utilizando vetor, teríamos que criar três vetores para o
armazenamento de cada informação, ou seja, um vetor para nomes,
outro para idades e outro para sexos. Mais do que isso, será necessário
o gerenciamento desses vetores de forma que cada posição do vetor
represente cada dado de um mesmo cliente. Por exemplo, o cadastro do

www.esab.edu.br 84
cliente de nome Marcelo, idade 43 anos e sexo masculino na posição 0
do vetor deverá gravar na posição 0 do vetor nomes o valor “Marcelo”,
na posição 0 do vetor idades o valor 43 e no vetor sexos na posição 0
o valor “M”. E quanto maior o número de informações de um cliente,
maior a quantidade de vetores. Ou seja, se houver um dado telefone e
salário, serão necessários mais dois vetores para armazenamento desses
novos dados, e gerenciados de forma simultânea. Note que as operações
de inserção, remoção e alteração terão que manipular todos os vetores de
forma simultânea, pois os dados foram divididos em estruturas de dados
diferentes, tornando o processo complexo.

O uso de tipos estruturados em vetores permite que um único vetor


seja criado, onde cada posição é composta pelos campos desse tipo
estruturado, facilitando a implementação e manutenção do algoritmo.

Veremos a seguir como declarar e manipular vetores de tipos estruturados


e vetores de ponteiros.

9.2 Vetores de tipos de dados e vetores de ponteiros


A função dos vetores é agrupar os elementos, criando um conjunto de
dados de forma mais organizada, e simplificando a programação já que
os valores podem ser acessados facilmente pela indicação da posição dos
dados dentro do vetor. Essa característica é independentemente do tipo
de dado que está sendo manipulado, e podemos escrever vetores de tipos
estruturados, como veremos a seguir.

Para declaração de um vetor de tipos estruturados, é fundamental que se


defina um novo tipo que será usado na declaração do vetor, sendo que
cada uma de suas posições terá os mesmos campos do tipo estruturado.
Portanto, para a manipulação do vetor, será necessário especificar a
posição do vetor e qual campo se deseja acessar.

Para exemplificar, o Algoritmo 24, escrito em C, apresenta a declaração


de um vetor do tipo estruturado produto, o mesmo que foi apresentado
no algoritmo 23. Observe:

www.esab.edu.br 85
Algoritmo 24 – Vetor de tipo estruturado
1 #include <stdio.h>
2 #include <stdlib.h>
3 struct registro{
4 int codigo;
5 char descricao[40];
6 float preco;
7 };
8 typedef struct registro tipo_produto;
9 tipo_produto estoque[3];
10 void cadastrar(){
11 system(“cls”);
12 int i=0;
13 for(i=0; i < 3;i++){
14 puts(“Informe o CODIGO:\n”);
15 scanf(“%d”,&estoque[i].codigo);
16 getchar();
17 puts(“Informe a DESCRICAO:\n”);
18 gets(estoque[i].descricao);
19 puts(“Informe o preco:\n”);
20 scanf(“%f”,&estoque[i].preco);
21 }
22 }
23 void listar(){
24 system(“cls”);
25 int i=0;
26 for(i=0; i < 3;i++){
27 printf(“%d - %s - %f\n”,estoque[i].codigo,
28 estoque[i].descricao,
29 estoque[i].preco);
30 }
31 }
32 int main(){
33 cadastrar();
34 listar();
35 system(“pause”);
36 return 0;
37 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 86
A linha 3 do código apresenta a declaração do tipo estruturado “registro”.
Na linha 8, é criado o tipo de dados chamado tipo_produto. Na linha
9, o vetor chamado estoque é declarado com três posições do tipo “tipo_
produto”. Isso significa que cada posição do vetor terá os campos código,
descrição e preço.

A função cadastrar() possui como lógica as seguintes regras, executar


um laço “para-faça”, acessando as posições 0, 1 e 2 do vetor estoque.
Cada posição do vetor do tipo estruturado “tipo_produto” possui três
campos: código, descrição e valor, assim, cada campo é acessado por
meio da sintaxe: estoque[posição].nome_do_campo, em que a posição é
representada pela variável “i” controlada pelo laço “para-faça”.

A cada vez que o laço é executado, é solicitado ao usuário que informe o


código do produto, que será armazenado na posição atual do vetor, no
campo código, por isso a sintaxe: estoque[i].codigo, o nome do produto,
armazenado no campo estoque[i].nome e o preço, armazenado no campo
estoque[i].preco. Ao final do processo de cadastramento, todos os campos
(codigo, nome e preço) de todas as posições do vetor estarão preenchidas.

A função listar() percorre cada posição do vetor acessando cada um dos


campos dessa posição e mostrando o valor cadastrado na função cadastrar().

A função main() faz a chamada das funções cadastrar() e listar().

Assim como podemos declarar um vetor de tipo estruturado, é possível


ainda a declaração de um vetor de ponteiros de tipos estruturados.

Na definição de um vetor de ponteiros, algumas mudanças na definição


do tipo estruturado são necessárias: a primeira é utilizar o símbolo de
asterisco (*), indicando que a variável é um tipo ponteiro. A segunda
é inicializar o vetor com NULL em todas as posições, dessa forma o
vetor ficará vazio. A terceira é alocar um espaço de memória para cada
elemento do vetor e deslocá-los quando não forem mais necessários.

No Algoritmo 25, é apresentada a rotina de criação e declaração do vetor


de ponteiros para o tipo estruturado.

www.esab.edu.br 87
Algoritmo 25 – Vetor de ponteiro para o tipo estruturado
1 #include <stdio.h>
2 #include <stdlib.h>
3 struct registro{
4 int codigo;
5 char descricao[40];
6 float preco;
7 };
8 typedef struct registro tipo_produto;
9 tipo_produto* tabela[3];

Fonte: Elaborado pelo autor (2013).

Note que a definição do novo tipo de dado tipo_produto, na linha


8, segue a mesma sintaxe dos algoritmos anteriores. Porém, na linha
9, a variável tabela é declarada como um vetor do tipo apontador de
tipo_produto, por isso o asterisco ao lado do tipo_produto. Esse vetor,
declarado a partir da variável tabela, guardará o endereço de cada produto
(codigo, descricao e preco) na memória. Por isso, é necessária uma rotina
que inicialize a tabela com todos os endereços valendo NULL.

No Algoritmo 26 é apresentada a rotina de inicialização do vetor.

Algoritmo 26 – Inicialização do vetor de ponteiros


1 void inicializar(){
2 int i=0;
3 for(i =0; i < 3; i++) tabela[i] = NULL;
4 }

Fonte: Elaborado pelo autor (2013).

Observe que o comando de laço for percorre cada posição do vetor e


inicializa a posição com NULL. Caso o vetor já tivesse algum ponteiro
(endereço de memória) nessas posições, eles seriam perdidos.

Para cada posição do vetor será armazenado um espaço de memória


calculado pela função sizeof(), que soma os espaços de memória para
armazenamento de cada tipo de dado de cada campo que compõe o tipo

www.esab.edu.br 88
estruturado (total = espaço para armazenamento do código + espaço
para armazenamento do nome + espaço para armazenamento do preco).
Para que esse espaço seja alocado, será utilizada a função malloc(). Esse
procedimento pode ser analisado no Algoritmo 27:

Algoritmo 27 – Cadastramento no vetor de ponteiros


1 void cadastrar(){
2 int i=0;
3 for(i=0; i < 3;i++){
4 if(tabela[i] == NULL) {
5 tabela[i] = (tipo_produto*) malloc(sizeof(tipo_
produto));
6 }
7 puts(“Informe o CODIGO:\n”);
8 scanf(“%d”,&tabela[i]->codigo);
9 getchar();
10 puts(“Informe a DESCRICAO:\n”);
11 gets(tabela[i]->descricao);
12 puts(“Informe o preco:\n”);
13 scanf(“%f”,&tabela[i]->preco);
14 system(“pause”);
15 return 0;
16 }
17 }

Fonte: Elaborado pelo autor (2013).

A função cadastrar percorre cada posição do vetor. Caso a posição não


tenha o endereço de um tipo_produto, ou seja, tabela[i]==NULL, é
alocado um espaço de memória para essa posição. O comando sizeof()
retornará o espaço necessário para armazenar um tipo_produto e o
comando malloc() fará a reserva do espaço. Como o vetor possui em cada
posição um ponteiro, o acesso aos campos se dá pela sintaxe tabela [i]-
>nome_campo.

Para liberar o espaço de memória alocado para o vetor, é necessária a


execução do comando free() para cada posição do vetor, liberando o
espaço previamente alocado pelo comando malloc(). O Algoritmo 28
apresenta essa rotina. Veja:

www.esab.edu.br 89
Algoritmo 28 – Liberação de memória no vetor de ponteiros
1 void desalocar(){
2 int i=0;
3 for(i=0; i < 3;i++){
4 free(tabela[i]);
5 }
6 }

Fonte: Elaborado pelo autor (2013).

O comando de laço percorre cada posição e executa o comando free()


para liberação de memória. Ao final do laço, o vetor não ocupará mais
espaço algum na memória principal do computador.

Você pode baixar o código com todas as rotinas de manipulação do vetor


de ponteiros no endereço: <http://pastebin.com/diUYps5i>.

Diferentemente de um vetor, que pode ter os valores manipulados, é


possível criar um conjunto de valores constantes em C. Esse processo é
chamado de tipo enumeração, o que estudaremos na sequência.

9.3 Tipo enumeração


Segundo Celes, Cerqueira e Rangel (2004, p. 112), “[...] um tipo
enumeração é um conjunto de constantes inteiras com nomes que
especifica os valores legais possíveis para uma variável daquele tipo”. É
uma forma mais elegante de organizar valores constantes.

A declaração do tipo enumeração em C utiliza a seguinte sintaxe:

enum nome_do_conjunto{
valor 1, valor 2, ..., valor N
};

www.esab.edu.br 90
O Algoritmo 29 apresenta um exemplo de utilização de enum, cuja
finalidade é criar um conjunto de países que podem ser escolhidos
pelo usuário. Para deixar o código mais claro, a comparação do
país selecionado pelo usuário utiliza o nome do país declarado na
enumeração, ao invés de um número, observe:

Algoritmo 29 – Definição de tipo enumeração


1 #include <stdio.h>
2 #include <stdlib.h>
3 enum enum_paises{
4 BRASIL, ARGENTINA, CHILE, URUGUAI, OUTROS
5 };
6 typedef enum enum_paises paises;
7 int main(){
8 paises destino;
9 puts(“Informe seu país de Destino”);
10 puts(“0-Brasil\n1-Argentina\n2-chile\n3-Uruguai\n4-
Outros”);
11 scanf(“%d”,&destino);
12 switch(destino){
13 case BRASIL:printf(“Bem vindo - Brasil”);break;
14 case ARGENTINA:printf(“Bem vindo - ARGENTINA”);break;
15 case CHILE:printf(“Bem vindo - CHILE”);break;
16 case URUGUAI:printf(“Bem vindo - URUGUAI”);break;
17 case OUTROS:printf(“Bem vindo - OUTROS”);
18 }
19 system(“pause”);
20 return 0;
21 }

Fonte: Elaborado pelo autor (2013).

Na linha 3 é criado um tipo enumeração chamado enum_paises, composto


pelos membros: BRASIL, ARGENTINA, CHILE, URUGUAI e
OUTROS. O tipo enumeração associa um valor inteiro para cada membro
começando em 0. Portanto: o item BRASIL vale 0, ARGENTINA vale
1 e assim sucessivamente até OUTROS, que valerá 4. Dessa forma, um
tipo enumeração pode ser comparado pelo valor do NOME ou pelo valor
associado. Na linha 8 é criada a variável destino do tipo países, que foi
declarado na linha 6. Com isso, para comparar se o valor da variável destino
é BRASIL, pode ser utilizado destino ==0 ou destino == BRASIL. Na linha
11 o algoritmo armazena, na variável destino, o valor informado pelo usuário
e no comando switch, nas linhas 12 a 18, é comparado qual foi esse valor.

www.esab.edu.br 91
Nesta unidade você pôde estudar como definir novos tipos de dados,
utilizando a cláusula typedef, que pode ser utilizada em variáveis simples
ou compostas, como no caso do vetor. Foi possível também estudar
como criar e manipular vetores de tipos de dados e ponteiros para tipos
de dados. Fechando a unidade, você estudou como definir e utilizar
o tipo enumeração, que permite a criação organizada de conjunto de
constantes.

Na próxima unidade, começaremos os estudos sobre as matrizes ou


vetores bidimensionais.

Atividade
Chegou a hora de você testar seus conhecimentos
em relação às unidades 1 a 9. Para isso, dirija-se
ao Ambiente Virtual de Aprendizagem (AVA) e
responda às questões. Além de revisar o conteúdo,
você estará se preparando para a prova. Bom
trabalho!

www.esab.edu.br 92
10 Matrizes – conceituação

Objetivo
Apresentar o conceito de matrizes.

Na unidade anterior você estudou, com conceitos e exemplos de


codificação em C, sobre os tipos estruturados, como criar novos tipos de
dados e estudou também o tipo enumeração.

Nesta unidade, começaremos nossos estudos sobre os vetores


bidimensionais (conhecidos como matrizes) e sobre alocação estática
versus alocação dinâmica.

O conteúdo desta unidade foi desenvolvido com base em Forbellone et


al. (2005) e Celes, Cerqueira e Rangel (2004).

A linguagem de programação C permite somente a alocação estática de


vetores bidimensionais, que reserva um espaço de memória necessário
para armazenar os elementos da matriz. Se uma matriz possui N linhas
por M colunas, o espaço armazenado corresponderá a N x M x o espaço
de memória que o tipo de dado da matriz ocupa. Mesmo que a matriz
não seja utilizada, esse espaço de memória é pré-alocado e liberado
somente quando o programa for encerrado.

Esse consumo de memória é uma preocupação constante dos


programadores no momento de definir uma matriz e escolher entre a
alocação estática ou dinâmica.

www.esab.edu.br 93
10.1 Alocação estática versus dinâmica
As matrizes estáticas possuem as mesmas limitações dos vetores
unidimensionais criados estaticamente, que é saber com antecedência
a sua quantidade exata de elementos, e uma vez definido o tamanho
do vetor ele não pode ser alterado, para mais ou menos, durante a
execução do programa. Se essa quantidade não é conhecida em tempo
de execução, é preciso utilizar a alocação dinâmica. Como na maioria
das vezes, durante a execução do programa não se conhece o número
de elementos dessas estruturas de dados, é fundamental o domínio da
estratégia da alocação dinâmica.

Mas antes disso, vamos avaliar alguns conceitos importantes com


relação à estrutura e às operações que podem ser executadas nos vetores
bidimensionais.

10.2 Vetores bidimensionais


Forbellone et al. (2005) destaca que os vetores têm como principal
característica a necessidade de apenas um índice para o endereçamento –
são estruturas unidimensionais. Já as matrizes, segundo Celes, Cerqueira
e Rangel (2004), são acessadas por indexação dupla, o primeiro índice
acessa a linha e o segundo índice acessa a coluna.

A Figura 24 ilustra uma matriz com quatro linhas e quatro colunas.


Observe:

0 1 2 3 Índice das colunas


0
1
2 Elemento da linha 2,
3 coluna 1 da matriz

Índice das linhas


Figura 24 – Matriz com quatro linhas e quatro colunas.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 94
Estudo complementar
Para saber mais sobre aplicação das matrizes,
acesse o link e veja alguns exemplo.

A matriz com quatro linhas e quatro colunas é indexada de 0 a 3 para


as linhas e colunas, ou seja, uma matriz com N linhas e M colunas será
indexada de 0 a N-1 linhas e de 0 a M-1 colunas. Cada elemento da
matriz é identificado pela referência do número da linha e número da
coluna para gravação ou leitura dos dados.

Os índices de uma matriz só podem ser números inteiros e os elementos


são todos do mesmo tipo, por isso as matrizes são conhecidas como
estruturas compostas homogêneas.

A matriz que possui o mesmo número de linhas e colunas é chamada de


matriz quadrada, como no caso da matriz da Figura 24.

Saiba mais
Para saber mais sobre as matrizes, você pode
acessar este endereço.

Toda matriz quadrada possui duas diagonais, a diagonal principal e a


diagonal secundária.

A Figura 25 apresenta a diagonal principal e a diagonal secundária de


uma matriz.
0 1 2 3
0
1
2
3
Diagonal Diagonal
secundária principal
Figura 25 – Diagonal principal e diagonal secundária de uma matriz.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 95
No caso da matriz da Figura 25, os elementos da diagonal principal são
indexados pelas linhas e colunas: [0][0]; [1][1]; [2][2] e [3][3]. Já os
elementos da diagonal secundária são indexados pelas linhas e colunas:
[0][3]; [1][2]; [2][1] e [3][0].

Para declarar matrizes bidimensionais, utiliza-se a seguinte sintaxe:

Tipo de dados nome_matriz [qde_linhas][qde_colunas];

Assim, a declaração de uma variável quadrada 2x2, ou seja, de ordem


dois (duas linhas por duas colunas de números inteiros), é declarada
assim: int matriz [2][2];

Agora que você já sabe como declarar a matriz, vamos ver como
preencher, listar e consultar dados nessa estrutura.

10.3 Função de inserção, listagem, consulta e


remoção em vetores bidimensionais
Para preencher as posições de uma matriz, é necessário percorrer todas
as linhas e colunas da estrutura de dados. E a forma mais simples de
fazer isso é preenchendo as colunas de cada linha antes de partir para a
próxima linha, conforme apresentado na Figura 26:

www.esab.edu.br 96
Passo 1 Passo 2
Linha 0: percorrer as colunas Linha 1: percorrer as colunas
0 1 2 3 0 1 2 3
0 0
1 1
2 2
3 3

Passo 3 Passo 4
Linha 2: percorrer as colunas Linha 3: percorrer as colunas
0 1 2 3 0 1 2 3
0 0
1 1
2 2
3 3

Figura 26 – Processo de preenchimento da matriz.


Fonte: Elaborada pelo autor (2013).

A cada processo, uma linha é preenchida, sendo que para cada linha
todas as colunas são “visitadas” para o preenchimento da matriz. Ao final
dos processos de cada linha, a matriz estará preenchida por completo.

O Algoritmo 30 apresenta a rotina de preenchimento de uma matriz de


ordem dois (duas linhas por duas colunas).

www.esab.edu.br 97
Algoritmo 30 – Preenchimento da matriz
1 #include <stdio.h>
2 #include <stdlib.h>
3 int matriz[2][2];
4 void preencherMatriz(){
5 system(“cls”);
6 int linha, coluna;
7 for(linha=0; linha<2; linha++){
8 for(coluna=0;coluna<2;coluna++){
9 printf(“Informe o valor para linha:%d e coluna:%d\
n”,linha,coluna);
10 scanf(“%d”,&matriz[linha][coluna]);
11 }
12 }
13 }
14 void main(){
15 preencherMatriz();
16 }

Fonte: Elaborado pelo autor (2013).

Na linha 3 do algoritmo é declarada a variável matriz com duas linhas e


duas colunas.

O comando system (“cls”) serve para limpar a tela do programa.

Foram declaradas, na linha 6, duas variáveis do tipo inteiro, chamadas


linha e coluna, para representar os índices da matriz.

O primeiro comando de laço for, na linha 7, será executado duas vezes


com a variável linha começando em 0 e terminando em 1 (linha < 2),
o que garante que todas as linhas da matriz serão visitadas. O segundo
comando de laço for, na linha 8, também será executado duas vezes com
a variável coluna começando em 0 e terminando em 1 (coluna < 2),
fazendo com que todas as colunas sejam visitadas.

Mas, como o comando de laço da linha 7 é executado primeiro, o


algoritmo fixará a linha para, posteriormente, começar o comando de
laço da linha 8, que será executado até o final; resultando no processo
de determinação da linha; e em seguida percorrerá todas as suas colunas
para então partir para a próxima linha.

www.esab.edu.br 98
A Figura 27 apresenta esse processamento. Observe:

Linha Coluna
0 0
1
1 0
1

Primeiro For Segundo For


(linha) (coluna)
Figura 27 – Execução dos comandos de laço.
Fonte: Elaborada pelo autor (2013).

Como o comando de laço da variável linha, representado pelo primeiro for,


na Figura 27, é executado e logo em seguida começa o comando de laço
da variável coluna, representado pelo segundo for na Figura 27, e como
um comando de laço precisa ser executado até o final para que o programa
tenha sequência no seu processamento, a variável linha começará valendo
0. Porém, terá que aguardar a execução do segundo laço, identificado como
coluna na figura, com os valores 0 e 1, até que o processamento retorne
para ele. Assim, para cada variável linha, representado por linha e primeiro
laço na Figura 27, há a repetição da variável coluna, representada por
coluna e segundo laço na Figura 27, gerando linha 0 com colunas 0 e 1,
depois linha 1 e, novamente, colunas 0 e 1.

Dessa forma, todas as posições da matriz serão visitadas.

Para listar os dados da matriz, será executada a mesma rotina, porém, em


vez de gravar os dados, eles serão listados na tela.

www.esab.edu.br 99
O Algoritmo 31 apresenta a rotina de listagem da matriz:

Algoritmo 31 – Listar dados da matriz


1 void listarMatriz(){
2 system(“cls”);
3 int linha, coluna;
4 for(linha=0; linha<2; linha++){
5 for(coluna=0;coluna<2;coluna++){
6 printf(“%d “,matriz[linha][coluna]);
7 }
8 printf(“\n”);
9 }
10 }

Fonte: Elaborado pelo autor (2013).

Observe que novamente são implementados dois laços, um para as


linhas, na linha 4 do algoritmo, e outro para as colunas, na linha 5 do
algoritmo.

Para consultar um determinado valor na matriz, é preciso percorrer todas


as posições e parar assim que o valor for encontrado ou até que toda a
matriz tenha sido visitada. A condição de parar assim que o valor seja
localizado diminui o tempo de processamento da pesquisa, evitando que
esse processo continue sem necessidade, pois já houve sucesso na busca
pelo valor.

www.esab.edu.br 100
O Algoritmo 32 apresenta a rotina de pesquisa na matriz:

Algoritmo 32 – Consultar na matriz


1 void consultar(){
2 system(“cls”);
3 int achou=0;
4 int valor;
5 printf(“Informe o valor para a PESQUISA:”);
6 scanf(“%d”,&valor);
7 int linha, coluna;
8 for(linha=0;linha<2; linha++){
9 for(coluna=0;coluna<2;coluna++){
10 if(matriz[linha][coluna] == valor){
11 achou = 1;
12 break;
13 }
14 }
15 }
16 if (achou) puts(“\nValor localizado na MATRIZ!”);
17 else puts(“\nValor INEXISTENTE na MATRIZ!”);
18 }

Fonte: Elaborado pelo autor (2013).

Foram criadas duas importantes variáveis na linha 3: a variável “achou”,


que começa valendo falso (0); e a variável “valor” que armazenará o valor
a ser pesquisado. Os dois laços para linha e coluna são executados da
mesma forma que nas rotinas de cadastramento e listagem. Porém, caso
o valor pesquisado seja localizado, o comando de laço é encerrado com
a instrução break, na linha 12. Antes disso, a variável “achou”, que valia
falso, é modificada para verdadeiro (1).

Ao sair dos comandos de laço, é verificado se a variável “achou” está


valendo verdadeiro ou falso para que seja mostrada a mensagem
correspondente ao processo de pesquisa.

Você pode baixar o código completo com as rotinas de gravação e leitura


da matriz, acessando o endereço: <http://pastebin.com/mzjvHDSK>.

www.esab.edu.br 101
Por fim, não é possível remover elementos de uma matriz, pois se trata
de uma alocação estática e assim como nos vetores unidimensionais, uma
vez alocado o espaço de memória, não há como liberá-lo em tempo de
execução.

Para que isso seja possível, teremos que criar matrizes dinamicamente
para poder alocar e liberar o espaço de memória em tempo de execução,
usando os comandos malloc() e free(). Mas isso é conteúdo para a
próxima unidade.

Nesta unidade você pôde estudar a estrutura do vetor bidimensional e


conhecer alguns conceitos importantes, como matriz quadrada, diagonal
principal e diagonal secundária. Além disso, analisou as funções para
preenchimento, listagem e consulta de dados nessa estrutura de dados.

Na próxima unidade, daremos sequência ao estudo das matrizes.

www.esab.edu.br 102
11 Matrizes – codificação

Objetivo
Apresentar a codificação e a manipulação de matrizes.

Na unidade anterior você começou os estudos sobre matriz, com a


conceituação de matriz e a avaliação das funções de inserção, listagem,
alteração e remoção de dados.

Nesta unidade você estudará como criar e manipular as matrizes de


forma dinâmica com gerenciamento do espaço de memória que é
alocado para essas estruturas de dados.

Os conceitos desta unidade foram desenvolvidos com base em Celes,


Cerqueira e Rangel (2004).

11.1 Matrizes dinâmicas


Quando declaramos uma matriz usando a sintaxe: tipo de dados nome_
da_matriz [qde_linhas] [qde_colunas], estamos declarando uma matriz
estática, ou seja, o espaço alocado para a estrutura de dados não mudará
durante a execução do programa. Celes, Cerqueira e Rangel (2004, p.
74) destacam que “[...] matrizes declaradas estaticamente possuem as
mesmas limitações dos vetores: precisamos saber de antemão as suas
dimensões”. Se as dimensões não são conhecidas em tempo de execução,
precisamos usar a alocação dinâmica.

Lembre-se de que as variáveis locais são criadas e removidas dinamicamente


durante a execução do programa. Criadas no momento em que a função
na qual foram declaradas é executada e removidas da memória no
momento que a função se encerra. Portanto, as matrizes declaradas de
forma local não necessitam da utilização da alocação dinâmica.

www.esab.edu.br 103
O problema encontra-se na utilização da linguagem C, que só permite a
alocação dinâmica para vetores unidimensionais.

11.2 Representação de matrizes


Bem, para resolver esse problema deveremos transformar uma matriz
bidimensional em um vetor unidimensional, assim poderemos aplicar
a alocação dinâmica. Para isso, vamos analisar a matriz apresentada na
Figura 28:

0 1 2
0
1
2

Figura 28 – Matriz bidimensional de ordem três.


Fonte: Elaborada pelo autor (2013).

A matriz possui três linhas e três colunas, ou seja, nove elementos


divididos em linhas e colunas.

Para compreender o funcionamento de uma matriz armazenada na forma


de um vetor unidimensional, em que apenas um índice é utilizado para
acessar a informação, e as operações de inserção e consulta aos dados,
vamos transformá-la em um vetor unidimensional, vamos criar um vetor
com nove elementos, conforme a Figura 29:

0 1 2 3 4 5 6 7 8

Figura 29 – Vetor unidimensional com nove elementos.


Fonte: Elaborada pelo autor (2013).

Para que o vetor unidimensional possa representar a matriz de ordem


três, vamos utilizar a seguinte regra: a cada três posições do vetor, teremos
uma linha da matriz, conforme a ilustração da Figura 30:

www.esab.edu.br 104
linha 0 linha 1 linha 2
0 1 2 3 4 5 6 7 8

Figura 30 – Representação das linhas da matriz no vetor.


Fonte: Elaborada pelo autor (2013).

Para garantir essa estrutura da matriz na forma de um vetor, é necessário


acessar o elemento da matriz. Para isso, utilize a posição do vetor que
será o número da linha desejada vezes a ordem da matriz + a posição da
coluna, ou seja:

posição = linha número de colunas + coluna

Por exemplo, para acessar o elemento da linha 1, da coluna 2 e da matriz,


a sua posição no vetor unidimensional será igual a cinco (linha 1 * ordem
3 + coluna 5).

A Figura 31 apresenta essa representação da regra de indexação da matriz


no vetor:
linha coluna
0 1 2
0 1x3+2=5
1
2 quantidade de
colunas da matriz

linha 0 linha 1 linha 2


0 1 2 3 4 5 6 7 8

Figura 31 – Cálculo da posição do vetor para indexação da matriz.


Fonte: Elaborada pelo autor (2013).

Com essa regra de cálculo, é possível localizar qualquer valor da matriz


no vetor.

www.esab.edu.br 105
O Algoritmo 33 apresenta a rotina para conversão da matriz em vetor:

Algoritmo 33 – Conversão da matriz em vetor


1 #include <stdio.h>
2 #include <stdlib.h>
3 int matriz[2][2];
4 int vetor[4];
5 void preencherMatriz(){
6 int i,j;
7 for(i=0; i < 2; i++){
8 for(j=0; j < 2; j++){
9 puts(“Informe um numero inteiro qualquer:”);
10 scanf(“%d”,&matriz[i][j]);
11 }
12 }
13 }
14 void gerarVetor(){
15 int i, j,p;
16 p=0;
17 for(i=0; i < 2; i++){
18 for(j=0; j < 2; j++){
19 vetor[p] = matriz[i][j];
20 p++;
21 }
22 }
23 }
24 void consultar(){
25 int posicao, linha, coluna;
26 puts(“Informe a linha da matriz:”);
27 scanf(“%d”,&linha);
28 puts(“Informe a coluna da matriz:”);
29 scanf(“%d”,&coluna);
30 posicao = linha * 2 + coluna;
31 printf(“O valor da matriz:%d”, vetor[posicao]);
32 }

Fonte: Elaborado pelo autor (2013).

Na linha 3 do algoritmo, foi declarada a matriz estática de ordem dois,


com duas linhas e duas colunas. Na linha 4, foi declarado o vetor com
quatro posições para armazenamento dos dados da matriz. A função
preencherMatriz(), da linha 5 à linha 13, faz a entrada de dados da
matriz com valores inteiros informados pelo usuário, percorrendo todas
as linhas e colunas da tabela.

www.esab.edu.br 106
A função gerarVetor(), que aparece das linhas 14 à 23, percorre todos os
elementos da matriz e copia os valores para o vetor unidimensional. Para
tanto foi criada a variável p, na linha 15, que representa cada posição do
vetor, começando em 0 e a qual é incrementada a cada inserção do valor
da matriz no vetor.

A função consultar, nas linhas 24 a 32, solicita para o usuário a linha


e a coluna da matriz que devem ser pesquisadas, porém os valores são
pesquisados no vetor por meio do cálculo da posição codificado na linha
30. Isto é, posição = linha * 2 + coluna.

A regra de conversão e armazenamento de uma matriz na forma de um


vetor unidimensional pode ser utilizada para qualquer tipo de matriz,
seja ela simétrica ou não. Para exemplificar, vamos transformar um
vetor bidimensional em um vetor unidimensional e implementaremos
as rotinas para identificar se o vetor bidimensional é simétrico ou não.
Também vamos estudar o que é uma matriz simétrica.

11.3 Representação de matrizes simétricas


Para a transformação de uma matriz em vetor, assim como para a criação
de qualquer tipo de estrutura de dados, o programador deve ter cuidado
com o desperdício de memória.

Um exemplo desse cuidado é o armazenamento de uma matriz simétrica


na forma de um vetor.

Antes de tratarmos disso, precisamos entender o que é uma matriz


simétrica. Para isso, vamos analisar a Figura 32:

Matriz Matriz Matriz


3 2 4 3 2 4 3 2 4 3 2 4
2 5 8 2 5 8 2 5 8
4 8 6 4 8 6 4 8 6

Figura 32 – Matriz simétrica.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 107
Observe que se girarmos a matriz de forma que os elementos da coluna
sejam apresentados nas linhas, a matriz ficará exatamente igual. Isso
se dá porque os elementos acima da diagonal principal são os mesmos
elementos abaixo da diagonal principal, conforme pode ser observado na
Figura 33:

Matriz
3 2 4
2 5 8
4 8 6
Diagonal
principal
Figura 33 – Matriz simétrica – diagonal principal.
Fonte: Elaborada pelo autor (2013).

Dessa forma, para armazenar uma matriz simétrica em um vetor


unidimensional não há necessidade de armazenar todos os dados, pois os
elementos acima e abaixo da diagonal principal são os mesmos, bastando
então que os elementos da diagonal principal e os elementos acima ou
abaixo sejam transferidos para o vetor. Veja a Figura 34, que mostra os
elementos da matriz simétrica armazenados no vetor unidimensional sem
a repetição dos valores acima da diagonal principal.

Matriz
3 2 4
2 5 8 3 5 6 2 4 8
4 8 6
Diagonal
principal
Figura 34 – Matriz simétrica – conversão da matriz para vetor unidimensional.
Fonte: Elaborada pelo autor (2013).

O Algoritmo 34 apresenta a codificação para transformação da matriz


simétrica em vetor unidimensional e como realizar a pesquisa no vetor
usando a linha e coluna da matriz:

www.esab.edu.br 108
Algoritmo 34 – Conversão da matriz simétrica em vetor
1 void gerarVetor(){
2 int i, j,p;
3 p=0;
4 for(i=0; i < 3; i++){
5 for(j=0; j < 3; j++){
6 if ( i>=j){
7 vetor[p] = matriz[i][j];
8 p++;
9 }
10 }
11 }
12 }
13 void consultar(){
14 int posicao, linha, coluna;
15 puts(“Informe a linha da matriz:”);
16 scanf(“%d”,&linha);
17 puts(“Informe a coluna da matriz:”);
18 scanf(“%d”,&coluna);
19 if(linha >= coluna) posicao = linha * (linha+1)/2+coluna;
20 else posicao = coluna * (coluna+1)/2+linha;
21 printf(“O valor da matriz:%d”, vetor[posicao]);
22 }

Fonte: Elaborado pelo autor (2013).

A função gerarVetor() percorrerá todos os elementos da matriz copiando


os elementos da diagonal principal (i==j) e os elementos abaixo da
diagonal principal (i > j). Outra forma de fazer isso seria copiar os
elementos da diagonal principal para o vetor e os elementos acima da
diagonal, ou seja, aqueles nos quais a condição (i<j) é atendida.

Para buscar os elementos abaixo da diagonal principal, é preciso


desconsiderar os elementos acima da diagonal. Assim para acessar um
elemento da terceira linha, é preciso desconsiderar dois elementos da
primeira linha, um elemento da segunda linha e assim por diante, até
chegar à linha desejada. Por isso, a regra de acesso aos elementos abaixo
da diagonal principal é: , conforme
apresentado na linha 19 do algoritmo. Ou seja, para acessar o elemento
da terceira linha (índice 2) e segunda coluna (índice 1), a posição do
vetor será 4, resultado de . Para acessar os elementos acima

da diagonal, temos: .

www.esab.edu.br 109
Nesta unidade você pôde estudar que na linguagem C não é possível
fazer alocação dinâmica de matrizes ou vetores bidimensionais. Mas,
por outro lado, há como fazer isso com vetores unidimensionais, para
tanto é preciso converter uma matriz bidimensional em um vetor
unidimensional para representá-la. Tão importante quanto converter
uma matriz em vetor unidimensional, é criar regras de acesso ao vetor a
partir dos índices da matriz – sendo que a conversão para o vetor deve
analisar o tipo de dados que está manipulando para que, se possível,
nele se ocupe menos espaço de armazenamento que se ocuparia na
matriz. Esse é o caso das matrizes simétricas, as quais possuem elementos
repetidos acima e abaixo da diagonal principal, o que torna desnecessário
o armazenamento de todos os elementos da matriz no vetor.

Na próxima unidade, vamos desenvolver exercícios de conversão da


matriz para o vetor utilizando a alocação dinâmica.

www.esab.edu.br 110
12 Matrizes – exercícios
Objetivo
Apresentar exercícios comentados sobre matrizes, com a diagonal
principal e diagonal secundária.

Na unidade anterior você estudou sobre a conversão de matrizes em vetor


unidimensional para que a estrutura possa ser alocada de forma dinâmica.

Nesta unidade, você poderá praticar os conceitos e algoritmos


apresentados nas unidades anteriores com a finalidade de armazenar uma
matriz em um vetor unidimensional, utilizando a estratégia de alocação
dinâmica. Para tanto, os seguintes passos foram definidos:

• criar uma variável ponteiro que armazene o endereço de memória do


bloco que representa os elementos da matriz;
• criar uma função que calcule o espaço de memória, o que será
necessário para o armazenamento da matriz na forma de um vetor
unidimensional;
• criar uma função que solicite ao usuário os elementos da matriz e
faça o armazenamento no vetor dinâmico;
• criar uma função que acesse os dados do vetor utilizando o conceito
de linha e coluna da matriz;
• criar uma função que liste os valores do vetor unidimensional na tela
com o formato de linhas e colunas, semelhante a uma matriz;
• criar uma função que liste os elementos da diagonal principal e os
elementos da diagonal secundária da matriz alocada em um vetor
unidimensional;
• criar uma função que verifique se a matriz é simétrica, ou seja, os
elementos da linha da matriz são iguais aos elementos das coluna da
matriz.

www.esab.edu.br 111
A seguir, são apresentados cada um desses passos na forma de funções
implementadas na linguagem C.

12.1 Exercícios comentados


Vamos começar declarando uma variável do tipo ponteiro para um
inteiro, a fim de armazenar o endereço de memória de todo o vetor que
representará a matriz que será manipulada.

O Algoritmo 35 apresenta a declaração desse ponteiro:

Algoritmo 35 – Declaração de ponteiro para o vetor


1 int* ponteiro_vetor;

Fonte: Elaborado pelo autor (2013).

Essa variável, ponteiro_vetor, armazenará o endereço de memória do


bloco de memória que representará um vetor de inteiros. De início, a
variável estará valendo NULL até que um bloco de memória seja alocado
e atribuído a essa variável. Para alocação de memória do vetor, será
necessário conhecer o número de linhas e colunas da matriz que será
convertida para um vetor unidimensional.

O Algoritmo 36 apresenta a função para cálculo do espaço de memória


para armazenamento do vetor.

Algoritmo 36 – Alocação dinâmica do vetor.


1 int* gerarVetor(int numtermos){
2 return (int*) malloc(numtermos*sizeof(int));
3 }

Fonte: Elaborado pelo autor (2013).

A função gerar vetor recebe como parâmetro a quantidade de elementos


a serem alocados. Como é um vetor de inteiros, cada posição do vetor
ocupará o espaço alocado para esse tipo na linguagem C, por isso é
utilizada a função sizeof(int). Dessa forma, o bloco todo de memória será o
espaço ocupado pelo tipo inteiro vezes o número de elementos do vetor. A
função retornará o endereço de memória resultante da função malloc().

www.esab.edu.br 112
Com o vetor alocado na memória, o próximo passo é cadastrar os valores
da matriz na forma de um vetor com os dados informados pelo usuário
do sistema.

O Algoritmo 37 apresenta a codificação da função para inserção dos


dados no vetor.

Algoritmo 37 – Cadastramento no vetor de inteiros


1 void cadastrar(int numtermos,int numcolunas){
2 int p=0,i=0,j=0;
3 for(p=0; p < numtermos;p++){
4 printf(“Informe um Valor para Cadastro Na Matriz na
linha:%d coluna%d\n”,i,j);
5 scanf(“%d”,&ponteiro_vetor[p]);
6 j++;
7 if(j==numcolunas){
8 i++;
9 j=0;
10 }
11 }
12 }

Fonte: Elaborado pelo autor (2013).

A função cadastrar recebe como parâmetro o número de termos e o total


de colunas da matriz. A variável p controla a posição de armazenamento
no vetor, a variável i representa a linha da matriz e a variável j a coluna
da matriz. No comando de laço da linha 3 do algoritmo, são preenchidas
todas as posições do vetor com o incremento do número de colunas
(variável j). Quando a variável j é igual ao número de colunas da matriz,
significa que todas as colunas da linha foram preenchidas, então a
variável i (que representa as linhas) é incrementada e a variável j (que
representa as colunas) volta a valer zero. Dessa forma, a mensagem
apresentada ao usuário será algo semelhante ao mostrado na Figura 35:

Figura 35 – Entrada de dados para o vetor.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 113
Estando a matriz preenchida, é possível listar todos os dados da matriz
que foram armazenados no vetor unidimensional.

O Algoritmo 38 apresenta a rotina para listagem dos dados da matriz:

Algoritmo 38 – Lista dos dados da matriz


1 void listar(int numtermos,int numcolunas){
2 puts(“\nElementos da MATRIZ\n”);
3 int p=0;
4 for(p=0; p < numtermos;p++){
5 printf(“%d “,ponteiro_vetor[p]);
6 if((p+1)%numcolunas==0) printf(“\n”);
7 }
8 }

Fonte: Elaborado pelo autor (2013).

A função listar recebe como parâmetro o número de elementos da


matriz e o seu total de colunas. O comando de laço for percorre todas as
posições do vetor e lista o valor cadastrado na tela. Caso a posição atual
do vetor + 1 dividida pelo número de colunas for zero, então a linha
toda foi mostrada e o algoritmo pula a linha para a listagem da próxima
linha. Por exemplo, uma matriz com três colunas terá como resultado
p+1
do cálculo (p+1)%3 (o resto da divisão ) os seguintes resultados:
3
0%3=0, 1%3=1, 2%3=2,3%3=0. Ou seja, a cada múltiplo do número de
colunas, a linha será “pulada”.

A Figura 36 apresenta como será a listagem dos dados da matriz.

Figura 36 – Listagem de dados da matriz.


Fonte: Elaborada pelo autor (2013).

Com os dados da matriz cadastrados, além de listar todos os dados,


podemos listar os elementos da diagonal principal ou da diagonal
secundária.

www.esab.edu.br 114
A Figura 37 apresenta os elementos da diagonal principal e diagonal
secundária.

0 1 2 0 1 2
0 0
1 1
2 2
Diagonal Diagonal
Principal Secundária
Figura 37 – Diagonal principal e diagonal secundária.
Fonte: Elaborada pelo autor (2013).

Observe que os elementos da diagonal principal possuem o mesmo


valor de linha e coluna: [0,0], [1,1] e [2,2]. Já os elementos da diagonal
secundária são aqueles em que a posição da coluna é igual ao número
de colunas da matriz menos a linha visitada menos um. O Quadro 5
apresenta essa regra para uma matriz com três linhas e três colunas:

Linha Regra Coluna


0 Coluna = 3-linha-1 3-0-1= 2
1 Coluna =3-linha-1 3-1-1= 1
2 Coluna =3-linha-1 3-2-1= 0

Quadro 5 – Teste de mesa: diagonal secundária.


Fonte: Elaborado pelo autor (2013).

Agora que sabemos a regra de acesso para a diagonal principal e


secundária, é preciso relembrar que para acessar um elemento da matriz
no vetor unidimensional, conforme foi visto na unidade 11, temos:
posição = linha × número de colunas + coluna.

O Algoritmo 39 apresenta as rotinas para listagem da diagonal principal


e diagonal secundária:

www.esab.edu.br 115
Algoritmo 39 – Listagem dos dados da diagonal principal e diagonal secundária
1 int acessar(int linha,int coluna,int numcolunas){
2 return ponteiro_vetor[linha * numcolunas+coluna];
3 }
4 void listarDiagonalPrincipal(int numlinhas,int numcolunas){
5 puts(“\nElementos da DIAGONAL PRINCIPAL da MATRIZ\n”);
6 if(numlinhas != numcolunas){
7 puts(“\n MATRIZ DEVE SER QUADRADA!\n”);
8 }
9 else{
10 int i=0;
11 for(i=0; i < numcolunas;i++){
12 printf(“%d “,acessar(i,i,numcolunas));
13 }
14 }
15 }
16 void listarDiagonalSecundaria(int numlinhas,int numcolunas){
17 puts(“\nElementos da DIAGONAL SECUNDARIA da MATRIZ\n”);
18 if(numlinhas != numcolunas){
19 puts(“\n MATRIZ DEVE SER QUADRADA!\n”);
20 }
21 else{
22 int i=0;
23 for(i=0; i < numcolunas;i++){
24 printf(“%d “,acessar(i,numcolunas-i-1,numcolunas));
25 }
26 }
27 }

Fonte: Elaborado pelo autor (2013).

A função acessar recebe como parâmetro a linha e coluna que se deseja


acessar na matriz, bem como seu número de colunas. A função retorna
o valor inteiro armazenado no vetor que representa os índices de linha e
coluna requisitados por meio do cálculo: linha × núm. colunas + coluna.

A função listarDiagonalPrincipal recebe como parâmetro o número de


linhas e colunas da matriz, percorre e lista os elementos da diagonal
principal, na qual a linha e coluna possuem o mesmo valor. Por isso,
apenas um comando de laço resolve o problema. Supondo uma matriz
de ordem três (três linhas e três colunas), percorrendo apenas as linhas
(0 a 2) e usando esse índice como linha e coluna, teremos as posições:

www.esab.edu.br 116
[0,0], [1,1] e [2,2]. Note que as posições são geradas e passadas como
parâmetro para a função acessar(), que retorna o número inteiro dessa
posição (linha, coluna).

Por fim, a função listarDiagonalSecundária é muito semelhante à função


da diagonal principal. Nesse caso, altera-se apenas a regra de definição
dos índices de linha e coluna que serão visitados e calcula-se a coluna a
partir da regra: numcolunas-i-1, na qual i é o número da linha.

Assim como é possível listar os elementos da matriz e das diagonais, é


possível verificar se uma matriz é simétrica. Esse conceito foi apresentado
na unidade 11, mas para relembrá-lo é importante mencionar que as
matrizes simétricas possuem os elementos abaixo da diagonal principal
iguais aos elementos acima da diagonal principal.

O Algoritmo 40 apresenta a rotina para verificação se a matriz é simétrica:

Algoritmo 40 – Verificação da matriz simétrica


1 void verificarSimetria(int numlinhas,int numcolunas){
2 int simetrica=1;
3 int i,j;
4 for(i=0; i < numlinhas;i++){
5 for(j=0; j < numcolunas;j++){
6 if(acessar(i,j,numcolunas) != acessar(j,i,numcolunas))
{
7 simetrica = 0;
8 break;
9 }
10 }
11 }
12 if(simetrica){
13 puts(“\n MATRIZ SIMETRICA \n”);
14 }else{
15 puts(“\n MATRIZ NAO SIMETRICA \n”);
16 }
17 }

Fonte: Elaborado pelo autor (2013).

Para verificar se a matriz é simétrica, são percorridos todos os elementos


da matriz, verificando se o elemento indexado por linha e coluna é igual
ao elemento da posição coluna e linha. Caso esses elementos não sejam

www.esab.edu.br 117
iguais, os laços são encerrados, pois a matriz não é simétrica. Dessa
forma, na linha 6, é verificado se o elemento da posição (i, j) é diferente
da posição (j,i). Caso seja verdadeira essa condição, a variável simétrica
recebe falso (0) e o laço é encerrado. A variável simétrica começou
valendo verdadeiro (1).

Caro aluno, baixe o código completo do algoritmo neste endereço e execute-o no


compilador C de sua preferência.

Nesta unidade, você pôde acompanhar como desenvolver em C rotinas


para criação e manipulação de uma matriz por meio de um vetor
unidimensional de ponteiros. As rotinas codificadas foram divididas em
gerarVetor(), que realiza a alocação dinâmica do vetor acessar(), que, por
sua vez, retorna o elemento do vetor representado pelas posições da matriz
usando a regra na qual a posição no vetor é igual ao índice da linha vezes
o número de colunas da matriz mais o índice da coluna a ser acessada.

Para cadastro dos dados no vetor, foi desenvolvida a função cadastrar()


e para listagem de todos os dados cadastrados, desenvolveu-se a função
listar().

O algoritmo permite também listar() os elementos da diagonal principal


– aqueles em que o índice da linha é igual ao índice da coluna, por
exemplo, os elementos (0,0), (1,1) e (2,2) – e da diagonal secundária
– aqueles em que o índice da coluna é o valor resultante da seguinte
operação: número de colunas da matriz menos a linha acessada menos 1,
por exemplo, (2,0), (1,1) e (0,2).

Por fim, a função verificarSimetria() analisa se a matriz é simétrica, ou


seja, as matrizes em que o elemento da posição “linha,coluna” é igual ao
elemento da posição “coluna,linha”.

Na próxima unidade, começaremos os estudos sobre os Tipos Abstratos


de Dados, também conhecidos como TAD.

www.esab.edu.br 118
Resumo

Na unidade 7 você estudou sobre as duas formas de passagem de


parâmetros, que podem ser por valor ou por referência. Na passagem
de parâmetros por valor, os argumentos passados para a função não
são alterados em caso de modificação dos parâmetros pela função. Já
na passagem de parâmetro por referência, os parâmetros recebem os
endereços de memória dos argumentos, o que faz com que as alterações
nos parâmetros reflitam nos argumentos. Ou seja, o resultado final do
parâmetro é compartilhado com o argumento.

Na unidade 8 o estudo se focou no tipo estruturado, também conhecido


por registro, que é um tipo de dados formado por outros tipos de dados.

Em C, os tipos estruturados são criados por meio da cláusula struct.

Para indicar o campo que se deseja manipular do tipo estruturado, a


sintaxe é “nome da variável”.”nome do campo”. Os tipos estruturados
podem ser manipulados por meio de ponteiros, o que possibilita a
alocação dinâmica de memória, e para referência do campo do tipo
estruturado que se deseja manipular, a sintaxe muda para “nome da
variável”->”nome do campo”.

Na unidade 9, você estudou como criar novos tipos de dados por meio
da cláusula typedef e como criar vetores de ponteiros para que essa
estrutura de dados utilize exatamente o espaço de memória necessário.
Nessa unidade você ainda pôde conhecer o tipo enumeração, que
permite o uso de termos para representar valores numéricos.

A unidade 10 apresentou os conceitos iniciais de matrizes com


apresentação das rotinas de inserção, listagem, consulta e remoção de
dados. Ainda nessa unidade, foram apresentados os conceitos de diagonal
principal e secundária.

www.esab.edu.br 119
Na unidade 11, você pôde analisar como converter matrizes para vetores
unidimensionais, já que na linguagem de programação C não é possível a
alocação dinâmica de vetores bidimensionais.

Por fim, na unidade 12, você acompanhou como construir códigos


em C para a manipulação de matriz, convertendo-a para um vetor
unidimensional.

www.esab.edu.br 120
13 Tipos abstratos de dados – parte I
Objetivo
Apresentar a importância da técnica de programação baseada na
definição de Tipos Abstratos de Dados (TAD).

Na unidade anterior você pôde desenvolver um programa em C


para manipulação de matrizes ou vetores bidimensionais alocados
dinamicamente por meio de um vetor unidimensional, pois em C
não é possível alocar dinamicamente vetores bidimensionais. Celes,
Cerqueira e Rangel (2004, p. 74) destacam que “[...] para trabalhar com
matrizes alocadas dinamicamente, temos de criar abstrações conceituais
com vetores para representar conjuntos bidimensionais”. O programa
permitiu um conjunto de operações com a matriz, como: gerar o vetor
dinamicamente, listar os dados da matriz, acessar uma posição da matriz
no vetor unidimensional e verificar se a matriz é simétrica. Note que o
programa utilizou duas estruturas de dados específicas, a matriz e o vetor
dinâmico e um conjunto de funções com rotinas específicas. Caso seja
necessário utilizar novamente essa matriz, o vetor e as funcionalidades
que o manipulam, todos os códigos terão que ser reescritos.

Será que existe uma técnica para que essas estruturas e suas operações
sejam reutilizadas sem a necessidade de novas implementações?

Uma aplicação computacional pode ser entendida como um programa


de computador que manipula dados, e a representação desses dados
manipulados podem ser representados por diferentes estruturas de
dados. A maioria das linguagens de programação disponibiliza um
conjunto de estruturas de dados predefinidas, como: variáveis, vetores
unidimensionais, vetores bidimensionais e tipos estruturados. A
identificação de qual estrutura de dados deve ser utilizada depende do
tipo de problema a ser solucionado.

www.esab.edu.br 121
Por exemplo, os tipos de dados básicos, que conhecemos também como
tipos primitivos, não têm uma estrutura sobre seus valores. Isso significa
que não podemos decompor um tipo primitivo em outras partes. A
maioria das linguagens de programação possui o tipo de dados lógico,
que pode assumir os valores true (verdadeiro) e false (falso). No caso da
linguagem C, não existe o tipo de dados lógico, ele é representado pelo
tipo inteiro (int) que pode assumir dois valores: 0 para falso e 1 para
verdadeiro. Tanto o tipo lógico das outras linguagens de programação
quanto o tipo inteiro da Linguagem C não podem ser decompostos em
outros tipos de dados. Já os tipos estruturados permitem a agregação
de mais de um valor à variável, podendo ter todos os valores do mesmo
tipo, no caso de vetores unidimensionais e bidimensionais ou de tipos
diferentes, como nos registros ou faixas de valores das enumerações.

Porém, todas as estruturas de dados disponibilizadas pelas linguagens de


programação não são capazes de representar os tipos de dados necessários
para a resolução de todos os problemas que estão relacionados com o
nosso dia a dia. Assim, surge a necessidade de novas estruturas de dados
especificadas pelo programador de forma a atender as necessidades
do usuário do programa. A partir de uma relação entre os dados e a
necessidade do problema a ser solucionado, a estrutura de dados deve
ser composta por tipos de dados primitivos e estruturados. Esses tipos
definidos pelo programador contêm, além de dados, operações que
os manipulam. Esse tipo é chamado de Tipo Abstrato de Dados, ou,
simplesmente, TAD. Com o auxílio teórico de Ascencio e Araújo (2010)
e Celes, Cerqueira e Rangel Netto (2004), nesta unidade veremos a
definição de TAD.

13.1 Definição
Os Tipos Abstratos de Dados são estruturas de dados que representam
dados que não foram previstos pela linguagem de programação e
são divididos em duas partes, os dados e as operações. As definições
de um TAD, o tipo de dado que será manipulado, as regras de
funcionamento e utilização, correspondem à escolha adequada da forma
de armazenamento dos dados e à definição das operações que podem
ser efetuadas sobre eles. Celes, Cerqueira e Rangel Netto (2004, p. 126)
destacam que “[...] o conceito de abstrato no TAD se refere à forma pela

www.esab.edu.br 122
qual o tipo foi implementado, isto deve ser esquecido, já que um TAD é
descrito pela finalidade do tipo e de suas operações, e não pela forma que
foi implementado”.

Por exemplo, se uma aplicação necessita de uma estrutura de dados que


represente uma conta bancária constituída pelo número da conta e seu
saldo é necessário não só a implementação desse tipo de dados (Conta
Bancária), o qual não existe em nenhuma linguagem de programação,
mas também a codificação das operações que podem ser realizadas
com esse dado, como a abertura da conta, com a definição do número
da conta e seu saldo inicial, depósito ou débito na conta e, por fim, a
impressão do saldo atual.

A Figura 38 apresenta a interface do TAD com as operações e o tipo de


dados ContaBancaria, observe:

Tipo Abstrato de Dados - TAD


Tipo Estruturado Tipo Estruturado
struct conta{ Criação de um Novo Tipo void abrirConta(ContaBancaria*, int, double);
int numero; void depositar(ContaBancaria*, double);
double saldo; + typedef struct conta ContaBancaria + void sacar(ContaBancaria*, double);
); void extratoSaldo(ContaBancaria*);

ContaBancaria conta;
int numero = 1234;
double saldoinicial = 100.00;
Como usar o TAD double valor = 800.00;
abrirConta(conta,saldoinicial);
depositar(conta, valor);
extratoSaldo(conta);
Figura 38 – Tipo Abstrato de Dados: ContaBancaria.
Fonte: Elaborada pelo autor (2013).

Observe que o Tipo Abstrato de Dados ContaBancaria é composto pelo


registro conta, o qual, por sua vez, é formado pelo número da conta e
o saldo. O TAD ContaBancaria também é constituído pelas operações
abrirConta, depositar, sacar e extratoSaldo. No programa que utiliza
o TAD, foram declaradas a variável conta, do tipo ContaBancaria, as
funções abrirConta, depositar e extratoSaldo, as quais realizam a abertura

www.esab.edu.br 123
da conta com o número identificador 1, o saldo inicial de 100.00, um
depósito de 800.00 e a impressão do saldo atual. Porém, o programador
não precisa saber como essas funções foram implementadas, é preciso
apenas saber como utilizá-las, quais os parâmetros de entrada, a
ordem dos parâmetros, seus respectivos tipos e o tipo de retorno da
função; o restante – como a forma pela qual os dados são atribuídos
para cada campo do registro que representa conta bancária, como um
valor é adicionado ao saldo quando a operação é de depósito ou como
é implementada a redução do saldo quando a operação é de saque
– é abstraído. Essas informações não precisam ser conhecidas pelo
programador que utiliza o Tipo Abstrato ContaBancaria.

Assim, a representação de um Tipo Abstrato de Dados, chamada


de interface TAD, é composta pela definição da estrutura de dados,
juntamente da especificação das operações aplicáveis sobre eles. A
utilização do tipo fica limitada ao acesso às operações, ocultando-se
como foram implementadas. O usuário do TAD recebe a especificação
dos dados e do conjunto de operações, mas a implementação permanece
invisível e inacessível. O usuário tem acesso somente às funcionalidades
oferecidas pelo tipo.

As principais vantagens no uso de TAD são:

• a utilização em diversas aplicações;


• a independência da aplicação em relação ao TAD, já que alterações
no TAD não refletem em alterações no programa que o utiliza. Por
exemplo, na operação que imprime o extrato da conta bancária, caso
seja necessário imprimir a data atual junto ao saldo, apenas o TAD
terá seu código alterado, o programa que utiliza essa operação não
precisará ser alterado.
Para tanto, as operações de um TAD devem se avaliadas com base no
problema que se deseja resolver e:

• deve conter um número pequeno de operações que em conjunto


possam resolver problemas mais complexos;
• cada operação deve ter uma finalidade clara e específica.

www.esab.edu.br 124
Para Ascencio e Araújo (2010, p. 469), os “[...] TADs são formados por
componentes que são as funções, com objetivos específicos”.

Os TAD são normalmente implementados por meio do conceito de


bibliotecas, em que os tipos de dados e operações são implementados em
arquivos separados. No caso da linguagem C, essa técnica é conhecida
como programação em módulos e compilação em separado.

13.2 Módulos e compilação em separado


Um programa em C pode ser dividido em vários arquivos-fonte,
organizando-os de forma que as funções em comum sejam agrupadas
no mesmo arquivo. Mais que organizar, a divisão em arquivos, a partir
do objetivo de cada rotina implementada, facilita a manutenção dos
sistemas, por exemplo, separando em arquivos-fonte a codificação das
funções que fazem a conexão e operações com sistemas gerenciadores
de banco de dados das funções que desenham as telas do programa ou
executam tarefas específicas da aplicação. Como cada arquivo-fonte
possui uma finalidade eles são nomeados de forma a facilitar o uso e
identificação, como por exemplo, telacadastrocliente, telarelatoriocliente,
o que permite deduzir qual o objetivo do arquivo e, logo, não é
necessário abrir cada arquivo para localizar onde está a codificação que
precisa ser melhorada ou corrigida, facilitando a manutenção do sistema.

Na linguagem C, utilizando funções específicas, alguns arquivos-fonte


podem ser implementados com apenas uma parte do programa e, assim,
podem ser utilizados por outros programas. Esses arquivos são chamados
de módulos e, portanto, um programa em C pode ser composto por
um ou vários módulos. Celes, Cerqueira e Rangel Netto (2004, p. 124),
destacam que “[...] o uso de vários módulos para pequenos programas
não se justifica, já que o uso de módulos visa dividir grandes problemas
em pequenos problemas, facilitando assim a implementação da solução”.
Ainda segundo Celes, Cerqueira e Rangel Netto (2004, p. 126), “[...] um
módulo agrupa vários tipos e funções com funcionalidades relacionadas,
caracterizando assim uma finalidade bem definida”.

www.esab.edu.br 125
Então, podemos entender que um TAD em C é desenvolvido na forma de
módulos, os quais são arquivos-fonte separados e carregados nos programas
que desejam utilizá-los. E mais, um TAD é composto pelos tipos de dados
e operações relacionadas e específicas com uma finalidade definida.

Nesta unidade estudamos o conceito de Tipo Abstrato de Dados, uma


técnica muito importante na definição de novos tipos estruturados
de dados com foco na resolução de problemas específicos. Um Tipo
Abstrato de Dados, ou TAD, é constituído por um ou vários dados e
operações que possibilitam a sua manipulação. Em C, a implementação
dos TADs se dá por meio da implementação de módulos, que são
arquivos-fonte com uma parte do programa.

Bem, na próxima unidade daremos sequência aos estudos sobre Tipos


Abstratos de Dados, abordando como implementar os módulos na
linguagem de programação C, como transformá-los em TAD e como
utilizá-los, pois é uma técnica de programação muito importante.

www.esab.edu.br 126
14 Tipos abstratos de dados – parte II
Objetivo
Apresentar a importância da técnica de programação baseada na
definição de Tipos Abstratos de Dados (TADs).

Na unidade anterior você estudou e analisou o Tipo Abstrato de Dados


e a técnica para definição de novos tipos estruturados de dados com
foco na resolução de problemas. Pôde observar que um Tipo Abstrato de
Dados, ou TAD, é constituído por um ou vários dados e operações que
possibilitam a sua manipulação.

Nesta unidade, com o apoio teórico de Forbellone et al. (2005) e Celes,


Cerqueira e Rangel Netto (2004), vamos verificar como codificar um
Tipo Abstrato de Dados em linguagem C por meio da implementação de
módulos, que são arquivos-fonte com uma parte específica do programa.

Forbellone et al. (2005, p. 154) destacam que a “[...] parametrização


de módulos possibilita uma maior generalização e, consequentemente,
um maior reaproveitamento dos módulos em um maior número de
situações diferentes”.

Como prática, vamos desenvolver o Tipo Abstrato de Dados


ContaBancaria, apresentado na unidade anterior, com as operações para
abertura da conta, depósito, saque e impressão do extrato.

14.1 Tipo Abstrato de Dados


Para definição do Tipo Abstrato de Dados em Linguagem C, o primeiro
passo é a descrição abstrata dos dados e operações que serão possíveis
sobre esses dados. Como a conta bancária é formada por dois campos,
número da conta e saldo, vamos utilizar o tipo estruturado (struct) para
declaração dessa estrutura de dados. Observe o Algoritmo 41:

www.esab.edu.br 127
Algoritmo 41 – Definição do registro para Conta Bancária
01 struct registro{
02 int numero;
03 double saldo;
04 };

Fonte: Elaborado pelo autor (2013).

O tipo estruturado registro é constituído de dois campos: número, que


armazena valores inteiros e saldo, que armazena valores double (real).

Para facilitar a utilização desse registro na definição de variáveis, vamos


torná-lo um tipo de dado por meio da cláusula typedef. A instrução em
C para isso é:

typedef struct registro ContaBancaria;

A partir dessa instrução, o novo tipo de dados chamado ContaBancaria


já pode ser utilizado. O próximo passo é declarar as operações que
podem ser realizadas com o tipo ContaBancaria. Para tanto, apenas
serão declarados os cabeçalhos das funções com os tipos de dados dos
parâmetros. O Algoritmo 42 apresenta essas instruções:

Algoritmo 42 – Declaração das operações do TAD ContaBancaria


01 void abrirConta(ContaBancaria*,int,double);
02 void depositar(ContaBancaria*,double);
03 void sacar(ContaBancaria*,double);
04 void extrato(ContaBancaria*);

Fonte: Elaborado pelo autor (2013).

Observe que as funções tiveram apenas os cabeçalhos e os tipos de cada


parâmetro declarados, e o parâmetro que manipula o tipo ContaBancaria
foi declarado como passagem de parâmetro por referência – por isso o
asterisco (*) ao lado direito do tipo de dado. As regras de funcionamento
de cada uma dessas funções não serão criadas nesse algoritmo, mas em
outro código. Por isso, esse tipo de módulo é chamado de verdadeiro
Tipo Abstrato de Dados.

www.esab.edu.br 128
O código completo do Tipo Abstrato de Dados ContaBancaria é
apresentado no Algoritmo 43:

Algoritmo 43 – Tipo Abstrato de Dados: conta.h


01 struct registro{
02 int numero;
03 double saldo;
04 };
05 typedef struct registro ContaBancaria;
06 void abrirConta(ContaBancaria*,int,double);
07 void depositar(ContaBancaria*,double);
08 void sacar(ContaBancaria*,double);
09 void extrato(ContaBancaria*);

Fonte: Elaborado pelo autor (2013).

Esse algoritmo não tem função isoladamente, como ele é parte de um


programa, de um módulo, ele deve ser referenciado em outro programa
para ser utilizado, por isso é chamado de interface do TAD. O algoritmo
será salvo com a extensão .h, pois é dessa forma que a linguagem C
cria arquivos que funcionarão como bibliotecas de funções. Portanto, o
algoritmo anterior será identificado como conta.h.

As bibliotecas ou arquivos com extensão .h são referenciados nos


programas em C por meio da cláusula #include. Assim, para que um
programa em C possa utilizar a biblioteca conta.h, é necessário que no
início do programa seja inserida a instrução #include “conta.h”, e o arquivo
“conta.h” deve estar no mesmo diretório do programa que o chama.

Para Celes, Cerqueira e Rangel Netto (2004, p. 124):

[...] estes módulos podem ser utilizados para compor vários programas e, assim,
poupar muito tempo de programação. Isto se deve a característica que estes módulos
podem ser utilizados em vários outros programas sem a necessidade de novas
codificações.

Bem, agora que temos a estrutura do TAD e sua interface, o próximo


passo é criar o código que implementa suas regras de negócio. O
Algoritmo 44 apresenta essas rotinas:

www.esab.edu.br 129
Algoritmo 44 – Regras de negócio da TAD – contabancaria.c
01 #include <stdio.h>
02 #include "conta.h"
03 void abrirConta(ContaBancaria*
contacorrente,int num,double valor){
04 contacorrente->numero = num;
05 contacorrente->saldo = valor;
06 }
07 void depositar(ContaBancaria*
contacorrente,double valor){
08 contacorrente->saldo = contacorrente->saldo
+ valor;
09 }
10 void sacar(ContaBancaria* contacorrente,double
valor){
11 contacorrente->saldo = contacorrente->saldo
- valor;
12 }
13 void extrato(ContaBancaria* contacorrente){
14 printf("R$:%f",contacorrente->saldo);
15 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 2 do algoritmo foi incluída a chamada para a


biblioteca conta.h por meio da instrução #include “conta.h”. A partir
dessa instrução, as funções abrirConta, depositar, sacar e extrato são
reconhecidas pelo algoritmo. Por isso, não há erro de compilação e,
diferentemente do arquivo conta.h, nesse arquivo os cabeçalhos das
funções são declarados com tipo de dado (o mesmo do cabeçalho
do arquivo conta.h) e nome do parâmetro. Os nomes das funções, a
ordem dos parâmetros e os tipos devem ser idênticos aos declarados no
cabeçalho no arquivo conta.h. Podemos utilizar qualquer nome para os
parâmetros, por exemplo, na linha 7, a função depositar foi declarada
com o parâmetro chamado contacorrente, que é do tipo ponteiro de
ContaBancaria, e o parâmetro chamado valor é do tipo double.

O arquivo que implementa as regras da interface do TAD deve ser salvo


com a extensão.c. Como é parte de um programa, esse arquivo também é
um módulo que poderá ser utilizado em outros programas.

Dessa forma, vamos identificar esse algoritmo como contabancaria.c.

www.esab.edu.br 130
Para utilizar o TAD ContaBancaria, basta que, no programa em que ele será
utilizado, seja incluída a instrução #include “contabancaria.c” e o tipo de
dado ContaBancaria. Assim, as operações estarão disponíveis para utilização.

Celes, Cerqueira e Rangel Netto (2004, p. 55) destacam que “[...] a


diretiva #include é seguida pelo nome do arquivo e o pré-processador da
linguagem a substitui pelo corpo do arquivo especificado. É como se o
texto do arquivo incluído fizesse parte do código-fonte”.

O Algoritmo 45 apresenta o exemplo de um programa em C que utiliza


o Tipo Abstrato de Dados ContaBancaria:

Algoritmo 45 – Uso do TAD ContaBancaria – main.c


01 #include <stdio.h>
02 #include<stdlib.h>
03 #include "contabancaria.c"
04 int main(){
05 ContaBancaria minhaconta;
06 abrirConta(&minhaconta,1,100);
07 extrato(&minhaconta);
08 printf("\n");
09 depositar(&minhaconta,1800);
10 extrato(&minhaconta);
11 printf("\n");
12 sacar(&minhaconta,500);
13 extrato(&minhaconta);
14 printf("\n");
15 system(“pause”);
16 return 0;
17 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 3 do código foi incluída a instrução #include


“contabancaria.c”. Com essa instrução o tipo de dados ContaBancaria,
que é o struct declarado no arquivo conta.h, passou a ser reconhecido
pelo algoritmo em questão, assim como as operações implementadas
(abrirconta, extrato, sacar e depositar).

www.esab.edu.br 131
Na linha 5 do código é declarada a variável minhaconta, do tipo
ContaBancaria. Na linha 6 essa variável que representa a conta é aberta
de forma que o número da conta é definido como 1 e o saldo inicial
em 100. Na linha 7 o saldo atual da conta é apresentado na tela, o qual
está em 100. Note que o parâmetro ContaBancaria foi declarado nos
métodos como do tipo por referência, por isso o uso do sinal de & ao
lado da variável (passagem do endereço da variável na memória).

Na linha 9 é realizado um crédito de 1.800 na conta e em seguida na


linha 12, um saque de 500. As linhas 10 e 13 apresentam o saldo atual
da conta após cada operação de saque ou depósito.

A Figura 39 apresenta a tela de execução do programa:

Figura 39 – Resultado da execução do programa.


Fonte: Elaborada pelo autor (2013).

A principal vantagem que deve ser destacada, e acredito que você


também a identificou, é o fato de na construção do algoritmo
que executa as operações de abertura e manipulação da conta, o
programador precisar apenas saber as operações implementadas no
TAD e os parâmetros que devem ser passados para que sejam executadas
corretamente. Como essas operações implementadas foram abstraídas,
não é preciso que o programador saiba ou tenha acesso a essas regras de
negócios, bastando apenas importar a biblioteca contabancaria.c por
meio da cláusula #include. A Figura 40 apresenta a ordem de utilização
da interface conta.h, do programa contacancaria.c até chegar ao
programa main.c:

www.esab.edu.br 132
conta.h
usada em interface

contabancaria.c
codificação usada em

main.c
uso (abstração)
Figura 40 – Uso e implementação da interface.
Fonte: Elaborada pelo autor (2013).

Nesta unidade você pôde estudar sobre como implementar um Tipo


Abstrato de Dados em C, avaliar a importância da criação de estruturas
de dados que possuem operações específicas para sua manipulação e, o
mais importante, o seu uso não necessita de conhecimento de como as
regras foram desenvolvidas, mas como devem ser usadas. Essa técnica
permite a construção de estruturas de dados com regras específicas,
organizadas e de fácil utilização.

Na próxima unidade vamos estudar a estrutura de dados Lista, que


possui propriedades e operações específicas que podem ser transformadas
em um TAD.

www.esab.edu.br 133
15 Listas encadeadas

Objetivo
Apresentar o conceito de listas encadeadas e seus tipos.

Na unidade anterior você estudou sobre como programar um Tipo


Abstrato de Dados em C e pôde avaliar a importância da criação de
estruturas de dados na forma de TAD, as quais possuem operações
específicas para sua manipulação e, o mais importante, o seu uso não
necessita de conhecimento de como as regras foram desenvolvidas.

Nesta unidade, fundamentados em Forbellone et al. (2005) e Celes,


Cerqueira e Rangel Netto (2004), vamos conhecer a estrutura de dados
lista encadeada e seus respectivos tipos.

Para representar um conjunto de dados, fizemos, até agora, referência


à estrutura de dados vetor unidimensional ou bidimensional, sendo a
forma mais simples para representar diversos elementos agrupados e
que formam um bloco de memória contíguo na memória principal do
computador, o que possibilita que qualquer elemento seja acessado por
meio de um índice que varia de 0 ao tamanho do vetor -1. Para Celes,
Cerqueira e Rangel Netto (2004, p. 135), o

[...] vetor é uma estrutura de dados randômica, já que possibilita o acesso a qualquer
elemento de forma aleatória, mas não é uma estrutura de dados flexível, pois
precisamos dimensioná-lo com um número máximo de elementos.

Para resolver esse contexto, no qual a utilização de um vetor depende


do conhecimento, de antemão, do número de elementos os quais serão
armazenados, é necessária a substituição do vetor por uma estrutura
de dados que cresça ou diminua conforme a necessidade de inclusão
(de novos) ou remoção de elementos. Essas estruturas são chamadas de
estruturas de dados dinâmicas, já que são implementadas por meio da
alocação dinâmica de elementos.

www.esab.edu.br 134
Essas estruturas dinâmicas são: listas encadeadas, pilhas, filas e árvores.
Nesta unidade começaremos a estudar as listas encadeadas.

Estudo complementar
Para saber mais sobre as listas encadeadas acesse
o link e veja um vídeo explicativo sobre o assunto.

15.1 Definição
A lista encadeada é uma estrutura de dados dinâmica que aloca um
espaço de memória para cada elemento inserido ou libera um espaço
de memória para cada elemento excluído, possibilitando, dessa forma,
o gerenciamento da memória alocada e resolvendo o problema de
desperdício de memória que ocorre com a utilização de um vetor.
Cada elemento da lista é chamado de nó ou nodo, sendo que cada nó é
formado, basicamente, por dois campos: o valor armazenado e o ponteiro
para o próximo elemento da lista.

A Figura 41 representa um nó da lista e ao lado uma lista de inteiros com


três elementos:
Valor Próximo
10 15 3 44
Nó Lista encadeada
Figura 41 – Nó e lista encadeada.
Fonte: Elaborada pelo autor (2013).

Ao ser utilizada, a lista encadeada não garante que os dados sejam


alocados contiguamente na memória principal, da mesma forma
que acontece com um vetor, portanto, não é possível acessar direto o
elemento da estrutura de forma aleatória. Assim, para que seja possível
utilizar a estrutura de forma organizada e simples, a lista encadeada
possui um elemento/nó que encabeça a lista, ou seja, o primeiro
elemento/nó de toda a estrutura. A partir desse primeiro elemento/
nó, os outros elementos/nodos são ligados ou encadeados. Portanto, do

www.esab.edu.br 135
primeiro elemento/nó se chega ao segundo/nó, do segundo se chega ao
terceiro e sucessivamente até se chegar ao último elemento/nó. Note que,
diferentemente de um vetor, no qual você informa a posição do vetor
(índice) para acessar o elemento desejado, sem necessidade de percorrer
toda a estrutura, para você acessar o elemento na lista encadeada é
preciso percorrer n-1 elementos.

A Figura 42 apresenta a lista encadeada com identificação do primeiro


elemento, guardando o endereço desse elemento na memória:
Descritor endX endZ endY endK
Primeiro 10 15 3 44
endX Lista encadeada
Figura 42 – Lista encadeada com identificação do primeiro elemento.
Fonte: Elaborada pelo autor (2013).

Observe que cada nó da lista possui um endereço de memória,


representados por endX, endZ, endY e endK. O endereço do primeiro
elemento é armazenado em uma variável chamada primeiro, que é
identificada como o descritor da lista.

Para facilitar a implementação da lista, é possível, também, criar um descritor


para armazenar o endereço do último elemento da lista, já que a cada
inserção ou remoção na lista é conhecido seu primeiro e último elemento.

A Figura 43 apresenta a lista encadeada com identificação do primeiro e


último elemento, guardando o endereço desses elementos na memória:
Lista encadeada
Descritores
endX endZ endY end K
Primeiro 10 15 3 44
endX

Descritor NULL
Último
end K

Figura 43 – Lista encadeada com identificação do primeiro e último elemento.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 136
Observe que a lista agrupa as informações de forma que estejam
relacionadas entre si com a informação do primeiro e último elemento
da lista na forma de um descritor que armazena o endereço desses
elementos. As operações mais frequentes em listas são: busca, inclusão e
remoção de um determinado elemento. Outras operações possíveis são:
a alteração, a combinação de duas ou mais listas em uma única lista e a
ordenação de elementos.

Segundo Forbellone et al. (2005, p. 156), “[...] a lista encadeada é um


conjunto de elementos individualizados em que cada um referencia outro
elemento distinto como sucessor”.

As listas podem ser classificadas como simplesmente encadeada,


duplamente encadeada ou circular, como será estudado a seguir.

15.2 Tipos de listas encadeadas


As listas encadeadas podem ser classificadas de acordo com o número
de encadeamentos em: simplesmente encadeadas ou duplamente
encadeadas. As listas simplesmente encadeadas utilizam um único
endereço de memória ou ponteiro para interligação dos elementos que
a compõem. Celes, Cerqueira e Rangel Netto (2004, p. 149), destacam
que “[...] nas listas simplesmente encadeadas cada elemento armazena
um ponteiro para o próximo elemento da lista”. Já as listas duplamente
encadeadas, se caracterizam pela utilização de dois ponteiros para o
encadeamento dos elementos. Segundo Celes, Cerqueira e Rangel Netto
(2004, p. 149), nesse tipo de lista “[...] cada elemento tem um ponteiro
para o próximo elemento e um ponteiro para o elemento anterior”.

A Figura 44 apresenta uma lista simplesmente encadeada e outra


duplamente encadeada:

www.esab.edu.br 137
p p p p
a - anterior
p - próximo
Lista simplesmente encadeada

a p a p a p a p

Lista duplamente encadeada


Figura 44 – Lista simplesmente encadeada e lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

A lista simplesmente encadeada possui apenas o ponteiro para o próximo


elemento, representado pela letra p. Já a lista duplamente encadeada
possui dois ponteiros, para o elemento anterior, representado pela letra a
e para o ponteiro posterior, representado pela letra p.

A vantagem do uso da lista duplamente encadeada é a possibilidade


de percorrer os elementos na ordem do primeiro para o último ou do
último para o primeiro, sem a necessidade de alterações na estrutura. Na
lista simplesmente encadeada é possível percorrer apenas do primeiro
para o último elemento.

A Figura 45 apresenta a forma de navegação nessas listas:


Sentido do primeiro para o último elemento da lista

p p p p

Lista simplesmente encadeada

Sentido do primeiro para o último elemento da lista


a p a p a p a p

Lista duplamente encadeada


Sentido do último para o primeiro elemento da lista
Figura 45 – Formas de navegação na lista simplesmente encadeada circular e na lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 138
Existe um tipo de lista em que a navegação se dá de forma circular,
estas são chamadas de listas circulares e são estruturas de modo que o
conceito de primeiro e último elemento não existem, uma vez que o
último elemento da lista tem como próximo elemento o primeiro da
lista, formando um ciclo. Dessa forma, para criação e utilização da lista
circular, é necessário apenas conhecer o primeiro elemento da lista, já que
o conceito de último não existe. A Figura 46 apresenta uma lista circular:

p p p p

Lista simplesmente encadeada


Primeiro circular

a p a p a p a p

Lista duplamente encadeada


Primeiro circular

Figura 46 – Lista simplesmente encadeada circular e lista duplamente encadeada circular.


Fonte: Elaborada pelo autor (2013).

Observe que o último elemento da lista tem como próximo elemento o


primeiro da lista e independe se a estrutura de dados utiliza um ou dois
ponteiros de encadeamento.

A utilização da lista duplamente encadeada é ideal para resolver


problemas em que há a necessidade de gerenciamento de processos
cíclicos, como por exemplo, o armazenamento de gravações realizadas
por câmeras de sistema de segurança. O espaço de armazenamento dos
vídeos das gravações possui capacidade definida pelo espaço livre do
componente de gravação, normalmente é o disco rígido do computador
(HD). Portanto, em algum momento não haverá mais espaço para
guardar os arquivos com as novas gravações. Uma solução é implementar
uma lista circular de forma que o último vídeo gravado aponte para o
primeiro e mais antigo vídeo gravado. Havendo necessidade de mais

www.esab.edu.br 139
espaço, o primeiro vídeo é removido para que haja espaço para a gravação
do vídeo mais atual, de forma que as gravações mais atuais sejam
mantidas e as mais antigas removidas.

A implementação das listas encadeadas, simplesmente ou duplamente


encadeadas, será discutida nas unidades posteriores, porém, há listas
com características específicas que independem de quantos ponteiros
são utilizados e que determinam as operações que podem ser executadas,
como: filas, pilhas e árvores.

A fila é uma estrutura semelhante à fila de banco, em que os clientes


que chegam mais cedo são atendidos de forma mais rápida. Para isso, a
estrutura só permite a inserção de elementos no final da fila e a remoção
do início da fila. Já a pilha é uma estrutura de dados semelhante a uma
pilha de pratos, em que um elemento está acima do outro. Para retirar
um prato, só é possível fazer isso pelo topo, assim como para colocar um
novo prato. Por isso, essa estrutura de dados só permite duas operações,
inserção no topo e remoção do topo.

Por fim, as árvores são listas que armazenam as informações por níveis, o
que muito se assemelha a uma árvore genealógica, em que os elementos são
organizados por níveis e hierarquicamente. Dessa forma, qualquer inserção
ou remoção de elementos deve manter a organização dos dados em níveis,
fazendo com que subam, desçam ou permaneçam no mesmo nível.

www.esab.edu.br 140
A Figura 47 apresenta essas listas específicas:

Inserção
Primeiro Último

Remoção FILA
Topo
Remoção Inserção

PILHA

ÁRVORE
Figura 47 – Fila, pilha e árvore.
Fonte: Elaborada pelo autor (2013).

Em outras unidades desta disciplina, essas estruturas serão abordadas


com mais detalhes.

Nesta unidade você pôde estudar o conceito da lista encadeada, que pode
ser: simplesmente ou duplamente encadeada, dependendo do número de
ponteiros utilizados na ligação entre os elementos, ou circular, quando o
último elemento aponta para o primeiro elemento da lista.

Você pôde também estudar sobre a aplicação da lista encadeada na


construção das filas, pilhas e árvores, que possuem regras específicas de
manipulação dos dados por meio das operações de inserção e remoção.
Na próxima unidade estudaremos sobre a lista simplesmente encadeada.

www.esab.edu.br 141
Listas encadeadas simples –
16 conceituação
Objetivo
Apresentar o conceito de listas encadeadas simples.

Na unidade anterior você estudou o conceito de lista encadeada, que


pode ser simplesmente ou duplamente encadeada ou circular. Você
também estudou sobre a aplicação da lista encadeada na construção das
filas, pilhas e árvores.

Nesta unidade, utilizando o trabalho de Ascencio e Araújo (2010), vamos


conceituar e estudar as operações possíveis na estrutura de dados de lista
simplesmente encadeada.

16.1 Definição
A lista simplesmente encadeada se caracteriza pelo uso de um ponteiro
para cada elemento inserido na lista, o que possibilita que um elemento
saiba quem é seu elemento subsequente, caso ele exista. Mais que isso,
por se tratar de uma estrutura de dados dinâmica, o espaço de memória
de cada elemento é alocado em tempo de execução para que seja utilizado
somente o espaço de memória que realmente se faz necessário, evitando o
desperdício de memória. Cada elemento é alocado no espaço de memória
livre e disponível, e quem gerencia esses espaços é o sistema operacional.
Esse processo não garante que os elementos da lista sejam alocados de forma
contígua, um elemento ao lado do outro (ASCENCIO; ARAÚJO, 2010).

O uso da lista simplesmente encadeada possibilita o gerenciamento da


memória, porém, problemas de falta de memória para alocação dos
elementos não são garantidos, pois se forem inseridos um total de elementos,
e o espaço de alocação for maior que o disponível, ocorrerá um erro e a
aplicação será encerrada. Por isso é essencial o gerenciamento desse espaço a
partir da liberação de memória quando um elemento for removido da lista.

www.esab.edu.br 142
Para a criação da lista simplesmente encadeada, é preciso o uso de um
ponteiro que indique o seu primeiro elemento. Então o percurso da
lista é feito a partir desse ponteiro, seguindo, consecutivamente, pelos
endereços existentes no campo próximo de cada elemento.

A Figura 48 apresenta a lista simplesmente encadeada:

e1 e2 e3 e4
10 e2 15 e3 04 e4 01 NULL

e1
Primeiro
Figura 48 – Lista simplesmente encadeada.
Fonte: Elaborada pelo autor (2013).

Observe que cada elemento da lista simplesmente encadeada foi alocado


em um endereço de memória representado pelos termos e1, e2, e3 e
e4. O descritor primeiro possui o endereço de memória do primeiro
elemento, e1. Cada elemento da lista simplesmente encadeada possui
um valor inteiro e o endereço do próximo elemento da lista. Assim,
podemos entender que o elemento do endereço e1 possui o valor 10 e
o seu campo próximo aponta para o elemento e2. O elemento e2, por
sua vez, possui o valor 15 e aponta para o próximo elemento, o e3, e
assim sucessivamente até o elemento e4, que é o último elemento da
lista e seu campo próximo vale NULL, uma vez que ele não aponta para
um próximo elemento. O valor NULL indica que o campo não possui
nenhum endereço de memória.

Para facilitar a manipulação da lista simplesmente encadeada, pode ser


adotado o uso de um descritor para o último elemento da lista. A Figura
49 apresenta a lista simplesmente encadeada com mais esse descritor:

www.esab.edu.br 143
e1 e2 e3 e4
10 e2 15 e3 04 e4 01 NULL

e1 e4
Primeiro Último
Figura 49 – Lista simplesmente encadeada com dois descritores.
Fonte: Elaborada pelo autor (2013).

Note que o último elemento não é NULL, porém o endereço do


próximo apontado por ele vale NULL, ou seja, o último elemento da
lista encadeada aponta como próximo para NULL, pois depois dele não
há mais nenhum elemento da lista.

Com esses dois descritores (o primeiro, que vale e1, e o último, e4), é
possível gerenciar e realizar as operações de inserção, consulta e remoção
de elementos na lista.

Uma lista simplesmente encadeada possibilita as seguintes operações:

• criação da lista dinamicamente;


• inserção no final da lista encadeada;
• inserção no início da lista encadeada;
• remoção do final da lista encadeada;
• remoção do início da lista encadeada.
Cada uma dessas operações possui regras bem específicas que serão
apresentadas a seguir.

Criação da lista simplesmente encadeada

Na criação da lista simplesmente encadeada, os dois descritores (primeiro e


último) começarão valendo NULL, já que nenhum elemento foi inserido
na lista. Assim, para saber se a lista simplesmente encadeada está vazia,
basta verificar se o descritor primeiro ou o último possui o valor NULL.

www.esab.edu.br 144
A Figura 50 apresenta a lista criada, observe:
primeiro último
NULL NULL
Figura 50 – Lista simplesmente encadeada com dois descritores criada.
Fonte: Elaborada pelo autor (2013).

Observe que a lista foi criada, mas como nenhum elemento foi inserido
até o momento, os descritores foram inicializados com NULL.

Inserção no final da lista

Na inserção no final da lista, o objetivo é garantir que o elemento


inserido seja sempre o último da lista. Caso a lista esteja vazia, na
primeira inserção, esse elemento será o último e o primeiro, já que a lista
possui um único elemento.

As regras para inserção no final são:

(a) se a lista está VAZIA então


descritor início recebe o endereço do novo elemento;
descritor último recebe o endereço do novo elemento; (c)
fim_se
(b) se lista não está vazia então
o último elemento da lista aponta como próximo para o novo elemento;
descritor último recebe o endereço do novo elemento (C)
fim_se

www.esab.edu.br 145
Para melhor visualização das regras, observe a Figura 51 que apresenta o
esquema da primeira inserção no final da lista:

primeiro primeiro primeiro


NULL e100 e100 e100
10 NULL
último e100 último e100 último
NULL 10 NULL NULL 10 NULL e100
Lista criada. Novo elemento Lista está vazia, aplica a regra (a), Aplica a regra (c). o novo elemento
a ser INSERIDO. o novo elemento é o primeiro da lista. é sempre o último, pois trata-se de
uma inserção no FINAL.
Figura 51 – Inserção do primeiro elemento no final da lista simplesmente encadeada.
Fonte: Elaborada pelo autor (2013).

A Figura 52 apresenta a sequência de inserção no final da lista quando


ela não estará mais vazia, portanto, as opções (b) e (c) são as que serão
executadas para a regra de inserção no final:

primeiro primeiro
e100 e100 e25 e100 e100 e25
10 55 10 e25 55
último último
e100 NULL NULL e100 NULL
Lista não está vazia, possui o elemento 10, O último elemento, referenciado pelo descritor ÚLTIMO,
e o novo valor 55 será inserido no final da lista. que é e100, recebe, no campo próximo, o endereço do
novo elemento e25, que será inserido. Portanto, o último
elemento aponta para quem será inserido na lista (b).

primeiro
e100 e100 e25
10 e25 55
último
e25 NULL
Como se trata de uma inserção no FINAL da lista, o
novo elemento passa a ser o último, assim, o descritor
ÚLTIMO deve apontar para o endereço do elemento
que foi inserido (c).
Figura 52 – Inserção do elemento no final da lista simplesmente encadeada que não está vazia.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 146
Observe que após a inserção do primeiro elemento, os descritores
(primeiro e último) possuem o mesmo valor (e100) e à medida que
novos elementos são inseridos no final da lista, apenas o descritor último
é atualizado com o endereço do elemento inserido.

Inserção no início da lista

Na inserção no início da lista, o objetivo é garantir que o elemento


inserido seja sempre o primeiro da lista. Caso a lista esteja vazia, na
primeira inserção esse elemento será o último e o primeiro, já que a lista
possui um único elemento.

As regras para inserção no início são:

(a) se a lista está VAZIA, então


descritor último recebe o endereço do novo elemento;
o novo elemento a ser inserido é referenciado pelo descritor primeiro. (c)
fim_se
(b) se lista não está vazia então
o novo elemento aponta como próximo para o primeiro elemento da lista;
o novo elemento a ser inserido é referenciado pelo descritor primeiro.(c)
fim_se

Para uma melhor visualização das regras, observe a Figura 53 que


apresenta o esquema da primeira inserção no início da lista:

www.esab.edu.br 147
e99
primeiro <. NULL
último <. NULL
23 NULL
Elemento a
ser inserido.

e99 e99 e99


primeiro <. NULL primeiro
último
23 NULL último
23 NULL
e99 e99
Lista está vazia, Como a operação é de
primeiro vale NULL, inserção no início da lista,
então o descritor sempre o novo elemento
último recebe o inserido passará a ser
endereço do elemento referenciado pelo descritor
a ser inserido (a). PRIMEIRO (c).
Figura 53 – Inserção do primeiro elemento no início da lista simplesmente encadeada.
Fonte: Elaborada pelo autor (2013).

A Figura 54 apresenta a sequência de inserção no início, quando a


lista não está mais vazia, portanto, as opções (b) e (c) são as que serão
executadas para a regra de inserção no início:

e08 último e99 último e99


98 NULL e99 e08 e99
Novo elemento 23 NULL 98 e99 23 NULL
a ser INSERIDO
no início da lista. Lista não está vazia,
primeiro e99 o novo elemento aponta primeiro e99
como próximo para o
primeiro elemento
da lista (b).

último e99
e08 e99
98 e99 23 NULL
Lista não está vazia, o novo
primeiro e08 elemento aponta como
próximo para o primeiro
elemento da lista (c).
Figura 54 – Inserção do elemento no início da lista simplesmente encadeada que não está vazia.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 148
Observe que na inserção no final da lista, esta aumentará para a direita, já
na inserção no início da lista, ela aumentará para esquerda. Na inserção
no final, sempre o elemento que será inserido será referenciado como
último e na inserção no início sempre o elemento novo a ser inserido será
referenciado como primeiro. Se a lista está vazia (primeiro vale NULL ou
último vale NULL), o descritor primeiro e último apontarão para o novo
elemento a ser inserido.

Remoção no final da lista

Na remoção no final da lista, o objetivo é garantir que o último elemento


seja removido e o penúltimo, caso exista, passe a ser o último elemento
da lista.

As regras para remoção do final são:

(a) se a lista está VAZIA, então


não tenta remover o elemento
fim_se
senão
(b) remove o último elemento
(c) se primeiro é igual ao último e igual ao elemento que será removido então
a lista ficará vazia pois está removendo o único elemento da lista
descritor último vale NULL e descritor primeiro vale NULL
senão
(d) achar penúltimo elemento
(e) penúltimo aponta como próximo para NULL
(f) descritor último aponta para o endereço do penúltimo
fim_senão

www.esab.edu.br 149
A Figura 55 apresenta a sequência de remoção do final quando a lista não
está vazia, portanto, as opções (b), (c), (d), (e) e (f ) serão executadas:

último último
e99 e99
(penúltimo) (penúltimo)
e08 e99 e08 e99
98 e99 23 98 e99 23
Lista não está vazia, então
e08 removerá o último elemento e08
e99. Como primeiro ou último Elemento removido, porém, o
primeiro não valem NULL, operação primeiro elemento anterior AINDA aponta
pode ser realizada. como próximo para o elemento
removido e o descritor ÚLTIMO
penúltimo - elemento que aponta aponta para o elemento que
como próximo para o último. foi removido.

último
último
e99
(penúltimo) e08
e08 e99 (penúltimo)
e08
98 NULL 23
98 NULL O descritor último será
Elemento já foi removido. atualizado para o endereço
e08 do penúltimo.
Penúltimo elemento terá seu e08
primeiro
campo próximo atualizado primeiro
para NULL. Caso contrário,
estará apontando para um
elemento que não existe.
Figura 55 – Remoção do elemento no final da lista simplesmente encadeada que não está vazia.
Fonte: Elaborada pelo autor (2013).

Observe que tão importante quanto a remoção do elemento que libera


o espaço de memória alocado dinamicamente é a conferência de que os
ponteiros de cada elemento foram atualizados corretamente, bem como
os descritores. Caso contrário, estarão referenciando o elemento que já
foi removido, gerando um erro grave na aplicação.

www.esab.edu.br 150
A Figura 56 apresenta a sequência de remoção do final quando a lista tem
apenas um elemento, portanto, as operações (b) e (c) serão executadas:

último último
e08 e08 último
e08 e08 NULL
98 NULL 98 NULL
NULL
e08 e08
primeiro
primeiro primeiro
Primeiro e último apontam O descritor primeiro e último Primeiro e último apontam
para o mesmo elemento, que continuam apontando para o para NULL, já que a lista, após
será removido. Portanto, a elemento que já foi removido. a remoção, ficou vazia.
lista só possui esse elemento,
não possui anterior e, ao
removê-lo a lista ficará vazia.

Figura 56 – Remoção do elemento no final da lista simplesmente encadeada com um elemento.


Fonte: Elaborada pelo autor (2013).

Observe, na Figura 56, que mesmo com a remoção do único elemento


da lista, os descritores primeiro e último continuam apontando para
o endereço do elemento removido. Por isso é essencial que, após a
remoção, ambos sejam atualizados para NULL.

Remoção no início da lista

Na remoção no início da lista, o objetivo é garantir que o primeiro


elemento seja removido e o segundo, caso exista, passe a ser o primeiro
elemento da lista.

www.esab.edu.br 151
As regras para remoção do início são:

(a) se a lista está VAZIA, então


não tenta remover o elemento
fim_se
senão
(b) remove o primeiro elemento
(c) se primeiro é igual ao último e igual ao elemento que será removido então
a lista ficará vazia pois está removendo o único elemento da lista
descritor primeiro e descritor último valem NULL
senão
(d) achar o segundo elemento
(e) descritor primeiro aponta para o endereço do segundo elemento
fim_senão

www.esab.edu.br 152
A Figura 57 apresenta a sequência de remoção do início quando a lista
não está vazia, portanto, as opções (b), (c), (d), e (e) serão executadas:

(segundo)
primeiro e44 e44 e28
20 e28 05 NULL
último e28

Segundo elemento da lista é


o elemento apontando como
próximo do primeiro. Lista não
está vazia, então remove o
primeiro elemento da lista.

(segundo)
primeiro e44 e44 e28 primeiro e28 e44 e28
20 e28 05 NULL 20 e28 05 NULL
último e28 último e28

Após a remoção o descritor O descritor primeiro é atualizado


primeiro, continua apontando para o endereço do segundo elemento.
para o elemento que foi removido.
Figura 57 – Remoção do elemento do início da lista simplesmente encadeada que não está vazia.
Fonte: Elaborada pelo autor (2013).

A remoção do início da lista com um elemento resultará na lista vazia,


como pode ser observado na Figura 58:

primeiro e28 e28 primeiro NULL


05
último e28 último NULL

A lista não está vazia, portanto o Como a lista ficará vazia,


primeiro elemento pode ser removido, os descritores primeiro e
porém, como é o único elemento, a último serão atualizados
lista ficará vazia. para NULL.
Figura 58 – Remoção do elemento do início da lista simplesmente encadeada com um único elemento.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 153
Observe que é muito importante que, na remoção dos elementos, os
descritores primeiro e último sejam atualizados para que referenciem
corretamente os dados da lista.

Assim como os elementos podem ser inseridos ou removidos da lista, eles


também podem ser listados e pesquisados.

Nesta unidade você pôde estudar sobre a lista simplesmente encadeada


e as operações que podem ser realizadas. A inserção pode ser realizada
no final da lista de forma que o elemento inserido sempre seja o último
da estrutura ou no início da lista, garantindo que o elemento inserido
sempre seja o primeiro elemento da lista. Na remoção, o que pode
ocorrer no início ou no final da lista, quando o elemento a ser removido
é o único, os descritores início e último, obrigatoriamente, precisam ser
atualizados para NULL, para que não façam referência ao elemento que
não pertence mais à lista. Na próxima unidade, vamos verificar como
implementar essas operações utilizando a linguagem C e o conceito de
tipos abstratos de dados.

www.esab.edu.br 154
Listas encadeadas simples –
17 codificação
Objetivo
Apresentar a codificação e a manipulação de listas encadeadas
simples.

Na unidade anterior você estudou o conceito de lista encadeada,


duplamente encadeada e as operações que podem ser realizadas. Nesta
unidade vamos estudar como implementar uma lista simplesmente
encadeada em C. Para isso, vamos utilizar os trabalhos de Ascencio e
Araújo (2010) e Celes, Cerqueira e Rangel Netto (2004).

Vamos iniciar pelo processo de desenvolvimento do arquivo lista.h que


será constituído pelos dados que serão manipulados na lista e as operações
que podem ser realizadas com esses dados, na forma de um Tipo Abstrato
de Dados. Esse arquivo será composto pelos tipos estruturados que
representarão cada nó da lista e os descritores primeiro e último da lista.
Cada operação (função) será representada apenas pelo cabeçalho das
funções. O Algoritmo 46 apresenta o código do arquivo lista.h:

Algoritmo 46 – Estrutura e operações da lista: lista.h


01 //Estrutura de cada elemento da lista
02 struct tipo_no{
03 int dado;
04 struct tipo_no *proximo;
05 };
06 //tipo No;
07 typedef struct tipo_no No;
08 //Estrutura da Lista
09 //Campos primeiro e ultimo do tipo ponteiro
10 struct tipo_lista{
11 No *primeiro;
12 No *ultimo;
13 };
14 //Tipo Lista
15 typedef struct tipo_lista Lista;

www.esab.edu.br 155
16 //Operações da Lista
17 void criarLista(Lista*);
18 int listaVazia(Lista*);
19 void inserirFinal(Lista*,int);
20 void inserirInicio(Lista*,int);
21 void removerInicio(Lista*);
22 void removerFinal(Lista*);
23 void listar(Lista*);
24 No* buscar(Lista*,int);
25 int contar(Lista*,No*,int);
26 No* primeiroElemento(Lista*)

Fonte: Elaborado pelo autor (2013).

Nas linhas 2 a 5 é criado o tipo estruturado tipo_no que representará


cada elemento da lista simplesmente encadeada. Cada tipo_no será
representado por um campo dado do tipo inteiro e um campo proximo
que t é um ponteiro para o tipo_no. Ou seja, cada tipo_no é formado
por um campo inteiro e um campo próximo, o qual apontará para
um elemento que também é do tipo_no. Celes, Cerqueira e Rangel
Netto (2004, p. 136) destacam que “[...] se trata de uma estrutura
autorreferenciada, pois além do campo armazenar a informação, há um
campo que é um ponteiro para uma próxima estrutura do mesmo tipo”.

Na linha 7 é criado um tipo de dados lista, um mnemônico para o tipo


estruturado tipo_no. Assim, para criar variáveis do tipo estruturado tipo_
no, basta utilizar o termo No. Essa estratégia é utilizada na definição do
tipo estruturado tipo_lista nas linhas 10 a 13, que especifica dois campos
do tipo ponteiro para o tipo estruturado tipo_no, primeiro e último, que
representarão os descritores da lista simplesmente encadeada. Na linha
15 é criado o tipo de dados lista, um mnemônico para o tipo estruturado
tipo_lista.

Da linha 17 a 26 são declarados os cabeçalhos das operações que podem


ser realizadas na lista simplesmente encadeada. O Quadro 6 apresenta as
operações e suas finalidades:

www.esab.edu.br 156
Operação Finalidade Parâmetros
Inicializar a lista Recebe como parâmetro o
void criarLista(Lista*); especificando os descritores endereço da lista que será
primeiro e último para NULL. manipulada.
Retornar 1 se a lista estiver
vazia ou 0 caso possua algum Recebe como parâmetro o
int listaVazia(Lista*); elemento. Para tanto, verifica endereço da lista que será
se os descritores primeiro ou manipulada.
último vale NULL.
Recebe como parâmetro o
Inserir um número inteiro no
endereço da lista que será
void inserirFinal(Lista*,int); final da lista simplesmente
manipulada e o valor inteiro
encadeada.
que será inserido.
Recebe como parâmetro o
Insere um número inteiro no
endereço da lista que será
void inserirInicio(Lista*,int); início da lista simplesmente
manipulada e o valor inteiro
encadeada.
que será inserido.
Remover o elemento da
Recebe como parâmetro o
primeira posição da lista
void removerInicio(Lista*); endereço da lista que será
simplesmente encadeada se
manipulada.
esta não estiver vazia.
Remover o elemento da
Recebe como parâmetro o
última posição da lista
void removerFinal(Lista*); endereço da lista que será
simplesmente encadeada, se
manipulada.
esta não estiver vazia.
Listar todos os elementos Recebe como parâmetro o
void listar(Lista*); da lista simplesmente endereço da lista que será
encadeada. manipulada.
Retornar o nó que possui o Recebe como parâmetro o
valor pesquisado e caso o endereço da lista que será
No* buscar(Lista*,int);
valor não seja localizado será manipulada e o valor inteiro
retornado um NULL. que será pesquisado.
Recebe como parâmetro
Retornar a quantidade
o endereço da lista e a
de elementos da lista,
int contar(Lista*,No*,int); contagem de elementos para
realizando um processo
cada execução do processo
recursivo.
recursivo.
Recebe como parâmetro o
Retornar o nó da primeira
No* primeiroElemento(Lista*) endereço da lista que será
posição da lista.
manipulada.

Quadro 6 – Operações da lista.


Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 157
Observe que cada operação contém apenas o cabeçalho da função. A
codificação de cada operação será implementada em outro arquivo-fonte
com extensão”.c” que utilizará o Tipo Abstrato de Dados e deverá fazer
uma referência ao arquivo lista.h por meio da cláusula #include.

Vamos analisar o código de cada uma das funções listadas anteriormente,


começando pelas operações de criação, inserção e remoção na lista.

17.1 Função de inicialização, inserção, busca e


remoção
A função de inicialização tem como principal objetivo criar a lista
e preencher os descritores primeiro e último da lista com NULL
(ASCENCIO; ARAÚJO, 2010). Essa operação deve ser executada toda
vez que se deseja criar uma lista simplesmente encadeada.

O Algoritmo 47 apresenta a codificação dessa operação:

Algoritmo 47 – Operação de inicialização da lista


01 #include <stdio.h>
02 #include "lista.h"
03 //Inicializar a Lista
04 void criarLista(Lista* lista){
05 lista->primeiro = NULL;
06 lista->ultimo=NULL;
07 }

Fonte: Elaborado pelo autor (2013).

Observe que é necessário, na linha 2, o uso da cláusula #include “lista.h”


para que a operação criarLista() seja reconhecida no algoritmo. Caso
contrário, um erro de compilação será apresentado com a mensagem de
que a função criarLista() não é reconhecida como um comando válido.
A operação recebe como parâmetro o endereço da lista e inicializa os
descritores primeiro e último com NULL, como pode ser verificado nas
instruções das linhas 5 e 6.

Para a manipulação da lista, principalmente nas operações de remoção,


consulta e listagem, é fundamental verificar se a lista não está vazia. O

www.esab.edu.br 158
Algoritmo 48 apresenta a codificação da operação para verificar se a
lista está vazia:

Algoritmo 48 – Operação para verificar se a lista está vazia


01 //Verificar se a lista está Vazia
02 int listaVazia(Lista* lista){
03 if ((lista->primeiro == NULL) || (lista-
>ultimo==NULL)) return 1;
04 else return 0;
05 }

Fonte: Elaborado pelo autor (2013).

A função listaVazia() retorna 1 (verdadeiro) ou 0 (falso) para a validação


se a lista está vazia ou não. A condição para a lista estar vazia é verificada
na linha 3, avaliando se o descritor primeiro ou último vale NULL.

Com a lista criada, é possível a operação de inserção de elementos na


lista, podendo ocorrer no início ou final da lista. O Algoritmo 49 contém
a codificação da regra para inserção no início da lista:

Algoritmo 49 – Operação para remover do início da lista


01 //Inserir no Início
02 void inserirInicio(Lista* lista,int valor){
03 No *novo; //declara o novo elemento
04 novo = (No*)malloc(sizeof(No)); Aloca espaço
para o novo elemento
05 novo->dado = valor; //armazena o valor no
campo dado do novo elemento
06 novo->proximo=NULL;//novo elemento não
aponta para outro elemento
07 if(listaVazia(lista)==1){//verifica se a
lista está vazia
08 lista->ultimo = novo; //se a lista está

vazia, define o novo elemento como último
da lista
09 }else{
10 novo->proximo = lista->primeiro;//se não
está vazia, o novo item aponta para o
primeiro
11 }
12 lista->primeiro = novo; //atualiza o
descritor primeiro para o elemento que foi
inserido
13 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 159
Você estudou na unidade 16 as regras de negócio para a operação de
inserção no início, portanto, vamos ressaltar as principais características
dessa operação. Sua principal função é inserir um novo elemento no
início da lista, por isso a linha 12 sempre será executada, atualizando o
descritor primeiro com o endereço do novo elemento. As linhas 3 a 6
criam um novo elemento nó, preenchendo-o com o valor passado como
parâmetro e definindo o campo próximo para NULL. Na linha 4 é
alocado um espaço de memória para o novo elemento, por isso o uso da
função malloc().

Assim como é possível inserir um elemento na lista, é possível remover


do início ou final da lista. O Algoritmo 50 apresenta a função para
remoção no final da lista:

Algoritmo 50 – Algoritmo de remoção do final da lista


01 void removerFinal(Lista* lista){
02 if(listaVazia(lista) != 1){//Lista não está
vazia
03 No *item = lista->ultimo; //guarda o
endereço do último elemento da lista
04 if(lista->primeiro == lista->ultimo){//
verifica se há um único elemento na lista
05 lista->primeiro = NULL;
06 lista->ultimo = NULL;
07 }
08 else{
09 //percorrer a lista para achar o último
elemento da lista
10 No* penultimo = lista->primeiro; //
começa do primeiro elemento
11 while(penultimo->proximo != lista-
>ultimo){ //até achar que aponta para o
último
12 penultimo = penultimo->proximo;
13 }
14 penultimo->proximo = NULL;//penultimo não
apontará mais para o último
15 lista->ultimo = penultimo;//atualiza o
ultimo da lista
16 }
17 free(item); //remove o elemento da memória
18 }
19 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 160
A função removerFinal() verifica se a lista não está vazia, para, então,
avaliar se o elemento que será removido é o único elemento da lista:
por isso, a condição (lista->primeiro == lista->ultimo) na linha 4. Se
a condição for verdadeira, atualiza os descritores inicio e ultimo para
NULL, pois após a remoção a lista estará vazia. Caso contrário, nas linhas
10 a 13, é realizado um laço que começa no primeiro elemento da lista
até encontrar o penúltimo elemento da lista, aquele elemento cujo campo
próximo vale o endereço do último elemento da lista, ou seja, o elemento
que aponta como próximo para o último elemento da lista: por isso,
na linha 11 a condição penultimo->proximo!= lista->ultimo. Ao final
do laço, na linha 14, a variável penultimo estará valendo o endereço do
último elemento da lista, e na instrução penultimo->proximo = NULL
esse elemento, que antes apontava para o último elemento, passará a
apontar para NULL. Por fim, na linha 17 o espaço de memória alocado
para o último elemento da lista é liberado, por isso podemos entender
que o último elemento é removido da lista, sendo que na linha 3 a
variável item foi inicializada com o endereço do último elemento da lista.

Note que independentemente da lista possuir um ou mais elementos,


a primeira instrução na linha 3 faz com que a variável item guarde o
endereço do último elemento da lista, o que é fundamental, pois na linha
6, caso a lista esteja vazia, o endereço do descritor ultimo é mudado para
NULL e, portanto, não seria possível resgatar o endereço do último; ou
na linha 15, executada quando a lista possui mais de um elemento, que
altera o endereço do último elemento para o endereço do penultimo,
o que impossibilita após essa instrução recuperar o endereço do último
elemento que será removido.

Para pesquisar um elemento na lista, é necessário percorrer todos os


elementos até que seja localizado o valor pesquisado ou a lista termine,
começando pelo primeiro elemento da lista.

www.esab.edu.br 161
O Algoritmo 51 apresenta a função para buscar o elemento da lista:

Algoritmo 51 – Algoritmo de pesquisa na lista


01 No* buscar(Lista* lista,int valor){
02 if(listaVazia(lista) == 1){
03 return NULL;
04 }else{
05 No* ponteiro = lista->primeiro;
06 while((ponteiro != NULL) && (ponteiro-
>dado != valor)){
07 ponteiro = ponteiro->proximo;
08 }
09 return ponteiro;
10 }
11 }

Fonte: Elaborado pelo autor (2013).

Para buscar um elemento na lista, a função retorna o endereço do nó que


possui o elemento pesquisado. Se o valor não for localizado, é retornado
o valor NULL. Caso a lista esteja vazia a função retornará um NULL,
por isso na linha 2 é verificado se a lista está vazia. Na linha 5 o ponteiro
é inicializado com o endereço do primeiro elemento da lista. Nas linhas
6 a 8, é realizado um laço enquanto a lista não termina (ponteiro !=
NULL) ou que o valor seja localizado (ponteiro->dado != valor). A cada
passo do laço, na linha 7, o ponteiro é mudado para o próximo elemento
da lista. Ao final do laço, a variável ponteiro terá o endereço do elemento
com o valor pesquisado ou estará valendo NULL e será retornado pela
função na linha 9.

Para ampliar seus estudos com relação à implementação da lista


simplesmente encadeada, acesse o endereço <http://pastebin.com/
g853up7E> que contém a estrutura do TAD, procure salvá-lo como
lista.h. Depois, acesse o endereço <http://pastebin.com/MiidckDQ> para
baixar o código completo com todas as operações da lista simplesmente
encadeada. Procure salvá-lo com o nome lista.c (usaremos esse código na
próxima unidade). Por fim, para testar o programa, baixe no endereço
<http://pastebin.com/HK14KnNZ> o código do programa principal que
testa as operações da lista simplesmente encadeada.

www.esab.edu.br 162
Como a lista possui um conjunto de elementos ligados pelo ponteiro
próximo de cada elemento, inclusive o primeiro elemento da lista, é
possível percorrer todos os elementos da estrutura de dados de forma
recursiva fazendo com que o endereço de memória comece no primeiro
elemento da lista e vá até o último elemento da lista, aquele cujo
ponteiro próximo vale NULL.

17.2 Implementações recursivas


Segundo Celes, Cerqueira e Rangel Netto (2004, p 144), a “[...] lista
pode ser implementada de maneira recursiva”, de forma que as operações
são codificadas chamando a si mesma, o que caracteriza a recursividade.
Usando desse recurso,vamos construir uma função para contar quantos
elementos possui a lista simplesmente encadeada.

Para saber quantos elementos possui a lista, é necessário verificar se a


lista está vazia. Se essa condição for verdadeira, a função retornará 0 e
o processo recursivo não será executado. Caso a lista não esteja vazia,
a lista será percorrida do primeiro até o último elemento por meio do
campo próximo de cada elemento, já que cada nó aponta para o próximo
elemento da lista. As chamadas recursivas serão encerradas quando o
elemento a ser pesquisado vale NULL, pois é a condição para que a lista
tenha se encerrado.

A Figura 59 apresenta uma lista simplesmente encadeada com três


elementos:
e2 e4 e88
10 e4 e88 15 20 NULL

e2 e88
primeiro último
Figura 59 – Lista simplesmente encadeada com três elementos.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 163
Note que para localizar o valor 100, a cada chamada recursiva a lista
recebe o endereço do elemento a ser pesquisado, começando em e2 que
vale 10. Como valor do endereço (10) é diferente de 100, uma nova
chamada recursiva será realizada com o endereço do próximo de e2 que é
e4 e vale 15. Como 15 não é o valor pesquisado (100), haverá uma nova
chamada da função com o próximo de e4 que é e88 e possui o valor 20,
que não é igual a 100 e, portanto, uma nova chamada da função será
realizada com o próximo de e88 que é NULL e, portanto, a pesquisa será
encerrada, uma vez que o endereço valendo NULL indica que toda a lista
já foi visitada.

Vamos analisar o código do Algoritmo 52 que implementa a operação de


contagem de elementos recursivamente:

Algoritmo 52 – Algoritmo de contagem recursiva


01 int contar(Lista* lista,No* ponteiro,int qde){
02 if(ponteiro==NULL) return qde;
03 else{
04 qde++;
05 return contar(lista,ponteiro-
>proximo,qde);
06 }
07 }

Fonte: Elaborado pelo autor (2013).

A cada chamada da função é passado, como parâmetro da lista que


será manipulada, o elemento da lista e a contagem atual. Por exemplo,
com base na lista da Figura 59, a primeira chamada será (lista,
primeiroElemento(lista),0), que pode ser entendida como (lista, e2, 0).
Na execução da função, na linha 2, como o ponteiro não vale NULL,
esta será chamada recursivamente como (lista, e4, 1), pois o ponteiro
foi alterado para o próximo dele e a quantidade foi incrementada em 1.
Na nova execução da função, como o ponteiro não vale NULL, a nova
chamada será (lista, e88, 2) e na execução, como a lista não está vazia,
uma nova chamada será executada com (lista, NULL, 3), pois o próximo
de e88 é NULL. Na execução da chamada, como o ponteiro vale NULL,
a função retornará 3, o valor da variável qde da função.

www.esab.edu.br 164
Portanto, a lista contará os elementos da lista, percorrendo recursivamente
os elementos desde o primeiro nó até que a lista se encerre.

Nesta unidade você pôde estudar como construir um Tipo Abstrato de


Dados que implementa as operações da lista simplesmente encadeada.

Na próxima unidade vamos fazer um exercício de codificação em C


com esse Tipo Abstrato de Dados de forma que seja criada uma lista
simplesmente encadeada que se mantenha ordenada a cada nova inserção
de elementos.

Tarefa dissertativa
Caro estudante, convidamos você a acessar o
Ambiente Virtual de Aprendizagem e realizar a
tarefa dissertativa.

www.esab.edu.br 165
Lista encadeada simples –
18 exercícios
Objetivo
Apresentar exercícios comentados sobre listas encadeadas simples.

Na unidade anterior você estudou como implementar uma lista


simplesmente encadeada em C, definindo um Tipo Abstrato de Dados
chamado nó e um Tipo Abstrato de Dados chamado lista, a partir do
tipo estruturado struct. Foram implementadas, também, as operações
para inserção, consulta e remoção de dados na lista.

O material desta unidade foi desenvolvido com base em Celes, Cerqueira


e Rangel Netto (2004).

Dando sequência aos nossos estudos, nesta unidade vamos desenvolver


uma lista simplesmente encadeada que, a cada inserção de um elemento
na lista, sempre se mantenha ordenada crescentemente. Utilizaremos a
operação de inserção, já apresentada na unidade anterior, no início e no
final da lista e vamos construir uma nova função com a inserção entre
dois elementos.

Celes, Cerqueira e Rangel Netto (2004, p 143), destacam que “[...] para
a manutenção da lista ordenada temos que encontrar a posição correta
para inserir o novo elemento”.

18.1 Exercícios comentados


Para entendermos como será o processo de inserção que mantém a lista
ordenada, vamos simular a inserção dos seguintes elementos: 10, 1, 15,
7. A primeira inserção do valor 10 pode ocorrer no início ou no final, já
que a lista está vazia. A Figura 60 apresenta esse processo:

www.esab.edu.br 166
Primeiro Primeiro
10 10

Último Último
Lista vazia Insere no início Lista vazia
ou no final
Figura 60 – Inserção com a lista vazia.
Fonte: Elaborada pelo autor (2013).

Observe que como a lista possui um único elemento, ela está ordenada.

Nas demais inserções, já que a lista não está vazia, caso o valor a ser
inserido seja menor ou igual ao valor do primeiro elemento, a inserção
será no início. Caso contrário, será verificado se o elemento a ser inserido
é maior ou igual ao valor do último elemento; sendo verdadeira a
condição, o valor é inserido no final da lista.

A Figura 61 apresenta o processo de inserção do valor 1 e do valor 15:

Primeiro Inserção do Primeiro


10 1 10
valor 1

Último Último

Inserção do Primeiro
1 10 15
valor 15

Último
Figura 61 – Inserção na lista já preenchida.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 167
Observe que a cada inserção, a lista se manteve ordenada: o valor 1 foi
inserido no início e o valor 10 foi inserido no final da lista.

Caso o valor a ser inserido não seja menor do que o primeiro elemento e
também não seja maior do que o último será necessária a inserção entre
dois elementos da lista. Para a inserção do valor 7, será preciso pesquisar
qual sua posição de inserção dentro da lista, pois o valor 7 não é menor
que o primeiro (1) e não é maior que o último (10).

Para achar a posição de inserção do valor 7, será preciso percorrer cada


elemento da lista, começando pelo primeiro até localizar um valor que
seja maior do que 7. A Figura 62 apresenta esse processo:

1 10 15 1 10 15

7
Ponteiro Ponteiro
Primeiro Último Primeiro Insere o valor 7 Último
entre os valores 1 e 10
Ponteiro começa do primeiro elemento.
Valor do próximo elemento (10) é
maior que o valor a ser inserido (7)?
Sim, então encerra a procura.

Figura 62 – Inserção entre dois elementos na lista.


Fonte: Elaborada pelo autor (2013).

Note que após a inserção do elemento com valor 7, a lista se manteve


ordenada.

Vamos verificar como criar essa operação de inserção que mantém a lista
ordenada, realizando pequenas alterações nos arquivos lista.h e lista.c
apresentados na unidade anterior que criam o Tipo Abstrato de Dados.

Vamos começar criando uma operação que retorne o nó/elemento


que está na última posição da lista e outra operação para inserir
ordenadamente o valor. Essa função será declarada no arquivo lista.h. O
Algoritmo 53 apresenta essa codificação:

www.esab.edu.br 168
Algoritmo 53 – Estrutura da lista: lista.h
01 //Estrutura de cada elemento da lista
02 struct tipo_no{
03 int dado;
04 struct tipo_no *proximo;
05 };
06 //tipo No
07 typedef struct tipo_no No;
08 //Estrutura da Lista
09 struct tipo_lista{
10 No *primeiro;
11 No *ultimo;
12 };
13 //Tipo Lista
14 typedef struct tipo_lista Lista;
15 //Operações da Lista
16 void criarLista(Lista*);
17 int listaVazia(Lista*);
18 void inserirFinal(Lista*,int);
19 void inserirInicio(Lista*,int);
20 void removerInicio(Lista*);
21 void removerFinal(Lista*);
22 void listar(Lista*);
23 No* buscar(Lista*,int);
24 int contar(Lista*,No*,int);
25 No* primeiroElemento(Lista*);
26 void inserirOrdenado(Lista*, int);
27 No* ultimoElemento(Lista*);

Fonte: Elaborado pelo autor (2013).

Note que na linha 26 foi inserido o cabeçalho da função


inserirOrdenado(), que recebe como parâmetro uma lista e um inteiro
no arquivo lista.h, apresentado na unidade 17, e que contém todas as
operações possíveis com a lista simplesmente encadeada. Na linha 27 foi
declarado o cabeçalho da operação que retornará o elemento da última
posição da lista.

Agora vamos alterar o algoritmo do arquivo lista.c, que vimos na


unidade anterior e pode ser baixado em <http://pastebin.com/
MiidckDQ>, incluindo as regras da função inserirOrdenado(Lista*, int)
e ultimoElemento(Lista*).

www.esab.edu.br 169
Primeiro vamos criar a regra da operação ultimoElemento(Lista*), como
pode ser visto no Algoritmo 54:

Algoritmo 54 – Função que retorna o último elemento da lista


01 No* ultimoElemento(Lista* lista){
02 return lista->ultimo;
03 }

Fonte: Elaborado pelo autor (2013).

A finalidade dessa função é retornar o elemento apontado pelo descritor


último da lista.

Com essa nova função já temos como construir a função que realizará a
inserção ordenada na lista, conforme o Algoritmo 55:

Algoritmo 55 – Função para inserção ordenada


01 void inserirOrdenado(Lista* lista, int valor){
02 No *ultimo = ultimoElemento(lista);
03 No *primeiro = primeiroElemento(lista);
04 if((listaVazia(lista)) || (ultimo->dado <=
valor)){
05 inserirFinal(lista,valor);
06 }
07 else{
08 if((primeiro->dado) >= valor){
09 inserirInicio(lista,valor);
10 }else{
11 No* ponteiro = primeiro;
12 while((ponteiro->proximo)->dado<valor){
13 ponteiro = ponteiro -> proximo;
14 }
15 //aloca espaço para o novo elemento
16 No *novo;
17 novo = (No*)malloc(sizeof(No));
18 novo->dado = valor;
19 //novo aponta como próximo para o próximo
do ponteiro
20 novo->proximo = ponteiro->proximo;
21 //ponteiro aponta como próximo para o novo
22 ponteiro->proximo = novo;
23 }
24 }
25 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 170
Na linha 2 é declarada e inicializada a variável ultimo com o nó que é
retornado pela função ultimoElemento(). Essa variável é utilizada como
condição do comando if da linha 4 para verificar se o elemento que será
inserido não é maior que o último elemento. Na linha 3 é declarada e
inicializada a variável primeiro com o nó que é retornado pela função
primeiroElemento(), utilizada na linha 8 para verificar se o valor a ser
inserido não é menor que o valor do primeiro elemento. Na linha 5, caso
a lista esteja vazia ou o valor inserido seja maior que o último elemento,
a função inserirFinal() é chamada, recebendo como parâmetro a lista
e o valor que será inserido. Na linha 9, se o valor inserido seja menor
que o último elemento, a função inserirInicio() é chamada, recebendo
como parâmetro a lista e o valor que será inserido. Nessas funções o valor
passado como parâmetro é alocado na memória e inserido no início ou
final da lista.

Caso a inserção no início ou final não for realizada, a função fará uma
pesquisa desde o primeiro elemento até localizar um elemento que
possua como próximo um valor maior do que o novo valor a ser inserido.
Por isso na linha 11 a instrução No* ponteiro = primeiro e na linha
12 a condição while((ponteiro->proximo)->dado<valor). A instrução
(ponteiro->proximo)->dado está referenciando o próximo endereço a
partir do ponteiro e então acessando o campo dado daquele endereço, ou
seja, o valor do próximo elemento a partir do elemento atual.

Ao final do laço, a variável ponteiro faz referência ao valor que apontará


para o novo elemento. Para a inserção do novo elemento, este apontará
como próximo para o próximo do ponteiro (regra a) e o ponteiro
apontará para o novo elemento (regra b), que podem ser visualizadas na
Figura 63:

www.esab.edu.br 171
1 2 3 4

(ER) (Posterior) (ER) (Posterior) (ER) (Posterior) (b)

Próximo (a) (a)

Elemento de Novo
Referência (ER) Objetivo: inserir o Novo Novo
novo elemento
Elemento apontado entre o elemento Novo aponta Elemento de referência
como próximo do de referência (ER) como próximo aponta como próximo
elemento de referência. e (posterior). para o posterior. para o novo elemento.
Figura 63 – Regras de inserção entre dois elementos na lista.
Fonte: Elaborada pelo autor (2013).

Observe que primeiro (a) o novo elemento aponta como próximo


para o elemento posterior, apontado como próximo do elemento de
referência para a operação. Depois o elemento de referência aponta
como próximo para o novo elemento (b), resultando no novo elemento
entre o elemento de referência e o posterior do elemento de referência.
Por exemplo, suponha que a lista possua os elementos 10,15, 21 e que
a intenção seja inserir o valor 12 entre os valores 10 e 15. Pois bem,
o valor 10 representa o elemento de referência (ER) e o valor 15 o
elemento posterior (o elemento próximo apontado por (ER)). Assim, o
novo elemento (12) apontará como próximo para o elemento posterior
(15), e o elemento referência (10) apontará como próximo para o novo
elemento (12). Resultado, a lista ficará 10, 12, 15 e 21.

Para testar os algoritmos apresentados nesta unidade, você pode baixar o


arquivo lista.h no endereço <http://pastebin.com/vLBJQt40>. O arquivo
lista.c, atualizado com as novas operações, você pode baixar em: <http://
pastebin.com/bSbJzCR3>.

Por fim, no endereço <http://pastebin.com/u1iizd0K> você terá acesso ao


código do programa que testa essas funcionalidades. Baixe os arquivos na
mesma pasta e execute-os para reforçar os assuntos abordados nesta unidade.

www.esab.edu.br 172
Nesta unidade você estudou como implementar uma lista simplesmente
encadeada em C com inserção ordenada, utilizando as funções de
inserção no início, fim e entre elementos.

Na próxima unidade você estudará um novo tipo de lista, a lista circular.

Atividade
Chegou a hora de você testar seus conhecimentos
em relação às unidades 10 a 18. Para isso, dirija-se ao
Ambiente Virtual de Aprendizagem (AVA) e responda
às questões. Além de revisar o conteúdo, você estará
se preparando para a prova. Bom trabalho!

www.esab.edu.br 173
Resumo

Na unidade 13 você estudou o conceito de Tipo Abstrato de Dados, uma


técnica fundamental e muito importante para definição de novos tipos
estruturados de dados com foco na resolução de problemas específicos.
Um Tipo Abstrato de Dados, ou TAD, é constituído por um ou vários
dados e operações que possibilitam a sua manipulação. Utilizando a
linguagem de programação em C, a implementação dos TADs se dá por
meio de módulos, que são arquivos-fonte com uma parte do programa,
também conhecido como bibliotecas.

Continuando os estudos sobre o Tipo Abstrato de Dados, você estudou,


na unidade 14, sobre como implementar um Tipo Abstrato de Dados
em C, uma técnica que permite a construção de estruturas de dados com
regras específicas, organizadas e de fácil utilização.

Na unidade 15, você conheceu o conceito da lista encadeada, que pode


ser simplesmente ou duplamente encadeada (dependendo do número de
ponteiros utilizados na ligação entre os elementos) ou circular (quando o
último elemento aponta para o primeiro elemento da lista). Você pôde,
também, conhecer sobre a aplicação da lista encadeada na construção
das filas, pilhas e árvores, que possuem regras específicas de inserção e
remoção dos dados.

Na unidade 16 você estudou sobre a lista simplesmente encadeada e


as operações que podem ser realizadas, como inserção no início e final,
assim como a remoção no início e final da lista.

Nas unidades 17 e 18 você acompanhou o desenvolvimento das operações


de manipulação da lista simplesmente encadeada, finalizando com o
desenvolvimento de uma rotina de inserção que mantém a lista ordenada,
utilizando as rotinas de inserção no início, fim e entre dois elementos.

www.esab.edu.br 174
19 Listas circulares – conceituação

Objetivo
Apresentar o conceito de listas circulares.

Na unidade anterior, você pôde desenvolver um algoritmo em C para


criação e manipulação de uma lista simplesmente encadeada, de forma que
a cada inserção de um novo elemento na lista, esta se mantivesse ordenada.

Nesta unidade, continuaremos os estudos sobre a lista simplesmente


encadeada com o objetivo de conceituar e compreender o funcionamento
da lista circular.

Esta unidade foi desenvolvida com base em Puga (2009) e Celes,


Cerqueira e Rangel Netto (2004).

Antes de começarmos os estudos sobre esse tipo peculiar de lista


simplesmente encadeada, vamos apenas relembrar o que é uma lista
simplesmente encadeada. Como o próprio nome sugere, é uma estrutura
de dados que utiliza um único ponteiro, normalmente chamado de
próximo, para interligar os seus elementos.

19.1 Definição
A lista circular é utilizada para representação de processos cíclicos, como
o armazenamento, em um computador, de informações em um arquivo
de log, no qual são registrados o nome do usuário do computador, a data
e hora do acesso ao computador e as operações que foram realizadas.

Como esse arquivo tende a ocupar muito espaço de armazenamento


devido ao grande número de registros que serão gravados, toda e
qualquer operação realizada no computador, independentemente do

www.esab.edu.br 175
usuário, será gravada nesse arquivo, e, no futuro, a falta de espaço no
disco rígido para atualização dos dados pode representar um grande
problema de integridade e confiabilidade das informações.

Uma estratégia que pode ser adotada para resolver esse problema é fazer
com que os registros mais antigos sejam substituídos pelos registros
mais atuais. Assim, o arquivo de log armazenará, sempre, no máximo
três registros e caso haja a necessidade de uma quantidade maior, os
elementos mais antigos serão substituídos. A Figura 63 apresenta a lista
circular com os registros de log:
Tamanho máximo da lista

início 1 2 3
01/10/2013 01/10/2013 01/10/2013
08:00:15 08:00:55 08:01:23
Funcionalidade Funcionalidade Funcionalidade
Usuário Usuário Usuário

Figura 63 – Lista circular de registro de log.


Fonte: Elaborada pelo autor (2013).

Observe na Figura 63 que a estrutura de dados representa a inserção


de três elementos e possui um elemento marcado como início (posição
1), porém, não possui um elemento marcado como último. Portanto, a
estrutura se caracteriza por não ter o último elemento, já que o elemento
que seria considerado o último da lista (posição 3) aponta como próximo
para o primeiro elemento da mesma lista (posição 1).

Cada elemento inserido na lista armazena a data, a hora e por quem


foi realizada a operação no computador. Os registros mais atuais são
inseridos no final da lista, de forma que os registros mais antigos fiquem
no início da lista.

Caso a quantidade de elementos inseridos seja maior que três elementos,


as informações mais antigas, no início da lista, serão substituídas por
novas informações a partir do primeiro elemento da lista (posição 1),
como pode ser observado na Figura 64.

www.esab.edu.br 176
início 1 2 3
02/10/2013 03/10/2013 05/10/2013
09:00:44 10:00:55 08:01:23
Funcionalidade Funcionalidade Funcionalidade
Usuário Usuário Usuário

1 2
01/10/2013 01/10/2013
08:00:15 08:00:55
Funcionalidade Funcionalidade
Usuário Usuário

Figura 64 – Lista circular de registro de log atualizada.


Fonte: Elaborada pelo autor (2013).

Note que dois novos registros foram inseridos na lista, nas posições 1
e 2, em substituição aos antigos elementos dessas posições. A próxima
inserção acontecerá em substituição ao elemento da posição 3, e assim
sucessivamente, começando novamente da posição 1, depois da posição 2
etc., constituindo um processo cíclico.

Diante dessa representação, torna-se mais fácil compreender porque essa


estrutura de dados é conhecida por ter o último elemento apontando
para o primeiro elemento.

Segundo Celes, Cerqueira e Rangel Netto (2004, p. 148), “[...] em uma


lista circular o último elemento tem como próximo o primeiro elemento
da lista, o que forma um ciclo. A rigor, nesse caso, não faz sentido falar
em primeiro e último elemento”.

Com relação à implementação das listas circulares, por se tratar de


uma lista simplesmente encadeada, as operações podem ser ajustadas a
partir das operações de inserção, remoção, consulta e listagem para listas
encadeadas, já definidas e estudadas nas unidades anteriores.

www.esab.edu.br 177
A criação da lista circular é um pouco mais simples se comparada com a
criação da lista encadeada, já que esse tipo de lista utiliza dois descritores
de início e último elemento, e a lista circular utiliza um único descritor
para o elemento de início, que também começará valendo NULL,
portanto, a lista será considerada vazia.

Já no caso de a lista circular possuir um único elemento, este deverá


apontar como próximo para ele mesmo, o que é representado na Figura
65, veja:

início

Figura 65 – Lista circular com um elemento.


Fonte: Elaborada pelo autor (2013).

Observe que na definição da lista circular, conforme vimos anteriormente,


o último elemento aponta para o primeiro, dessa forma, mesmo com um
único elemento na lista, essa regra deve ser garantida e mantida.

As operações possíveis na lista circular são:

• inserção no início da lista: operação semelhante à realizada na lista


encadeada, porém, quando a inserção resulta no único elemento da
lista, esse elemento deve apontar como próximo para ele mesmo.
Lembrando que o último elemento da lista sempre deverá apontar
para o primeiro, o que garante, assim, o processo cíclico;
• inserção no final: operação semelhante à realizada na lista
encadeada, porém, o elemento inserido não terá como próximo o
valor NULL, mas sim o endereço do primeiro elemento da lista.
Caso o elemento inserido seja o único da lista, deverá apontar como
próximo para ele mesmo. Mais uma vez, o último elemento da
lista sempre deverá apontar para o primeiro, o que garante, assim o
processo cíclico;

www.esab.edu.br 178
• inserção entre dois elementos: a inserção entre dois elementos
na lista circular possui as mesmas regras de funcionamento da lista
simplesmente encadeada, sem a necessidade de ajustes ou alterações;
• remoção do início: caso a remoção do início da lista resulte em
um único elemento (a), este deverá apontar como próximo para ele
mesmo, caso contrário, o último elemento da lista apontará como
próximo para o novo primeiro elemento da lista (b). A Figura 66
representa essas duas operações, observe:

início início

(a)

início início

(b)

Figura 66 – Remoção no início da lista circular.


Fonte: Elaborada pelo autor (2013).

Note que em ambos os casos o último elemento sempre aponta como


próximo para o primeiro elemento da lista circular;

• remoção no final: caso a remoção do final da lista resulte em um único


elemento (a), este deverá apontar como próximo para ele mesmo,
caso contrário, o penúltimo elemento da lista passará a ser o último
elemento e deverá apontar como próximo para o primeiro elemento da
lista (b). A Figura 67 representa essas duas operações, observe:

www.esab.edu.br 179
início início

(a)

início início

(b)

Figura 67 – Remoção no final da lista circular.


Fonte: Elaborada pelo autor (2013).

Note que em ambos os casos o último elemento sempre aponta como


próximo para o primeiro elemento da lista circular, regra que garante a
circularidade da lista;

• consultar e listar: a listagem e consulta de elementos na lista circular


implementam as mesmas operações da lista simplesmente encadeada,
porém, a condição para que a pesquisa seja encerrada se dá quando o
elemento pesquisado aponta como próximo para o primeiro elemento
da lista, diferente da lista simplesmente encadeada, cuja condição é o
endereço do elemento pesquisado valer NULL.
Cada elemento da lista circular possui a mesma estrutura do elemento da
lista simplesmente encadeada, formada por dois campos que representam
o dado que será armazenado e o endereço do próximo elemento da lista,
como pode ser observado no Algoritmo 56:

Algoritmo 56 – Definição da estrutura do elemento da lista circular


01 //Estrutura de cada elemento da lista
02 struct tipo_no{
03 int dado;
04 struct tipo_no *proximo;
05 };

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 180
Note que essa codificação define a estrutura do elemento que irá compor
a lista e possui um campo que se autorreferencia, já que o campo
próximo aponta para um elemento com a mesma estrutura do tipo_no.

Nesta unidade, você pôde estudar sobre a estrutura de dados lista circular
que por definição não possui o conceito de último elemento, uma vez
que o último elemento sempre aponta como próximo para o primeiro
elemento da lista, formando um processo cíclico. Você pôde também
conhecer e compreender as regras de inserção, remoção e consulta de
dados nesse tipo de lista.

Na próxima unidade veremos como implementar uma lista circular em C.

Até Lá!

Fórum
Caro estudante, dirija-se ao Ambiente Virtual de
Aprendizagem da instituição e participe do nosso
Fórum de discussão. Lá você poderá interagir com
seus colegas e com seu tutor de forma a ampliar,
por meio da interação, a construção do seu
conhecimento. Vamos lá?

www.esab.edu.br 181
20 Listas circulares – codificação

Objetivo
Apresentar a codificação e a manipulação de listas circulares.

Na unidade anterior, você pôde estudar sobre a estrutura de dados em


uma lista circular, que por definição não possui o conceito de último
elemento, já que o que seria o último elemento da lista aponta como
próximo para o primeiro elemento desta, formando uma estrutura de
dados cíclica.

Nesta unidade, com o apoio teórico de Celes, Cerqueira e Rangel Netto


(2004), você estudará como implementar em C as regras de inicialização,
inserção, busca e remoção nesse tipo de lista.

20.1 Função de inicialização, inserção, busca e


remoção
Para exemplificar uma lista circular, cada elemento da lista será
implementado de forma que seja possível armazenar um valor do tipo
inteiro, e por se tratar de uma lista simplesmente encadeada, cada
elemento terá um campo chamado próximo para armazenar o endereço
do próximo elemento da lista. Observe essas definições no Algoritmo 57:

Algoritmo 57 – Definição do nó e da lista


01 struct tipo_no {
02 int dado;
03 struct tipo_no * proximo;
04 };
05 typedef struct tipo_no No;
06 struct tipo_lista{
07 No *primeiro;
08 };
09 typedef struct tipo_lista ListaCircular;

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 182
Note que as linhas 1 a 4 criam tipo estruturado tipo_no, composto por
dois campos, dado e proximo, e que representará cada elemento que será
manipulado na lista circular. Na linha 5 é definido, por meio da cláusula
typedef, o tipo de dado No. As linhas 6 a 8 definem a estrutura da lista
circular formada por um descritor apenas, chamado primeiro, que é do
tipo No. Por fim, na linha 9 é declarado o tipo ListaCircular.

Como a lista circular possui um único descritor, a operação de


inicialização da lista deverá preencher o descritor primeiro com NULL,
como é apresentado no Algoritmo 58:

Algoritmo 58 – Inicialização da lista circular


01 void inicializarLista(ListaCircular* lista){
02 lista->primeiro = NULL;
03 }

Fonte: Elaborado pelo autor (2013).

A função inicializar() deverá ser chamada uma única vez, no momento da


criação da lista circular. Celes, Cerqueira e Rangel Netto (2004, p. 148)
salientam que “[...] casos em que a lista esteja vazia devem ser tratados,
de forma que o ponteiro para um elemento inicial deve valer NULL”.

Para a inserção dos elementos na lista circular, será implementada a


operação de inserção no início da lista, cujas regras são:

www.esab.edu.br 183
Alocar espaço de memória para o novo elemento;
Gravar nesse espaço de memória o dado;
Se lista está vazia então
Descritor primeiro aponta para o novo elemento;
Novo elemento aponta como próximo para ele mesmo;
fim_se
senão
Novo elemento aponta como próximo para o primeiro elemento da lista;
Percorrer a lista até localizar o elemento (último) que aponta com o próximo para o
primeiro elemento da lista;
O último elemento, que apontava para o primeiro da lista, passa a apontar para o
novo
elemento inserido;
fim_senão;
Descritor primeiro aponta para o novo elemento;

Note que o conceito de último elemento da lista circular não existe, no


entanto, podemos considerar como último o elemento que aponta como
próximo para o primeiro elemento da lista circular.

Agora, vamos verificar como essas regras serão codificadas em C.


Acompanhe o Algoritmo 59:

www.esab.edu.br 184
Algoritmo 59 – Inserção no início da lista circular
01 void inserirInicio(ListaCircular* lista, int
valor){
02 No *novo;
03 novo = (No*)malloc(sizeof(No));
04 novo->dado = valor;
05 if(lista->primeiro == NULL){
06 novo->proximo = novo;
07 }else{
08 novo->proximo = lista->primeiro;
09 No* aux = lista->primeiro;
10 while(aux->proximo != lista->primeiro){
11 aux = aux -> proximo;
12 }
13 aux->proximo = novo;
14 }
15 lista->primeiro = novo;
16 }

Fonte: Elaborado pelo autor (2013).

Note que a linha 2 declara a variável novo, que armazenará o endereço


de alocação do No na memória do computador. Na linha 3, é alocado
um espaço de memória para essa variável. Na linha 4 o valor passado
como parâmetro no método é inserido no campo dado da variável novo,
alocada na memória. Na linha 5 é verificado se a lista está vazia (lista-
>primeiro ==NULL), caso essa condição seja verdadeira, o descritor
primeiro da lista apontará para o endereço do novo elemento que está
sendo inserido (lista->primeiro = novo). Em caso de condição falsa, a
lista é vazia, e as instruções das linhas 8 a 12 serão executadas, sendo que
na linha 8 o novo elemento inserido passa a apontar como próximo para
o primeiro elemento da lista. A instrução da linha 9 atribui à variável
aux o endereço do primeiro elemento da lista. Essa variável representará
o endereço de cada elemento da lista, já que a variável começa valendo
o endereço do primeiro elemento e a cada vez que o comando de laço
da linha 10 é executado essa variável é atualizada com o endereço do
próximo elemento apontado por ela. Observe a Figura 68:

www.esab.edu.br 185
Passo

(aux)
1

(aux)
2

(aux)
3

Figura 68 – Percorrendo a lista circular.


Fonte: Elaborada pelo autor (2013).

Observe que o ponteiro aux começa valendo o endereço do primeiro


elemento. No comando de laço a condição avaliada é se o próximo
elemento de aux é diferente do primeiro elemento da lista. Como no
passo 1 essa condição é verdadeira, aux avança para o próximo elemento
da lista e recomeça o laço. No passo 2 do laço é verificado se o próximo
elemento de aux é diferente do primeiro elemento da lista, e como no
passo 2 essa condição é verdadeira, aux avança para o próximo elemento
da lista e recomeça o laço. No passo 3 do laço é verificado se o próximo
elemento de aux é diferente do primeiro elemento da lista, e como a
condição é falsa, o laço é encerrado e aux estará valendo o endereço do
último elemento da lista, que na instrução da linha 14, apontará para o
novo elemento da lista.

Por fim, na linha 15, o novo elemento inserido passa a ser reconhecido
como o primeiro da lista.

A lógica da operação de localização do último elemento da lista pode


ser ajustada para a implementação da regra de consulta de um valor na
lista circular, já que a pesquisa começa do primeiro elemento e vai até o

www.esab.edu.br 186
último elemento ou até que o valor seja localizado, como é apresentado
no Algoritmo 60:

Algoritmo 60 – Buscando valor na lista circular


01 void buscar(ListaCircular* lista,int valor){
02 if(lista->primeiro != NULL){
03 No* aux = lista->primeiro;
04 int achou =0;
05 do{
06 if(aux->dado == valor){
07 achou = 1;
08 }else{
09 aux = aux->proximo;
10 }
11 }while((aux != lista->primeiro) && (achou
==0));
12 if(achou == 1) printf("\n Valor Localizado
na Lista Circular!");
13 else printf("\n Valor Inexistente na Lista
Circular!");
14 }
15 }

Fonte: Elaborado pelo autor (2013).

Note que na linha 3 a variável aux começa valendo o endereço do


primeiro elemento da lista e na linha 4 a variável achou começa valendo
0 (falso). Nas instruções das linhas 5 a 10, é verificado se o valor no
endereço da variável aux é igual ao valor procurado na lista, e se a
condição for verdadeira, a variável achou é mudada para 1 (verdadeiro),
caso contrário a variável aux é atualizada para o endereço do próximo
elemento da lista. A busca é controlada pelo laço (do-while), que a cada
interação verifica se o endereço da variável aux é igual ao endereço do
primeiro elemento da lista (o que significa que a lista voltou para o
seu início) ou se a variável achou vale 1 (o que significa que o valor foi
localizado). Ao sair do laço, nas linhas 12 e 13, é verificado se a variável
achou vale 1, então o valor foi localizado, caso contrário o valor não
existe na lista.

O Algoritmo 61 apresenta a regra de remoção no final da lista circular,


que também utilizará as regras de percorrer todos os elementos da lista
para encontrar o último elemento (aquele que aponta para o primeiro),
pois ele será removido:

www.esab.edu.br 187
Algoritmo 61 – Removendo valor no final da lista circular
01 void removerFinal(ListaCircular* lista){
02 if(lista->primeiro != NULL){
03 if((lista->primeiro)->proximo == lista-
>primeiro){
04 No* aux = lista->primeiro;
05 free(aux);
06 lista->primeiro = NULL;
07 }else{
08 No* aux = lista->primeiro;
09 No* anterior = NULL;
10 while(aux->proximo != lista->primeiro){
11 anterior = aux;
12 aux=aux->proximo;
13 }
14 free(aux);
15 anterior->proximo = lista->primeiro;
16 }
17 }
18 }

Fonte: Elaborado pelo autor (2013).

Observe que na instrução da linha 3 é verificado se o primeiro elemento


da lista aponta para ele mesmo, pois se essa condição for verdadeira, a lista
possui um único elemento, que é o primeiro da lista. Sendo removido
pela instrução da linha 5, em seguida, na linha 6, o descritor primeiro é
atualizado para NULL, uma vez que a lista estará vazia após a remoção.

Caso a lista não tenha apenas um elemento, serão percorridos todos


os elementos da lista até que se localize o último elemento. Por isso,
na linha 8, a variável aux é inicializada com o endereço do primeiro
elemento, e na condição do comando de laço, na linha 10, é verificado se
o próximo endereço apontado pela variável aux é diferente do endereço
do primeiro elemento da lista. A instrução da linha 11 é responsável por
guardar o endereço do penúltimo elemento da lista, conforme ilustração
da Figura 69:

www.esab.edu.br 188
Passo

(aux)
(anterior <- NULL) 1

(anterior) (aux)
2

(anterior) (aux)
3

Figura 69 – Percorrendo a lista circular para localizar o último e penúltimo elemento.


Fonte: Elaborada pelo autor (2013).

Observe que a cada laço a variável aux armazena o endereço do elemento


pesquisado e a variável anterior o endereço do elemento já visitado. Ao final
do laço, a variável aux representará o último elemento da lista, e a variável
anterior o penúltimo elemento da lista. Na linha 13 o último elemento é
removido e o penúltimo elemento passará a ser o último após a remoção.

Nesta unidade, você pôde estudar como implementar em C as operações


de inserção no início da lista, a remoção no final, bem como inicializar e
buscar um elemento na lista.

Na próxima unidade, estudaremos outras operações com a lista circular,


na forma de exercícios comentados.

www.esab.edu.br 189
21 Listas circulares – exercícios

Objetivo
Apresentar exercícios comentados sobre listas circulares.

Na unidade anterior, você estudou como implementar em C as operações


de inserção no início da lista, a remoção no final e como inicializar e
buscar um elemento na lista.

Nesta unidade, vamos implementar uma lista circular com três


descritores, o início da lista, já utilizado nas codificações anteriores, e
mais dois descritores, responsáveis por armazenar o maior e menor valor
da lista. Para isso, utilizaremos Celes, Cerqueira e Rangel Netto (2004)
como aporte teórico.

21.1 Exercícios comentados


A estrutura de cada elemento da lista circular será a mesma dos exercícios
anteriores, contendo o dado do tipo inteiro e o campo próximo que
aponta para o elemento subsequente da estrutura de dados. O Algoritmo
62 apresenta essa estrutura:

Algoritmo 62 – Estrutura de cada elemento da lista circular


01 struct tipo_no {
02 int dado;
03 struct tipo_no * proximo;
04 };

Fonte: Elaborado pelo autor (2013).

A lista circular que será implementada possuirá uma estrutura diferente,


conforme é apresentada no Algoritmo 63:

www.esab.edu.br 190
Algoritmo 63 – Estrutura da lista circular
01 struct tipo_lista{
02 No *primeiro;
03 int maior,
04 int menor;
05 };

Fonte: Elaborado pelo autor (2013).

Como a intenção é gerenciar a lista de forma que se conheça o maior e


menor valor inserido na lista circular, a sua estrutura será composta pelos
descritores primeiro, maior e menor.

Na inicialização da lista circular, o descritor primeiro será inicializado


com NULL e os descritores maior e menor com 0. O Algoritmo 64
apresenta essa regra:

Algoritmo 64 – Inicialização da lista circular


01 void inicializarLista(ListaCircular* lista){
02 lista->primeiro = NULL;
03 lista->maior = 0;
04 lista->menor = 0;
05 }

Fonte: Elaborado pelo autor (2013).

Sempre que a lista circular for inicializada, seguindo o padrão, o maior e


menor valor valerão zero. Já o descritor começará valendo NULL.

A inserção na lista circular será realizada no final da lista. A cada


operação de inserção será verificado se o novo valor é maior que o maior
valor já inserido na lista ou menor que o menor valor inserido na lista.
Caso uma dessas condições seja verdadeira, o novo valor será guardado
no descritor maior ou menor. Caso a lista tenha apenas um elemento,
esse elemento será o primeiro, o maior e o menor da lista. O Algoritmo
65 apresenta essas regras:

www.esab.edu.br 191
Algoritmo 65 – Inserção no final da lista circular
01 void inserirFinal(ListaCircular* lista, int
valor){
02 No *novo;
03 novo = (No*)malloc(sizeof(No));
04 novo->dado = valor;
05 if(lista->primeiro == NULL){
06 novo->proximo = novo;
07 lista->maior = valor;
08 lista->menor = valor;
09 lista->primeiro = novo;
10 }else{
11 if(valor > lista->maior) lista->maior =
valor;
12 else
13 if(valor < lista->menor) lista->menor =
valor;
14 No* aux = lista->primeiro;
15 while(aux->proximo != lista->primeiro){
16 aux = aux->proximo;
17 }
18 aux->proximo = novo;
19 novo->proximo = lista->primeiro;
20 }
21 }

Fonte: Elaborado pelo autor (2013).

Caso a instrução da linha 5, que verifica se a lista está vazia (lista-


>primeiro==NULL), seja verdadeira, o valor do elemento inserido na
lista é armazenado nos descritores maior (linha 7) e menor (linha 8).
Se a lista tem um único valor, ele é o maior e menor valor da lista, e
o endereço do elemento inserido na lista é armazenado no descritor
primeiro (linha 9). Por se tratar de uma lista circular, o único elemento
deve apontar para ele mesmo, por isso a instrução da linha 6 (novo-
>proximo = novo).

As instruções das linhas 11 a 20 somente serão executadas se a lista não


estiver vazia, sendo que a instrução da linha 11 verifica se o novo valor
inserido é maior que o maior valor existente na lista, e caso essa condição
seja verdadeira, o descritor maior será atualizado com esse novo valor.
Já a instrução da linha 13 verifica se o novo valor é menor que o menor
valor da lista, e caso essa condição seja verdadeira, o descritor menor será
atualizado com esse novo valor.

www.esab.edu.br 192
As instruções das linhas 14 a 17 fazem com que todos os elementos da
lista sejam visitados até que se chegue ao último elemento (aquele que
aponta para o primeiro elemento, aux->proximo!= lista->primeiro).
Ao sair do comando de laço, a variável aux terá o endereço do último
elemento, que na instrução da linha 18 passa a apontar para o novo
elemento e que, por sua vez, na linha 19, passa a apontar para o primeiro
elemento da lista. A Figura 70 representa esse processo de inserção no
final quando a lista não está vazia:

primeiro primeiro

(novo)
(a) (b)
primeiro primeiro
(aux)

(novo) (novo)
(c) (d)
Figura 70 – Inserção no final da lista circular.
Fonte: Elaborada pelo autor (2013).

Observe que a inserção do novo elemento ocorrerá no final da lista (b),


portanto, o último elemento da lista (que apontava para o primeiro),
representado pela variável aux, passará a apontar como próximo para o
novo elemento (c). Por fim, o novo elemento apontará para o primeiro
para que o processo cíclico seja mantido (d).

Para remover os elementos da lista circular, faremos a remoção do início


da lista, e caso o elemento removido seja o maior ou menor da lista,
será preciso percorrê-la para localizar o novo maior ou menor entre os
elementos que nela sobraram. Segundo Celes, Cerqueira e Rangel Netto
(2004, p. 140),

[...] a função para retirar um elemento da lista é mais complexa. Se descobrimos que
o elemento a ser retirado é o primeiro da lista, devemos fazer o novo valor da lista
passar a ser o ponteiro para o segundo elemento da lista, e então podemos liberar o
espaço para o elemento que desejamos retirar.
www.esab.edu.br 193
O Algoritmo 66 apresenta essa regra de remoção:

Algoritmo 66 – Remoção no início da lista circular


01 void removerInicio(ListaCircular* lista){
02 if(lista->primeiro !=NULL){
03 if(lista->primeiro->proximo == NULL){
04 No* aux = lista->primeiro;
05 free(aux);
06 lista->primeiro = NULL;
07 lista->maior=0;
08 lista->menor=0;
09 }else{
10 No* aux = lista->primeiro;
11 No* p = aux->proximo;
12 No* ultimo = NULL;
13 if(lista->primeiro->dado == lista->maior)
lista->maior = p->dado;
14 if(lista->primeiro->dado == lista->menor)
lista->menor = p->dado;
15 while(p != aux){
16 if(p->dado > lista->maior) lista->maior
= p->dado;
17 else
18 if(p->dado < lista->menor) lista->menor
= p->dado;
19 if(p->proximo == aux) ultimo = p;
20 p=p->proximo;
21 }
22 lista->primeiro = aux->proximo;
23 ultimo->proximo = lista->primeiro;
24 free(aux);
25 }
26 }
27 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 3 é verificado se a lista possui um único elemento


(lista->primeiro->proximo == NULL). Caso a condição seja verdadeira,
o único elemento da lista será removido, por isso os descritores primeiro,
maior e menor são inicializados com NULL e 0 respectivamente. Caso
a lista não tenha apenas um elemento, o endereço do primeiro elemento
(linha 10) é armazenado na variável aux, e na variável p o endereço
do segundo elemento (aux->proximo). Na linha 13 é verificado se o
elemento que está sendo removido do início da lista é o maior elemento,

www.esab.edu.br 194
caso a condição seja verdadeira, o descritor maior é atualizado com
o valor do segundo elemento da lista. Na linha 14 é verificado se o
elemento que está sendo removido do início da lista é o menor elemento,
caso a condição seja verdadeira, o descritor menor é atualizado com o
valor do segundo elemento da lista. Nas linhas 15 a 22, é realizada uma
pesquisa em todos os elementos da lista, verificando se o valor é o maior
(linha 16) ou o menor (linha 18). Caso o elemento visitado na lista seja
o último (linha 20), o endereço desse elemento é armazenado na variável
ultimo. A instrução da linha 23 especifica que o início da lista passa
a ter o endereço do segundo elemento da lista (o próximo do aux) e a
instrução da linha 24 especifica que o último elemento passa a apontar
para o primeiro da lista. Somente na instrução da linha 25 será removido
o elemento que era o primeiro da lista.

A Figura 71 representa esse processo de remoção no início da lista que


não está vazia:
Maior: 15 Menor: 8 Maior: 15 Menor: 8 Maior: 15 Menor: 8
(aux) (aux)
primeiro primeiro primeiro
p (último)
15 8 10 15 8 10 8 10

(a) (b)

Maior: 10 Menor: 8 Maior: 10 Menor: 8


primeiro
p (último) (último)
8 10 8 10

(c) (d)
Figura 71 – Remoção do primeiro elemento da lista.
Fonte: Elaborada pelo autor (2013).

Observe que com a remoção do primeiro elemento da lista, o qual


também era o maior da lista, foi necessário que, no processo de percorrer
todos os elementos da lista circular, buscando encontrar o último
elemento, fosse verificado qual o novo maior valor, já que o menor valor
não foi alterado com a remoção no início da lista.
www.esab.edu.br 195
Caro aluno, para testar as operações de inserção e remoção apresentadas
nesta unidade, baixe o código-fonte de todo o exercício no endereço
<http://pastebin.com/cUV3qB2Q> e utilize a ferramenta de
programação de sua preferência para compilar e executar o programa.

Nesta unidade, você teve a oportunidade de aprofundar seus


conhecimentos sobre a lista circular, desenvolvendo as rotinas de
inserção no final e remoção no início da lista; mais que isso, foi possível
implementar essas rotinas de forma que o maior e menor valor cadastrados
possam ser consultados a qualquer momento e com valores atuais.

Na próxima unidade, começaremos o estudo da lista duplamente


encadeada. Vamos em frente!

www.esab.edu.br 196
Listas duplamente encadeadas –
22 conceituação
Objetivo
Apresentar o conceito de listas duplamente encadeadas.

Na unidade anterior, você estudou sobre a lista circular e como


desenvolver as rotinas de inserção no final e remoção do início da lista,
com a possibilidade de consultar o maior e menor valor da lista.

Nesta unidade, começaremos a conhecer a lista duplamente encadeada,


sua definição, como inicializá-la, as regras para as operações de inserção,
busca e remoção de dados.

O material desta unidade foi desenvolvido com base em Celes, Cerqueira


e Rangel Netto (2004) e Forbellone et al. (2005).

22.1 Definição
Nas unidades anteriores, pudemos estudar e desenvolver a lista
simplesmente encadeada, que se caracteriza por cada elemento armazenar
um ponteiro para o próximo elemento da lista. Celes, Cerqueira e
Rangel Netto (2004, p. 149) destacam que um elemento com um único
ponteiro “[...] não permite percorrer eficientemente os elementos em
ordem inversa, isto é, do final para o início da lista”. Isso ocorre em
razão da característica da lista simplesmente encadeada, que caminha no
sentido do início para o próximo elemento da lista, até que se chegue ao
último elemento.

O uso de um único ponteiro em cada elemento também dificulta a


remoção dos elementos no final da lista, já que obriga que ela seja
percorrida desde o início até que se encontre o penúltimo elemento, o
qual, após a remoção do último elemento, será definido como último da

www.esab.edu.br 197
lista. Esse processo, que percorre todos os elementos até que se chegue ao
penúltimo, requer um considerável tempo de processamento, que tende a
aumentar com a inclusão de novos elementos.

Para facilitar a inserção no final ou entre elementos, bem como a


possibilidade de percorrer a lista em dois sentidos, é possível utilizar mais
um ponteiro para cada elemento, responsável por indicar o elemento
anterior. Assim, para cada elemento da lista tem-se a referência do
próximo e do elemento antecessor.

Para Forbellone et al. (2005, p. 175), nas listas duplamente encadeadas


“[...] além de cada elemento indicar o elemento seguinte, também
indicam aquele que o antecede”.

A Figura 72 representa uma lista duplamente encadeada:

primeiro último
NULL

NULL

a p a p a p

Figura 72 – Lista duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Observe que cada elemento possui dois ponteiros, para o elemento


anterior (a) e para o elemento posterior (p). O primeiro elemento, como
não possui elemento anterior a ele, tem ponteiro anterior com valor
NULL. Do mesmo modo, como o último elemento não possui elemento
posterior a ele, tem ponteiro próximo com valor NULL.

Segundo Celes, Cerqueira e Rangel Netto (2004, p. 149), “[...] a partir


do descritor último, podemos percorrer a lista em ordem inversa,
bastando acessar continuamente o elemento anterior até alcançar o
primeiro elemento da lista”. A Figura 73 representa essas duas formas de
percorrer a lista duplamente encadeada:

www.esab.edu.br 198
sentido, primeiro-último
primeiro último

NULL

NULL
a p a p a p

sentido último-primeiro
Figura 73 – Percorrendo a lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Observe que cada elemento, por possuir dois ponteiros, permite


facilmente que os dois sentidos sejam utilizados para percorrer a lista,
sem a necessidade de alterações ou ajustes na sua estrutura.

Veremos a seguir como inicializar, inserir, pesquisar e remover elementos


nesse tipo de lista.

22.2 Função de inicialização, inserção, busca e


remoção
Na inicialização da lista duplamente encadeada, as regras a serem
implementadas são as mesmas da lista simplesmente encadeada, de forma
que os descritores primeiro e último sejam inicializados com NULL, uma
vez que a lista estará vazia. Caso a lista possua apenas o descritor início,
de qualquer forma ele necessita ser inicializado com NULL, lembrando
que nas listas circulares o descritor último não existe, pois o elemento
que seria o último sempre indica para o primeiro elemento da lista.

As principais alterações nas regras de funcionamento da lista duplamente


encadeada em relação ao funcionamento da lista simplesmente encadeada
ocorrem nas operações de inserção e remoção de elementos, pois as
operações precisam atualizar além do ponteiro próximo, também o
ponteiro anterior. Essa ação facilita a implementação das regras, pois para
localizar o ponteiro anterior ou próximo de um determinado elemento
não é preciso percorrer a lista, o elemento de referência já possui, na
sua estrutura, essas informações nos campos anterior e posterior, como
veremos a seguir.

www.esab.edu.br 199
Inserção no início da lista: a inserção do início da lista tem como
finalidade garantir que o elemento inserido seja definido como o
primeiro da lista. Na inserção do novo elemento na lista, o que muda
é que o primeiro elemento deverá apontar como anterior para o novo
elemento inserido (b), e o novo elemento deverá apontar como próximo
para o primeiro elemento (a), por fim, o novo elemento deve ser definido
como primeiro da lista (c). A Figura 74 apresenta essas regras:

primeiro último (novo) primeiro último

(a)

(novo) primeiro último primeiro último

(b) (c)
Figura 74 – Inserção no início da lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Inserção no final da lista: a inserção do final da lista tem como


finalidade garantir que o elemento inserido seja definido como o último
da lista. Na inserção do novo elemento na lista, o novo elemento deverá
apontar como anterior para o último elemento inserido (a), e o último
elemento deverá apontar como próximo para o novo elemento (b),
por fim, o novo elemento deve ser definido como último da lista (c). A
Figura 75 apresenta essas regras:

www.esab.edu.br 200
primeiro último último primeiro (novo)

(a)

último primeiro (novo) primeiro último

(b) (c)
Figura 75 – Inserção no final da lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Remoção do início da lista: a remoção do início da lista tem como


finalidade garantir que o segundo elemento, caso exista, seja definido
como o novo início da lista. Na remoção do primeiro elemento, o novo
primeiro elemento será o próximo elemento apontado pelo primeiro
elemento (a), depois o primeiro elemento será removido (a), e em
seguida o novo primeiro elemento deverá mudar o apontador anterior
para NULL (b). Por fim, o segundo elemento da lista será referenciado
pelo descritor primeiro (c). A Figura 76 apresenta essas regras:

primeiro último primeiro último

(a)

primeiro último primeiro último

(b) (c)
Figura 76 – Remoção do início da lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 201
Remoção do final da lista: a remoção do final da lista tem como
finalidade garantir que o penúltimo elemento, caso exista, seja definido
como o novo final da lista. Na remoção do último elemento, o novo
último elemento será o elemento anterior apontado pelo último
elemento (a), depois o último elemento será removido (a), e, em seguida,
o novo último elemento deverá mudar o apontador próximo para
NULL (b). Por fim, o penúltimo elemento da lista será referenciado pelo
descritor último (c). A Figura 77 apresenta essas regras:

primeiro último primeiro último

(a)

primeiro último primeiro último

(b) (c)

Figura 77 – Remoção do final da lista duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Busca na lista: a pesquisa por um valor na lista duplamente encadeada


segue as mesmas regras da lista simplesmente encadeada, porém, o
sentido da busca pode ser do último para o primeiro elemento (b),
utilizando o ponteiro anterior para percorrer a lista ou o sentido do
primeiro para o último (a), utilizando o ponteiro próximo para percorrer
a lista. A Figura 78 apresenta essas regras:
sentido, primeiro-último - usando o ponteiro próximo

primeiro último
NULL

NULL

a p a p a p

sentido, último-primeiro - usando o ponteiro anterior


Figura 78 – Busca na lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 202
Inserção de elemento entre outros dois elementos: a inserção entre
dois elementos na lista duplamente encadeada é mais simples e rápida. A
Figura 79 apresenta essas regras:
anterior ao REF anterior ao REF
primeiro último primeiro último primeiro último

1 3 1 3 1 3
(REF) (REF) (REF)
(novo) 2 (novo) 2 (novo) 2
(a) (b) (c)
anterior ao REF anterior ao REF
primeiro último primeiro último

1 3 1 3
(REF) (REF)
(novo) 2 (novo) 2
(d) (e)
Figura 79 – Inserção entre dois elementos na lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Observe que o elemento com valor 2 será inserido entre os elementos 1 e


3, e o elemento 3 é a referência para inserção. O elemento 1 é facilmente
acessado na lista como sendo o anterior da referência, assim, o elemento
anterior da referência pode apontar como próximo para o novo elemento
(b). Dessa forma, o elemento 1 apontará como próximo para o novo
elemento, o valor 2, que deverá apontar como anterior para o anterior da
referência (c), ou seja, o novo elemento 2 terá como o anterior o mesmo
anterior da referência que é o valor 1. O novo elemento, valor 2, terá
como próximo a própria referência, assim, o valor apontará para o valor
3 (d). Por fim, a referência (valor 3) terá como anterior o novo valor
inserido (e).

Remoção do elemento entre dois elementos: a remoção de um


elemento que está entre dois outros elementos utiliza os ponteiros
anterior e próximo do elemento que será removido, facilitando a
operação. A Figura 80 apresenta essa regra:

www.esab.edu.br 203
primeiro último
(REF)
1 2 3
(a)

primeiro último
(REF)
1 2 3
(b)

primeiro último
(REF)
1 2 3
(c)

primeiro último
(REF)
1 2 3
(d)

primeiro último

1 3
(e)
Figura 80 – Remoção do elemento entre dois elementos na lista duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Observe que o elemento indicado como REF será removido (a),


portanto, o elemento anterior ao REF deverá apontar como próximo
para o elemento posterior ao REF (b). Ou seja, como o elemento
com valor 2 apontava como próximo para o elemento 3, o elemento
1 também apontará como próximo para o elemento 3. O elemento
posterior de REF (3) terá como elemento anterior o mesmo anterior ao
REF (1), assim, o elemento 3 apontará como anterior para o elemento
1 (c). Após esses referenciamentos (b e c), o elemento REF pode ser
removido (e).

www.esab.edu.br 204
Nesta unidade, você pôde estudar a lista duplamente encadeada, que
recebe esse nome porque cada elemento da lista utiliza dois ponteiros
para interligar os elementos, chamados anterior e posterior. Esses dois
ponteiros possibilitam que a lista possa ser percorrida em dois sentidos,
do primeiro para o último e do último para o primeiro. Além disso, as
operações de inserção e remoção, principalmente no final da lista e entre
elementos, tornam-se mais simples com o uso desses dois ponteiros,
evitando que pesquisas sejam realizadas para localizar os elementos
anterior ou posterior de uma referência na lista.

Na próxima unidade, veremos como implementar algumas dessas


operações utilizando a linguagem de programação C.

www.esab.edu.br 205
Listas duplamente encadeadas –
23 codificação
Objetivo
Apresentar a codificação e manipulação de listas duplamente
encadeadas.

Na unidade anterior, você estudou e conheceu a lista duplamente


encadeada, que utiliza dois ponteiros para interligar os elementos
anterior e próximo. Esses dois ponteiros possibilitam que a lista possa ser
percorrida em dois sentidos, do primeiro para o último e do último para
o primeiro. Além disso, as operações de inserção e remoção se tornam
mais simples com o uso desses dois ponteiros.

Nesta unidade, com o apoio teórico de Celes, Cerqueira e Rangel Netto


(2004), veremos como implementar em C as rotinas de inicialização,
inserção, remoção e busca.

23.1 Codificação das funções de inicialização,


inserção, busca e remoção
Para a codificação das operações de inicialização, inserção, busca e
remoção de elementos na lista duplamente encadeada, vamos codificar as
seguintes regras:

• declarar, para armazenamento dos dados, a estrutura que representará


cada elemento da lista com um campo do tipo inteiro e dois campos
do tipo No, que armazenarão o endereço do elemento anterior e
posterior. Para tanto serão utilizados os comandos struct e typedef;
• declarar a estrutura que representará a lista duplamente encadeada
com dois campos do tipo No, que armazenarão o endereço do
primeiro e último elemento. Para tanto serão utilizados os comandos
struct e typedef;

www.esab.edu.br 206
• para inicializar a lista, os ponteiros anterior e próximo da lista serão
inicializados com NULL;
• para a inserção na lista, serão implementadas as funções de inserção
no início e no final da lista. As duas funções receberão como
parâmetro a lista a ser manipulada e o valor a ser inserido;
• a remoção da lista será realizada no final, assim, a função de remoção
receberá como parâmetro a lista que será manipulada;
• a rotina de busca de valores na lista será implementada de forma
que receba como parâmetro a lista a ser manipulada e o valor a
ser pesquisado. A função retornará o endereço do elemento, se
localizado, ou valerá NULL, caso o valor não exista na lista.
Veremos a seguir como implementar essas regras e analisar as observações
sobre cada uma das rotinas. Ao final da unidade, você poderá baixar na
internet todo o código desenvolvido.

23.2 Exercícios Comentados


Nesse exercício vamos desenvolver uma lista duplamente encadeada para
armazenar o maior e menor valor existente na lista em dois de seus novos
descritores. Isso será feito de forma que toda operação de inserção ou
remoção verifique se o elemento que está sendo inserido é o maior ou
o menor elemento da lista e, assim, atualize essas informações nos seus
descritores. Dessa forma, a informação do maior e do menor valor da
lista pode ser consultada, bastando para isso que os descritores com o
maior e menor valor sejam acessados.

Vamos primeiramente analisar a estrutura de cada elemento da lista


duplamente encadeada que possui três campos, o valor inteiro que
será manipulado, representado pelo campo dado, o ponteiro anterior,
representado pelo campo anterior, e o ponteiro próximo, representado pelo
campo próximo, ambos do tipo No*. O Algoritmo 67 apresenta essa regra:

www.esab.edu.br 207
Algoritmo 67 – Declaração do elemento da lista duplamente encadeada
01 struct tipo_no {
02 int dado;
03 struct tipo_no * proximo;
04 struct tipo_no * anterior; novo campo
05 };
06
07 typedef struct tipo_no No;

Fonte: Elaborado pelo autor (2013).

Observe que a estrutura tipo_no é muito semelhante à estrutura do


elemento simplesmente encadeada, a única diferença é a inclusão do
campo anterior. A estrutura será representada pelo tipo No, declarado na
linha 7.

A lista duplamente encadeada será implementada com dois descritores,


primeiro e último, por isso a estrutura será definida com dois campos do
tipo No, conforme é apresentado no Algoritmo 68:

Algoritmo 68 – Estrutura da lista duplamente encadeada


08 struct tipo_lista {
09 No *primeiro;
10 No *ultimo;
11 };
12 typedef struct tipo_lista ListaDupla;

Fonte: Elaborado pelo autor (2013).

Observe que na linha 12, é declarado o tipo de dados ListaDupla.

A inicialização da lista duplamente encadeada será responsável por


inicializar os dois descritores, primeiro e último com NULL, conforme o
Algoritmo 69:

Algoritmo 69 – Inicialização da lista duplamente encadeada


13 void inicializarLista (ListaDupla* lista){
14 lista->primeiro = NULL;
15 lista->ultimo = NULL;
16 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 208
Observe que a função de inicialização recebe como parâmetro um
argumento do tipo ListaDupla.

Para inserção na lista duplamente encadeada, serão desenvolvidas as


regras de inserção no início, final e entre elementos. O Algoritmo 70
apresenta a rotina de inserção no final da lista:

Algoritmo 70 – Inserção no final da lista duplamente encadeada


17 void inserirFinal (ListaDupla* lista, int valor){
18 No *novo;
19 novo = (No*)malloc(sizeof (No));
20 novo->dado = valor;
21 novo->anterior = NULL;
22 novo->proximo = NULL;
23 if(lista->primeiro == NULL){
24 lista->primeiro == novo;
25 }else{
26 lista->ultimo->proximo = novo;
27 novo->anterior = lista->ultimo;
28 }
29 lista->ultimo = novo;
30 }

Fonte: Elaborado pelo autor (2013).

Observe que a função de inserção no final da lista recebe como


parâmetro a lista que será manipulada e o valor que será inserido. As
instruções das linhas 18 a 22 são responsáveis por declarar a variável novo
(linha 18), e alocar um espaço de memória para essa variável (alocação
dinâmica na linha 19), sendo a instrução da linha 20 responsável por
armazenar o novo valor no campo dado da variável alocada em memória,
logo em seguida, nas linhas 21 e 22, os campos anterior e proximo são
inicializados com NULL.

Se for a primeira inserção na lista (lista->primeiro == NULL), o descritor


primeiro é inicializado com o endereço do primeiro elemento, caso
contrário, o último elemento da lista deverá apontar como próximo
para o novo elemento (linha 26) e o novo elemento deverá apontar
como anterior para o último elemento (linha 27). Como se trata de
uma inserção no final, o novo elemento sempre será referenciado pelo
descritor último.

www.esab.edu.br 209
A inserção no início possui o mesmo processo de alocação de memória
e inicialização dos ponteiros anterior e próximo do novo elemento,
porém, a manipulação dos descritores primeiro e último possuem regras
diferentes. O Algoritmo 71 apresenta essas regras:

Algoritmo 71 – Inserção no início da lista duplamente encadeada


31 void inserirInicio (ListaDupla* lista, int valor){
32 No *novo;
33 novo = (No*)malloc(sizeof (No));
34 novo->dado = valor;
35 novo->anterior = NULL;
36 novo->proximo = NULL;
37 if(lista->primeiro == NULL){
38 lista->primeiro == novo;
39 }else{
40 lista->proximo = lista->primeiro;
41 novo->primeiro->anterior = novo;
42 }
43 lista->primeiro = novo;
44 }

Fonte: Elaborado pelo autor (2013).

Observe que na instrução da linha 37 é verificado se a lista está vazia, e caso


essa condição seja verdadeira, o endereço do novo elemento é referenciado
como último elemento da lista (linha 30). Caso a lista não esteja vazia, o
novo elemento deverá apontar como próximo para o primeiro elemento
da lista (linha 40), e o primeiro elemento deverá apontar como anterior
para o novo elemento (linha 41). Por fim, na linha 43, o novo elemento é
referenciado como primeiro elemento da lista.

O Algoritmo 72 apresenta a regra de remoção do elemento do final da lista:

www.esab.edu.br 210
Algoritmo 72 – Remoção do final da lista duplamente encadeada
47 void inserirFinal (ListaDupla* lista){
48 if(lista->primeiro!= NULL){
49 if(lista->primeiro->proximo == NULL){
50 No* aux = lista->ultimo;
51 lista->primeiro = NULL;
52 lista->ultimo = NULL;
53 free(aux);
54 }else{
55 No* aux = lista->ultimo;
56 lista->ultimo->anterior->proximo=NULL;
57 lista->ultimo = lista->ultimo->anterior;
58 free(aux);
59 }
60 }
61 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 48 é verificado se a lista possui um único elemento.


Caso essa condição seja verdadeira, o único elemento da lista será
removido, por isso os descritores primeiro e último serão inicializados com
NULL nas linhas 51 e 52, e na linha 53 o elemento é removido da lista.

Caso a lista não possua um único elemento, na variável aux é


armazenado o endereço do último elemento da lista (linha 55), e na
linha 56, a instrução faz com que o penúltimo elemento da lista (lista-
>ultimo->anterior) passe a apontar como próximo para NULL, uma vez
que ele apontava para o último elemento que será removido. Na linha 57
a instrução define que o último elemento passará a ser o penúltimo, e, na
linha 58, o antigo último elemento é removido.

Para buscar um elemento na lista, serão percorridos todos os elementos


da lista até que esta termine ou o valor pesquisado seja localizado,
conforme o Algoritmo 73:

www.esab.edu.br 211
Algoritmo 73 – Consulta do valor na lista duplamente encadeada
77 No* consultar(ListaDupla* lista, int valor){
78 No* aux = lista->primeiro;
79 if (aux != NULL){
80 while((aux != NULL) && (aux->dado !=valor )){
81 aux = aux -> proximo;
82 }
83 return aux:
84 }
85 }

Fonte: Elaborado pelo autor (2013).

Observe que a função retorna o endereço do elemento localizado na


lista, conforme a linha 77, que define o método como: No* consultar
(ListaDupla* lista, int valor). A função recebe como parâmetro a lista
que será manipulada e o valor a ser pesquisado. Na linha 78 é declarada
a variável aux que é inicializada com o endereço do primeiro elemento
da lista. Na linha 79 é verificado se a lista não está vazia, ou seja, o
descritor primeiro deve ser diferente de NULL. Na instrução da linha
80 é verificado se o endereço pesquisado é diferente de NULL (lista
não terminou) e se o valor do endereço pesquisado é diferente do valor
pesquisado (valor não foi localizado). Se a condição é verdadeira, o
comando de laço enquanto parte para o próximo elemento da lista
(aux = aux ->proximo). Ao sair do laço, enquanto a variável aux terá
armazenado o endereço do valor localizado ou estará valendo NULL,
pois foram pesquisados todos os elementos da lista até que ela terminasse.

Caro aluno, na internet você pode baixar o código completo apresentado


nesta unidade por meio do endereço <http://pastebin.com/g7xQvmBp>.
Utilize a ferramenta de sua preferência para executar e testar o algoritmo.

Nesta unidade, você estudou como inicializar e manipular uma lista


duplamente encadeada por meio das rotinas de inserção no início e fim da
lista, bem como a remoção do início da lista e a busca por dados na lista.

Na próxima unidade, veremos como essas regras são aplicadas para a


construção de uma lista circular duplamente encadeada.

www.esab.edu.br 212
Listas circulares duplamente
24 encadeadas – conceituação
Objetivo
Apresentar o conceito de listas circulares duplamente encadeadas.

Na unidade anterior, você pôde desenvolver um algoritmo em C para


criação e manipulação de uma lista duplamente encadeada com a
inserção no final e início da lista, a busca por elementos na lista e a
remoção no final da lista.

Nesta unidade, continuaremos os estudos sobre a lista duplamente encadeada


com o objetivo de conceituar e compreender o funcionamento da lista
circular utilizando dois ponteiros, anterior e próximo, para cada elemento.

Esta unidade foi desenvolvida com base em Puga (2009), Celes,


Cerqueira e Rangel Netto (2004).

24.1 Definição
A proposta da lista circular para representação de processos cíclicos
é mantida nas listas circulares duplamente encadeadas, pois essa é a
característica mais forte desse tipo de lista. Lembre-se de que a lista
circular não possui o conceito de último elemento, independentemente
do tipo (simplesmente encadeada ou duplamente encadeada), pois o que
seria o último elemento aponta como próximo para o primeiro elemento
da lista. A Figura 81 apresenta a lista circular duplamente encadeada:

primeiro
p p p

a a a
Figura 81 – Lista circular duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 213
Observe que cada lista possui dois apontadores, um direcionado para
o próximo elemento (p) e outro para o elemento anterior (a). Puga
(2009, p. 200) destaca que a lista duplamente encadeada determina que
“[...] cada elemento possui um campo que aponta para seu predecessor
(anterior) e outro para seu sucessor (próximo)”. Portanto, a lista circular
duplamente encadeada terá o primeiro elemento da lista apontando
como anterior para o último elemento inserido na lista, e o último
elemento inserido na lista apontando como próximo para o primeiro
elemento. Como se trata de uma lista circular, o conceito de último
elemento da lista não existe, por isso a lista possui um único descritor
para o primeiro elemento da lista (primeiro). Para Celes, Cerqueira e
Rangel Netto (2004, p. 152),

[...] a lista circular também pode ser construída com encadeamento duplo. Nesse
caso, o que seria o último elemento da lista passa a ter como próximo o primeiro
elemento, que por sua vez, passa a ter o último como anterior.

Note que por se tratar de uma lista circular e por não ter o conceito de
final da lista, esta não possui descritor último, apenas o descritor primeiro.

Veremos a seguir quais as regras a serem executadas para os processos


de inicialização, inserção, remoção e busca na lista circular duplamente
encadeada.

www.esab.edu.br 214
24.2 Função de inicialização, inserção, busca e
remoção
A função de inicialização da lista circular duplamente encadeada
não muda em relação à lista simplesmente encadeada, pois
independentemente do tipo de lista circular, ela terá apenas o descritor
primeiro para ser inicializado com NULL.

A inserção na lista circular duplamente encadeada pode ocorrer no início,


fim ou entre dois elementos. As regras dessas inserções mudam muito
pouco em relação a essas mesmas operações com a lista duplamente
encadeada não circular.

A inserção entre dois elementos na lista circular duplamente encadeada


possui as mesmas regras da lista duplamente encadeada não circular. A
Figura 82 apresenta essa operação:

www.esab.edu.br 215
primeiro
p (ref) p p

a a a
p
(novo)
a
primeiro
p (ref) p p

a a a
(1) p
(novo)
a
primeiro
p (ref) p p

a a a
(2) p
(novo)
a
primeiro
p (ref) p p

a a a
p (3)
(novo)
a
primeiro
p (ref) p p

a a (4) a
p
(novo)
a
primeiro
p (novo) (ref) p p

a a a a

Figura 82 – Inserção entre elementos na lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).
www.esab.edu.br 216
Observe que para inserção entre dois elementos, o novo elemento passa
a ser referenciado como próximo do elemento anterior da referência de
inserção (1) e esse novo elemento passa a referenciar como anterior o
elemento anterior à referência de inserção (2). Com essas duas operações, a
ligação entre o elemento anterior da referência (ref) e o novo elemento está
garantida. O novo elemento deve apontar como próximo para o elemento
referência (3) e este deve apontar com anterior para o novo elemento (4), o
que garante a ligação entre o novo elemento e a referência.

A inserção no início da lista circular duplamente encadeada é semelhante


à inserção no início da lista duplamente encadeada não circular, estudada
nas unidades anteriores, porém, para garantir o processo cíclico da lista
circular, o novo elemento inserido no início deve apontar como anterior
para o último elemento da lista. A Figura 83 apresenta essa operação:

www.esab.edu.br 217
primeiro
(1) p p
(novo)
a a

primeiro
(1) p p
(novo)
(2) a a

primeiro (3)
(1) p p
(novo)
(2) a a

primeiro (3)
(1) p p
(novo)
(2) a a
(4)
(5)
primeiro (3)
(1) p p
(novo)
(2) a a
(4)

Figura 83 – Inserção no início da lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Observe que para inserção no início da lista, o novo elemento passa a


referenciar como próximo o primeiro elemento da lista (1), e o primeiro
elemento deve apontar como anterior para o novo elemento (2). O
último elemento da lista deve apontar como próximo para o novo
elemento da lista (3), o novo elemento deve apontar como anterior
para o último da lista (4) e por fim, o novo elemento inserido deve ser
definido como primeiro da lista (5).

www.esab.edu.br 218
A inserção no final da lista circular duplamente encadeada é semelhante à
inserção no final da lista duplamente encadeada não circular, já estudada.
Porém, para garantir o processo cíclico da lista circular, o novo elemento
inserido no final deve apontar como próximo para o primeiro elemento
da lista. A Figura 84 apresenta essa operação:

primeiro
p p (1) (novo)

a a

primeiro
p p (novo)

a a (2)

primeiro
p p (3) (novo)

a a

primeiro
p p (novo)

a a
(4)
Figura 84 – Inserção no final da lista circular duplamente encadeada.
Fonte: Elaborada pelo autor (2013).

Observe que para inserção no final da lista, o último elemento da lista


passa a apontar como próximo o novo elemento da lista (1), e o novo
elemento deve apontar como anterior para o último elemento (2). O
último elemento da lista deve apontar como próximo para o primeiro
elemento da lista (3) e o primeiro elemento deve apontar como anterior
para o último da lista (4).

Como a lista circular possui cada elemento com dois ponteiros, a busca
por elementos pode ser realizada em dois sentidos, partindo do último
elemento até chegar ao primeiro elemento ou do primeiro elemento até
chegar ao último da lista. A Figura 85 apresenta essas formas de busca:

www.esab.edu.br 219
primeiro

Figura 85 – Inserção no final da lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Observe que no sentido do “primeiro – primeiro” é utilizado o ponteiro


anterior, fazendo com que a busca comece do último elemento (anterior
do primeiro) e, sucessivamente, vá percorrendo cada elemento por meio
do ponteiro anterior, até que se chegue novamente ao primeiro elemento.
Como a lista circular não possui um descritor para o último elemento,
para listar no sentido do último elemento inserido para o primeiro
elemento, a listagem começa do elemento anterior do primeiro (último
inserido), e visita cada elemento da lista usando o ponteiro anterior de
cada elemento, até chegar novamente no primeiro elemento, tendo,
assim, percorrido todos os elementos da lista.

Já na busca no sentido “primeiro – último” é utilizado o ponteiro


próximo de cada elemento, sucessivamente, até que se chegue ao último
elemento da lista.

Para remoção do primeiro elemento da lista circular duplamente


encadeada, é necessário que, antes da remoção do primeiro elemento (1),
o descritor próximo passe a apontar para o segundo elemento (1), caso
ele exista, pois a lista pode ter um único elemento. É preciso também que
o último elemento aponte como próximo para o primeiro da lista (2),
e que o primeiro aponte como anterior para o último (3). A Figura 86
apresenta essas regras:

www.esab.edu.br 220
primeiro

(1)

primeiro

(2)

primeiro

primeiro (3)

Figura 86 – Remoção do início da lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Para remoção do último elemento da lista circular duplamente encadeada,


é necessário que, antes da remoção do último elemento (1), o elemento
anterior ao último passe a apontar como próximo para o primeiro
elemento (1) caso ele exista, e o primeiro elemento passe a apontar como
anterior para o último (2). A Figura 87 apresenta essas regras:

www.esab.edu.br 221
primeiro

primeiro (1)

primeiro

(2)

Figura 87 – Remoção do início da lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Por fim, vamos verificar as regras de remoção do elemento entre outros


dois elementos. Veja na Figura 88:

primeiro
(1)

(2)

primeiro
(1)

(2)

Figura 88 – Remoção do elemento entre elementos na lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Observe que o elemento a ser removido é referência para as operações,


pois o elemento anterior ao “removido” deve apontar como próximo
para o elemento posterior ao “removido” (1), e o elemento posterior ao
“removido” deve apontar como anterior para o elemento anterior ao
“removido” (2). Após essas ligações, o elemento a ser removido pode ser
excluído da lista.

www.esab.edu.br 222
Nesta unidade, você pôde analisar e estudar as operações de inicialização,
inserção no início, entre elementos e fim da lista, bem como as operações
de remoção do início, entre elementos e fim da lista. Foi possível também
estudar as duas formas de busca na lista circular duplamente encadeada,
usando o ponteiro anterior de cada elemento, no sentido “último-
primeiro”, e o ponteiro próximo no sentido “primeiro-último”.

Na próxima unidade, estudaremos como implementar essas operações


utilizando a linguagem C.

www.esab.edu.br 223
Resumo

Na unidade 19 foi estudado o funcionamento da lista circular, um


tipo peculiar de lista simplesmente encadeada que representa processos
cíclicos, já que o último elemento tem como próximo o primeiro
elemento da lista, o que forma um ciclo. A rigor, nesse caso não faz
sentido falar em primeiro e último elemento.

Na unidade 20, você estudou como implementar em C as regras de


inicialização, inserção, remoção e busca na lista circular simplesmente
encadeada. Sendo que as operações de inserção e remoção podem ser
realizadas no início, final e entre elementos da lista.

Na unidade 21 você teve a oportunidade de estudar como implementar


uma lista circular com três descritores, o início da lista e mais dois
descritores, responsáveis por armazenar o maior e o menor valor da lista.
Dessa forma, quando da inserção ou remoção de elementos na lista, os
descritores maior e menor são atualizados automaticamente de forma a
garantir que a qualquer momento se possa consultar o maior e o menor
valor cadastrados na lista.

Na unidade 22 você foi apresentado à definição da lista duplamente


encadeada e a como inicializá-la. Vimos também as regras para as
operações de inserção, busca e remoção de dados. Sendo que a lista
duplamente encadeada se caracteriza por ter cada elemento com dois
ponteiros, anterior e próximo. Assim, para cada elemento da lista, tem-se
a referência do próximo elemento e do elemento antecessor.

A unidade 23 apresentou como implementar em C as rotinas de


inicialização, inserção, remoção e busca em uma lista duplamente
encadeada. Estudamos como codificar as operações de inserção e
remoção no início, final e entre elementos.

www.esab.edu.br 224
Na unidade 24 estudamos os conceitos da lista duplamente encadeada
aplicada a uma lista circular, na forma de uma lista circular duplamente
encadeada. Analisamos as regras para implementação das operações de
inserção, remoção, busca e inicialização desse tipo de lista.

www.esab.edu.br 225
Listas circulares duplamente
25 encadeadas − codificação
Objetivo
Apresentar a codificação e a manipulação de listas circulares
duplamente encadeadas.

Na unidade anterior você pôde estudar as operações de inicialização,


inserção no início entre elementos e fim da lista circular duplamente
encadeada, bem como as operações de remoção do início entre elementos
e no fim da lista. Foi possível, também, estudar as duas formas de busca
na lista circular duplamente encadeada a partir da utilização do ponteiro
anterior de cada elemento no sentido “primeiro-primeiro” e do ponteiro
posterior no sentido “primeiro-último”. Nesta unidade, vamos estudar,
por meio de um exercício comentado, como codificar as operações de
inicialização, inserção, busca e remoção na lista circular duplamente
encadeada utilizando linguagem C.

Esse conteúdo foi desenvolvido com base em Cerqueira e Rangel Netto


(2004).

Vamos iniciar apresentando e debatendo as rotinas de inicialização,


inserção, busca e remoção e depois vamos desenvolver um exercício
que utiliza essas funções para criação e manipulação de uma lista que
armazenará um valor inteiro qualquer.

25.1 Codificação das funções de inicialização,


inserção, busca e remoção
A característica mais importante da lista circular duplamente encadeada
é que mesmo utilizando dois ponteiros, a representação de processos
cíclicos é mantida. A Figura 89 apresenta a lista circular duplamente
encadeada que será implementada nesta unidade:

www.esab.edu.br 226
primeiro
Emq1
anterior anterior
número número
próximo próximo

Figura 89 – Lista circular duplamente encadeada.


Fonte: Elaborada pelo autor (2013).

Observe que cada nó da lista será formado por um campo numérico, o


endereço do próximo elemento e endereço do elemento anterior. Celes,
Cerqueira e Rangel Netto (2004, p. 152), destacam que “[...] uma lista
circular também pode ser construída com encadeamento duplo. Nesse
caso, o que seria o último elemento da lista passa a ter como próximo o
primeiro elemento, que, por sua vez, passa a ter o último como anterior”.
A função de inserção dos dados na lista circular duplamente encadeada
terá o seguinte cabeçalho: void cadastrar(ListaCircular* lista, int
valor), em que o parâmetro lista representa a lista circular a qual será
manipulada, e o parâmetro valor representa o valor inteiro que será
inserido na lista. Como regra, vamos definir que a lista circular não possa
armazenar dois valores iguais, por isso durante a inserção será verificado
se o novo valor informado já existe na lista e, caso exista, uma mensagem
será apresentada ao usuário alertando que o valor já existe, caso
contrário, o valor será inserido na lista. Para tanto, será implementada
a função buscar com o seguinte cabeçalho: int buscar(ListaCircular*
lista, int valor), em que a função recebe como parâmetro a lista que será
manipulada e o valor que será pesquisado, assim, a função retornará um
tipo inteiro, 1 (verdadeiro) se o valor já existe ou 0 (falso) caso o valor
não exista na lista.

Ao inserir um novo elemento na lista, os dados serão cadastrados no


final, e por se tratar de uma lista circular duplamente encadeada, as
seguintes regras deverão ser implementadas:

www.esab.edu.br 227
• o último elemento da lista deverá apontar como próximo para o
novo elemento;
• o novo elemento deverá apontar como anterior para o último;
• o novo elemento deverá apontar como próximo para o primeiro
elemento;
• o primeiro elemento deverá apontar como anterior para o novo
elemento.
Como a lista circular não possui o descritor último, o qual armazena
o endereço de memória do último elemento inserido na lista circular,
é preciso percorrer a lista toda para encontrar o último nó inserido, o
qual é o elemento que aponta como próximo para o primeiro elemento
da lista. Para estabelecer essa regra de pesquisa do último elemento (o
qual aponta como próximo para o primeiro da lista), será implementada
uma função genérica para encontrar um determinado elemento (nó). A
função receberá como parâmetro a lista a ser manipulada e o nó acerca
do qual se deseja saber que elemento aponta para ele na lista e, então,
serão pesquisados todos os elementos da lista, partindo do primeiro
elemento até encontrar o nó que tenha como valor do campo próximo o
nó que está sendo pesquisado. Essa função possui o seguinte cabeçalho:
No* apontaPara(ListaCircular* lista,No* elemento), em que o
parâmetro lista representa a lista a ser manipulada e o parâmetro No* o
nó a ser apontado por algum elemento na lista. A função apontaPara()
retornará o endereço do nó, que aponta para o elemento pesquisado.
Por exemplo, vamos supor que a lista possua os valores A, B, C e D, se
a chamada da função for apontaPara(lista,endereço de ‘D’), a função
retornará o endereço do elemento ‘C’, pois a procura é pelo elemento
que aponta para o ‘D’. Se a chamada da função for para encontrar quais
variáveis apontam para ‘B’, a função retornará o endereço de ‘A’, já que
‘A’ aponta para ‘B’ na lista A, B, C e D.

Para listar os elementos, será implementada a função listar, com o


seguinte cabeçalho: void listar(ListaCircular* lista,char opcao), em
que lista representa a lista a ser manipulada, e opção representa o tipo
de listagem. Caso seja ‘E’, de esquerda, os elementos serão listados no
sentido “primeiro-primeiro” usando o campo “proximo” de cada nó, e
caso seja ‘D’, de direita, os elementos serão listados no sentido “último-
primeiro” usando o campo “anterior” de cada nó.

www.esab.edu.br 228
Na inicialização da lista, como só existe um descritor para o primeiro
nó da lista, este será inicializado com NULL, e a função terá o seguinte
cabeçalho: void inicializar(ListaCircular* lista), em que o parâmetro
lista representa a lista circular duplamente encadeada.

Por fim, a remoção será implementada de forma que o último


elemento seja removido, e a função tenha o seguinte cabeçalho: void
remover(ListaCircular* lista), em que o parâmetro lista representa a
lista a ser manipulada.

Vamos criar o algoritmo que implementa essas regras de forma que seja
possível inserir, listar, remover e cadastrar elementos na lista circular
duplamente encadeada.

25.2 Exercícios comentados


Vamos desenvolver o programa de computador que permita o
cadastramento na lista circular duplamente encadeada.

Inicialmente, serão criados os tipos estruturados e os tipos de dados


que serão manipulados no programa. O Algoritmo 74 apresenta essas
definições:

Algoritmo 74 – Definição de tipos e estruturas


01 struct item_no{
02 struct item_no *anterior;
03 struct item_no *proximo;
04 int valor;
05 };
06 typedef struct item_no No;
07 struct lista_circular{
08 No *primeiro;
09 };
10 typedef struct lista_circular ListaCircular;

Fonte: Elaborado pelo autor (2013).

Nas linhas 1 a 5, é declarada a estrutura do registro item_no com


os campos: anterior e proximo, os quais armazenarão o endereço do

www.esab.edu.br 229
elemento anterior e posterior ao nó, e o valor, um inteiro. Na linha
6 é declarado o tipo de dado No, que representa o registro item_no
e, nas linhas 7 a 9, é declarado o tipo estruturado lista_circular com
apenas um campo chamado primeiro, que armazenará o endereço do
primeiro elemento da lista. Por fim, na linha 10 é criado o tipo de dados
ListaCircular que representa o tipo estruturado lista_circular.

Considerando essas instruções, o importante é que teremos a lista circular


duplamente encadeada representada pelo tipo de dado ListaCircular com
um único campo, chamado primeiro, que é do tipo No (valor, próximo e
anterior), lembrando que a lista circular não possui o descritor último.

Vamos criar a função que inicializa a lista, atribuindo NULL para o


descritor primeiro e, além disso, criar uma função que cria e preenche o
nó da lista. O Algoritmo 75 apresenta essas regras:

Algoritmo 75 – Inicialização da lista e criação do nó


01 void inicializar(ListaCircular* lista){
02 lista->primeiro = NULL;
03 }
04 No* instanciar(int valornumerico){
05 No* novo = (No*) malloc(sizeof(No));
06 novo->valor = valornumerico;
07 novo->anterior = NULL;
08 novo->proximo = NULL;
09 return novo;
10 }

Fonte: Elaborado pelo autor (2013).

Nas linhas 1 a 3, a função inicializar recebe como parâmetro a lista que


será manipulada, do tipo ListaCircular, que, na linha 2, tem o descritor
primeiro inicializado com NULL. Na linha 4, a função instanciar
é declarada com o parâmetro valornumerico para recebimento do
valor inteiro que será inserido na lista. Na linha 5, o ponteiro novo
aloca espaço para armazenamento do tipo estruturado “No” e guarda,
na variável “novo”, o endereço dessa alocação na memória. A linha
06 atribui ao campo valor do ponteiro “novo” o valor do parâmetro
valornumerico. As instruções das linhas 7 e 8 inicializam os descritores
anterior e proximo do ponteiro novo, respectivamente, com NULL.

www.esab.edu.br 230
A linha 9 retorna o ponteiro novo com o campo valor previamente
preenchido e os descritores anterior e proximo com NULL para fora do
método. Ou seja, o método cria um novo nó, o preenche com o valor
informado pelo usuário, inicializa os descritores anterior e proximo com
NULL e depois o retorna o ponteiro “novo” já preenchido.

O próximo passo é desenvolver uma rotina para pesquisa de um código


na lista, verificando se ele já foi cadastrado, e outra que retorne um nó
que aponte para um determinado elemento que também é um nó. Caso
o valor procurado não exista, a função retornará um NULL. Celes,
Cerqueira em Rangel Netto (2004, p. 151) destacam que “[...] a função
de busca recebe a informação referente ao elemento que queremos buscar
e tem como valor de retorno o ponteiro do nó da lista que representa
o elemento. Caso o elemento não seja encontrado na lista, o valor
retornado é NULL”.

O Algoritmo 76 apresenta essas rotinas na forma de funções:

Algoritmo 76 – Funções buscar e apontaPara


01 No* apontaPara(ListaCircular* lista,No*
elemento){
02 No* p = lista->primeiro;
03 if(p==NULL) return NULL;
04 while( (p!=NULL) && (p->proximo!=elemento)) {
05 p= p->proximo;
06 }
07 return p;
08 }
09 int buscar(ListaCircular* lista, int pvalor){
10 if(lista->primeiro == NULL) return 0;
11 No* p = lista->primeiro;
12 do{
13 if(p->valor == pvalor){
14 return 1;
15 }
16 p = p->proximo;
17 }while(p!=lista->primeiro);
18 return 0;
19 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 231
Analisando a função apontaPara, na linha 1, ela declara o cabeçalho da
função apontaPara(ListaCircular* lista,No* elemento) que deve retornar
o endereço do nó que aponta para o elemento procurado. Na linha 2, a
variável p é inicializada com o endereço do primeiro elemento da lista, e
a linha 3 verifica se esse endereço vale NULL, ou seja, se a lista está vazia,
então retorna NULL, pois não existe um nó que aponte para o elemento
procurado, logo, a função é encerrada. Se a lista não está vazia, é realizado,
nas linhas 4 a 6, um laço que percorre cada elemento da lista (p=p-
>proximo), enquanto o nó da lista (p) não aponta como próximo para o
elemento que está sendo procurado (p->proximo!=elemento), e a lista não
tenha terminado (p!=null). Na linha 7 é retornado o endereço do elemento
da lista (p) que aponta como próximo para o elemento procurado.

Com relação à função buscar, as instruções das linhas 9 a 19


implementam as regras da função que busca o valor na lista verificando
se ele já foi cadastrado. Na linha 10 é verificado se a lista está vazia (lista-
>primeiro == NULL) e, caso a condição seja verdadeira, a função retorna
falso (0), pois o valor não pode ser localizado na lista vazia. Na linha 11
fica definido que a pesquisa pelo valor comece do primeiro elemento da
lista, No* p=lista->primeiro, e se repita até que o elemento pesquisado
seja o primeiro nó (ou seja, que todos os elementos sejam visitados) e o
valor não tenha sido achado. As instruções das linhas 13 a 17 verificam
se o valor do nó (p->valor) é igual ao valor procurado (pvalor) e, caso
a condição seja verdadeira, a função retornará 1 (verdadeiro). Na linha
18, a função retornará 0 (falso), pois encontraremos a função retorna
verdadeiro (1) quando o valor armazenado no nó (p->valor) for igual ao
valor procurado (pvalor), mas se a função pesquisar todos os elementos
sem ter achado o valor, ao sair do controle de laço da linha 17, a função
deverá retornar 0 (falso).

Como outras operações possíveis na lista circular duplamente encadeada,


temos as funções de inserção, remoção e listagem dos elementos.

Vamos, agora, implementar as funções de inserção (cadastrar), remoção e


listagem dos elementos da lista. O Algoritmo 77 apresenta essas funções:

www.esab.edu.br 232
Algoritmo 77 – Inserção, listagem e remoção
01 void cadastrar(ListaCircular* lista, int pvalor){
02 int achou = buscar(lista, pvalor);
03 if(achou == 1){
04 system("cls");
05 printf("Valor ja foi Cadastrado! , valor repetido
!\n");
06 system("pause");
07 }
08 else{
09 No* item = instanciar(pvalor);
10 if(lista->primeiro == NULL){
11 lista->primeiro = item;
12 item->anterior = lista->primeiro;
13 item->proximo = lista->primeiro;
14 }
15 else{
16 No* ultimo = apontaPara(lista,lista-
>primeiro);
17 ultimo->proximo = item;
18 item->anterior = ultimo;
19 item->proximo = lista->primeiro;
20 lista->primeiro->anterior = item;
21 }
22 }
23 }
24 void listar(ListaCircular* lista,char opcao){
25 system("cls");
26 if(lista->primeiro ==NULL){
27 printf("LISTA VAZIA!\n");
28 }
29 else{
30 printf("LISTAGEM\n");
31 No* p = lista->primeiro;
32 No* ultimo = apontaPara(lista,lista->primeiro);
33 if(opcao == 'D' || opcao == 'd') p=ultimo;
34 do{
35 printf("Valor:%d\n",p->valor);
36 if(opcao=='E' || opcao == 'e') p=p->proximo;
37 else p=p->anterior;
38 }while(((opcao== 'E' || opcao == 'e') &&
(p!=lista->primeiro)) ||
39 ((opcao== 'D' || opcao == 'd') &&
(p!=ultimo)));
40 }
41 system("pause");
42 }

www.esab.edu.br 233
43 void remover(ListaCircular* lista){
44 if(lista->primeiro == NULL){
45 system("cls");
46 printf("Lista Vazia!\n");
47 system("pause");
48 }
49 else{
50 if(lista->primeiro == lista->primeiro->proximo){
51 No* aux = lista->primeiro;
52 lista->primeiro = NULL;
53 free(aux);
54 }else{
55 No* ultimo = apontaPara(lista,lista-
>primeiro);
56 No* penultimo = apontaPara(lista,ultimo);
57 penultimo->proximo = lista->primeiro;
58 lista->primeiro->anterior = penultimo;
59 free(ultimo);
60 }
61 printf("Elemento Removido Com Sucesso!\n");
62 system("pause");
63 }
64 }

Fonte: Elaborado pelo autor (2013).

Note que a função cadastrar, as instruções da linha 01 a 23, são


responsáveis pela implementação das regras de inserção de um novo
elemento no final da lista. Na linha 2 a variável achou é inicializada
com 1 ou 0 (verdadeiro ou falso) a partir do retorno da função buscar,
que percorre a lista verificando se o valor informado no parâmetro
pvalor já existe na lista. Na linha 3 verifica-se se a variável achou vale
verdadeiro (1, e, em caso afirmativo, é apresentada uma mensagem de
que o valor já foi cadastrado e a função é encerrada. Caso o valor do
novo valor não exista na lista, um novo elemento é criado e armazenado
pela função instanciar, na variável item, com o valor passado como
parâmetro, o que pode ser verificado na instrução da linha 9. Na linha
10, a condição if(lista->primeiro == NULL) verifica se a lista está vazia e,
em caso verdadeiro, será a primeira inserção na lista. Assim, o descritor
primeiro armazenará o elemento do item (lista->primeiro = item),
conforme linha 11, e como ele só tem um elemento, o item apontará
como anterior e como próximo para o primeiro elemento, instruções
das linhas 12 e 13, respectivamente. Se a lista já tiver elementos

www.esab.edu.br 234
inseridos, as instruções das linhas 16 a 21 são executadas, e na linha 16
a variável último é inicializada com o endereço do último elemento da
lista (que aponta como próximo para o primeiro), na instrução No*
ultimo = apontaPara(lista,lista->primeiro). A função apontaPara() foi
implementada de forma que devolva o endereço do nó que aponta
como próximo para o elemento pesquisado e que também é um nó.
As instruções das linhas 17 a 20 realizam as seguintes ações: o último
elemento passa a apontar como próximo para o novo elemento (item),
o novo elemento aponta como anterior para o último da lista, o novo
elemento apontará como próximo para o primeiro elemento da lista e o
primeiro elemento da lista apontará como anterior para o novo elemento,
garantindo, assim, o seu processo cíclico.

A função listar permite percorrer a lista em dois sentidos: esquerda,


quando a variável opção vale ‘E’ ou ‘e’, começando do primeiro elemento
da lista (instrução da linha 31) e percorrendo toda a lista por meio do
descritor proximo (instrução p=p->proximo da linha 38); ou direita,
quando a variável opção vale ‘D’ ou ‘d’, começando do último elemento
(instrução da linha 32) e percorrendo cada nó da lista por meio do
descritor anterior (instrução p=p->anterior da linha 39) quando o valor
da variável opção é ‘D’.

Por fim, a função remover pode ser dividida em três regras gerais,
conforme a Figura 90:
void remover(ListaCircular* lista){
if(lista->primeiro == NULL) {
system(”cls”);
printf(”Lista Vazia!\n”); 1
system(”pause”);
}
else{
if(lista->primeiro == lista->primeiro->proximo) {
No* aux = lista->primeiro;
lista->primeiro = NULL; 2
free(aux);
}else{
No* ultimo = apontaPara(lista,lista->primeiro);
No* penultimo = apontaPara(lista, ultimo);
penultimo->proximo = lista->primeiro;
lista->primeiro->anterior = penultimo;
3
free(ultimo);
}
printf(”Elemento Removido Com Sucesso!\n”);
system(”pause”);
}
}

Figura 90 – Função remover.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 235
As regras do quadro 1 da Figura 90 são executadas quando a lista estiver
vazia, ou seja, lista->primeiro vale NULL, portanto não é possível
remover o último elemento da lista e a função é encerrada. As regras do
quadro 2 representam as instruções para quando a lista não está vazia,
mas possui um único elemento, por isso o primeiro elemento, que
teve seu endereço armazenado na variável aux, é removido na função
free(aux). O quadro 3 da figura representa a remoção do último elemento
da lista quando esta não está vazia, e possui mais de um elemento em que
a variável ultimo recebe o endereço do último elemento da lista, o qual
será removido, e a variável penultimo recebe o endereço do elemento
que aponta para o último, e essa variável será apontada como anterior ao
primeiro elemento. Ou seja, o último elemento é removido e o primeiro
elemento aponta como anterior para o penúltimo e o penúltimo aponta
como próximo para o primeiro.

Assim, todo o código para manipulação da lista circular duplamente encadeada foi
implementado. Acessando o endereço <http://pastebin.com/M4T48s2U> você pode
fazer o download de todo o código e testá-lo no seu computador.

Nesta unidade, você pôde estudar e verificar como desenvolver um


programa para inserção, remoção, busca e inicialização de uma lista
circular duplamente encadeada.

Na próxima unidade vamos iniciar os estudos sobre a estrutura de dados


pilha, que possui uma forma peculiar de funcionamento.

Até lá.

www.esab.edu.br 236
26 Pilhas – conceituação

Objetivo
Apresentar o conceito de pilhas.

Na unidade anterior você pôde estudar, por meio de um exercício


comentado, como codificar as operações de inicialização, inserção, busca e
remoção na lista circular duplamente encadeada utilizando linguagem C.

Nesta unidade, estudaremos a estrutura de dados pilha, sua definição e


operações possíveis, representadas como interface da pilha.

Esse conteúdo foi desenvolvido com base em Celes, Cerqueira e Rangel


Netto (2004) e Puga (2009).

Vamos iniciar os estudos de forma a contextualizar o que é uma pilha,


para tanto, faremos uma analogia com uma pilha de pratos.

26.1 Definição
Caro aluno, para iniciarmos os estudos com relação à estrutura de dados
pilha, vamos fazer uma analogia com uma pilha de pratos. Veja a Figura 91:

remover inserir

topo topo

base base
Figura 91 – Pilha de pratos.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 237
Analisando essa pilha de pratos, note que podemos fazer a inserção
ou a remoção de um prato, mas tanto a inserção quanto a remoção só
poderão ser realizadas no topo da pilha. Outra informação importante
é que a pilha é formada por duas extremidades, o topo e a base, mas
como as operações só podem ser realizadas no topo, na implementação
dessa estrutura de dados, essa informação não é necessária. Na pilha de
pratos, ela é necessária para manter os pratos em pé e organizados. Se
desejarmos colocar um prato na pilha, colocamos pelo topo, se a opção
for por remover um prato da pilha, o retiramos do topo. Para ter acesso
a um determinado prato, é preciso remover o prato do topo até chegar
ao elemento desejado. Portanto, quando um elemento é inserido na
pilha ele passa a ser o topo, e quando um elemento é removido da pilha,
o elemento abaixo, caso exista, passa a ser o topo da pilha. A Figura 92
apresenta a sequência de duas remoções e uma inserção em uma pilha
que inicialmente possuía cinco elementos:

remover
remover
inserir
topo
topo topo
topo

base base base base


Figura 92 – Manipulação da pilha.
Fonte: Elaborada pelo autor (2013).

Note que todo acesso à pilha se dá pelo topo, tanto para inserção quanto
para remoção, logo, os elementos da pilha são retirados na ordem inversa
à ordem de inserção, assim, o primeiro a sair é o último que entrou.
Celes, Cerqueira e Rangel Netto (2004, p. 160) conceituam a pilha
como uma “[...] estrutura de dados conhecida como LIFO (last in first
out), do inglês, o último a entrar é o primeiro a sair”.

www.esab.edu.br 238
Estudo complementar
Leio o texto que apresenta o jogo Torre de Hanoi,
ele utiliza os conceitos de empilhar e desempilhar
(inserir e remover na pilha).

Puga (2009, p. 226), conceitua a pilha como “[...] uma estrutura de


dados LIFO ou PEPS, sendo UEPS as iniciais de Último a Entrar
Primeiro a Sair, e isto só acontece porque apenas uma das extremidades
da pilha está acessível”.

Uma vez que as operações são realizadas somente no topo, a pilha possui
duas operações básicas: empilhar um novo elemento, inserindo-o no
topo e desempilhar, removendo o elemento do topo. Celes, Cerqueira e
Rangel Netto (2004, p. 160), destacam que “[...] normalmente estas duas
operações são identificadas como push (empilhar) e pop (desempilhar)”.
A Figura 93 apresenta o resultado das operações push(10), push(20),
pop() e push(15) em uma pilha:

20 topo 20 15 topo
base 10 topo base 10 base 10 topo base 10

push(10) push(20) pop() push(15)

Figura 93 – Manipulação da pilha – push e pop.


Fonte: Elaborada pelo autor (2013).

Na primeira inserção, quando a pilha possui um único elemento, note


que tanto a base quanto o topo valem o mesmo elemento.

A estrutura de dados pilha pode ser implementada utilizando um vetor


ou uma lista encadeada, sendo que a implementação por meio de vetor
é mais simples e fácil. Porém, possui limitações por obrigar que se
conheça o número máximo de elementos que serão manipulados, o que
não ocorre no uso da lista em que a alocação de espaço de memória é
realizada em tempo de execução, alocando e desalocando os elementos da
pilha quando necessário.

www.esab.edu.br 239
Como na pilha, as inserções e remoções são feitas por uma única
extremidade (topo), não sendo necessário que se conheça o elemento da
outra extremidade, diferentemente do que ocorre com a lista encadeada,
em que as inserções e remoções podiam ocorrer no início, entre
elementos e no final da estrutura de dados.

Assim, podemos entender que a principal diferença entre a pilha e a fila


está nas operações possíveis de serem realizadas: as operações da pilha são
reduzidas e específicas, ocorrendo inserção no topo (push) e remoção do
topo (pop); já a lista possibilita a inserção e remoção dos elementos de
qualquer posição.

As pilhas são muito utilizadas em programas de computadores, por


exemplo, quando você está utilizando um editor de texto e utiliza a opção
de desfazer, isso é possível porque existe uma pilha que vai armazenando
o que você digita e, quando escolhe a opção desfazer, o texto atual é
removido da pilha e o texto que estava abaixo do topo (que agora é topo) é
apresentado no documento. Quando você navega pela internet utilizando
um navegador, ao clicar no botão voltar, novamente você está acessando
uma pilha que armazena os endereços (URL) já visitados, assim, cada vez
que você clica no botão voltar, o endereço atual é removido da pilha, e o
endereço que agora é topo é carregado pelo navegador.

Na indústria, a pilha é muito utilizada em softwares para o controle


de armazenamento de produtos em contêiners para evitar a retirada
de produtos até a chegada de outro produto desejado, assim, a lista de
entregas define a ordem de armazenamento dos produtos, os que sairão
primeiro são colocados por último. A Figura 94 apresenta a ordem de
entrega dos produtos e a sua ordem de armazenamento.
Ordem de entrega Ordem de armazenamento

Produto A Produto D
Produto B Produto C
Produto C Produto B
Produto D Produto A

Figura 94 – Lista de entregas e lista de armazenamento.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 240
Observe que a ordem de armazenamento dos produtos é inversa à ordem
das entregas, de forma que o produto que será entregue primeiro seja
armazenado por último no contêiner.

Pela variedade de aplicação da pilha é muito importante conhecer como


se implementa esse tipo de estrutura de dados e suas operações. Por isso,
a seguir, iniciaremos os estudos sobre a estrutura da pilha e suas operações
para criar, inserir e remover elementos e verificar se a pilha está vazia.

26.2 Interface do tipo pilha


De modo independente da estratégia de implementação da pilha por
meio de um vetor ou de uma lista encadeada, a pilha possui as seguintes
operações:

• Criar: cria a estrutura de dados do tipo pilha;


• Push: insere um elemento no topo da pilha;
• Pop: remove um elemento da pilha;
• Pilha_Vazia: verifica se a pilha está vazia.
Considerando que a pilha pode ser implementada por meio de um vetor,
a operação de criação da pilha tem como finalidade definir o tamanho
do vetor e, consequentemente, a quantidade máxima de elementos que
podem ser inseridos. A Figura 95 representa uma pilha formada por um
vetor com cinco posições:

Pilha
0 1 2 3 4
Figura 95 – Pilha implementada com vetor.
Fonte: Elaborada pelo autor (2013).

Observe que cada posição da pilha é indexada, começando de 0 e


terminando em 4 e, como a pilha está vazia, a primeira inserção será
feita na posição 0, depois 1 e assim, sucessivamente, até a posição 4.
Logo, será preciso uma variável que controle a posição de inserção,
funcionando de forma a representar o topo da pilha. A Figura 96
apresenta o processo de inserção na pilha:

www.esab.edu.br 241
topo
(0)
Pilha Push(23)
0 1 2 3 4
topo
(0)
Pilha 23
0 1 2 3 4
Figura 96 – Operação de push no vetor.
Fonte: Elaborada pelo autor (2013).

A variável que controla o topo da pilha começou valendo 0, porém


com a inserção do valor 23 na pilha, o elemento foi inserido no topo
(posição 0), e, na próxima inserção, a variável de controle do topo será
incrementada, desde que ela seja menor do que 4 (última posição do
vetor), para que o novo elemento seja inserido no topo da pilha (posição
à frente). Para a remoção de um elemento do topo da pilha, a operação
deverá decrementar a variável topo em 1, para que o topo retorne à
posição anterior, desde que o valor do topo seja maior que 0. A Figura 97
ilustra a remoção na pilha implementada por meio do vetor:
topo
(2)
Pilha 23 08 10 Pop()
0 1 2 3 4
topo
(1)
Pilha 23 08 10
0 1 2 3 4
Figura 97 – Operação de pop no vetor.
Fonte: Elaborada pelo autor (2013).

Note que a operação de pop() faz com que a posição de controle do


topo seja decrementada, voltando uma posição e podendo retornar até a
posição 0, que representa a base da pilha.

www.esab.edu.br 242
Como as posições da pilha variam de 0 a 4, a variável topo valerá -1 para
representar que nenhum elemento foi inserido, assim, para saber se a
pilha está vazia basta verificar se a posição do topo vale -1, sendo que na
primeira inserção na pilha a variável será incrementada em 1, mudando,
portanto, para o valor 0. Na remoção, quando o topo estiver valendo 0, o
topo é o primeiro elemento da lista e a variável será decrementada em 1,
portanto, mudando para -1, o que representa que a pilha está vazia.
topo
(2)

topo topo
(-1) Push(44) 44 Pop() (-1) 44
0 1 2 3 4 0 1 2 3 4 0 1 2 3 4
Figura 98 – Controle da posição do topo.
Fonte: Elaborada pelo autor (2013).

Observe que o topo começou valendo -1, e na primeira inserção o valor


mudou para 0. Em caso de remoção do único elemento da pilha, o valor
da variável topo é decrementado e valerá -1. Portanto, controlando a
posição de inserção no vetor é possível implementar uma pilha utilizando
um vetor. Na inserção a variável de controle do topo é incrementada e na
remoção a variável de controle do topo é decrementada.

Nesta unidade, você pôde compreender o funcionamento da estrutura de


dados pilha, também conhecida como LIFO, em que o último elemento
inserido é o primeiro a ser removido, e todas as operações são realizadas
no topo da pilha: a inserção, conhecida como push e a remoção,
conhecida como pop.

Na próxima unidade daremos sequência ao estudo da pilha, primeiro vamos


implementá-la na forma de um vetor e depois na forma de uma lista, com as
operações de criação, push, pop e verificação se a pilha está vazia.

Tarefa dissertativa
Caro estudante, convidamos você a acessar o
Ambiente Virtual de Aprendizagem e realizar a
tarefa dissertativa.

www.esab.edu.br 243
27 Pilhas – codificação – parte I

Objetivo
Apresentar a codificação e a manipulação de pilhas.

Na unidade anterior, você pôde estudar e compreender o funcionamento


da estrutura de dados pilha, também conhecida como LIFO, em que
o último elemento inserido é o primeiro a ser removido e todas as
operações são realizadas no topo da pilha (a inserção, conhecida como
push, e a remoção, conhecida como pop).

Nesta unidade, veremos como implementar as operações de criação, push,


pop e verificação se a pilha está vazia. Faremos isso na pilha, primeiramente,
na forma de um vetor e, em seguida, como uma lista encadeada.

Esse conteúdo foi desenvolvido apoiando-se em Celes, Cerqueira e


Rangel Netto (2004) e Puga (2009).

Vamos começar estudando como implementar a pilha na forma de um vetor.

27.1 Implementação de pilha com vetor


Para implementação da pilha, as operações básicas são: criar a pilha,
inserir no topo (push), remover do topo (pop) e verificar se a pilha está
vazia, logo, a primeira rotina que será estudada nesta unidade é a criação
da lista. O Algoritmo 78 apresenta a instrução que cria a pilha:

Algoritmo 78 – Criação da pilha


01 int pilha[5];
02 int qtde = 5;
03 int topo = -1;

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 244
A instrução int pilha [5] define o tamanho do vetor pilha com cinco
posições e que será manipulado na forma de uma pilha, com as operações
de push() para inserção e pop() para remoção. A variável qtde representa
a quantidade máxima de elementos da pilha, e a variável topo a posição
do vetor que representa o topo da pilha.

Para verificar se a pilha está vazia basta verificar se a variável topo vale
-1. A função que verifica se a pilha está vazia retorna verdadeiro (1) se
o topo vale -1 ou falso (0) caso o topo seja diferente de -1. Observe no
Algoritmo 79:

Algoritmo 79 – Função Pilha_Vazia


01 int pilha_vazia(){
02 if(topo == -1) return 1;
03 else return 0;
04 }

Fonte: Elaborado pelo autor (2013).

Para implementação da função push() será informado o valor inteiro que


será inserido no vetor manipulado na forma de uma pilha. O Algoritmo
80 apresenta essa função:

Algoritmo 80 – Função Push


01 void push(int dado){
02 if(topo < (qtde-1)){
03 topo++;
04 pilha[topo] = dado;
05 }else{
06 system("cls");
07 printf("Pilha Cheia!\n");
08 system("pause");
09 }
10 }

Fonte: Elaborado pelo autor (2013).

Na linha 2 do Algoritmo 80 é verificado se a pilha está cheia (se o


valor da variável topo for menor que a última posição do vetor (qtde-
1)) e, em caso de condição verdadeira, a variável topo é incrementada
(linha 3) e o vetor na posição representada pela variável topo recebe o

www.esab.edu.br 245
valor da variável dado (linha 4). Se a pilha está cheia, ou seja, atingiu
a quantidade máxima de elementos, uma mensagem é apresentada ao
usuário informando que a pilha está cheia (linhas 6 a 9).

Para remoção do elemento da pilha, a variável topo terá seu valor


decrementado em 1, desde que o valor da variável topo seja maior que
-1. O valor -1 representa que a pilha está vazia, por isso o menor valor da
variável topo será -1. O Algoritmo 81 apresenta a rotina da função pop:

Algoritmo 81 – Função Pop


01 void pop(){
02 if(topo > -1){
03 topo--;
04 }else{
05 system("cls");
06 printf("Pilha Vazia!\n");
07 system("pause");
08 }
09 }

Fonte: Elaborado pelo autor (2013).

Como a função pop controla a variável topo, ela não recebe parâmetros e
é importante que o usuário receba uma mensagem avisando que a pilha
está vazia de forma a mantê-lo informado sobre a sua situação.

Por fim, a última função tem como finalidade listar os elementos da


pilha, os quais são apresentados no Algoritmo 82:

www.esab.edu.br 246
Algoritmo 82 – Função Listar
01 void listar(int *pilha){
02 if(pilha_vazia()==1){
03 system("cls");
04 printf("Pilha Vazia!\n");
05 }
06 else{
07 system("cls");
08 do{
09 printf("%d\n",pilha[topo]);
10 pop();
11 }while(pilha_vazia()==0);
12 }
13 system("pause");
14 }

Fonte: Elaborado pelo autor (2013).

A pilha só permite que seja visualizado o elemento que está no seu


topo, dessa forma, para listar todos os elementos da pilha é preciso
listar o elemento do topo e, então, removê-lo para que o elemento
abaixo do topo se torne o novo topo e possa ser impresso na tela. As
instruções das linhas 2 a 5 verificam se a pilha está vazia e, em caso de
condição verdadeira, a mensagem de “pilha vazia” é mostrada ao usuário.
As instruções das linhas 8 a 13 implementam um laço que mostra o
valor que está no topo da pilha (linha 9), depois o elemento do topo
é removido pela função pop (linha 10) e, caso a pilha não esteja vazia
(pilha_vazia() ==0), o laço é repetido, mostrando o novo topo e depois
removendo-o. Esse processo se repete até que a pilha esteja vazia, pois
todos os elementos foram listados. Assim, ao listar os elementos da pilha,
todos os elementos serão removidos e a pilha estará vazia.

Caro aluno, acessando o endereço clicando aqui você poderá efetuar o download do
algoritmo elaborado anteriormente e executá-lo no seu computador.

Agora que sabemos como implementar uma pilha como vetor, vamos
verificar como implementar a estrutura de dados pilha usando a lista
encadeada.

www.esab.edu.br 247
27.2 Implementação de pilha com lista
A principal vantagem da implementação da pilha usando a lista
encadeada, está no fato de não ser necessário conhecer previamente o
número de elementos que serão manipulados. Celes, Cerqueira e Rangel
Netto (2004, p. 164) destacam que “[...] quando o número máximo de
elementos que serão armazenados na pilha não é conhecido, devemos
implementar a pilha usando uma estrutura de dados dinâmica, no caso,
uma lista encadeada”.

Para tanto, vamos implementar o tipo estruturado que representa cada


item da pilha, o nó da pilha e o tipo estruturado que representará a
pilha na forma de uma lista simplesmente encadeada. O Algoritmo 83
apresenta essa codificação:

Algoritmo 83 – Definição do nó e da pilha


01 struct item_no{
02 int dado;
03 struct item_no *anterior;
04 };
05 typedef struct item_no No;
06 struct lista_pilha{
07 No *topo;
08 };
09 typedef struct lista_pilha Pilha;

Fonte: Elaborado pelo autor (2013).

Note que cada nó da pilha, definido como struct item_no nas instruções
das linhas 1 a 4, é formado por um valor inteiro, armazenado pelo
campo dado e pelo campo anterior, que armazena o endereço do nó
predecessor. Na linha 5 é criado o tipo de dados No para representar
a estrutura de cada elemento da pilha. As instruções das linhas 6 a 8
definem a estrutura da pilha, formada por um único descritor, o topo
da pilha. Na linha 9 é criado o tipo de dados pilha, que representa a
estrutura de dados que será manipulada no programa em C. As funções
que criam a pilha e verificam se ela está vazia podem ser visualizadas no
Algoritmo 84:

www.esab.edu.br 248
Algoritmo 84 – Funções Pilha_Vazia e Criar
01 int pilha_vazia(Pilha* pilha){
02 if(pilha->topo == NULL) return 1;
03 else return 0;
04 }
05 Pilha* criar(){
06 Pilha* pilha = (Pilha*)
malloc(sizeof(Pilha));
07 pilha->topo = NULL;
08 return pilha;
09 }

Fonte: Elaborado pelo autor (2013).

A função pilha_vazia() verifica se a pilha passada como parâmetro possui


o campo topo valendo NULL, em caso de condição verdadeira, a função
retornará verdadeiro (1), caso contrário retornará falso (0). A função
criar retorna o endereço de memória da pilha criada pela função malloc()
da linha 6, sendo que a pilha já é criada com o descritor topo valendo
NULL, pois ela é criada vazia.

A função para inserção é apresentada no Algoritmo 85:

Algoritmo 85 – Função Push


01 void push(Pilha* pilha, int info){
02 No* novo = (No*) malloc(sizeof(No));
03 novo->dado = info;
04 novo->anterior = NULL;
05 if(pilha->topo != NULL){
06 novo->anterior = pilha->topo;
07 }
08 pilha->topo = novo;
09 }

Fonte: Elaborado pelo autor (2013).

Observe que a função push recebe como parâmetro a lista que será
manipulada e o valor que será inserido na pilha. Na linha 2 é criado,
dinamicamente, o novo elemento que será inserido na pilha, que tem o
campo dados preenchido com o valor do parâmetro “info” na linha 3.
A linha 4 define que, por padrão, todo novo elemento inserido tem o
campo anterior valendo NULL.

www.esab.edu.br 249
Na linha 5 verifica-se se a pilha não está vazia, ou seja, se já existe um
elemento inserido na pilha. Logo, o novo elemento inserido deve apontar
como anterior para o topo da pilha (novo->anterior = pilha ->topo, na
linha 6), e, tratando-se de uma inserção na pilha, todo elemento inserido
passa a ser o topo da pilha, por isso a instrução da linha 8 é sempre
executada, estando a pilha vazia ou não. A Figura 99 apresenta esse
processo de inserção no topo:

novo novo anterior topo novo anterior


topo topo topo

Cria novo elemento Novo aponta para topo como interior Novo é topo

Figura 99 – Função Push na pilha como lista encadeada.


Fonte: Elaborada pelo autor (2013).

Portanto, se a pilha está vazia, o novo elemento é definido como topo


da pilha. Caso contrário, o novo elemento aponta como anterior para o
topo da pilha e depois o novo elemento passa a ser o topo.

Por fim, o Algoritmo 86 apresenta as funções de remoção e listagem dos


elementos da pilha:

www.esab.edu.br 250
Algoritmo 86 – Funções Pop e Listar
01 void pop(Pilha *pilha){
02 if(pilha_vazia(pilha)==1){
03 system("cls");
04 printf("Pilha Vazia!\n");
05 system("pause");
06 }
07 else{
08 No* aux = pilha->topo;
09 if(aux->anterior == NULL){
10 pilha->topo = NULL;
11 }else{
12 pilha->topo = pilha->topo->anterior;
13 }
14 free(aux);
15 }
16 }
17 void listar(Pilha *pilha){
18 if(pilha_vazia(pilha)==1){
19 system("cls");
20 printf("Pilha Vazia!\n");
21 system("pause");
22 }
23 else{
24 system("cls");
25 printf("__________
LISTAGEM__________\n");
26 do{
27 printf("%d\n",pilha->topo->dado);
28 pop(pilha);
29 }while(pilha_vazia(pilha)==0);
30 system("pause");
31 }
32 }

Fonte: Elaborado pelo autor (2013).

A função pop() verifica se a pilha está vazia, condição da linha 2, e, em


caso de condição verdadeira, uma mensagem de “Pilha Vazia” é mostrada
ao usuário do programa. Caso contrário, na linha 9 é verificado se a
pilha possui apenas um elemento, assim, esse elemento é removido e
a pilha passa a ter o topo valendo NULL. Caso a pilha possua mais de
um elemento, o descritor topo, que armazena o endereço do elemento
que está no topo da pilha, será atualizado com o endereço do elemento
abaixo do topo que está sendo removido. O endereço do elemento abaixo

www.esab.edu.br 251
do topo está armazenado no campo anterior do topo, assim, o descritor
topo passa a conter o endereço do campo anterior do topo (instrução
da linha 12) e, na linha 13, o antigo topo é removido. Por exemplo, se o
topo da pilha é o elemento X e o elemento abaixo do topo é o elemento
Z, então, Z é o anterior de X. Como o elemento X será removido, o topo
passará a ser o anterior de X, que é o elemento Z, e, após a remoção do
elemento X, teremos a pilha com Z no topo.

Puga (2009, p. 231) destaca que “[...] a função desempilhar não recebe
outro parâmetro que não seja a pilha que está sendo manipulada, pois a
remoção é sempre do elemento que está no topo”. A função listar mostra
todos os elementos do topo da pilha, desde que ela não esteja vazia (por
isso a condição da linha 18), não estando vazia, cada elemento do topo é
mostrado na tela, e, em seguida, o topo é removido pela função pop() da
linha 28, até que a pilha esteja vazia.

Bem, dessa forma você pode analisar as rotinas para inserção, remoção e listagem em
uma pilha implementada por meio da lista encadeada, e o código do algoritmo pode
ser baixado clicando aqui.

Nesta unidade, você pôde analisar como desenvolver uma pilha na forma
de vetor e lista encadeada, com as operações de inserção (push), remoção
(pop) e listagem dos elementos da pilha, sendo que por característica da
estrutura de dados, a listagem de todos os dados da pilha resulta em uma
pilha vazia.

Na próxima unidade, vamos dar sequência ao estudo da estrutura de


dados pilha com o desenvolvimento de um exercício comentado.

Atividade
Chegou a hora de você testar seus conhecimentos
em relação às unidades 19 a 27. Para isso, dirija-se ao
Ambiente Virtual de Aprendizagem (AVA) e responda
às questões. Além de revisar o conteúdo, você estará
se preparando para a prova. Bom trabalho!

www.esab.edu.br 252
28 Pilhas – codificação – parte II

Objetivo
Apresentar a codificação e a manipulação de pilhas.

Na unidade anterior, você pôde analisar como desenvolver uma pilha


na forma de vetor e lista encadeada com as operações de inserção
(push), remoção (pop) e listagem dos elementos da pilha, sendo que por
característica da estrutura de dados, a listagem de todos os dados da pilha
resulta em uma pilha vazia.

Nesta unidade, vamos dar sequência ao estudo da pilha com o


desenvolvimento de um exercício comentado.

Esse conteúdo foi desenvolvido com base em Celes, Cerqueira e Rangel


Netto (2004).

28.1 Exercícios comentados


Vamos desenvolver uma pilha na forma de uma lista encadeada em que
cada nó será formado pelo número da ordem de inserção na pilha e pela
descrição do produto. A pilha será formada pelo descritor topo e pelo
descritor quantidade (qtde). A Figura 100 representa a pilha que será
implementada:

2 B topo 2 B
qtde qtde qtde qtde
Pilha
0 Vazia 1 1 A topo 2 1 A 1 1 A topo

push(A) push(B) pop()


Figura 100 – Pilha com quantidade de elementos.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 253
Observe que, inicialmente, a quantidade (qtde) de elementos da pilha
começa valendo 0, a cada inserção do produto é gerado um número de
ordem (1, 2,..., N) e a quantidade é incrementada em 1. Com a remoção
do elemento do topo, a quantidade é decrementada em 1, de forma que
se tenha, no descritor qtde, o valor que representa a quantidade atual
de elementos da pilha. Vamos começar criando os tipos estruturados de
dados que serão usados na implementação da pilha. O Algoritmo 87
apresenta a declaração do tipo estruturado No e o tipo estruturado Pilha:

Algoritmo 87 – Definição do tipo No e do tipo Pilha


01 #include <stdio.h>
02
03 int main(void)
04 {
05 int x, y, sucessor_x, sucessor_y, soma,
dobro_x;
06 double metade_y, media;
07 printf(“Digite primeiro número inteiro.\n”);
08 scanf(“%d”, &x);
09 printf(“Digite segundo número inteiro.\n”);
10 scanf(“%d”, &y);
11 sucessor_x = x + 1;
12 sucessor_y = y + 1;

Fonte: Elaborado pelo autor (2013).

O tipo estruturado item_no, declarado nas linhas 1 a 5, será composto


pelo endereço do nó anterior, pelo número da ordem de inserção do
produto e o nome produto (campo produto). Na linha 6 é criado o
tipo de dado No. Já a estrutura da pilha, instruções das linhas 7 a 10, é
formada pelos campos topo, que é o endereço de um nó da pilha, e pelo
campo qtde, que armazena o número de elementos da pilha. Na linha 11
é criado o tipo de dados Pilha e na linha 12 é declarada a variável item,
que representa a ordem de inserção dos elementos, sendo que, a cada
inserção, o valor será incrementado em 1.

Portanto, a cada inserção de um elemento no topo da pilha (push),


o descritor “qtde” deverá ser incrementado em 1, e na remoção do
elemento do topo (pop) o descritor “qtde” deverá ser decrementado em
1, garantindo, dessa forma, que o descritor “qtde” represente a situação
real com relação ao número de elementos existentes na pilha.

www.esab.edu.br 254
O Algoritmo 88 apresenta a função de inserção de elementos na pilha:

Algoritmo 88 – Função Push


01 void push(Pilha *pilha, char nomeproduto[40]){
02 No* novo = (No*) malloc(sizeof(No));
03 novo->ordem=item;
04 strcpy(novo->produto,nomeproduto);
05 novo->anterior = NULL;
06 pilha->qtde = pilha->qtde + 1;
07 item++;
08 if(pilha->topo!=NULL) novo->anterior =
pilha->topo;
09 pilha->topo = novo;
10 }

Fonte: Elaborado pelo autor (2013).

A função de push recebe como parâmetro dois valores, a pilha que será
manipulada e o nome do produto. Na linha 2 é declarada a variável
novo, que aloca dinamicamente um espaço na memória. Na linha 3,
a ordem de inserção do produto é determinada pelo valor da variável
item, que, na linha 7, é incrementado em 1. Na linha 4 é atribuído ao
campo produto o nome do produto, e na linha 5 o campo anterior é
inicializado com NULL. Na linha 6, o campo que controla a quantidade
de elementos da pilha é incrementado em 1 (pilha->qtde = pilha->qtde
+ 1). A instrução da linha 8 faz a ligação entre o novo elemento inserido
e o topo da pilha, ligando-os pelo campo anterior do novo elemento
(novo->anterior = pilha->topo), desde que a pilha tenha pelo menos um
elemento, e na linha 9 o novo elemento é definido como topo da pilha.

Saiba mais
Acessando o endereço clicando aqui é possível
revisar o funcionamento da função strcpy,
responsável por atribuir um valor a uma variável
do tipo literal.

O Algoritmo 89 apresenta a função para remoção do elemento do topo


da pilha:

www.esab.edu.br 255
Algoritmo 89 – Função Pop
01 void pop(Pilha *pilha){
02 if(pilha->topo != NULL){
03 No* aux = pilha->topo;
04 pilha->topo = pilha->topo->anterior;
05 pilha->qtde=pilha->qtde-1;
06 free(aux);
07 }
08 }

Fonte: Elaborado pelo autor (2013).

Na linha 1 é verificado se a pilha não está vazia e, caso não esteja,


primeiramente o endereço do elemento que está no topo da pilha (linha
2) é armazenando na variável aux, depois, na instrução da linha 3, o
topo da pilha passa a ser o elemento anterior ao topo. Na linha 4, a
quantidade de elementos da pilha é decrementada em 1, e, na linha 5,
por meio da função free(), o elemento do topo é removido da memória.

Vamos ver como esvaziar toda a pilha. A próxima função tem como
finalidade limpar a pilha removendo todos os elementos, desde que a
pilha não esteja vazia.

Celes, Cerqueira e Rangel Netto (2004, p. 166) destacam que “[...] a


função que libera a pilha deve antes liberar todos os elementos da pilha”.
O Algoritmo 90 apresenta essa função:

Algoritmo 90 – Função para esvaziar a pilha


01 void esvaziarPilha(Pilha *pilha){
02 if(pilha->topo == NULL){
03 printf("PILHA VAZIA!\n");
04 system("pause");
05 }else{
06 do{
07 pop(pilha);
08 }while(pilha->topo != NULL);
09 printf("PILHA ESVAZIADA!\n");
10 system("pause");
11 }
12 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 256
Observe que na linha 2 verifica-se se a pilha está vazia (pilha->topo ==
NULL) e, caso a condição seja verdadeira, uma mensagem “PILHA
VAZIA!” é exibida ao usuário. A pilha não estando vazia, é realizado um
laço condicional que remove o elemento do topo, por meio da função
pop(pilha), enquanto a pilha não estiver vazia (while(pilha->topo !=
NULL)). Após o encerramento do laço, na linha 9, uma mensagem
“PILHA ESVAZIADA!” é mostrada ao usuário.

Para a criação da pilha, a função criar() será desenvolvida da seguinte forma:

Algoritmo 91 – Função para criar a pilha


01 Pilha* criar(){
02 Pilha* pilha = (Pilha*)
malloc(sizeof(Pilha));
03 pilha->topo = NULL;
04 pilha->qtde = 0;
05 return pilha;
06 }

Fonte: Elaborado pelo autor (2013).

A função criar aloca um espaço de memória para a pilha que será


manipulada (instrução da linha 2), atribuindo o valor NULL ao campo
topo e o valor 0 para a quantidade, pois a lista é criada sem elementos.
Celes, Cerqueira e Rangel Netto (2004, p. 165) destacam que “[...] a função
criar aloca a estrutura da pilha e inicializa a pilha como sendo vazia”.

Você pode fazer o download de todo o código implementado nesta unidade clicando aqui.

Nesta unidade, você pôde estudar como construir uma pilha que utiliza
dois descritores, o topo, responsável por identificar o elemento no topo
da pilha, e a quantidade (qtde), para controlar o número de elementos
existentes na pilha. Como regras de manipulação da pilha foram
implementadas as funções de push (inserção), pop (remoção), listagem e
esvaziamento da pilha.

Na próxima unidade vamos iniciar o estudo de outra estrutura de


dados, a fila.

www.esab.edu.br 257
29 Filas – conceituação

Objetivo
Apresentar o conceito de filas.

Na unidade anterior, você pôde estudar como construir uma pilha que
utiliza dois descritores, o topo, responsável por identificar o elemento
no topo da pilha, e a quantidade (qtde), para controlar o número de
elementos existentes na pilha. Como regras de manipulação da pilha
foram implementadas as funções de push (inserção), pop (remoção),
listagem e esvaziamento da pilha.

Nesta unidade vamos conhecer a estrutura de dados fila.

Esse conteúdo foi desenvolvido com base em Celes, Cerqueira e Rangel


Netto (2004).

Vamos iniciar fazendo uma analogia da estrutura de dados fila com uma
fila de banco.

29.1 Definição
Quem nunca enfrentou uma fila em um banco ou restaurante? A fila faz
parte das nossas atividades no dia a dia, ainda mais neste mundo cada vez
mais corrido e competitivo, não é mesmo?

www.esab.edu.br 258
A Figura 101 representa uma fila:

início final

Saída Entrada
Figura 101 – Fila de pessoas.
Fonte: Elaborada pelo autor (2013).

Observe que na estrutura de dados do tipo fila, o acesso aos dados


segue uma regra, as inserções na fila acontecem no final da estrutura de
dados, e a remoção dos elementos no início da fila, logo, a estrutura de
dados fila possui duas extremidades, o início e o final, e, dessa forma, o
primeiro a entrar é o primeiro a sair.

Estudo complementar
Ficou curioso e quer saber mais sobre o
funcionamento de uma fila de banco? Então
acesse o link.

Celes, Cerqueira e Rangel Netto (2004, p. 171) destacam que “[...] a


estrutura de dados fila é conhecida como estrutura FIFO – first in, first
out, em inglês, primeiro a entrar, primeiro a sair”.

Já para Puga (2009, p. 219), a pilha pode ser definida como a estrutura
na qual “[...] os elementos são atendidos ou utilizados, sequencialmente,
na ordem em que são armazenados, cujas operações de inserção são feitas
por uma extremidade, e as de remoção, por outra”.

O uso da estrutura de dados fila é muito comum em programas de


computadores, por exemplo, a fila de impressão, que armazena e gerencia
os arquivos que são enviados para a impressora, garantindo que eles serão
impressos seguindo a ordem de chegada na fila: quanto mais cedo chegar,
mais rápido será enviado para impressão.

www.esab.edu.br 259
Independentemente do tipo de fila que será utilizado, a estrutura de
dados fila visa garantir a prioridade de chegada, quem chega mais cedo
é atendido primeiro, portanto, não é permitida a inserção de elementos
que não no final da fila e muito menos a remoção que não ocorra no
início da fila.

Diante desse contexto, é muito comum o questionamento com relação


à prioridade no atendimento das filas, já que não pode haver inserção
ou remoção em qualquer lugar desta. Com relação ao atendimento,
por exemplo, de idosos, os quais são chamados antes, haveria, então,
uma alteração na ordem da fila? Não, pois os sistemas de controle de
atendimento usam duas filas, uma para os clientes que não são idosos e
outra para idosos, dessa forma, uma fila não interfere na estrutura de dados
da outra, porém, a ordem de retirada da fila de idosos é maior que a da fila
de não idosos, logo, a prioridade dentro de cada fila não é alterada.

A estrutura de dados fila é simples, possui apenas três operações básicas:

• inserir no final;
• remover do início;
• listar os itens da fila.

www.esab.edu.br 260
Diferentemente da estrutura de dados pilha, a listagem dos elementos da fila não
exige que os elementos sejam removidos, portanto, após listar os elementos a fila não
estará vazia. A Figura 102 apresenta a visão das duas estruturas de dados, na forma
de uma pilha de pratos e de uma fila de atendimento em banco:

Pilha Fila
Figura 102 – Visão da pilha e fila.
Fonte: Elaborada pelo autor (2013).

Observe que a pilha pode ser representada como uma estrutura na vertical, já que
possui apenas uma extremidade, o topo, e quando olhamos, a visão se dá de cima
para baixo, então visualizamos apenas o primeiro prato, e para saber se há outro,
temos que remover o prato do topo. Já a fila pode ser representada como uma
estrutura na horizontal, pois possui duas extremidades, início e final, o que permite a
visualização de todos os elementos sem a necessidade de remoção de elementos.

A estrutura de dados fila pode ser implementada: por meio de um vetor


unidimensional, que exige a definição da quantidade de elementos; ou na
forma de uma lista simplesmente encadeada, que aloca cada elemento,
dinamicamente, durante a execução do programa.

Para a implementação da fila na forma de um vetor, algumas regras são


necessárias, como:

• definir o tamanho da fila;


• criar uma variável para controlar a posição de inserção;
• na remoção é preciso deslocar os elementos para o início da estrutura;
• listar os elementos da fila.

www.esab.edu.br 261
Para ajudar na compreensão das regras de inserção e remoção, veja a
Figura 103:

Posição de (PI) (PI) (PI) (PI)


Inserção A A B B
(-1) 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3
inserir “A” inserir “B” remover

Figura 103 – Operação de inserção e remoção na fila.


Fonte: Elaborada pelo autor (2013).

Observe que a inserção começa na posição 0, pois se o vetor está vazio, o


início e final da fila são a mesma posição. A cada inserção na fila, o valor é
gravado na posição de inserção e depois a posição é incrementada para que
a nova inserção, caso seja realizada, ocorra na posição posterior, desde que
a fila não esteja cheia, pois no caso do vetor, a quantidade de elementos
da fila é limitada pelo tamanho do vetor. Na remoção, o elemento da
posição 0 (início da fila) é removido e todos os elementos posteriores serão
“puxados para esquerda”, sentido do início da fila, e a posição de inserção
volta uma posição, já que um elemento da fila foi removido.

No caso da implementação da fila, as regras são mais simples:

• criar a fila;
• inserir no final;
• remover do início;
• listar os elementos da fila.
Diferentemente do vetor, não há necessidade de controle de posições de
inserção e remoção, mas é preciso que a lista possua dois descritores para
armazenamento do endereço do primeiro elemento e do último elemento
da fila, como pode ser visualizado na Figura 104:

www.esab.edu.br 262
B

primeiro último primeiro último

A B C A B C D
insere “D”
Remover (A)

primeiro último

A B C D

Figura 104 – Operação de inserção e remoção na fila na forma de lista encadeada.


Fonte: Elaborada pelo autor (2013).

Note que a cada inserção o último elemento da fila passada aponta


como próximo para o novo elemento, que depois passa a ser definido
como o último elemento da fila. E na remoção, não é preciso “puxar” os
elementos da fila para a esquerda, a operação é mais simples, o elemento
que era o primeiro da fila é removido e o segundo elemento (próximo do
primeiro) passa a ser o início da fila.

Veremos a seguir como essas operações são representadas na forma da


interface da fila.

29.2 Interface do tipo fila


Independentemente do tipo de estrutura, vetor ou lista simplesmente
encadeada, a implementação da fila se dá por meio das seguintes funções:

• criar a fila: no caso do vetor, define o seu tamanho. Na forma da lista


encadeada é utilizada para alocação de memória e inicialização do
primeiro e último descritores;
• inserir: no caso do vetor, verifica se há nele posições vagas, insere na
posição de inserção e incrementa. Para a lista encadeada, faz com
que o último elemento aponte para o novo, definindo-o como o
último da fila por meio do último descritor;

www.esab.edu.br 263
• remover: verifica se a fila está vazia ou não (para vetor ou lista) e,
sendo um vetor, remove o elemento da posição 0, realizando uma
operação de repetição trazendo todos os elementos que sobraram
na fila para a posição atual -1. No caso da lista, remove o elemento
apontado pelo descritor início e define o novo início como sendo o
próximo elemento apontado pelo antigo início;
• listar: verifica se a fila está vazia ou não (para vetor ou lista) e
percorre todos os elementos da estrutura de dados, sem a necessidade
de remoção dos elementos.
Nesta unidade, você pôde estudar o funcionamento e a definição da
estrutura de dados fila, que pode ser implementada por meio de um
vetor ou lista simplesmente encadeada com as operações de inserção no
final, remoção no início e listagem dos elementos.

Na próxima unidade, vamos estudar como codificar essas operações para


uma fila na forma de vetor e lista encadeada.

www.esab.edu.br 264
30 Filas – codificação

Objetivo
Apresentar a codificação e a manipulação de filas.

Na unidade anterior, você pôde estudar o funcionamento e a definição


da estrutura de dados fila, que pode ser implementada por meio de um
vetor ou lista simplesmente encadeada com as operações de inserção no
final, remoção no início e listagem dos elementos.

Nesta unidade, vamos codificar a fila na forma de um vetor com


as operações de inserção no início, remoção no final e listagem dos
elementos.

Esse conteúdo foi desenvolvido com base em Celes, Cerqueira e Rangel


Netto (2004).

Vamos iniciar estudando a implementação da fila por meio de um vetor.

30.1 Implementação de fila com vetor


Para implementação da fila por meio de um vetor vamos definir um vetor
com dez posições e a variável pi, que representará a posição de inserção
na fila. O Algoritmo 92 apresenta essas instruções:

Algoritmo 92 – Declaração da fila e variável de controle da inserção dos dados


01 int fila[10]
02 int pi=0

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 265
A fila será representada por um vetor de dez posições de inteiros e a variável
pi é inicializada com 0, já que a primeira inserção será nessa posição.

Celes, Cerqueira e Rangel Netto (2004, p. 173) destacam que para a


implementação da inserção na fila “[...] os índices são incrementados de
maneira que seus valores progridam, incrementando um índice em uma
unidade”.

Assim, a função de inserção receberá como parâmetro um valor inteiro


e, caso a fila não esteja cheia, o valor será inserido, caso contrário
uma mensagem será mostrada ao usuário. Sendo possível a inserção,
o valor será gravado no vetor e a variável pi (posição de inserção) será
incrementada em 1. O Algoritmo 93 apresenta essas regras:

Algoritmo 93 – Inserção na fila


01 void inserir(int info){
02 if(pi < 10){
03 fila[pi] = info;
04 pi++;
05 }else{
06 system("cls");
07 printf("Fila Cheia\n");
08 system("PAUSE");
09 }
10 }

Fonte: Elaborado pelo autor (2013).

Na linha 2 é verificado se há posições vagas na fila, sendo que o máximo


de elementos na fila é dez (tamanho da fila), por isso verifica-se se a
condição da posição de inserção (pi) é menor que dez, caso não seja
menor, será mostrada uma mensagem ao usuário de fila cheia (instruções
das linhas 6 a 8). Caso a fila não esteja cheia, na posição de inserção
(pi) do vetor, é armazenado o valor do parâmetro info e, na linha 4, essa
posição é incrementada para que a inserção ocorra sempre no final da fila
(posição mais à direita).

Para listar os elementos já inseridos na fila, será preciso percorrer toda a


estrutura desde a posição 0 até a posição anterior à posição de inserção,
como pode ser visto no Algoritmo 94:

www.esab.edu.br 266
Algoritmo 94 – Função de listagem
01 void listar(){
02 system("cls");
03 if(pi == 0){
04 printf("Fila Vazia\n");
05 }else{
06 int i=0;
07 for(i=0; i < pi;i++){
08 printf("%d ",fila[i]);
09 }
10 printf("\n");
11 }
12 system("PAUSE");
13 }

Fonte: Elaborado pelo autor (2013).

Note que na linha 3 é verificado se a fila está vazia e, caso a condição


(pi==0) seja verdadeira, a mensagem de “fila vazia” é exibida ao usuário.
Se a fila não está vazia, todos os itens são listados, começando da posição
0 (i=0) até chegar na posição menor que pi, por isso o comando de laço
contado da linha 7 é for(int i=0; i < pi;i++). Na linha 8, cada elemento
da fila é mostrado na tela. Por exemplo, se a inserção é realizada na
posição 0, pi valerá 1, se for na posição 4, após a inserção, pi valerá 5,
pois na rotina de inserção o vetor na posição (pi) recebe o valor e depois
pi é incrementado. Portanto, pi está sempre uma posição à frente, livre
para que a nova inserção a utilize. A Figura 105 ilustra essa regra:

próxima
(pi) inserção
0 1 2 3
1 15
Figura 105 – Inserção na fila como vetor.
Fonte: Elaborada pelo autor (2013).

Note que ao inserir o valor 15 na posição 1 do vetor, a variável pi foi


incrementada para que a próxima inserção seja realizada na posição 2,
porém, as posições utilizadas para a fila com elementos inseridos são 0
e 1, assim, para listar os elementos da fila a posição começará em 0 e
terminará em 1 (pi-1).

www.esab.edu.br 267
Por fim, a próxima função consiste na remoção do elemento do início da
fila, posição 0, desde que ela não esteja vazia, o que pode ser observado
no Algoritmo 95:

Algoritmo 95 – Remoção na fila


01 void remover(){
02 if(pi ==0){
03 system("cls");
04 printf("Fila Vazia!\n");
05 system("PAUSE");
06 }else{
07 int i=0;
08 for(i=0; i < pi-1;i++){
09 fila[i] = fila[i+1];
10 }
11 pi--;
12 }
13 }

Fonte: Elaborado pelo autor (2013).

Na linha 2 é verificado se a fila está vazia (pi==0) e, caso a condição seja


verdadeira, uma mensagem de fila “vazia” é apresentada ao usuário. Se
a fila não está vazia, todos os elementos a partir da posição 0 (que será
removido) são puxados uma posição para a esquerda, controlados pelo
comando de laço da linha 8 (for i=0; i < pi-1;i++). A Figura 106 ilustra
esse processo de deslocamento para a esquerda:

(PI) (PI) (PI)


0 1 2 3 0 1 2 3 0 1 2 3
10 15 21 15 15 21 15 21 21

Fila Fila Fila


fila[0] <- fila[1] fila[1] <- fila[2] pi--

Figura 106 − Deslocando elementos para a esquerda.


Fonte: Elaborada pelo autor (2013).

Observe que a cada repetição do comando de laço a posição “i” do vetor


recebe o valor da posição “i+1”. Assim, a posição 0 recebe o elemento
da posição 1, a posição 1 do vetor recebe o valor da posição 2 e assim

www.esab.edu.br 268
sucessivamente até a posição pi-1. Após a cópia dos elementos da posição
“i+1” para “i”, deslocando os elementos da fila para o seu início, a
variável pi tem posição de inserção decrementada em 1, de forma que a
próxima inserção, caso aconteça, ocorra no lugar do valor da posição 2,
que contém uma cópia do valor que estava nessa posição, mas que já foi
deslocado para a posição anterior (à esquerda).

Lembre-se de que na listagem, o comando de laço que percorre a fila


começa em 0 e termina na posição anterior a pi (pi-1), dessa forma, o
valor que ficou “sobrando” na fila não será mostrado e, assim que houver
uma nova inserção, ele será trocado por um novo elemento. Essa forma de
remoção é chamada de “exclusão lógica”, sendo que o valor está na fila, mas
não é visitado, pois uma condição lógica impede que isso ocorra (i < pi).

Saiba mais
Você pode complementar seus estudos sobre a
exclusão lógica clicando aqui.

Note que a exclusão lógica se faz necessária devido ao uso do vetor que
não possibilita a remoção de uma posição específica da memória, já que,
uma vez alocado, o bloco de memória se mantém até que o programa
se encerre. No caso da fila implementada com uso da lista encadeada
a remoção será física, removendo o elemento da memória durante a
execução do programa.

Aproveite para fazer o download de todos os algoritmos apresentados nesta unidade


clicando aqui.

Nesta unidade, você estudou como implementar as funções de inserção,


remoção e listagem dos elementos de uma fila criada a partir de um
vetor. Na próxima unidade, vamos desenvolver a fila utilizando uma lista
simplesmente encadeada na forma de um exercício comentado.

www.esab.edu.br 269
Resumo

Na unidade 25 apresentamos como implementar uma lista circular


duplamente encadeada com as operações inserção no final, remoção
no início e busca nos dois sentidos: iniciando pelo primeiro elemento,
percorrendo a estrutura de dados usando o ponteiro próximo e iniciando
do primeiro e percorrendo a lista usando o ponteiro anterior. Vimos,
também, como inicializar a lista por meio da função de inicialização que
preenche o descritor primeiro com NULL.

Na unidade 26 foi apresentada a definição da estrutura de dados pilha,


que se caracteriza como uma estrutura de dados conhecida como LIFO
(last in first out), do inglês, o último a entrar é o primeiro a sair. As
operações na pilha são realizadas somente no topo, logo, a pilha possui
duas operações básicas, empilhar um novo elemento, inserindo-o no
topo, e a operação de desempilhar, removendo o elemento do topo. A
operação de inserção no topo é chamada de push e a remoção do topo
de pop. Vimos que a estrutura de dados pilha pode ser implementada
utilizando um vetor ou uma lista encadeada.

Na unidade 27 foi estudado como implementar as operações de criação,


push, pop e verificação se a pilha está vazia na pilha na forma de um
vetor, e na forma de lista encadeada.

Na unidade 28 foram implementadas as operações de push, pop,


criação e esvaziamento da pilha na forma de uma lista encadeada com
a utilização de um descritor para armazenamento da quantidade de
elementos existentes na pilha.

Na unidade 29 foi apresentada a estrutura de dados fila, também


conhecida como estrutura FIFO – first in, first out, em inglês, primeiro
a entrar, primeiro a sair. A fila pode ser definida como a estrutura na qual
os elementos são atendidos ou utilizados, sequencialmente, na ordem em

www.esab.edu.br 270
que são armazenados, e cujas operações de inserção são feitas por uma
extremidade e as de remoção, por outra. Assim, as operações possíveis
em uma fila são: a inserção no final, a remoção no início e a listagem dos
elementos da fila.

Na unidade 30 foi implementada uma fila utilizando vetor (uma


das formas de implementação), sendo que a outra é como uma lista
encadeada. Na implementação com vetor foi utilizada uma variável que
representa a posição de inserção no vetor, que é incrementada na inserção
e decrementada na remoção do primeiro elemento. A remoção na fila
requer que todos os elementos a partir do primeiro sejam conduzidos
uma posição à esquerda.

www.esab.edu.br 271
31 Filas – exercícios

Objetivo
Apresentar exercícios comentados sobre filas.

Na unidade anterior, estudamos como implementar as funções de


inserção, remoção e listagem dos elementos de uma fila criada a partir de
um vetor.

Nesta unidade, vamos desenvolver uma fila por meio de uma lista
simplesmente encadeada, com as operações de inserção, remoção e
listagem dos dados. Criaremos uma lista que simule a lista de arquivos
para impressão utilizada pelo sistema operacional, em que cada item da
lista deverá conter o nome do arquivo que será impresso e seu tamanho
em bytes.

Este conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

Vamos iniciar apresentando a estrutura de cada elemento da fila,


representada pelo tipo estruturado item_no e a estrutura da fila,
representada também por um tipo estruturado, chamado fila. Depois
abordaremos as rotinas de inicialização, inserção, listagem e remoção na fila.

31.1 Exercícios
A fila que programaremos simula uma fila de arquivos, uma estrutura de
dados muito utilizada pelo sistema operacional para controle dos arquivos
que são enviados para a impressora. Por se tratar de uma fila, o primeiro
arquivo inserido será o primeiro arquivo a ser impresso e removido da
fila, garantindo o conceito FIFO (First In First Out – primeiro a entrar,
primeiro a sair). A Figura 107 representa a fila que será implementada:

www.esab.edu.br 272
inserir
próximo próximo próximo
nomearquivo nomearquivo nomearquivo
remover tamanho tamanho tamanho

início final NULL


Figura 107 – Fila de arquivos.
Fonte: Elaborada pelo autor (2013).

Observe que cada elemento da fila é composto pelo nome do arquivo,


o tamanho do arquivo e o descritor próximo que armazena o endereço
do próximo elemento da lista, e a lista é composta pelos descritores de
início e final da lista. Celes, Cerqueira e Rangel Netto (2004, p. 176),
destacam que para implementar a fila com lista simplesmente encadeada
é preciso “[...] usar dois ponteiros que representam o início e final da fila,
e as operações são realizadas nas extremidades opostas, onde a remoção
ocorre no início da fila e a inserção no final da fila”.

O Algoritmo 96 apresenta as instruções para declaração da fila que


vamos manipular no exercício.

Algoritmo 96 – Definição da Fila de Arquivos


01 typedef char literal[30];
02 struct item_no{
03 literal nomearquivo;
04 int tamanho;
05 struct item_no *proximo;
06 };
07 typedef struct item_no No;
08 struct fila{
09 No* inicio;
10 No* final;
11 };
12 typedef struct fila FilaArquivos;

Fonte: Elaborado pelo autor (2013).

Na linha 01 está sendo declarado um tipo chamado literal, que


representa um vetor de no máximo 30 caracteres e será utilizado para
declaração e armazenamento do nome do arquivo, como pode ser visto

www.esab.edu.br 273
na linha 03, na declaração do campo nomearquivo do tipo estruturado
item_no, que representa cada item da fila, com os campos nomearquivo,
tamanho e próximo. Na linha 07 é criado um novo tipo chamado No
para representar cada item da fila do tipo estruturado item_no. Nas
linhas 08 a 11, é declarado o tipo estruturado fila que representa a fila
que será manipulada, com os descritores inicio e final, ambos do tipo
ponteiro de No. Na linha 12 é criado o tipo de dados FilaArquivos que
representa o tipo estruturado fila, com seus dois campos, inicio e final.

Com a estrutura da fila de arquivos definida, o próximo passo é a


implementação das rotinas de inicialização da fila, que deverá inicializar
os dois descritores, inicio e final com NULL, como pode ser visto no
Algoritmo 97.

Algoritmo 97 – Inicialização da fila


01 void inicializar(FilaArquivos* arquivos){
02 arquivos->inicio = NULL;
03 arquivos->final = NULL;
04 }

Fonte: Elaborado pelo autor (2013).

A função inicializar recebe como parâmetro o endereço da fila que será


manipulada por meio do parâmetro chamado arquivos, que inicializa os
dois descritores, inicio e final, com NULL.

Para a inserção na fila, é preciso conhecer o nome e o tamanho do


arquivo que será incluído, por isso a função de inserção receberá como
parâmetro a fila que está sendo manipulada, o nome e o tamanho do
arquivo, como podemos ver no Algoritmo 98.

www.esab.edu.br 274
Algoritmo 98 – Inserção na fila
01 void inserir(FilaArquivos* arquivos, literal
nome, int tam){
02 No* novo = (No*) malloc(sizeof(No));
03 strcpy(novo->nomearquivo,nome);
04 novo->tamanho = tam;
05 novo->proximo = NULL;
06 if(arquivos->inicio == NULL){
07 arquivos->inicio = novo;
08 }else{
09 arquivos->final->proximo = novo;
10 }
11 arquivos->final = novo;
12 }

Fonte: Elaborado pelo autor (2013).

Na linha 02 é alocado um espaço de memória para o novo elemento,


já que se trata de uma alocação dinâmica, que na instrução da linha 03
armazena o nome do arquivo passado no parâmetro nome no campo
nomearquivo. Na instrução da linha 04, o valor do parâmetro tam é
armazenado no campo tamanho do novo elemento. Como se trata de um
novo elemento, o campo próximo do novo elemento é inicializado com
NULL já que ele não aponta para nenhum outro elemento da fila. Caso
a fila esteja vazia (arquivos->inicio == NULL), o novo elemento será
definido como o início da fila (arquivos->inicio = novo), caso contrário,
a fila não está vazia e o último elemento da fila apontará como próximo
para o novo elemento (arquivos->final->proximo = novo).

Independentemente de a fila estar vazia ou não, o novo elemento sempre


será definido como o final da fila, pois se trata de uma inserção no final,
por isso a instrução da linha 11, arquivos->final = novo.

Se a inserção precisa receber como parâmetro a fila que está sendo


manipulada e os dados do elemento que será adicionado na fila, a
operação de remoção que ocorre no início da fila só precisa receber como
parâmetro a fila que está sendo manipulada. A rotina deverá verificar se a
fila não está vazia para, então, caso ela não esteja vazia, remover o arquivo
que está no início da fila. Caso contrário, a operação de remoção não será
processada. O Algoritmo 99 apresenta essa lógica.

www.esab.edu.br 275
Algoritmo 99 – Remoção na fila
01 void remover(FilaArquivos* arquivos){
02 if(arquivos->inicio!=NULL){
03 No* aux = arquivos->inicio;
04 printf(“O valor %s foi removido da fila\
n”,aux->nomearquivo);
05 arquivos->inicio = arquivos->inicio-
>proximo;
06 if(arquivos->inicio == NULL) {
07 arquivos->final = NULL; //fila ficará
vazia
08 }
09 free(aux);
10 }
11 }

Fonte: Elaborado pelo autor (2013).

Observe que primeiro, na instrução da linha 02, é verificado se a fila


não está vazia (arquivos->inicio!=NULL), pois a remoção somente será
realizada se houver algum item inserido na fila. Se a fila não está vazia, na
linha 03, na variável aux é armazenado o endereço do primeiro elemento
da fila, representado pelo descritor arquivos->inicio, que será utilizado na
instrução da linha 09 (free(aux)) para que o elemento seja desalocado da
memória e removido da fila. Na linha 04, a instrução (arquivos->inicio
= arquivos->inicio->proximo;) faz com que descritor inicio da fila passe
a armazenar o elemento do segundo elemento da fila (o próximo do
primeiro). Porém, caso a fila possua um único elemento, o próximo do
inicio vale NULL. Dessa forma, o descritor inicio será atualizado para
NULL, por isso, na linha 06 é verificado se a operação da linha 05 não
resultou no descritor inicio valendo NULL (operação de remoção estará
excluindo o único elemento da fila). Caso a condição seja verdadeira, o
descritor final também será atualizado para NULL.

Pronto! Programamos todas as rotinas para inserção, remoção e listagem


dos arquivos da nossa fila de impressão.

www.esab.edu.br 276
Todo o código para manipulação da fila de impressão do exercício desenvolvido nesta
unidade pode ser acessado clicando aqui, no qual você pode fazer o download de
todo o código e testá-lo no seu computador.

Nesta unidade, você pôde estudar e verificar como desenvolver um


programa para inserção, remoção, busca e inicialização em um fila
implementada por meio de uma lista simplesmente encadeada.

Na próxima unidade, vamos continuar estudando a implementação das


filas, mas para isso vamos utilizar uma lista duplamente encadeada. Ou
seja, cada item da lista possuirá além do descritor próximo, mais um
descritor, o anterior, que armazenará o endereço do item anterior da fila.

Fórum
Caro estudante, dirija-se ao Ambiente Virtual de
Aprendizagem da instituição e participe do nosso
Fórum de discussão. Lá você poderá interagir com
seus colegas e com seu tutor de forma a ampliar,
por meio da interação, a construção do seu
conhecimento. Vamos lá?

www.esab.edu.br 277
32 Fila dupla

Objetivo
Apresentar o conceito, a aplicação e a manipulação de fila dupla.

Na unidade anterior, você estudou como desenvolver um programa para


inserção no final da fila, remoção no início da fila, busca e inicialização
em uma fila implementada por meio de uma lista simplesmente
encadeada. A fila foi implementada para simular as operações de uma fila
de arquivos, armazenando em cada item da fila o nome do arquivo e o
seu tamanho.

Nesta unidade, vamos continuar estudando a implementação das filas,


mas utilizaremos dois ponteiros para cada item da fila, por meio de uma
lista duplamente encadeada, na qual cada item da fila representará o
número de um pedido, que pode ser utilizado por um restaurante para
saber a ordem dos pedidos que serão produzidos na cozinha. O conteúdo
desta unidade foi desenvolvido com base na referência bibliográfica de
Celes, Cerqueira e Rangel Netto (2004).

Vamos iniciar apresentando a estrutura de cada elemento da fila,


representada pelo tipo estruturado item_no e a estrutura da fila,
representada também por um tipo estruturado, chamado fila. Porém,
vamos identificar que cada item da lista possuirá dois descritores,
próximo e anterior. Depois abordaremos as rotinas de inicialização,
inserção, listagem e remoção na fila implementada a partir de uma lista
duplamente encadeada.

www.esab.edu.br 278
32.1 Definição
O que diferencia a fila que utiliza a lista simplesmente encadeada da fila
com lista duplamente encadeada é apenas a quantidade de descritores
para cada nó, mudando de um descritor (apenas o próximo) na lista
simples, para dois descritores (anterior e próximo) na duplamente
encadeada. Porém, as operações continuam sendo as mesmas, inserção no
final da fila, remoção no início da fila, listagem e consulta aos dados, de
forma que a estrutura de dados fila mantenha a regra de FIFO (primeiro
a entrar, primeiro a sair). Para Celes, Cerqueira e Rangel Netto (2004,
p. 181), nesse tipo de fila, com lista duplamente encadeada “[...] em
cada nó da fila, além da referência para o próximo elemento, há também
uma referência para o elemento anterior, para que seja possível acessar o
elemento posterior e adjacente de cada elemento da fila”.

Vamos verificar como implementar a fila com lista duplamente


encadeada por meio das operações de inicialização, inserção, remoção e
listagem dos elementos.

32.2 Implementação de fila dupla com lista


Para contextualizar o uso da fila com lista duplamente encadeada, vamos
criar uma fila de controle dos pedidos de um restaurante, assim cada
item da fila armazenará o número do pedido que deve ser atendido.
Os pedidos que são inseridos mais cedo na fila possuem prioridade de
atendimento, garantindo assim a característica de FIFO da fila, onde o
pedido que é inserido primeiro é atendido (e removido) primeiro.

Para tanto, faremos uma alteração no código que define a estrutura de


cada nó da fila, o tipo estruturado item_no, pois a lista duplamente
encadeada utiliza dois ponteiros de referência para o elemento anterior
e posterior, e na lista simplesmente encadeada há apenas uma referência
para o elemento posterior.

O Algoritmo 100 apresenta a declaração de cada item da fila com lista


duplamente encadeada e da fila com os descritores inicio e final.

www.esab.edu.br 279
Algoritmo 100 – Estrutura da fila com lista duplamente encadeada
01 struct item_no{
02 int numeropedido;
03 struct item_no *proximo;
04 struct item_no *anterior;
05 };
06 typedef struct item_no No;e
07 struct fila{
08 No* inicio;
09 No* final;
10 };
11 typedef struct fila FilaPedidos;

Fonte: Elaborado pelo autor (2013).

Observe que cada item da fila, representado pelo tipo estruturado


item_no e declarado nas linhas 01 a 05, possui o campo numeropedido
(linha 02), do tipo inteiro, e os campos proximo (linha 03) e anterior
(linha 04), que são referências (endereço de memória) para o elemento
posterior e antecessor de cada elemento da fila, respectivamente. Em
relação à estrutura de dados fila, não há mudanças: ela é formada por
dois descritores, inicio e final, que armazenam o endereço de memória do
primeiro e último elemento inseridos na fila, e são declarados por meio
do tipo estruturado fila, nas linhas 07 a 09.

Como a declaração da fila não mudou em relação à fila com lista


simplesmente encadeada, a função de inicialização também não mudará,
como pode ser visto no Algoritmo 101:

Algoritmo 101 − Inicialização da fila duplamente encadeada


01 void inicializar(FilaPedidos* pedidos){
02 pedidos->inicio = NULL;
03 pedidos->final = NULL;
04 }

Fonte: Elaborado pelo autor (2013).

A função inicializar recebe a fila que será manipulada por meio do


parâmetro pedidos, que é uma referência para a FilaPedidos, e inicializa
os descritores inicio e final com NULL.

www.esab.edu.br 280
Vamos analisar a próxima rotina de inserção no final da fila duplamente
encadeada, representada pelo Algoritmo 102:

Algoritmo 102 − Inserção na fila duplamente encadeada


01 void inserir(FilaPedidos* pedidos,int numero){
02 No* novo = (No*) malloc(sizeof(No));
03 novo->numeropedido = numero;
04 novo->proximo = NULL;
05 novo->anterior = NULL;
06 if(pedidos->inicio == NULL){
07 pedidos->inicio = novo;
08 }else{
09 pedidos->final->proximo = novo;
10 novo->anterior = pedidos->final;
11 }
12 pedidos->final = novo;
13 }

Fonte: Elaborado pelo autor (2013).

A função inserir recebe como parâmetro a referência da fila que será


manipulada (parâmetro pedidos) e o número do pedido (parâmetro
numero). Na instrução da linha 02, é alocado um espaço de memória
para o novo elemento, e na instrução da linha 03 é copiado o valor do
parâmetro numero para o campo numeropedido do novo elemento.
Como se trata de uma lista duplamente encadeada, para compor a fila,
o novo elemento terá os campos próximo e anterior inicializados com
NULL. Lembre-se de que na fila simplesmente encadeada apenas o
campo próximo era utilizado.

Na instrução da linha 06 é verificado se a fila está vazia (pedidos-


>inicio == NULL). Caso a condição seja verdadeira, o novo elemento
será referenciado como inicio da fila (pedidos->inicio = novo). Caso
contrário, o último elemento da fila apontará como próximo para o novo
elemento (pedidos->final->proximo = novo) e o novo elemento apontará
como anterior para o último da fila (novo->anterior = pedidos->final).

Na instrução da linha 12, o novo elemento é referenciado como último


da fila (pedidos->final = novo).

www.esab.edu.br 281
Vamos analisar agora a rotinas de remoção do primeiro elemento da fila
duplamente encadeada, apresentada no Algoritmo 103:

Algoritmo 103 – Remoção na fila duplamente encadeada


01 void remover(FilaPedidos* pedidos){
02 if(pedidos->inicio!=NULL){
03 No* aux = pedidos->inicio;
04 pedidos->inicio = pedidos->inicio-
>proximo;
05 printf(“Pedido numero:%d foi removido da
fila\n”,aux->numeropedido);
06 if(pedidos->inicio == NULL){
07 pedidos->final = NULL;
08 }
09 else{
10 pedidos->inicio->anterior = NULL;
11 }
12 free(aux);
13 }
14 }

Fonte: Elaborado pelo autor (2013).

A função remover recebe como parâmetro apenas a referência da fila que


está sendo manipulada, e a remoção somente será realizada se a fila não
estiver vazia, por isso, a condição na instrução da linha 02, if(pedidos-
>inicio!=NULL). Caso a condição seja verdadeira (a fila não está vazia),
na instrução da linha 03 é armazenado o endereço do primeiro elemento
que será removido na variável aux, que é utilizado como parâmetro na
função free(aux) na linha 12, que desaloca esse elemento da memória
removendo-o da fila. Como o primeiro elemento (início da fila) será
removido, o segundo elemento da fila passa a ser o início da fila. Por isso,
a instrução da linha 04, pedidos->inicio = pedidos->inicio->proximo,
determina que o descritor início (pedidos->inicio) seja atualizado com o
valor do endereço de memória no qual foi alocado o segundo elemento
da fila (pedidos->inicio.>proximo). Porém, se a fila tiver um único
elemento, o campo próximo do início vale NULL, portanto, pedidos-
>inicio também valerá NULL. Isso significa que após a remoção a fila
ficará vazia, logo, na linha 05 é verificado se o início da fila vale NULL
(pedidos->inicio == NULL). Caso a condição seja verdadeira, o final da
fila também valerá NULL (pedidos->final = NULL).

www.esab.edu.br 282
Caso não se trate da remoção do único elemento da fila, a instrução da
linha 10 será executada, fazendo com que no início da fila o primeiro
elemento tenha como anterior o valor NULL, pois antes dele não existirá
nenhum outro elemento da fila.

Por fim, vamos analisar as regras de implementação da rotina de listagem


dos itens da fila duplamente encadeada apresentadas no Algoritmo 104:

Algoritmo 104 – Listagem dos elementos da fila duplamente encadeada


01 void listar(FilaPedidos* pedidos,char tipo){
02 system("CLS");
03 printf("LISTAGEM\n");
04 No* item = pedidos->inicio;
05 if(tipo == 'D' || tipo == 'd') {
06 item = pedidos->final;
07 }
08 while(item != NULL){
09 printf("Pedido Numero:%d \n",item-
>numeropedido);
10 if(tipo == 'C' || tipo =='c'){
11 item = item->proximo;
12 }else{
13 item = item->anterior;
14 }
15 }
16 system("PAUSE");
17 }

Fonte: Elaborado pelo autor (2013).

Sabemos que uma das características fundamentais das estruturas de


dados duplamente encadeadas é a possibilidade de listar os elementos
do início para o final e do final para o início, sem que alterações sejam
realizadas na estrutura de dados. Assim, a rotina de listagem está sendo
desenvolvida de forma que a função receba como parâmetro a fila que
será manipulada (pedidos) e o tipo, que pode ser ‘C’ para crescente,
listando do início para o final da fila, usando o descritor próximo de cada
item da fila; ou ‘D’ para decrescente, listando do final para o início da
fila, usando o descritor anterior de cada item.

www.esab.edu.br 283
Em princípio, a listagem começará no início da fila, por isso, na linha
03 a variável item é inicializada com o endereço do primeiro elemento
da fila (pedidos->inicio). Na linha 05 é verificado se o tipo escolhido é
decrescente (D) e em caso verdadeiro, a variável item é inicializada com
o endereço do último elemento da fila (pedidos->final). As instruções das
linhas 06 a 11 representam as rotinas para percorrer cada elemento da
fila e mostram o número do pedido cadastrado. Sendo que, se o tipo é
crescente (C), a listagem utilizará o descritor próximo de cada elemento
para percorrer a fila (if (tipo == ‘C’ || tipo ==’c’) item = item->proximo).
Caso contrário, será utilizado o descritor anterior de cada item da fila
(else item = item->anterior).

Dessa forma, a fila pode ser listada nos dois sentidos, sem a necessidade
de alterações na organização dos elementos. Por exemplo, para listar
crescentemente a chamada da função será listar(Fila,’C’), e para listar
decrescentemente a chamada será listar(Fila,’D’), onde Fila representa a
fila dinamicamente alocada que será manipulada.

Você pode baixar todo o código do exemplo desenvolvido nesta unidade, com
exemplos para a chamada da função listar de forma crescente e decrescente, assim
como as rotinas de inserção, remoção e inicialização, acesse clicando aqui.

Nesta unidade, por meio de um exemplo que cria uma fila de controle
de pedidos, com as operações de inicialização, remoção e listagem
dos elementos em dois sentidos, crescente e decrescente, você pôde
desenvolver e analisar as diferenças entre uma fila simplesmente
encadeada e outra duplamente encadeada.

Na próxima unidade, iniciaremos os estudos da estrutura de dados


árvore, conceituando e conhecendo as terminologias relacionadas com
essa nova estrutura de dados.

Até lá!

www.esab.edu.br 284
33 Árvores – parte I

Objetivo
Apresentar o conceito de árvores.

Na unidade anterior você pôde desenvolver e analisar as diferenças entre


uma fila simplesmente encadeada e uma fila duplamente encadeada, por
meio do desenvolvimento de um exemplo que cria uma fila de controle
de pedidos mediante a função de inicialização; a remoção no início, por
meio da função remover; e a listagem dos elementos em dois sentidos,
crescente (C) e decrescente (D).

Nesta unidade, iniciaremos os estudos sobre as árvores, conceituando e


conhecendo as terminologias relacionadas com essa nova estrutura de dados.

Este conteúdo foi desenvolvido com base nas referências bibliográficas de


Celes, Cerqueira e Rangel Netto (2004) e Puga (2009).

Vamos iniciar apresentando a definição de árvore binária.

33.1 Definição
Nas unidades anteriores, pudemos abordar o estudo das estruturas
de dados lista, filas e pilhas, que são consideradas estruturas de dados
lineares, assim como a estrutura de dados vetor. A conotação de
linear está associada à forma pela qual os dados são representados
sequencialmente, com cada elemento um ao lado do outro. A Figura 108
apresenta as estruturas de dados estudadas anteriormente.

www.esab.edu.br 285
Vetores Listas encadeadas saída Fila
0 1 2 3
(simples) entrada

NULL NULL
(dupla)
Pilha
NULL NULL
pop push
(circular)
topo

Figura 108 – Estruturas de dados lineares.


Fonte: Elaborada pelo autor (2013).

Lembrando que o vetor é uma estrutura de dados que se caracteriza por


ter um índice que representa a posição do elemento dentro da estrutura
(começando em 0 e terminando em tamanho do vetor -1), e requer que
se conheça previamente o número de elementos que serão manipulados.
Já as listas encadeadas se caracterizam pela alocação dinâmica dos
elementos, que podem ser criados ou removidos da estrutura de dados
em tempo de execução. Isso possibilita um gerenciamento do espaço de
memória utilizado, podendo ser simplesmente encadeada (utiliza uma
referência para cada elemento, normalmente chamado de próximo),
duplamente encadeada (utiliza duas referências, normalmente chamadas
de próximo e anterior) e circular (o último elemento aponta como
próximo para o primeiro elemento da estrutura de dados). Por fim,
temos as estruturas de dados com regras de utilização, o caso da fila que
só permite a inserção no final e a remoção no início, e a pilha, que só
permite a inserção no topo (push) e a remoção no topo (pop).

Vamos agora imaginar uma estrutura que possua uma representação não
linear, hierárquica, como uma lista de diretórios do computador, em
que um diretório pode estar dentro de outro e assim subsequentemente,
como apresentado na Figura 109:

www.esab.edu.br 286
Figura 109 – Lista de diretórios.
Fonte: Elaborada pelo autor (2013).

Note que seria muito complicado representar essa estrutura de diretórios


por meio de uma estrutura de dados linear, já que os elementos não estão
dispostos lado a lado, mas um dentro do outro. Além disso, à medida
que vamos navegando pelos diretórios eles podem ter vários arquivos e
ainda outros diretórios, os quais mudam de acordo com as pastas que são
visitadas. Assim, precisaremos usar uma estrutura de dados hierárquica,
mais especificamente, uma árvore. A Figura 110 apresenta a estrutura de
dados para armazenamento dos diretórios do computador.

www.esab.edu.br 287
Unidade
C:

Arquivos de Dev-CPP Usuários


Programas

Windows

Boot

PCAI Pasta A Pasta B

cs-CZ DA-DK

Figura 110 – Árvore para armazenamento dos diretórios.


Fonte: Elaborada pelo autor (2013).

Observe que os dados são representados hierarquicamente, pois para


acessar um determinado diretório pode ser necessário que se visite outros
diretórios até se chegar ao diretório desejado. Por exemplo, os diretórios
“Arquivos de Programas”, “DEV-CPP” e “Usuários” são acessados a partir
do elemento “Unidade C:”. Já para acessar o diretório “Boot”, é preciso
navegar pelos diretórios “Unidade C:”, “Usuários”, “Windows” e “Boot”,
ou seja, é preciso descer na estrutura a partir do primeiro elemento até
se chegar ao elemento desejado. Celes, Cerqueira e Rangel Netto (2004,
p. 185), definem a árvore como uma estrutura de dados “[...] adequada
para a representação de dados que devem ser dispostos de maneira
hierárquica”. Já Puga (2009, p. 232), define a árvore como “[...] uma

www.esab.edu.br 288
estrutura de dados não linear, que possui propriedades especiais e admite
muitas operações, como pesquisa, inserção, remoção, e são úteis para
implementação de algoritmos que necessitam de estruturas hierárquicas”.

A árvore é uma estrutura de dados bem peculiar, por isso, possui uma
terminologia própria para identificação de seus elementos e grupo de
elementos, como veremos a seguir.

33.2 Terminologias aplicadas a árvores (grau de um


nó, folha, nível do nó, altura da árvore etc.)
Uma árvore, de modo geral, tem as seguintes características (PUGA, 2009):

• nó – cada elemento da árvore é chamado de nó;


• nó raiz – nó do topo da árvore, do qual descendem os demais nós;
• nó interior – nó do interior da árvore que possui descendentes;
• nó folha ou terminal – nó que não possui descendentes;
• grau do nó – número de descendentes de um nó da árvore;
• grau da árvore – maior grau de todos os nós;
• altura da árvore – número máximo de níveis de uma árvore;
• trajetória – caminho percorrido de um nó origem até um nó destino.
Para compreendermos a terminologia das árvores, vamos analisar a
Figura 111.

Níveis

A 0

B C 1

D E F G H 2

I 3

Figura 111 – Árvore.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 289
Analisando a árvore da Figura 111, podemos concluir que:

• o nó A é chamado de raiz, pois todos os nós descendem dele. O nó


A possui grau 2, pois ele possui dois filhos, nós B e C;
• o nó B possui grau 2, pois possui dois filhos, os nós D e E. O nó B
pode ser chamado também de pai dos nós D e E;
• o nó C possui grau 3, pois possui três nós filhos, F, G e H;
• os nós E, F, G, H e I são chamados de folhas, pois não possuem
filhos, portanto possuem grau;
• os nós A, B, C e D são chamados de nós interiores, pois possuem
filhos;
• a árvore possui grau 3, grau máximo em toda a árvore, pois o maior
número de filhos é do nó C, com três filhos;
• a altura da árvore é 3, o maior nível (0 a 3);
• a trajetória do nó A até o nó G é A, C e G.
Note que na representação gráfica não há um sentido para as ligações
entre os elementos, mas devemos sempre considerar que o sentido vai do
nó pai para o nó filho.

Uma árvore pode ser classificada como “[...] genérica ou binária, dependendo
do número máximo de filhos para cada nó” (PUGA, 2009, p. 186).

Vamos começar nossos estudos a partir da árvore binária, sendo que as


terminologias se mantêm independentes do tipo de árvore.

www.esab.edu.br 290
33.3 Árvore binária
As árvores binárias são assim nomeadas por possuírem no máximo dois
filhos para cada nó. Segundo Puga (2009, p. 234), as árvores binárias
“[...] possuem nós com grau menor ou igual a 2, isto é, nenhum nó
possui mais que dois descendentes diretos (dois filhos)”.

A Figura 112 apresenta uma árvore binária.

(-) 10 (+) Nível - 0

08 21 Nível - 1

04 09 55 Nível - 2
Figura 112 – Árvore binária.
Fonte: Elaborada pelo autor (2013).

Note que cada nó possui no máximo dois filhos (grau <= 2). A raiz da
árvore é o nó com valor 10 e a árvore possui como folha os nós 04, 09 e
55, pois eles não possuem filhos. Já os nós 10, 08 e 21 são considerados
nós internos, pois possuem filhos. A altura da árvore é 2, já que é este o
maior nível da árvore (nível 0 possui o nó 10, nível 1 possui os elementos
08 e 21, e nó 2 possui os elementos 04, 09 e 55).

A árvore binária possui uma característica muito importante quanto


à posição dos nós: os valores menores que os do pai sempre estão à
esquerda e os valores maiores que os do pai sempre à direita.

Nesta unidade, você estudou que a estrutura de dados árvore é adequada


para a representação de dados que devem ser dispostos de maneira
hierárquica e pode ser definida como uma estrutura de dados não linear,
pois possui propriedades especiais e admite muitas operações, como:
pesquisa, inserção, remoção. São úteis para implementação de algoritmos
que necessitam de estruturas hierárquicas.

www.esab.edu.br 291
A árvore é uma estrutura de dados bem peculiar. Por isso, possui uma
terminologia própria para identificação de seus elementos e grupo de
elementos, como: nó, raiz, folha, grau do nó, grau e altura da árvore,
trajetória e nó interior.

Você viu também que uma árvore pode ser classificada como generalizada
ou binária, dependendo do número máximo de filhos para cada nó. As
árvores binárias são assim nomeadas porque apresentam no máximo
dois filhos para cada nó e possuem uma característica muito importante
quanto à posição dos nós: os valores menores que os do pai sempre estão
à esquerda e os valores maiores que os do pai sempre à direita.

Na próxima unidade, veremos como codificar e manipular as árvores


binárias em C.

www.esab.edu.br 292
34 Árvores – parte II

Objetivo
Codificação de árvores em C.

Na unidade anterior, você estudou que a estrutura de dados árvore é


uma estrutura não linear e adequada para a representação de dados que
devem ser dispostos de maneira hierárquica. A árvore é uma estrutura de
dados bem peculiar, por isso, possui um jargão próprio para identificação
de seus elementos e grupo de elementos, como: nó, raiz, folha, grau
do nó, grau e altura da árvore, trajetória e nó interior. Uma árvore
pode ser classificada como generalizada ou binária, dependendo do
número máximo de filhos para cada nó. As árvores binárias possuem
como característica a posição dos nós em relação ao nó pai, sendo que
os valores menores que os do pai sempre estão à esquerda e os valores
maiores que os do pai sempre à direita.

Nesta unidade, vamos estudar como programar uma árvore utilizando a


linguagem C e iniciaremos apresentando a definição de árvore binária.
Para tanto, utilizaremos como referências Celes, Cerqueira e Rangel
Netto (2004) e Puga (2009).

34.1 Codificação de árvores em C


As árvores binárias são assim nomeadas por possuírem no máximo dois
filhos para cada nó. Além disso, possuem uma propriedade importante
de acordo com a qual os valores menores que os de seu pai ficam à sua
esquerda, e os valores maiores que os de seu pai, ficam à sua direita.

Vamos utilizar essa propriedade para desenvolver uma árvore binária, que
armazena os valores informados pelo usuário, valores estes expressos em
números inteiros, de forma que os valores menores fiquem à esquerda
e os valores maiores à direita. A Figura 113 apresenta o processo de
inserção na árvore binária.

www.esab.edu.br 293
Valores a serem inseridos:
[15, 21, 55, 08, 12, 33]

Insere 15 (1) Insere 8 (4) Insere 33 (6)


se for maior -> direita se for maior -> direita
15 (raiz) se for menor -> esquerda se for menor -> esquerda
Insere 21 (2)
se for maior -> direita 15 (raiz) 15 (raiz)
se for menor -> esquerda
08 21 08 21

15 (raiz) 55 12 55

21 33
Insere 55 (3) Insere 12 (5)
se for maior -> direita se for maior -> direita
se for menor -> esquerda se for menor -> esquerda

15 (raiz) 15 (raiz)
21 08 21
55 12 55

Figura 113 – Inserção na árvore binária.


Fonte: Elaborada pelo autor (2013).

Observe que a primeira inserção define o valor que será a raiz da árvore.
A cada inserção é verificado se o valor é maior ou menor do que o valor
da raiz. Se o valor for maior é inserido à direita, se for menor é inserido
à esquerda. Porém, essas posições já podem ter sido ocupadas por
outra inserção anterior, logo, é preciso que o processo de inserção faça
uma nova verificação do valor que está na posição que seria a inserção,
avaliando se o valor a ser inserido é maior ou menor que esse valor já
inserido: se for maior que o valor já inserido, o novo valor é inserido
à direita, se for menor que o valor já inserido, o novo valor é inserido
à esquerda do valor já inserido. O processo se repete para cada nó que
já está ocupado até que se ache uma posição livre para inserção. Por
exemplo, a Figura 113 apresenta o processo para inserção do valor 33.
Como ele é maior que o nó 15 (raiz), deve ser inserido à direita do nó
15, mas nessa posição já existe o nó 21. Então, pergunta-se ao nó 21 se o

www.esab.edu.br 294
valor 33 é maior ou menor que ele (21), como é maior, deve ser inserido
à direita, mas já existe o nó 55 nessa posição, então, se pergunta ao nó 55
se 33 é maior ou menor do que ele, como é menor, é inserido à esquerda
do nó 55.

Assim podemos concluir que todos os nós possuem os valores menores


que ele à esquerda e maiores que ele à direita.

Bem, vamos ver como programar essa regra de inserção utilizando a


linguagem C.

Para tal, vamos definir a estrutura de cada nó da árvore que é formada


por um valor inteiro e duas referências, para o nó da esquerda e para o nó
da direita. O Algoritmo 105 apresenta essa lógica.

Algoritmo 105 – Estrutura da árvore binária


01 struct noarvore {
02 struct noarvore* esq;
03 int valor;
04 struct noarvore* dir;
05 };
06 typedef struct noarvore No;
07 typedef No* ArvoreBinaria;

Fonte: Elaborado pelo autor (2013).

Cada nó da árvore é composto por uma referência para o nó da esquerda


(esq) e uma referência para o nó da direita (dir), além do valor inteiro
que será armazenado. Na linha 6 a instrução typdef define um novo
tipo de dado chamado No que representa o tipo estruturado noarvore e
na linha 7 é definido um novo tipo chamado ArvoreBinaria, que é uma
referência, ou seja, um ponteiro para um tipo No.

Segundo Puga (2009, p. 234), “[...] o nó da árvore possui duas variáveis,


esquerda e direita, do tipo apontador e que serão utilizadas para fazer a
referência aos nós localizados à direita e à esquerda do nó pai”.

Como a árvore deve iniciar vazia, sem elementos, vamos criar uma
função que inicialize a árvore com NULL. O Algoritmo 106 apresenta
essa função.

www.esab.edu.br 295
Algoritmo 106 – Inicializar a árvore binária
01 void inicializar(ArvoreBinaria *arvore){
02 *arvore = NULL;
03 }

Fonte: Elaborado pelo autor (2013).

A função recebe como parâmetro a referência (endereço na memória) da


árvore binária que será manipulada e a inicializa com NULL.

Para a inserção dos elementos na árvore binária, serão passados como


parâmetros a referência da árvore que será manipulada e o valor a ser
inserido. Como regra, caso a árvore esteja vazia (vale NULL), será
inserido o valor como sendo a raiz da árvore, e as demais inserções
verificarão se o valor a ser inserido é menor ou maior que o valor do nó
pesquisado, sendo que os valores menores são inseridos à esquerda e os
maiores à direita do nó pesquisado. Caso a posição de inserção já esteja
preenchida, é preciso “descer” nos níveis da árvore até localizar uma
posição de inserção livre e disponível.

A função será implementada de forma recursiva, ou seja, a função chama


a si mesma até que seja localizada uma posição de inserção disponível.
De acordo com Puga (2009, p. 237) a função de inserir “[...] percorre
recursivamente a árvore partindo da raiz, buscando uma posição de
referência nula à esquerda ou à direita para, então, inserir o novo elemento”.

O Algoritmo 107 apresenta a lógica para inserção na árvore binária:

www.esab.edu.br 296
Algoritmo 107 – Inserção na árvore binária
01 void inserir(ArvoreBinaria *arvore,int
novovalor){
02 if ((*arvore) = = NULL) {
03 *arvore = (No *) malloc(sizeof(No));
04 (*arvore)->esq = NULL;
05 (*arvore)->dir = NULL;
06 (*arvore)->valor = novovalor;
07 }
08 if (novovalor < (*arvore)->valor) {
09 inserir((&(*arvore)->esq, novovalor);
10 }
11 else{
12 if (novovalor > (*arvore)->valor) {
13 inserir(&((*arvore)->dir, novovalor);
14 }
15 }
16 }

Fonte: Elaborado pelo autor (2013).

Na linha 02, a instrução if((*arvore) == NULL) ) verifica se o endereço


alocado na memória para a árvore está disponível, ou seja, se não foi
alocado ainda, não existe um nó nessa posição (raiz, esquerda ou direita).
O nó será alocado na memória na instrução da linha 03 e as referências
da esquerda e da direita desse nó valerão NULL, já que o nó não possui
filhos quando da sua criação. Portanto, as instruções das linhas 03 a
06 criam um nó na árvore com as referências da esquerda e da direita
valendo NULL e com o campo valor preenchido com o novo valor
passado como parâmetro. É importante ressaltar que isso só ocorrerá
quando a posição na árvore estiver vazia ((*arvore) == NULL).

Na linha 08 é verificado se o novo valor a ser inserido é maior que o


valor do nó da árvore e, caso a condição seja verdadeira, a função inserir
é chamada recursivamente, na linha 09, recebendo como parâmetro
o endereço do filho da esquerda do nó que está sendo avaliado
(&((*arvore)->esq) e também o novo valor a ser inserido. Caso o novo
valor não seja menor que o valor do nó que está sendo verificado, mas
seja maior (linha 12), a função inserir é chamada recursivamente, na
linha 13, recebendo como parâmetro o endereço do filho da direita do
nó que está sendo avaliado (&((*arvore)->dir) e também o novo valor a
ser inserido.

www.esab.edu.br 297
Na chamada recursiva, com o endereço da esquerda ou da direita, junto
com o valor a ser inserido, a função verificará novamente na linha 02
se essa nova referência está livre (NULL). Se estiver livre, o nó é criado,
com esquerda e direita valendo NULL e o novo valor gravado no nó.
Caso contrário, o processo recursivo se repete (descendo na árvore) até
que um nó vazio seja localizado.

Por exemplo, na inserção do valor 15 na árvore, que está vazia, será


criado o nó com as referências da esquerda e direita valendo NULL,
e o valor 15 será armazenado nesse novo nó criado. Já na inserção do
valor 21, será testado se a árvore está vazia, e como ela não estará, o
próximo passo será verificar se o valor 21 é menor que o valor 15, do
primeiro elemento, como não é menor, na sequência será testado se o
valor 21 é maior que 15. Como é maior, a função inserir será chamada
recursivamente com o endereço à direita de 15, que é NULL, e o valor
21, que é o valor a inserir, ficando a chamada assim: inserir(endereço da
direita de 15, 21). Assim que a função for executada, como o endereço à
direita de 15 é NULL, será criado um novo nó para essa posição (direita
de 15), com o valor 21, e esse nó terá as referências da esquerda e direita
valendo NULL.

O processo se repete para cada elemento inserido na lista, de forma que


recursivamente a árvore vá descendo para esquerda ou direita, a partir do
nó raiz, até localizar um nó disponível para o novo elemento.

Por fim, vamos desenvolver uma função para listar os nós da árvore,
que deverá ser recursiva, de forma que a listagem comece na raiz e vá
descendo à esquerda ou à direita de cada nó que não seja NULL. De
acordo com Celes, Cerqueira e Rangel Netto (2004, p. 190) a função
para exibição dos elementos da árvore “[...] percorre recursivamente a
árvore, visitando todos os nós e imprimindo sua informação”.

www.esab.edu.br 298
Essa regra é apresentada no Algoritmo 108:

Algoritmo 108 – Listagem dos elementos da árvore binária


01 void exibirNos(ArvoreBinaria arvore, char
tipo){
02 if (arvore != NULL){
03 printf("(%d) %c \n", arvore->valor, tipo);
04 if (arvore->esq != NULL) exibirNos(arvore-
>esq,'E');
05 if (arvore->dir != NULL) exibirNos(arvore-
>dir,'D');
06 }
07 }

Fonte: Elaborado pelo autor (2013).

Para listagem dos elementos da árvore binária, a função recebe como


parâmetro a referência da árvore que está sendo manipulada e o tipo, um
caractere. O parâmetro tipo tem como finalidade mostrar um R (raiz), E
(filho da esquerda) ou D (filho da direita), para que o usuário possa ter
uma noção de onde o elemento se encontra na árvore.

O valor do nó da árvore é mostrado na tela desde que a sua referência


seja diferente de NULL (condição da linha 2), e caso o nó listado possua
um filho à esquerda, a função é chamada recursivamente com a referência
para esse filho à esquerda e o tipo valendo ‘E’. Havendo um filho à
direita, a função será chamada recursivamente com a referência para
esse filho à direita e o tipo ‘D’. Na chamada recursiva, caso o endereço
de referência do nó (arvore) seja diferente de NULL, seu valor é listado
na tela, seguido do tipo (R, E ou D). Em seguida o processo se repete,
buscando o filho da esquerda e da direita de cada nó até que a árvore
chegue ao último nó que é a folha (sem filhos).

Você pode fazer o download de todo o código apresentado nesta unidade clicando
aqui. Procure baixá-lo e executá-lo no seu computador. Ele foi desenvolvido
utilizando a ferramenta DevC++, mas pode ser testado em qualquer compilador da
linguagem C.

www.esab.edu.br 299
Nesta unidade você pôde estudar como implementar uma árvore binária
utilizando a linguagem de programação C. Primeiramente, foi preciso
definir a estrutura de cada nó da árvore, formada por duas referências,
esquerda e direita, que apontam para os nós filhos da esquerda e da
direita de cada nó da árvore. Como cada nó armazenará um inteiro, o
tipo estruturado nó foi composto ainda por um campo chamado valor,
do tipo inteiro.

Você pôde ainda verificar que a inserção na árvore binária se dá de forma


recursiva, os valores menores são inseridos à esquerda do nó pai e os
valores maiores à direita do nó pai. Isso ocorre desde que essas posições
estejam vazias, desocupadas. Caso contrário, é preciso descer à esquerda
ou à direita do nó até encontrar uma posição livre, que mantenha as
regras de maiores à direita e menores à esquerda.

Por fim, você pôde analisar que a impressão dos valores de uma árvore
também utiliza a recursividade, listando o valor armazenado em cada nó,
desde que ele seja diferente de NULL. Em seguida, a função para listar os
dados é chamada recursivamente com a referência da esquerda e da direita
do nó pai. Ou seja, a listagem percorre a árvore listando o valor do pai,
depois o valor do filho da direita e em seguida do filho da esquerda.

Na próxima unidade, estudaremos as formas de caminhar em uma


árvore, chamadas de percurso da árvore, que pode ser prefixado, central e
pós-fixado.

www.esab.edu.br 300
35 Árvores – parte III

Objetivo
Apresentar o conceito, a aplicação e a manipulação de árvores.

Na unidade anterior, você pôde estudar e implementar uma árvore


binária definindo a estrutura de cada nó da árvore com duas referências
que apontam para os nós filhos da esquerda e da direita de cada nó da
árvore. Como cada nó armazenará um inteiro, o tipo estruturado nó foi
composto ainda por um campo chamado valor.

Você pôde ainda verificar que a inserção na árvore binária se dá de forma


recursiva: os valores menores são inseridos à esquerda do nó pai e os
valores maiores à direita do nó pai. E caso as posições de inserção estejam
ocupadas, é preciso descer à esquerda ou à direita do nó até achar uma
posição livre.

A impressão dos valores de uma árvore também utiliza a recursividade,


onde a listagem percorre a árvore listando o valor do pai, depois o valor
do filho da direita e em seguida do filho da esquerda.

Nesta unidade, estudaremos as formas de percorrer os elementos da


árvore, chamadas de percurso da árvore, que pode ser prefixado, central e
pós-fixado.

Esse conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

Vamos iniciar apresentando a definição de árvore binária.

www.esab.edu.br 301
35.1 Ordens de percurso em árvores binárias
(prefixado, central e pós-fixado)
Na unidade anterior, a implementação dos elementos da árvore binária
utilizou como regra a impressão dos dados do nó pai, depois dos nós
filhos da esquerda e depois dos nós filhos da direita. Essa forma de
imprimir os nós da árvore é chamada de percurso prefixado ou pré-
ordem, mas não é a única forma de listar os elementos da árvore.

Celes, Cerqueira e Rangel Netto (2004, p. 193), destacam que a forma


de imprimir chamada de prefixada pode ser representada por “[...]
trata raiz, percorre os elementos da esquerda, percorre os elementos da
direita, ou seja, R, E, D”. Onde R é raiz, E, esquerda, e D, direita. Ou
seja, a prioridade é imprimir o elemento da raiz (ou nó pai), depois o da
esquerda, e por último o da direita.

A Figura 114 apresenta uma árvore binária com sete elementos.

2 6

1 3 5 7

Figura 114 – Árvore binária com sete elementos.


Fonte: Elaborada pelo autor (2013).

Percorrendo essa árvore utilizando o percurso prefixado (R, E, D), os


elementos serão impressos na seguinte sequência: 4, 2, 1, 3, 6, 5, 7.

A Figura 115 apresenta o percurso prefixado.

www.esab.edu.br 302
(1) R
(2)
E Pai (5)
4

(3) D E
2 6
E (4) (6) D (7)

1 3 5 7

Figura 115 – Percurso prefixado na árvore binária.


Fonte: Elaborada pelo autor (2013).

Observe que primeiro será impresso o nó raiz, que é 4, depois a


prioridade é imprimir os elementos da esquerda, até chegar em uma
folha (nó sem filhos). Assim, serão impressos os valores 2 (esquerda
da raiz), 1 (esquerda do 2) e então será impresso o valor 3, já que não
há mais elementos à esquerda. Após a listagem de todos os elementos
à esquerda da raiz, começa a listagem dos elementos à direita da raiz,
com prioridade para o nó pai, depois esquerda e por último, direita. Por
isso, o próximo elemento a ser impresso é o 6, primeiro nó pai à direita,
depois o valor 5, que está à esquerda e por último o valor 7, à direita.

O Algoritmo 109 apresenta a lógica de listagem dos elementos pelo


percurso prefixado.

Algoritmo 109 – Percurso prefixado


01 void prefixado(ArvoreBinaria no){
02 if (no != NULL){
03 printf("%d\n",no->valor);
04 if (no->esq != NULL) prefixado(no->esq);
05 if (no->dir != NULL) prefixado(no->dir);
06 }
07 }

Fonte: Elaborado pelo autor (2013).

A função prefixado recebe como parâmetro o nó que será manipulado,


sendo que na chamada da função esse nó será o primeiro elemento da
árvore, ou seja, a raiz. Se o nó for diferente de NULL, o seu valor é
impresso na tela, e na sequência é chamada recursivamente a função

www.esab.edu.br 303
prefixado que recebe o endereço do nó da esquerda, caso ele não seja
NULL. Depois é chamada recursivamente a função prefixado que recebe
o endereço do nó da direita, gerando assim o percurso R, E e D.

Outra forma de listar os elementos da árvore é utilizando o percurso


central, ou ordem simétrica, que segundo Celes, Cerqueira e Rangel
Netto (2004, p. 193), “[...] percorre os filhos da esquerda, trata a raiz, e
percorre os filhos da direita, ou seja, E, R, D”.

Para exemplificar o percurso central, vamos usar a mesma árvore com


sete elementos utilizada no percurso prefixado. Sendo que para o
percurso central, a prioridade é listar os elementos mais à esquerda,
depois o nó pai e por último os filhos mais à direita.

A Figura 116 apresenta o percurso central na árvore com sete elementos.

(4) R

4
(2) (6)
R R
(1) D E
2 6
E (3) (5) D (7)

1 3 5 7

Figura 116 – Percurso central na árvore binária.


Fonte: Elaborada pelo autor (2013).

Observe que o resultado da listagem será 1, 2, 3, 4, 5, 6 e 7, ou seja, os


elementos ordenados crescentemente. Isso ocorre porque o percurso deve
descer até o elemento mais à esquerda da árvore, que é o 1, para depois
imprimir o elemento do nó pai do 1, que é o 2, para então imprimir
o elemento mais à direita, que é o 3. Como não há mais elementos à
esquerda, o próximo a ser impresso é o pai do 2, que é o 4. Como todos
os elementos à esquerda da raiz, inclusive a raiz foram impressos, o
próximo passo são os elementos à direita da raiz. Como a ordem é E, R e
D, deve-se descer ao elemento mais à esquerda, que é 5, depois o pai do
5, que é o 6, e por fim o elemento à direita, que é o 7.

www.esab.edu.br 304
O Algoritmo 110 apresenta a lógica de listagem dos elementos pelo
percurso central:

Algoritmo 110 – Percurso central


01 void central(ArvoreBinaria no){
02 if (no != NULL){
03 if (no->esq != NULL) central (no->esq);
04 printf("%d\n",no->valor);
05 if (no->dir != NULL) central (no->dir);
06 }
07 }

Fonte: Elaborado pelo autor (2013).

Note que se o nó é diferente de NULL, a função central é chamada


recursivamente com a referência do nó da esquerda do nó visitado, até
que ele chegue a uma folha (nó valendo NULL), que é o elemento mais
à esquerda. O valor nó é impresso e, recursivamente, a função central é
chamada com o elemento da direita do nó acessado, resultando na ordem
E, R e D.

Por fim, a última forma de percurso é o pós-fixado, que possui como


regra que se busque o elemento mais à esquerda, depois o elemento mais
à direita e por último o nó pai, resultando na ordem E, D, R.

A Figura 117 apresenta o percurso pós-fixado na árvore binária com sete


elementos:

www.esab.edu.br 305
4 4 4

2 6 2 6 2 6

1 3 5 7 1 3 5 7 1 3 5 7
(1) nó mais à esquerda (2) nó mais à direita (3) nó pai

4 4 4

2 6 2 6 2 6

1 3 5 7 1 3 5 7 1 3 5 7
(4) nó mais à esquerda (5) nó mais à direita (6) nó pai

2 6

1 3 5 7
(7) nó pai
Figura 117 – Percurso pós-fixado na árvore binária.
Fonte: Elaborada pelo autor (2013).

Observe que no percurso pós-fixado, o primeiro passo é buscar o


elemento mais à esquerda da árvore, que é o valor 1, depois o elemento
mais à direita, que é o 3, e por fim o valor do nó pai, o 2. Com todos
os elementos da esquerda visitados, exceto a raiz, que tem a menor
prioridade na visita, pois a ordem é E, D, R, busca-se o elemento mais à
esquerda do lado direito da árvore, que é o 5, depois o elemento mais à
direita, o 7, por fim o nó pai do 5 e 7, o valor 6, e então a raiz da árvore,
o valor 4. Logo, os elementos listados serão: 1, 3, 2, 5, 7, 6 e 4.

O Algoritmo 111 apresenta a lógica de listagem dos elementos pelo


percurso pós-fixado.

www.esab.edu.br 306
Algoritmo 111 – Percurso pós-fixado
01 void posfixado(ArvoreBinaria no){
02 if (no != NULL){
03 if (no->esq != NULL) posfixado (no->esq);
04 if (no->dir != NULL) posfixado (no->dir);
05 printf("%d\n",no->valor);
06 }
07 }

Fonte: Elaborado pelo autor (2013).

Note que se o nó é diferente de NULL, a função central é chamada


recursivamente com a referência do nó à esquerda do nó visitado até que
ele chegue a uma folha (nó valendo NULL), que é o elemento mais à
esquerda. Depois a função central é chamada com o elemento da direita
do nó acessado, até chegar a uma folha. Por fim, o elemento raiz ou nó
pai é impresso.

Você pode fazer o download de todo o código apresentado nesta unidade clicando
aqui. Procure baixá-lo e executá-lo no seu computador.

Nesta unidade, você pôde estudar as três formas de se percorrer os nós


de uma árvore binária, chamadas de percurso na árvore, que podem ser:
prefixado, central e pós-fixado. A regra de percurso do tipo prefixado
é raiz ou nó pai, depois percorre os elementos da esquerda, e por fim,
percorre os elementos da direita, ou seja, R, E, D. Onde R é raiz, E,
esquerda, e D, direita. A prioridade é imprimir o elemento da raiz (ou
nó pai), depois o da esquerda, e por último o da direita. Já o percurso
de tipo central tem como ordem de visita dos nós: E (esquerda), R (raiz)
e D (direita). Por fim o percurso pós-fixado tem como sequência: E
(esquerda), D (direita) e R (raiz). Para implementação desses percursos
na forma de funções, é necessária a utilização da recursividade, ou seja, a
função chama a si própria.

www.esab.edu.br 307
Na próxima unidade, abordaremos outro tipo de árvore, que se chama
genérica, e estudaremos o conceito, a aplicação e a manipulação desse
tipo de árvore.

Tarefa dissertativa
Caro estudante, convidamos você a acessar o
Ambiente Virtual de Aprendizagem e realizar a
tarefa dissertativa.

www.esab.edu.br 308
36 Árvores – parte IV

Objetivo
Apresentar o conceito, a aplicação e a manipulação de árvores.

Na unidade anterior, você estudou as três formas de se percorrer os nós


de uma árvore binária, chamadas de percurso na árvore, que pode ser:
prefixado, central e pós-fixado. A regra de percurso do tipo prefixado
é raiz ou nó pai, depois percorre os elementos da esquerda, e por fim,
percorre os elementos da direita, ou seja, R, E, D. Onde R é raiz, E,
esquerda, e D, direita. A prioridade é imprimir o elemento da raiz (ou
nó pai), depois o da esquerda, e por último o da direita. Já o percurso
do tipo central tem como ordem de visita dos nós: E (esquerda), R (raiz)
e D (direita), e por fim o percurso pós-fixado tem como sequência: E
(esquerda), D (direita) e R (raiz). Para implementação desses percursos
na forma de funções, é necessária a utilização da recursividade, ou seja, a
função chama a si própria.

Nesta unidade, abordaremos outro tipo de árvore, que se chama genérica,


e estudaremos o seu conceito, sua aplicação e sua manipulação.

Este conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

www.esab.edu.br 309
36.1 Árvores genéricas
Nas unidades anteriores sobre árvores, foi possível estudar que as árvores
binárias possuem no máximo dois nós filhos para cada nó pai, ou seja, o grau
de cada nó pode ser 0, 1 ou 2. A Figura 118 apresenta uma árvore binária.

(-) 10 (+) Nível - 0

08 21 Nível - 1

04 09 55 Nível - 2
Figura 118 – Árvore binária.
Fonte: Elaborada pelo autor (2013).

Note que cada nó possui no máximo dois filhos e os valores maiores


que os do nó pai estão à direita e os valores menores que os do nó pai,
à esquerda. Diferentemente da árvore binária, as árvores genéricas
podem ter mais de dois nós filhos, podendo ser ternárias (três filhos),
quaternárias (quatro filhos) e assim sucessivamente. Celes, Cerqueira
e Rangel Netto (2004), conceituam a árvore genérica como “[...] uma
estrutura de árvore que pode ter mais de duas subárvores associadas”,
na qual cada subárvore corresponde aos nós filhos e seus sucessores. A
Figura 119 apresenta uma árvore genérica.
raiz
A

B C D
subárvore

E F G

H
subárvore subárvore
subárvore

Figura 119 – Árvore genérica.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 310
Observe que a árvore é formada pelos elementos: A, B, C, D, E, F, G
e H, mas também é constituída de três subárvores, com os elementos
A, B e E na primeira subárvore, apenas o nó C na segunda subárvore
e os nós D, F, G e H, na terceira subárvore. Segundo Celes, Cerqueira
e Rangel Netto (2004, p. 186), “[...] a subárvore é um conjunto de
elementos interligados da árvore”. Portanto, podemos também definir os
elementos D, F e G como uma subárvore, pois qualquer conjunto de nós
interligados é uma subárvore, independentemente de ser binária ou não.

O maior grau da árvore é três, pois a maior quantidade de filhos para um


nó pai ocorre para o nó A, que é a raiz, tendo três filhos (B, C e D), logo a
árvore é terciária (a quantidade de filhos deve ser menor ou igual a três).

Assim, a árvore genérica pode ser entendida como uma árvore que possui
um número variável de filhos e maior que dois.

Para a definição da estrutura do nó de uma árvore genérica, a quantidade


de referências de cada nó dependerá do tipo de árvore, se é ternária (três
filhos), quaternária (quatro filhos) etc. O Algoritmo 112 apresenta a
declaração de um nó terciário.

Algoritmo 112 – Nó da árvore terciária


01 struct noarvore {
02 struct noarvore* esq;
03 struct noarvore* dir;
04 struct noarvore * centro;
05 int valor;
06 };

Fonte: Elaborado pelo autor (2013).

Cada nó da árvore possui três referências, identificadas como esq


(esquerda), dir (direita) e centro, e um valor que será armazenado no nó,
chamado valor e do tipo inteiro. Caso a árvore fosse quaternária, mais
uma referência seria inserida na declaração do tipo estruturado, conforme
o Algoritmo 113.

www.esab.edu.br 311
Algoritmo 113 – Nó da árvore quaternária
01 struct noarvore {
02 struct noarvore* esq;
03 struct noarvore* dir;
04 struct noarvore * centro;
05 struct noarvore * proximo;
06 int valor;
07 };

Fonte: Elaborado pelo autor (2013).

Observe que a única alteração, na linha 5, foi a inclusão do campo


chamado proximo, que também é uma referência para um nó da árvore.
O campo poderia ter qualquer nome, nossa escolha pelo nome proximo
foi aleatória. A Figura 120 apresenta as duas estruturas dos nós terciário e
quaternário.
Árvore Ternária Árvore Quaternária
valor Raiz valor Raiz
esquerda direita esquerda proximo
centro centro direita
valor valor valor valor valor valor valor

NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL
NULL NULL NULL NULL NULL NULL NULL
NULL NULL NULL NULL NULL NULL NULL

Figura 120 – Árvore genérica ternária e quaternária.


Fonte: Elaborada pelo autor (2013).

Note que as duas estruturas são semelhantes, o que as diferencia é a


quantidade de nós filhos.

Como o número de nós filhos em uma árvore genérica é variável, esse


tipo de árvore não possui nenhuma regra de ordenação, como na árvore
binária, em que os elementos menores ficam à esquerda do nó pai e os
maiores à direita deste.

Para compreender a aplicação da árvore genérica, vamos imaginar que


um sistema deve armazenar todos os álbuns lançados por uma banda,
com o título de cada música e a sua duração. Vamos utilizar uma
árvore genérica para representar os álbuns da banda e as músicas, cuja
quantidade pode variar para cada álbum lançado.

www.esab.edu.br 312
A Figura 121 apresenta essa árvore.
Banda

Álbum 1 Álbum 2 Álbum 3


NULL

Música Música Música Música Música Música Música Música Música Música Música
1 2 3 4 1 2 3 1 2 3 4

Figura 121 – Árvore genérica - discografia.


Fonte: Elaborada pelo autor (2013).

Observe que a raiz da árvore armazena os dados da banda, como o nome


da banda e suas referências, em um total de três, e aponta para um tipo de
nó cuja estrutura é formada pelos dados do álbum, como o seu nome. O
número de referências do nó banda (raiz) que apontam para o nó do tipo
álbum dependerá do número de álbuns lançados pela banda. Nesse caso,
são três álbuns representados como Álbum 1, Álbum 2 e Álbum 3. Já cada
nó do tipo álbum possui um conjunto de referências que apontam para um
nó do tipo música, que armazena os dados da música, como nome e tempo
de duração. A quantidade de referências para as músicas de cada álbum
depende da quantidade de músicas que cada álbum possui. Caso algum
álbum não tenha a mesma quantidade de músicas que outro, as referências
que não forem utilizadas terão o valor NULL.

Assim, a árvore possui um nó do tipo banda que aponta para um


conjunto de nós do tipo álbum, que apontam para um conjunto de
referências do tipo música. Caso surja um novo álbum da banda,
será preciso acrescentar uma nova referência na raiz (do tipo banda)
apontando para um novo nó do tipo álbum, que por sua vez, apontará
para um conjunto de novos nós do tipo música.

Para pesquisar uma determinada música de um álbum específico, a


partir da raiz é pesquisado cada álbum para localizar qual deles possui o
nome a ser localizado. Localizando esse álbum, serão percorridas todas as
músicas apontadas por ele até localizar a música com o nome pesquisado.
Achando o nó música, o sistema terá acesso a todos os seus dados, como
nome e tempo de duração.

www.esab.edu.br 313
Note que a estrutura representa hierarquicamente as informações,
iniciando na banda, passando pelo álbum e chegando às músicas.

As árvores genéricas são muito utilizadas no desenvolvimento de jogos


para o armazenamento de possíveis jogadas, onde cada nó armazena uma
jogada específica e seus nós filhos armazenam possíveis sequências de
jogadas. Assim, se uma jogada armazenada no nó pai ocorrer, o sistema
busca nos nós filhos possíveis ações para o jogo, na forma de jogadas
preestabelecidas.

Nesta unidade, você pôde estudar sobre as árvores genéricas, que se


diferenciam das árvores binárias pela quantidade de nós filhos para cada
nó pai. As árvores genéricas possuem grau maior que dois, ou seja, cada
nó pai tem três ou mais filhos. As árvores com grau três são conhecidas
como ternárias, com grau quatro são chamadas de quaternárias e assim
por diante.

Como o número de nós filhos em uma árvore genérica é variável, esse


tipo de árvore não possui nenhuma regra de ordenação, como na árvore
binária, em que os elementos menores ficam à esquerda do nó pai e os
maiores à direita do nó pai.

Por fim, você estudou que a subárvore é um conjunto de elementos


interligados da árvore, logo, qualquer árvore, binária ou genérica, pode
ser formada por um conjunto de subárvores.

Na próxima unidade, faremos um exercício de implementação em C de


uma árvore genérica.

Até lá!

Atividade
Chegou a hora de você testar seus conhecimentos
em relação às unidades 26 a 36. Para isso, dirija-se ao
Ambiente Virtual de Aprendizagem (AVA) e responda
às questões. Além de revisar o conteúdo, você estará
se preparando para a prova. Bom trabalho!

www.esab.edu.br 314
Resumo

Na unidade 31, você estudou como desenvolver um programa para


inserção, remoção, busca e inicialização em uma fila implementada por
meio de uma lista simplesmente encadeada.

Na unidade 32, você pôde desenvolver e analisar as diferenças entre uma


fila simplesmente encadeada e duplamente encadeada por meio de um
exemplo que cria uma fila de controle de pedidos.

Na unidade 33, você estudou que a estrutura de dados árvore é adequada


para a representação de dados que devem ser dispostos de maneira
hierárquica, e pode ser definida como uma estrutura de dados não linear.
A árvore é uma estrutura de dados bem peculiar e por isso possui uma
terminologia própria para identificação de seus elementos e grupo de
elementos, como: nó, raiz, folha, grau do nó, grau e altura da árvore,
trajetória e nó interior. Uma árvore pode ser classificada como genérica ou
binária, dependendo do número máximo de filhos para cada nó. As árvores
binárias são assim nomeadas por possuírem no máximo dois filhos para
cada nó e possuem, ainda, uma característica muito importante quanto à
posição dos nós: os valores menores que os do pai sempre estão à esquerda
e os valores maiores que os do pai sempre à direita.

Na unidade 34, você pôde estudar e implementar uma árvore binária


utilizando a linguagem de programação C, definindo a estrutura de cada
nó da árvore com duas referências, implementando a função de inserção
na árvore binária e a impressão dos valores de uma árvore binária.

Na unidade 35, você estudou as três formas de se percorrer os nós de


uma árvore binária, chamadas de percurso na árvore, que podem ser:
prefixado, central e pós-fixado. A regra de percurso do tipo prefixado
é: R (raiz), E (esquerda), D (direita). Já o percurso do tipo central tem
como ordem de visita dos nós: E, R e D. Por fim, o percurso pós-fixado

www.esab.edu.br 315
tem como sequência: E, D e R. Para implementação desses percursos na
forma de funções, é necessária a utilização da recursividade, ou seja, a
função chama a si própria.

Na unidade 36, você pôde estudar sobre as árvores genéricas, que se


diferenciam das árvores binárias pela quantidade de nós filhos para cada
nó pai. Como o número de nós filhos em uma árvore genérica é variável,
esse tipo de árvore não possui nenhuma regra de ordenação.

www.esab.edu.br 316
37 Árvores – parte V

Objetivo
Apresentar exercícios comentados sobre árvores.

Na unidade anterior, abordamos as árvores genéricas, que se diferenciam


das árvores binárias pela quantidade de nós filhos para cada nó pai.
Vimos que as árvores genéricas possuem grau maior que 2, ou seja, cada
nó pai tem três. As árvores com grau 3 são conhecidas como ternárias, as
com grau 4 são chamadas de quaternárias, as com grau 5 de quintenárias
e assim por diante. Como o número de nós filhos em uma árvore
genérica é variável, esse tipo de árvore não possui nenhuma regra de
ordenação, como na árvore binária, em que os elementos menores ficam
à esquerda do nó pai e os maiores à direita do nó pai.

Por fim, você também estudou que a subárvore é um conjunto de


elementos interligados da árvore, logo, qualquer árvore, binária ou
genérica, pode ser formada por um conjunto de subárvores.

Nesta unidade, por meio de um exercício comentado, vamos desenvolver


três rotinas de manipulação de uma árvore binária na forma de funções
específicas, que deverão, respectivamente, verificar se um determinado
valor existe na árvore binária e retornar o maior e o menor valor
cadastrados na árvore binária. Para o cadastro dos elementos na árvore
binária, utilizaremos a regra de inserção dos menores à esquerda e
dos maiores à direita do nó pai, já abordada e discutida nas unidades
anteriores, por isso, não será discutida novamente.

Este conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

Vamos começar pelas definições da estrutura de cada nó da árvore, assim


como da árvore binária, depois implementaremos as rotinas de pesquisas.

www.esab.edu.br 317
No final desta unidade, você terá o endereço de internet para fazer o
download de todo o código desenvolvido.

Vamos lá!

37.1 Exercícios comentados


Vamos desenvolver uma árvore binária para armazenamento de números
inteiros. Como cada nó da árvore aponta como referência para dois
outros nós, o da esquerda e o da direita, a estrutura de cada nó será
constituída por um valor, o nó da esquerda e o nó da direita, conforme o
Algoritmo 114.

Algoritmo 114 – Estrutura do nó da árvore


01 struct noarvore {
02 struct noarvore* esq;
03 int valor;
04 struct noarvore* dir;
05 };
06 typedef struct noarvore No;
07 typedef No* ArvoreBinaria;

Fonte: Elaborado pelo autor (2013).

Note que as duas referências de cada nó para os nós da esquerda e da


direita são chamadas respectivamente de esq e dir, e a informação do tipo
inteiro será armazenada no campo chamado valor. Cada elemento da
árvore será representado pelo tipo No, definido na linha 06 do código,
e a árvore binária será representada pelo tipo ArvoreBinaria, criada na
linha 07.

Celes, Cerqueira e Rangel Netto (2004, p. 189), reforçam esse conceito


de cada nó da árvore destacando que “[...] cada nó deve armazenar três
informações: a informação propriamente dita e dois ponteiros para as
subárvores, à esquerda e à direita”.

Agora que já temos a definição de cada nó da árvore, vamos rever o


algoritmo de inserção na árvore binária.

www.esab.edu.br 318
Algoritmo 115 − Inserção na Árvore Binária
01 void inserir(ArvoreBinaria *arvore,int
novovalor){
02 if ((*arvore) = = NULL) {
03 *arvore = (No *) malloc(sizeof(No));
04 (*arvore)->esq = NULL;
05 (*arvore)->dir = NULL;
06 (*arvore)->valor = novovalor;
07 }
08 if (novovalor < (*arvore)->valor) {
09 inserir((&(*arvore)->esq), novovalor);
10 }
11 else{
12 if (novovalor > (*arvore)->valor) {
13 inserir(&((*arvore)->dir),
novovalor);
14 }
15 }
16 }

Fonte: Elaborado pelo autor (2013).

Para a inserção dos elementos na árvore binária, serão passados como


parâmetros a referência da árvore (ArvoreBinaria *arvore) que será
manipulada, e o valor a ser inserido (int novovalor), e como regra,
a inserção só ocorrerá em um nó disponível (*arvore vale NULL),
conforme a condição apresentada na linha 02. Caso o nó esteja
disponível, um novo nó é alocado na memória, o dado é armazenado no
campo valor desse novo nó, que apontará para o elemento da esquerda e
da direita com NULL. Caso a posição de inserção não esteja disponível, é
verificado se o valor a ser inserido é menor ou maior que o nó referência
(que já ocupa a posição na árvore), em que os valores menores são
inseridos à esquerda e os maiores à direita desse nó referência. Caso a
posição de inserção já esteja preenchida, é preciso “descer” nos níveis da
árvore, caminhando à esquerda ou à direita do nó referência, até localizar
uma posição de inserção livre, disponível. A função é implementada de
forma recursiva, onde a função chama a ela mesma até que seja localizada
uma posição de inserção disponível (*arvore == NULL). A Figura 122
apresenta essa lógica de inserção na árvore binária.

www.esab.edu.br 319
Valores a serem inseridos:
[15, 21, 55, 08, 12, 33]

Insere 15 (1) Insere 8 (4) Insere 33 (6)


se for maior -> direita se for maior -> direita
15 (raiz) se for menor -> esquerda se for menor -> esquerda
Insere 21 (2)
se for maior -> direita 15 (raiz) 15 (raiz)
se for menor -> esquerda
08 21 08 21

15 (raiz) 55 12 55

21 33
Insere 55 (3) Insere 12 (5)
se for maior -> direita se for maior -> direita
se for menor -> esquerda se for menor -> esquerda

15 (raiz) 15 (raiz)
21 08 21
55 12 55

Figura 122 – Inserção na árvore binária.


Fonte: Elaborada pelo autor (2013).

Note que os elementos menores ficam à esquerda do seu nó pai e os


maiores, à direita. É importante lembrar que isso vale para todos os nós
da árvore.

Bem, como já temos a rotina de inserção na árvore, vamos desenvolver


uma função para pesquisar se um determinado valor existe ou não na
árvore.

O algoritmo para verificação se um determinado valor existe ou não


na árvore receberá dois parâmetros: a referência da árvore que será
manipulada (ArvoreBinaria arvore) e o valor pesquisado (int chave),
conforme o Algoritmo 116.

www.esab.edu.br 320
Algoritmo 116 – Pesquisa na árvore binária
01 int verificar(ArvoreBinaria arvore,int chave){
02 if (arvore != NULL){
03 if(arvore->valor == chave){
04 return 1;
05 }
06 else{
07 if(arvore->valor < chave){
08 return verificar(arvore->dir,chave);
09 }
10 else{
11 return verificar(arvore-
>esq,chave);
12 }
13 }
14 }else{
15 return 0;
16 }
17 }

Fonte: Elaborado pelo autor (2013).

Veremos que a regra do algoritmo é bem simples, já que a árvore binária


está organizada de forma que os valores maiores estão à direita de cada nó
visitado e os valores menores, à esquerda. Assim, na linha 02 é verificado
se o endereço do nó é diferente de NULL, ou seja, se o nó existe e, por
consequência, possui um valor armazenado nele. Na linha 03 é verificado
se o valor que está no nó referência é igual ao valor pesquisado (chave),
em caso de condição verdadeira (arvore->valor == chave), a função
retorna 1 (em que 1 é verdadeiro) e a função é encerrada. Caso o valor do
nó referência (arvore->valor) não seja igual ao valor da chave, é verificado
na condição da linha 05 se o valor do nó referência é menor que o valor
pesquisado (chave). Se a condição for verdadeira, significa que o valor
pesquisado deve estar à direita desse nó, pois os valores maiores que
o nó pai ficam à direita, assim, recursivamente, a função para achar
o maior valor é chamada com o endereço da referência da direita do
nó e a chave a ser pesquisada (verificar (arvore->dir, chave)). Caso o
valor do nó referência seja maior que o valor da chave, significa que o
valor pesquisado só poderá existir à esquerda do nó, por isso na linha
07 há uma chamada recursiva da função verificar recebendo o valor de
referência do nó da esquerda e o valor pesquisado (verificar (arvore->esq,
chave)). Esse processo de descer à esquerda ou à direita será reexecutado

www.esab.edu.br 321
recursivamente até que o valor seja localizado (arvore->valor == chave),
retornando 1, ou até que se chegue em um endereço que vale NULL
(arvore == NULL). Isso significa que o valor não foi localizado e não há
mais nós à esquerda ou à direita (chegou a um nó folha da árvore), logo o
valor não será localizado e a função retornará 0 (falso).

As funções para achar o maior valor e menor valor possuem lógica


semelhante ao processo de pesquisa, pois como os valores maiores
ficam à direita de cada nó, para achar o maior valor da árvore é preciso
que seja verificado se existe um valor à direita do nó. Se existir, deve-se
descer à direita na árvore, repetindo o processo até que não existam mais
elementos à direita. Logo, o nó à direita será o maior valor da árvore. O
Algoritmo 117 apresenta essa lógica.

Algoritmo 117 – Localizar o Maior valor


01 int acharMaior(ArvoreBinaria arvore){
02 if( arvore->dir != NULL){
03 printf("Descendo a Direita de
%d\n",arvore->valor);
04 return acharMaior(arvore->dir);
05 }
06 else{
07 return arvore->valor;
08 }
09 }

Fonte: Elaborado pelo autor (2013).

Observe que na linha 02 do Algoritmo 117 é verificado se existe um


nó à direita do nó referência (arvore->dir != NULL), caso não exista,
então esse nó referência possui o maior elemento da árvore, e a função
retornará esse valor por meio da instrução return arvore->valor. Caso
exista um nó à direita, recursivamente a função que retorna o maior valor
será chamada novamente, com o endereço de referência do nó da direita,
por isso a instrução return acharMaior(arvore->dir). Portanto, a partir do
nó raiz a função acharMaior() vai descendo à direita na árvore até chegar
no último nó à direita, que possui o maior valor da árvore.

www.esab.edu.br 322
Se o maior valor da árvore está à direita, o menor valor estará à esquerda
da árvore, uma vez que o menor valor está sempre à esquerda do nó
pai. Assim, muito semelhante ao acharMaior(), a função para achar o
menor valor começará a verificar, a partir da raiz, se existe um elemento
à esquerda do nó referência. Se existir, esse elemento à esquerda será
visitado recursivamente até que não exista um elemento à esquerda.
Logo, o último nó à esquerda representa o menor elemento inserido
na árvore. O Algoritmo 118 apresenta a lógica da função para achar o
menor valor.

Algoritmo 118 – Localizar o Menor valor


01 int acharMenor(ArvoreBinaria arvore){
02 if( arvore->esq != NULL){
03 printf("Descendo a Esquerda de
%d\n",arvore->valor);
04 return acharMenor(arvore->esq);
05 }
06 else{
07 return arvore->valor;
08 }
09 }

Fonte: Elaborado pelo autor (2013).

Note que o Algoritmo 118 verifica se existe um nó à esquerda na árvore,


por isso a condição da linha 02, if( arvore->esq != NULL). No caso dessa
condição ser verdadeira, recursivamente é chamada a função acharMenor()
com o endereço do nó da esquerda, por isso na linha 04 a instrução
return acharMenor(arvore->esq). A chamada recursiva se repete até que
seja encontrado um nó sem filho à esquerda, o que representa que esse nó
sem filhos é o menor valor da árvore. Ou seja, a partir do nó raiz, os nós à
esquerda são visitados até que não se tenha mais nó à esquerda, em que o
último nó à esquerda é o menor valor inserido na árvore.

Caro aluno, você pode fazer o download dos códigos desenvolvidos nesta unidade,
com as funções de inserção, verificação e localização do maior e menor valor clicando
aqui. Faça o download dos códigos e execute-os no seu computador, usando
a ferramenta de sua preferência, sendo que todo o código foi desenvolvido na
ferramenta DEV C++.

www.esab.edu.br 323
Nesta unidade, você pôde implementar as rotinas de inserção, consulta
e localização do maior e menor valor da árvore binária, sendo que todas
essas funções se caracterizam por ser funções recursivas, que chamam a
si próprias. Na função de consulta para verificação de um determinado
valor na árvore binária, a lógica aplicada foi descer à esquerda quando
o valor a ser localizado é menor que o valor do nó pesquisado, e à
direita quando o valor pesquisado é maior que o valor do nó visitado.
Esse processo de descida na árvore se repete até que o valor pesquisado
seja localizado ou até que o nó visitado não tenha mais filhos a serem
visitados. Já a localização do maior valor da árvore é realizada de forma
recursiva a partir do nó raiz – um percurso na árvore que chegue até o
elemento mais à direita, consequentemente o maior valor da árvore. Para
localização do menor valor da árvore, é realizado um percurso de forma
recursiva a partir do nó raiz na árvore que chegue até o elemento mais à
esquerda, consequentemente o menor valor da árvore.

Na próxima unidade, iniciaremos os estudos sobre as técnicas de pesquisa


e ordenação de dados, com a definição e contextualização do que são
pesquisa e ordenação.

www.esab.edu.br 324
Métodos de pesquisa e ordenação:
38 conceituação
Objetivo
Apresentar os conceitos dos métodos de pesquisa e ordenação.

Na unidade anterior, você implementou as rotinas de inserção, consulta,


localização do maior e menor valor das árvores binárias, sendo que todas
essas funções se caracterizam por ser funções recursivas, que chamam a
si próprias.

A função de consulta para verificação de um determinado valor na árvore


binária caminha à esquerda na árvore quando o valor a ser localizado
é menor que o valor do nó pesquisado, e à direita quando o valor
pesquisado é maior que o valor do nó visitado. Esse processo de descida
na árvore se repete até que o valor pesquisado seja localizado ou até que
o nó visitado não tenha mais filhos a serem visitados. Já a localização
do maior valor da árvore é realizada de forma recursiva a partir do nó
raiz – um percurso na árvore que chegue até o elemento mais à direita,
consequentemente o maior valor da árvore. Para localização do menor
valor da árvore, a operação é realizada de forma recursiva a partir do nó
raiz – um percurso na árvore que chegue até o elemento mais à esquerda,
consequentemente o menor valor da árvore.

Nesta unidade, estudaremos a definição e contextualização do que


é pesquisa e do que é ordenação em uma estrutura de dados. Este
conteúdo foi desenvolvido com base na referência bibliográfica de Celes,
Cerqueira e Rangel Netto (2004).

Vamos iniciar estudando a definição de pesquisa.

www.esab.edu.br 325
38.1 Definição de pesquisa
Nesta unidade, vamos nos focar na discussão de algoritmos muito
utilizados, a pesquisa e a ordenação de dados. Começaremos pela
operação de busca que é muito comum em aplicações computacionais,
independentemente da estrutura de dados que é utilizada. Nós mesmos
implementamos rotinas de pesquisa de dados em vetores, listas
simplesmente encadeada, listas duplamente encadeadas e em árvores
binárias, nas unidades 17, 22 e 25.

Celes, Cerqueira e Rangel Netto (2004, p. 256) destacam importantes


características que devem ser consideradas na implementação de
algoritmos de pesquisa de dados, como: “[...] ser eficiente e rápida, para
não inviabilizar a operação”.

A pesquisa pode ser realizada em vetores ou listas encadeadas, sendo


um algoritmo de busca extremamente simples, uma vez que, dado uma
estrutura de dados com “n” elementos, a busca por um determinado
elemento deverá iniciar pelo primeiro e terminar no último elemento
ou quando o valor pesquisado for encontrado. Esse tipo de pesquisa é
conhecido como pesquisa linear, pois consiste em percorrer a estrutura de
dados, elemento por elemento, para verificar se o elemento pesquisado
existe (CELES; CERQUEIRA; RANGEL NETTO, 2004). A Figura 123
apresenta a busca linear em uma estrutura de dados do tipo vetor com
quatro elementos:

www.esab.edu.br 326
(p)
0 1 2 3 0 1 2 3
10 4 12 7 10 4 12 7 vetor[p] == 12?
Localizar [ 12 ] Localizar [ 12 ]

(p) NÃO
0 1 2 3
10 4 12 7 vetor[p] == 12?
Localizar [ 12 ]

(p) NÃO
0 1 2 3
10 4 12 7 vetor[p] == 12?
Localizar [ 12 ]

SIM - ACHOU

Figura 123 – Pesquisa no vetor.


Fonte: Elaborada pelo autor (2013).

Observe que a pesquisa começa na primeira posição do vetor, em que


“p” começa valendo 0 e representa cada posição visitada no vetor. A
cada posição que é visitada, é verificado se o valor armazenado nessa
posição é igual ao valor pesquisado, nesse caso o valor pesquisado é 12.
Se o valor armazenado no vetor for igual ao valor pesquisado (vetor[p]
== 12), a pesquisa é encerrada, pois o valor foi localizado. Caso o valor
armazenado no vetor não corresponda ao valor pesquisado, a variável
“p” é incrementada em 1, passando para a posição à frente no vetor.
Então é novamente verificado se valor armazenado no vetor é igual
ao valor pesquisado. Se for igual, a busca se encerra, caso contrário, a
variável “p” é incrementada e o processo de verificação se o valor do vetor
corresponde ao valor pesquisado é novamente realizado. Esse processo se
repetirá até que o valor seja localizado no vetor ou até que toda estrutura
de dados tenha sido percorrida. Como o vetor possui quatro posições,
com os índices de 0 a 3, caso o valor não exista, a posição “p” valerá 4,
portanto, todo o vetor já foi visitado e o valor não foi localizado.

Logo, a finalidade da rotina de busca é saber se um determinado


elemento está presente ou não na estrutura de dados (CELES;
CERQUEIRA; RANGEL NETTO, 2004).

www.esab.edu.br 327
Podemos notar que o algoritmo de pesquisa linear é realmente simples.
Este precisa apenas de uma variável para representar cada elemento
da estrutura de dados. Se o valor pesquisado não for localizado,
desloca-se para o próximo elemento da estrutura de dados. Também
podemos destacar que usando essa forma de pesquisa linear, quanto
maior o número de elementos da estrutura de dados, mais demorada
será a pesquisa, já que é preciso percorrer todos os elementos até que
se localize o elemento desejado ou é preciso que a estrutura de dados
seja percorrida por inteiro. Celes, Cerqueira e Rangel Netto (2004, p.
257), destacam que “[...] o desempenho computacional deste algoritmo
varia linearmente em relação ao tamanho do problema”, isto porque o
algoritmo terá que percorrer todos os elementos da estrutura de dados
para verificar se um determinado elemento existe ou não. Logo, é preciso
criar estratégias que possam acelerar o processo de pesquisa e uma delas é
ordenar os elementos da estrutura de dados, como veremos a seguir.

38.2 Definição de ordenação.


Conforme vimos na seção anterior, podemos melhorar o algoritmo de
pesquisa tornando-o mais rápido e eficiente com o uso de técnicas de
ordenação dos dados. Mas o que é ordenar uma estrutura de dados?

A ordenação consiste na organização dos elementos da estrutura de


dados seguindo alguma ordem, que pode ser crescente ou decrescente.
E essa ordenação pode ocorrer no momento da inserção do elemento
na estrutura de dados, de forma que a estrutura esteja sempre ordenada,
ou a partir de um conjunto de elementos já inseridos na estrutura de
dados, aplicando-se um algoritmo de ordenação (CELES; CERQUEIRA;
RANGEL NETTO, 2004).

A Figura 124 apresenta a ordenação dos valores de um vetor no


momento da inserção dos valores.

www.esab.edu.br 328
Elementos para inserção: 10, 15, 12, 44
Insere o 10
Insere o 10 no ínicio
10
Insere o 15
Insere o 15 no final
10 15
Insere o 12
Insere o 12 entre o 10 e o 15
10 12 15
Insere o 12
Insere o 44 no final
10 12 15 44
Figura 124 – Ordenação na inserção.
Fonte: Elaborada pelo autor (2013).

Observe que a cada inserção o valor já é inserido na posição correta de


forma que o vetor sempre se mantenha ordenado. Para tanto, os valores
maiores que todos os valores do vetor são inseridos no final do vetor, já
os valores menores que todos os valores do vetor são inseridos no início
da estrutura de dados. Caso o valor a ser inserido não seja maior ou
menor que todos os elementos do vetor, é preciso localizar a sua posição
de inserção, como no caso do valor 12, que deve ficar entre os valores 10
e 15. Ao final das operações de inserção no início, meio ou fim, o vetor
estará ordenado crescentemente. Essa forma de ordenação é conhecida
como algoritmo de inserção, pois cada valor já é inserido na posição
adequada e em nenhum momento o vetor fica desordenado durante o
processo de classificação.

A Figura 125 apresenta a forma de ordenação na qual os valores do vetor


já foram inseridos e precisam ser ordenados.
23 10 8 18 24 Vetor inicial

10 08 18 23 24

08 10 18 23 24

08 10 18 23 24

Figura 125 – Ordenação dos elementos existentes no vetor.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 329
Observe que o vetor foi previamente preenchido e os valores estão
desordenados. Para a ordenação de forma crescente, a cada passo da
ordenação, pelo menos um dos valores existentes no vetor será ordenado,
os maiores valores serão deslocados para o final do vetor e os menores
para o início do vetor. Ao final do processo de ordenação, todos os
valores estarão classificados, mas até lá o vetor esteve desordenado em
alguns momentos. Essa forma de ordenação é conhecida como algoritmo
de troca, uma vez que os valores são trocados durante o processo de
classificação. Por exemplo, na ordenação do valor 23, esse valor foi
trocado de posição com os valores 10, 8, 18, sucessivamente, até que o
valor 23 ficasse na posição correta (antes do valor 24).

Existem vários algoritmos de ordenação e é fundamental que sejam


eficientes em termos de tempo (rápidos) e de espaço (devem ocupar
pouco espaço da memória durante a execução). Eles podem ser aplicados
para qualquer tipo de informação: números inteiros e reais, literal ou
caractere.

A Figura 126 apresenta a ordenação de um vetor de literais, preenchido


com nomes.

Marcelo Ana Lucas

Lucas Ana Marcelo

Ana Lucas Marcelo

Figura 126 – Ordenação dos elementos existentes no vetor de literais.


Fonte: Elaborada pelo autor (2013).

Observe que o processo de ordenação é por troca e os valores estão sendo


classificados em ordem crescente.

Nesta unidade, você pôde estudar a definição de pesquisa e ordenação,


em que a pesquisa tem como finalidade descobrir se um determinado
elemento está ou não presente na estrutura de dados. Vimos que a
ordenação consiste na organização dos elementos da estrutura de dados
seguindo alguma ordem, que pode ser crescente ou decrescente. Além

www.esab.edu.br 330
disso, a ordenação pode ocorrer no momento da inserção do elemento na
estrutura de dados, de forma que a estrutura esteja sempre ordenada, ou
a partir de um conjunto de elementos já inseridos na estrutura de dados.

Na próxima unidade, vamos conhecer detalhadamente o algoritmo de


ordenação chamado de bubble sort ou, simplesmente, método de bolha.

www.esab.edu.br 331
Métodos de ordenação: bubble sort
39 – parte 1
Objetivo
Apresentar o conceito e o funcionamento do algoritmo bubble sort.

Na unidade anterior, você pôde estudar a definição de pesquisa e


ordenação, em que a pesquisa tem como finalidade descobrir se um
determinado elemento está ou não presente na estrutura de dados. E que
a ordenação consiste na organização dos elementos da estrutura de dados
seguindo alguma ordem: crescente ou decrescente. A ordenação pode
ocorrer no momento da inserção do elemento na estrutura de dados, de
forma que a estrutura esteja sempre ordenada ou a partir de um conjunto
de elementos já inseridos na estrutura de dados.

Nesta unidade, vamos abordar e estudar o algoritmo de ordenação


chamado de bubble sort ou, simplesmente, método de bolha, com ênfase
na sua definição e características de funcionamento.

Este conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004) e Ascencio e Araújo (2010).

Vamos iniciar estudando a definição do método de ordenação bubble sort.

39.1 Definição
O algoritmo de ordenação bolha, ou bubble sort, recebe esse nome
como representação da sua forma de funcionamento, que empurra os
maiores valores para o final da estrutura de dados quando da ordenação
crescente. Caso a ordenação seja decrescente, o sentido da bolha muda
e os maiores valores são deslocados para o início da estrutura de dados.
A ideia fundamental do algoritmo é realizar uma série de comparações
dos elementos da estrutura de dados, em pares. Quando esses dois

www.esab.edu.br 332
elementos comparados estão fora de ordem (crescente ou decrescente), há
uma troca de posição entre eles, de forma que o primeiro elemento seja
comparado com o segundo, o segundo elemento com o terceiro e assim
sucessivamente, até que seja comparado o penúltimo elemento com o
último (CELES; CERQUEIRA; RANGEL NETTO, 2004). A Figura
127 apresenta esse processo de comparação.

0 1 2 3

0 1 2 3

0 1 2 3

0 1 2 3
Figura 127 – Ordenação do método de bolha.
Fonte: Elaborada pelo autor (2013).

Observe que a cada passo dois elementos da estrutura de dados são


comparados, começando pelas posições 0 e 1, depois 1 e 2 e por fim as
posições 2 e 3. Nessa comparação de cada par de elementos, no caso de
que os elementos estejam fora de ordem, eles serão trocados, havendo
uma inversão dos valores de cada posição. Por exemplo, o elemento da
posição 1 será armazenado na posição 0, e o elemento da posição 0 será
armazenado na posição 1, trocando os elementos do par de posições:
0,1. Esse processo de comparação e inversão de valores de cada par
que não estão ordenados resultará na ordenação do maior elemento,
empurrado para o final da estrutura de dados. E em seguida, o segundo
maior, depois o terceiro, e assim sucessivamente, até que todos os valores
estejam ordenados.

A Figura 128 apresenta a lógica de deslocamento dos elementos (offset)


da estrutura de dados usando o método de bolha para a ordenação.

www.esab.edu.br 333
PASSO 1 PASSO 2 PASSO 3

0 1 2 3 0 1 2 3 0 1 2 3

0 1 2 3 0 1 2 3 0 1 2 3
(maior)

0 1 2 3 0 1 2 3
(maior)
0 1 2 3
(maior)
Figura 128 – Ordenação do método de bolha: passo a passo.
Fonte: Elaborada pelo autor (2013).

Observe que a cada passo com certeza um elemento estará ordenado, já


que na comparação de cada par de elementos, os que estão desordenados
são invertidos. Assim, no primeiro passo são comparados os elementos
das posições: 0 e 1, 1 e 2, 2 e 3, resultando que o elemento ordenado
fique na posição 3. Como o elemento da posição 3 já está ordenado,
no próximo passo serão visitadas as posições: 0 e 1, 1 e 2, resultando
no elemento ordenado na posição 2. Assim, os elementos das posições
2 e 3 já estão ordenados. No terceiro passo são visitadas as posições 0
e 1, resultando no valor ordenado na posição 1. Como os elementos
das posições 1, 2 e 3 estarão ordenados após os três passos, o elemento
da posição 0 consequentemente também estará, portanto, todo o vetor
estará ordenado.

Vamos analisar com mais detalhe como se dá o funcionamento do


método de bolha quanto à ordenação crescente ou decrescente, à
comparação entre os elementos do par e à troca dos elementos quando
estão fora de ordem.

www.esab.edu.br 334
39.2 Funcionamento do algoritmo
Como foi visto na seção anterior, o algoritmo do método de bolha
(bubble sort) consiste em percorrer os elementos do vetor, em pares,
comparando se esses elementos do par estão fora de ordem. Se a
ordenação for crescente, para que os elementos sejam considerados fora
de ordem, o elemento da esquerda do par deve ser maior que o elemento
da direita do par. Já se a ordenação for decrescente, o par é considerado
fora de ordem se o elemento da esquerda do par é menor que o elemento
da direita. A Figura 129 apresenta essa regra:

Exemplo 1 Exemplo 2
0 1 2 3 0 1 2 3
10 8 ordem crescente 18 21 ordem decrescente

10 8 Par fora da 18 21 Par fora da


ordem crescente ordem decrescente
Figura 129 – Ordenação do método de bolha: pares desordenados.
Fonte: Elaborada pelo autor (2013).

Observe que no Exemplo 1 da Figura 129, em que se deseja ordenar o


vetor crescentemente, o par formado pelos elementos das posições 0 e 1
do vetor está desordenado, já que o valor da posição 0 (10) é maior que o
valor da posição 1 (8). Já os elementos das posições 2 e 3 estão ordenados
crescentemente. No Exemplo 2, que apresenta um vetor que deve ser
ordenado decrescentemente, o par formado pelos elementos das posições
0 e 1 está desordenado, pois o valor da posição 0 é menor que o valor da
posição 1. Os demais valores desse vetor estão ordenados (15 e 10).

Logo, podemos dizer que no Exemplo 1, o elemento da posição 0


do vetor é maior que o valor da posição 1 do mesmo vetor. Podemos
concluir que o vetor está desordenado, ou seja, se qualquer elemento da
posição anterior (i) for maior que o valor da posição posterior (i+1), o
vetor está desordenado crescentemente.

Para o Exemplo 2, podemos considerar, portanto, que se qualquer


elemento da posição anterior (i) for menor que o valor da posição
posterior (i+1), o vetor está desordenado decrescentemente.

www.esab.edu.br 335
Agora que sabemos como o método de bolha identifica se os elementos
do par estão em desordem, podemos nos questionar: como ele faz para
organizá-los?

Ascencio e Araújo (2010, p. 22), destacam que o algoritmo de ordenação


bolha

[...] efetua comparações entre dados armazenados em um vetor de tamanho n, e


cada elemento da posição i do vetor será comparada com o elemento da posição i+1,
e quando a ordenação procurada (crescente ou decrescente) é encontrada, uma troca
de posições entre os elementos é feita.

Ou seja, estando os elementos do par fora de ordem, para ordená-los


basta invertê-los, trocando os valores de posição. A Figura 130 apresenta
essa lógica:
0 1 2 3
10 8 5 21 aux
(1)
10
0(2)1 2 3
8 8 5 21
(3)
0 1 2 3
8 10 5 21

Figura 130 – Ordenação do método de bolha: ordenação do par.


Fonte: Elaborada pelo autor (2013).

Note que o primeiro passo (1) para ordenação do par formado pelos
valores das posições 0 e 1, que são 10 e 8, respectivamente, é copiar o
valor da posição 0 do vetor para uma variável auxiliar, pois no momento
em que o 8, menor valor do par, for copiado para posição 0, o valor
10 será perdido. Após a cópia do valor da posição da esquerda para a
variável auxiliar, o valor da posição da direita do par pode ser copiado
para a posição da esquerda do par, conforme o passo (2) na Figura 130.
Por fim, o valor armazenado na variável auxiliar é copiado para a posição
da direita do par (3), resultando em um novo par com o menor valor à
esquerda e o maior valor do par à direita. Pronto, o par estará ordenado,
mas não significa que todo o vetor esteja ordenado. Logo, o processo
precisa continuar até que todos os elementos estejam ordenados, como
pode ser observado na Figura 131.

www.esab.edu.br 336
Processo 1 Processo 2 Processo 3
0 1 2 3 0 1 2 3 0 1 2 3
10 8 5 21 8 5 10 21 5 8 10 21
0 1 2 3 0 1 2 3 0 1 2 3
8 10 5 21 5 8 10 21 5 8 10 21
(ordenados)
0 1 2 3 0 1 2 3
8 5 10 21 5 8 10 21
(ordenados)
0 1 2 3
8 5 10 21

(ordenado)

Figura 131 – Ordenação do método de bolha: ordenação do vetor crescentemente.


Fonte: Elaborada pelo autor (2013).

Note que no primeiro processo foram comparadas as posições dos vetores


0 com 1, 1 com 2 e 2 com 3, sendo que sempre que os elementos do par
estavam fora de ordem, eles foram trocados, o que garante no final do
processo que, com certeza, o último elemento estará ordenado. Logo,
essa última posição não precisa mais ser visitada para ordenação. No
segundo processo, foram comparadas as posições dos vetores 0 com 1 e
1 com 2, trocando os elementos que estão fora de ordem e resultando
em mais um elemento ordenado, com certeza, na penúltima posição.
No segundo processo, foram comparadas as posições do vetor 0 com 1,
trocando as posições do par se estiverem em desordem, resultando em
mais um elemento ordenado com certeza, o antepenúltimo elemento.
Como o vetor possui quatro elementos, e três deles já foram ordenados,
o elemento restante, na posição 0, consequentemente estará ordenado,
portanto todo o vetor estará em ordem.

Assim, podemos concluir que o método de bolha realiza “n” comparações


entre os dados armazenados, sendo que serão realizados “n-1” processos
de ordenação e em cada processo serão realizadas “n-i” comparações,
em que i é o número do processo. Por exemplo, no vetor da Figura
131, visto anteriormente, o vetor tinha quatro elementos (10, 8, 5 e
21), logo, foram realizados três processos de ordenação (4-1 = 3), já que
cada processo ordena com certeza um elemento, e se ordenarmos três,
o último já estará ordenado, por isso, n-1 processos. No processo um,

www.esab.edu.br 337
foram realizadas três comparações das posições: 0 com 1, 1 com 2 e 2
com 3. Ou seja, n(4) – processo (1) = 3 comparações. Já no processo
dois, como um elemento já estará ordenado antes de o processo começar
(ordenado no processo um), as comparações foram das posições: 0 com
1, e 1 com 2, ou seja duas comparações (n vale 4 e o processo vale 2,
então n-2 = 2). Por fim, no processo três foi realizada uma comparação
das posições 0 e 1 (n=4 – processo = 3, resultado: 1 comparação).

Portanto, o algoritmo de ordenação do método de bolha, ou bubble sort,


tem como regra controlar o número de processos de ordenação e, para
cada processo, o número de comparações a serem executadas, e estando
os elementos que compõem cada par comparado fora de ordem, eles
devem ser trocados de posição.

Nesta unidade, você pôde conhecer as características do método


de ordenação bubble sort, mais conhecido como bolha, que recebe
esse nome como representação da sua forma de funcionamento, que
empurra os maiores valores para o final da estrutura de dados quando
a ordenação é crescente, e os maiores valores para o início da estrutura
de dados quando a ordenação é decrescente. A ideia fundamental do
algoritmo é realizar uma série de comparações dos elementos da estrutura
de dados em pares e quando esses dois elementos estão fora de ordem
(crescente ou decrescente), há uma troca de posição entre eles, de forma
que o primeiro elemento seja comparado com o segundo, e o segundo
elemento com o terceiro, assim sucessivamente, até que seja comparado
o penúltimo elemento com o último elemento. Portanto, podemos
concluir que o método de bolha realiza “n” comparações entre os dados
armazenados, sendo que serão realizados “n-1” processos de ordenação
e em cada processo serão realizadas “n-i” comparações, em que i é o
número do processo.

Assim, o algoritmo de bubble sort controla o número de processos


de ordenação e para cada processo o número de comparações a serem
executadas. Estando os elementos que compõem cada par comparado
fora de ordem, eles devem ser trocados de posição.

Na próxima unidade, vamos estudar como transformar essas regras em


um algoritmo em C que possibilite a ordenação de um vetor utilizando o
método de bolha.

www.esab.edu.br 338
Métodos de ordenação: bubble sort
40 – parte 2
Objetivo
Apresentar a codificação do algoritmo bubble sort.

Na unidade anterior, você pôde conhecer as características do método


de ordenação bubble sort, mais conhecido como bolha, que recebe esse
nome como representação da sua forma de funcionamento, que empurra
os maiores valores para o final da estrutura de dados quando a ordenação
é crescente e os maiores valores para o início da estrutura de dados,
quando da ordenação decrescente. A ideia fundamental do algoritmo é
realizar uma série de comparações dos elementos da estrutura de dados,
em pares, e quando esses dois elementos estão fora de ordem (crescente ou
decrescente), há uma troca de posição entre eles, de forma que o primeiro
elemento seja comparado com o segundo e o segundo elemento com o
terceiro, e assim sucessivamente, até que seja comparado o penúltimo
elemento com o último. Portanto, podemos concluir que o método de
bolha realiza “n” comparações entre os dados armazenados, sendo que serão
realizados “n-1” processos de ordenação e em cada processo serão realizadas
n-i comparações, em que i é o número do processo.

Portanto, o algoritmo de bubble sort controla o número de processos


de ordenação e, para cada processo, o número de comparações a serem
executadas. Estando os elementos que compõem cada par comparado
fora de ordem, eles devem ser trocados de posição.

Nesta unidade, vamos estudar como transformar estas regras em um


algoritmo em C que possibilite a ordenação de um vetor utilizando o
método de bolha.

Esta unidade foi desenvolvida com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

www.esab.edu.br 339
Vamos iniciar codificando o algoritmo de ordenação do método de
bolha, na forma de uma função, e depois vamos desenvolver um
algoritmo como inserção, listagem e ordenação de um vetor para
validação do algoritmo apresentado.

40.1 Codificação do algoritmo


Para o funcionamento do método de ordenação bolha, duas rotinas
são essenciais: a primeira delas é controlar o número de processos de
ordenação e comparações; a outra é realizar a troca dos elementos da
estrutura de dados.

Vamos começar codificando a função que realizará a troca dos elementos


do vetor já que a função será utilizada na rotina de controle de processos
de ordenação e comparações.

O Algoritmo 119 apresenta a função trocar.

Algoritmo 119 – Trocar elementos do vetor


01 void trocar(int vetor[], int esq, int dir){
02 int aux = vetor[esq];
03 vetor[esq] = vetor[dir];
04 vetor[dir] = aux;
05 }

Fonte: Elaborado pelo autor (2013).

A função trocar() recebe como parâmetros um vetor de inteiros,


chamado de vetor, o índice da posição da esquerda do par a ser trocado,
chamado de esq, e o índice da posição da direita do par, chamado de
dir. O objetivo da função é realizar a troca do valor da posição “esq” do
vetor, pelo valor da posição “dir” do mesmo vetor, que representam o par
a ser ordenado. Como haverá uma troca de elementos e para que o valor
que está armazenado na posição da esquerda do par não seja perdido, na
linha 02 do algoritmo a instrução int aux = vetor[esq] realiza a cópia do
valor da posição da esquerda na variável aux (auxiliar). Na linha 03, o
valor da posição da direita é copiado para a posição da esquerda do vetor,
na instrução vetor[esq] = vetor[dir].

www.esab.edu.br 340
Por fim, o valor da variável aux é copiada para a posição da direita do
vetor, na instrução vetor[dir] = aux da linha 04 do algoritmo.

Essa função será chamada sempre que os valores do par do vetor


estiverem fora de ordem, seja essa ordem crescente ou decrescente.

Vamos agora implementar a função que percorrerá todos os elementos do


vetor, comparando-os de dois em dois: caso os elementos do par estejam
foram de ordem, serão trocados; caso contrário, os valores continuarão
na mesma posição. O Algoritmo 120 apresenta a lógica de ordenação do
método de bolha.

Algoritmo 120 – Método de bolha


01 void ordenar_bolha(int vetor[], int tamanho){
02 int houve_troca =1;
03 int passo=1,comp;
04 while ((passo<tamanho) && (houve_troca ==
1)){
05 houve_troca = 0;
06 for(comp=0; comp < tamanho-passo;comp++)
{
07 if(vetor[comp] > vetor[comp+1]){
08 houve_troca = 1;
09 trocar(vetor,comp,comp+1);
10 }
11 }
12 passo++;
13 }
14 }

Fonte: Elaborado pelo autor (2013).

Observe que a função ordenar_bolha() recebe como parâmetro o vetor


que será manipulado, chamado vetor, e o tamanho desse vetor. Na linha
02 do algoritmo é criada a variável houve_troca, que é inicializada com 1
(verdadeiro), que tem como finalidade armazenar 1, quando houver pelo
menos uma troca no vetor, e 0, quando não houver trocas. Essa variável
é fundamental para a eficiência do algoritmo, pois a ordenação só
executará um novo processo de ordenação se pelo menos uma troca tiver
sido efetuada no processo anterior. Caso contrário, se não houve troca no
processo anterior é porque o vetor já está ordenado e não é preciso um
novo processo de ordenação.

www.esab.edu.br 341
Celes, Cerqueira e Rangel Netto (2004, p. 242), com relação ao uso da
variável houve_troca destacam que essa técnica “[...] evita que o processo
continue mesmo depois de o vetor estar ordenado, o que permite
interromper o processo quando houver uma passagem inteira sem trocas”.

A variável “passo”, declarada na linha 03, serve para controlar o número


do passo de ordenação em execução, e começa valendo 1. A variável
“comp” tem como finalidade controlar o número de comparações de
cada processo de ordenação.

A instrução while ((passo<tamanho) && (houve_troca == 1)) da linha


04, tem como finalidade controlar o número de processos de ordenação,
que deverá ser menor que o tamanho do vetor, ou seja, se o vetor tem dez
elementos, os processos vão de um até nove. Para que um novo processo
de ordenação seja iniciado, é preciso verificar se houve troca no processo
anterior, por isso a condição (houve_troca == 1). Logo, os processos de
ordenação serão executados de “1 a tamanho-1” vezes e desde que no
processo anterior algum elemento tenha sido trocado de posição. Na
instrução da linha 05, a variável houve_troca é iniciada com 0 (falso) e
somente será definida como verdadeira (1) se uma troca de elementos
for realizada. Por isso a instrução houve_troca = 1 na linha 09, a qual
será executada se o valor da posição da esquerda do vetor (comp) for
maior que o valor da posição da direita (comp + 1), por isso a instrução
if(vetor[comp] > vetor[comp+1]) na linha 08. Por fim, na instrução da
linha 10, se os valores do vetor estiverem fora de ordem, a função trocar
será chamada, recebendo o vetor manipulado como parâmetro, a posição
da esquerda do par (comp) e a posição da direita do par (comp+1). A
função trocar fará a inversão dos valores do par, de forma que o maior
esteja à direita e o menor, à esquerda.

Após o término da comparação dos elementos do vetor, na linha 13,


o número do processo é incrementado (passo ++) e um novo processo
de ordenação é reiniciado, desde que o número do processo seja menor
que o tamanho do vetor e pelo menos uma troca tenha sido efetuada no
processo anterior.

Vamos desenvolver um programa que permita a inserção e listagem dos


elementos de um vetor para validarmos as regras das funções trocar e
ordenar_bolha.

www.esab.edu.br 342
40.2 Exercícios comentados
Para inserir os elementos no vetor, será desenvolvida uma função que
recebe como parâmetro o vetor que será manipulado e o tamanho do
vetor, conforme o Algoritmo 121.

Algoritmo 121 – Inserir no vetor


01 void inserir(int vetor[],int tamanho){
02 int i=0;
03 for(i=0;i<tamanho;i++){
04 printf("Informe um VALOR:\n");
05 scanf("%d",&vetor[i]);
06 }
07 printf("\n");
08 }

Fonte: Elaborado pelo autor (2013).

Observe que a função percorre todas as posições do vetor, por meio


da instrução for(i=0;i<tamanho;i++), e é solicitado ao usuário o valor
para ser inserido no vetor, pela instrução scanf(“%d”,&vetor[i]), o que
permitirá ao final do comando de laço que todos os valores tenham sido
preenchidos no vetor.

Vamos agora desenvolver a função para listar os elementos do vetor. O


Algoritmo 122 apresenta esta função.

Algoritmo 122 – Listar os elementos do vetor


01 void listar(int vetor[], int tamanho){
02 int i=0;
03 for(i=0;i<tamanho;i++){
04 printf("%d ", vetor[i]);
05 }
06 printf("\n");
07 system(“pause”);
08 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 343
A função listar recebe como parâmetros o vetor que será manipulado (int
vetor[]) e o tamanho desse vetor (int tamanho), e a partir da posição 0
do vetor (int i=0) são percorridos e listados todos os valores cadastrados
no vetor, da posição 0 até a última posição, tamanho-1 (instrução
for(i=0;i<tamanho;i++) da linha 03).

Com a implementação da função listar(), já temos condição de inserir,


listar e ordenar os valores do vetor. E para facilitar a interação do
programa com os seus usuários, vamos criar uma função com um menu
com essas operações. O Algoritmo 123 apresenta a função menu().

Algoritmo 123 – Função Menu


01 void menu(){
02 int dados[5];
03 int op=0;
04 do{
05 system("cls");
06 printf("1-Inserir\n2-Listar\n3-Ordenar\
n4-Sair\nEscolha uma OPCAO:\n");
07 scanf("%d",&op);
08 switch(op){
09 case 1:inserir(dados,5);break;
10 case 2:listar(dados,5);break;
11 case 3:ordenar_bolha(dados,5);
12 printf("Ordenacao REALIZADA COM
SUCESSO!\n");
13 printf("Para visualizar a como o
vetor ficou, escolha a OPCAO LISTAR (2)!\n");
14 system("pause");
15 }
16 }while(op!=4);
17 }

Fonte: Elaborado pelo autor (2013).


Para testar as funções de inserção, listagem e ordenação foi criado um
vetor com cinco posições chamado dados, que é passado como parâmetro
para as funções inserir(), listar() e ordenar_bolha(), junto com o tamanho
do vetor. Dependendo da opção escolhida pelo usuário, ele poderá
preencher o vetor, listá-lo e ordená-lo crescentemente.

www.esab.edu.br 344
Você pode baixar e testar o algoritmo com todas as funções apresentadas nesta
unidade clicando aqui. Para testar com um novo tamanho de vetor, basta alterar o
tamanho do vetor dados na sua definição, na linha 02 do algoritmo, só não se esqueça
de alterar na chamada das funções inserir(), listar e ordenar_bolha() o tamanho, que
era 5, para o novo tamanho que foi definido.

Nesta unidade, você pôde implementar duas regras fundamentais para o


funcionamento do algoritmo de bolha: a função trocar, que tem como
finalidade inverter o par de elementos do vetor que estão fora de ordem;
e a função ordenar_bolha(), que controla o número de processos e
comparações que serão realizadas para que o vetor fique ordenado. Para
testar essas duas funções, trocar() e ordenar_bolha(), foram desenvolvidas
as funções de inserção e listagem dos dados do vetor. Por fim, você pôde
implementar uma função de menu() para melhor interação do usuário
com o sistema.

Na próxima unidade, vamos conhecer outro método de ordenação, o


algoritmo quick sort.

www.esab.edu.br 345
Métodos de ordenação: quick sort
41 – parte 1
Objetivo
Apresentar o conceito e o funcionamento do algoritmo quick sort.

Na unidade anterior foram implementadas as duas regras fundamentais


para o funcionamento do algoritmo de bolha: a função trocar, que tem
como finalidade inverter o par de elementos do vetor que está fora de
ordem; e a função ordenar_bolha(), que controla o número de processos
e comparações que serão realizadas para que o vetor fique ordenado. Para
testar essas duas funções, trocar() e ordenar_bolha(), foram desenvolvidas
as funções de inserção e listagem dos dados do vetor.

Por fim, você pôde implementar uma função de menu() para melhorar a
interação do usuário como o sistema.

Nesta unidade, vamos conhecer outro método de ordenação, o algoritmo


quick sort.

Esta unidade foi desenvolvida com base na referência bibliográfica de


Ascencio e Araújo (2010).

Vamos iniciar os estudos conhecendo a definição desse método de


ordenação.

www.esab.edu.br 346
41.1 Definição
O algoritmo de ordenação quick sort possui como principal característica
a ordenação do vetor em partes, tendo como referência um elemento
chamado de pivô, que pode ser qualquer elemento já existente no vetor.
O pivô pode ser o primeiro ou o último, ou seja, qualquer elemento já
cadastrado no vetor, sendo muito comum a utilização do vetor como
sendo o elemento do meio do vetor. A cada execução do processo de
ordenação, os elementos do vetor são comparados com o elemento pivô,
de forma que os valores menores que o pivô fiquem à sua esquerda e os
elementos maiores que o pivô, à sua direita, o que resulta na ordenação
do pivô, ou seja, o pivô está na posição correta. Estando o pivô ordenado,
o vetor será dividido em duas partes, a parte à esquerda do pivô e a parte
à direita do pivô. Cada uma dessas partes define um pivô como referência
e os valores são ordenados, até que se tenham os menores à esquerda do
pivô e os maiores à direita. Esse processo se repete até que todo o vetor
esteja ordenado.

Vamos considerar o vetor a ser ordenado com os seguintes valores: [5, 8,


3, 1, 6, 2].

Para a ordenação utilizando o método quick sort, é preciso definir o


elemento pivô, que será o elemento do meio do vetor. Como o vetor
possui seis elementos, nas posições de 0 a 5 do vetor, o elemento pivô
será a posição 2 do vetor, que possui o valor 3. Logo, a posição do pivô
é o resultado da soma da primeira posição do vetor (0) com a última
posição do vetor (5), dividida por dois, desconsiderando a parte decimal.
A Figura 132 apresenta essa regra:
pivô = (0+5) = 2.5 2
2
Figura 132 – Cálculo da posição do pivô.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 347
A definição da posição do elemento pivô se dá pela soma da posição
inicial com a posição final do segmento, dividido por dois. Como não há
posição de vetor com número real, é considerada apenas a parte inteira
do resultado da divisão como valor de posição do pivô. Como o primeiro
segmento é o vetor inteiro, a posição inicial é 0 e a posição final 5, já que
temos um vetor de seis posições.

A partir do conhecimento da posição do pivô, consideraremos o vetor


em duas partes, os elementos 5 e 8 à esquerda, e os elementos 1, 6 e 2 à
direita do pivô. A Figura 133 apresenta a divisão do vetor:

0 1 2 3 4 5
5 8 3 1 6 2
pivô
(parte 1) (parte 2)
Figura 133 – Vetor particionado.
Fonte: Elaborada pelo autor (2013).

O valor da posição do pivô serve de referência para o processo de


ordenação, de forma que à sua esquerda devem ficar os valores menores
do que ele, e à direita os maiores. Logo, o processo de ordenação consiste
em procurar um elemento que esteja à esquerda do pivô e que seja maior
do que o valor do pivô, achando à direita um valor menor que o valor do
pivô. Assim, esses dois valores serão trocados. Veja a Figura 134.

valor maior valor menor trocar

0 1 2 3 4 5 0 1 2 3 4 5
5 8 3 1 6 2 2 8 3 1 6 5
pivô pivô
(parte 1) (parte 2) (parte 1) (parte 2)
Figura 134 – Ordenação por quick sort.
Fonte: Elaborada pelo autor (2013).

Observe que à esquerda, já na posição 0 do vetor, existe um valor maior


que o valor do pivô, e na última posição do vetor existe um valor menor
que o valor do pivô, portanto, esses dois valores são trocados. O processo
se repete, para encontrar um valor maior que o pivô na parte 1 e um
valor menor que o pivô na parte 2. A Figura 135 apresenta a sequência
da ordenação.

www.esab.edu.br 348
valor maior valor menor trocar

0 1 2 3 4 5 0 1 2 3 4 5
2 8 3 1 6 5 2 1 3 8 6 5
pivô pivô
(parte 1) (parte 2) (parte 1) (parte 2)
Figura 135 – Ordenação por quick sort.
Fonte: Elaborada pelo autor (2013).

Note que novamente foram localizados dois valores fora de ordem,


os quais, consequentemente, foram trocados. No próximo processo
de ordenação, será pesquisado um valor maior que o valor do pivô à
esquerda, que não existe, e um valor menor que o valor do pivô à direita,
que também não existe. Logo, o valor 3 está na posição correta no vetor.

A partir da ordenação do valor do pivô, dois novos processos de


ordenação quick sort serão iniciados, um para a primeira parte do vetor,
das posições 0 a 1, e outra para a segunda parte do vetor, das posições 3 a
5. Para cada uma das partes, será definido um pivô e todo o processo de
ordenação com troca dos elementos será repetido. A Figura 136 apresenta
essa sequência de ordenação.
0 1 2 3 4 5
2 1 3 8 6 5
(pivô)
(parte 1) (parte 2)

0 1 3 4 5
2 1 8 6 5
(pivô) (pivô)
trocar trocar
0 1 3 4 5
1 2 5 6 8
(pivô) (pivô)
Figura 136 – Ordenação por quick sort.
Fonte: Elaborada pelo autor (2013).

Observe que os elementos das posições 0 e 1 têm como pivô o valor


2, e na ordenação, em que se procura um valor maior à esquerda, será
localizado o próprio valor do pivô, o valor 2, e à direita, o menor valor
em relação ao valor do pivô será o valor 1. Fazendo a troca entre os
elementos, esse novo segmento estará ordenado com os valores 1 e

www.esab.edu.br 349
2. No segmento representado pelos valores 3 a 5, o pivô foi definido
como sendo o elemento da posição 4 (pivô = 3 + 5/2, pivô vale 4) e o
maior valor à esquerda do pivô é o valor 8, e o menor valor à direita é o
valor 5. Fazendo a troca desses valores, o segmento estará ordenado: 5,
6 e 8. Com a ordenação do segmento um (valores 1 e 2), do primeiro
pivô (valor 3), e do segundo segmento (valores 5, 6, 8 ) o vetor estará
ordenado:1, 2, 3, 5, 6 e 8.

Pode nos parecer que o algoritmo de quick sort seja demorado pelo
fato de termos vários processos e novos segmentos, mas é exatamente
essa característica que o torna eficiente ao ordenar vários segmentos ao
mesmo tempo, de forma recursiva, como veremos a seguir.

41.2 Funcionamento do algoritmo


O algoritmo de ordenação quick sort particiona o vetor em dois
segmentos por meio de um procedimento recursivo, que se repete até que
cada novo segmento tenha um único elemento. Para Ascencio e Araújo
(2010) o algoritmo de quick sort se caracteriza pela técnica da divisão e
conquista, e funciona da seguinte forma:

• dividir: o vetor é particionado em dois subvetores não vazios, da


posição 0 até a posição do pivô -1, e da posição do pivô +1 até a
última posição do vetor. O cálculo da posição do pivô faz parte do
processo de divisão, particionamento do vetor. Para determinar a
posição do pivô, escolhe-se o elemento que se encontra na metade
do vetor original, e os elementos são organizados de forma que os
elementos à esquerda do pivô são menores ou iguais ao valor do
pivô, e os elementos à direita do pivô são maiores ou iguais ao valor
do pivô;
• conquistar: os dois subvetores, da posição 0 a pivô -1, e pivô + 1 até
a última posição do vetor, são ordenados por chamadas recursivas do
quick sort;
• combinar: durante o processo de ordenação os valores são
organizados no próprio vetor, não exigindo outras estruturas de
dados para armazenamento das informações.

www.esab.edu.br 350
A ideia principal do algoritmo é particionar o vetor em dois e ordenar os
valores de forma que os valores menores que o pivô fiquem à sua esquerda
e os maiores que o valor do pivô, à sua direita. A implementação do quick
sort é realizada de forma recursiva para tornar mais eficiente a ordenação
dos dois segmentos gerados a partir do vetor original.

A Figura 137 apresenta a divisão recursiva adotada pelo algoritmo de


quick sort no vetor com os elementos: 5, 8, 3, 1, 6 e 2.

0 1 2 3 4 5
2 1 3 8 6 5 (0 a 5)

0 1 3 4 5
(0 a 1) 2 1 8 6 5 (3 a 5)

0 1 3 4 5
1 2 5 6 8

0 1 3 4 5
1 2 5 6 8
(0 a 0) (1 a 1) (3 a 3) (4 a 4)

Figura 137 – Particionamento do quick sort.


Fonte: Elaborada pelo autor (2013).

Note que na ordenação do vetor com os valores: [5, 8, 3, 1, 6, 2], o


primeiro processo de ordenação começou na posição 0 até a posição 5,
tendo como pivô a posição 2, ou seja, quick(0 a 5), com pivô valendo
2. Com a ordenação do valor do pivô (3), duas novas ordenações foram
iniciadas, quick(0 a 1), com pivô valendo 0, ou seja, da posição 0 até
posição do pivô -1, e quick(3 a 5), com posição inicial valendo pivô +1 e
posição final a última posição do vetor, e pivô valendo 4. Cada processo
de quick () ordena o segmento e, se for preciso, novos segmentos são
gerados, até que se tenha um único valor em cada segmento.

www.esab.edu.br 351
Nesta unidade, você pôde conhecer as características que definem o
método de ordenação quick sort, cuja ideia principal é particionar
o vetor em duas partes e ordenar os valores de forma que os valores
menores que o valor do pivô fiquem à sua esquerda e os maiores que o
valor do pivô à sua direita. A implementação do quick sort é realizada
de forma recursiva para tornar mais eficiente a ordenação dos dois
segmentos gerados a partir do vetor original, e pode ser representada por
três ações importantes: dividir, conquistar e combinar.

Na próxima unidade, vamos estudar como implementar o algoritmo de


quick sort utilizando a linguagem de programação C.

www.esab.edu.br 352
Métodos de ordenação: quick sort
42 – parte 2
Objetivo
Apresentar a codificação do algoritmo quick sort.

Na unidade anterior você pôde conhecer as características que definem


o método de ordenação quick sort, cuja ideia principal é particionar
o vetor em dois e ordenar os valores de forma que os valores menores
que o valor do pivô fiquem à sua esquerda e os maiores que o valor do
pivô, à sua direita. A implementação do quick sort é realizada de forma
recursiva para tornar mais eficiente a ordenação dos dois segmentos
gerados a partir do vetor original, e pode ser representado por três ações
importantes: dividir, conquistar e combinar.

Nesta unidade, vamos estudar como implementar o algoritmo de quick


sort utilizando a linguagem de programação C.

Este conteúdo foi desenvolvido com base na referência bibliográfica de


Celes, Cerqueira e Rangel Netto (2004).

Vamos iniciar os estudos conhecendo as principais funções do


algoritmo, a função de troca e a função de ordenação recursiva com
particionamento do vetor.

www.esab.edu.br 353
42.1 Codificação do algoritmo
O algoritmo de quick sort pode ser dividido em duas operações
principais, a troca dos elementos que estão fora de ordem, e a ordenação
da estrutura de dados que executa várias divisões da estrutura de dados,
de forma recursiva. A operação de troca de valores na estrutura de dados
já foi utilizada por nós no algoritmo de ordenação bubble sort, o método
de bolha, por isso vamos apenas apresentá-la.

Algoritmo 124 – Trocar elementos do vetor


01 void trocar(int vetor[], int esq, int dir){
02 int aux = vetor[esq];
03 vetor[esq] = vetor[dir];
04 vetor[dir] = aux;
05 }

Fonte: Elaborado pelo autor (2013).

Vamos apenas lembrar que a finalidade dessa função é receber o vetor


que está sendo manipulado como parâmetro, e as posições da esquerda e
direita do vetor que serão utilizadas para a troca dos elementos, em que
o valor da posição da esquerda será copiado para a posição da direita, e o
elemento da posição da direita será copiado para a posição da esquerda,
invertendo os valores. No caso do algoritmo de quick sort, a posição da
esquerda representa o índice do valor maior ou igual ao valor do pivô no
vetor e a posição da direita representa o índice do valor menor ou igual
ao valor do pivô no vetor.

Essa função trocar será utilizada na função de ordenação, como pode ser
visto no Algoritmo 125.

www.esab.edu.br 354
Algoritmo 125 – Ordenação por quick sort
01 void ordenar_quick(int vetor[], int inicial,
int final){
02 int i,j,pivo;
03 i = inicial;
04 j = final;
05 pivo = vetor[(inicial+final)/2];
06 while (i <= j){
07 while (vetor[i] < pivo && i < final)
i++;
08 while (vetor[j] > pivo && j > inicial)
j--;
09 if (i <= j){
10 trocar(vetor,i,j);
11 i++;
12 j--;
13 }
14 }
15 if( j > inicial) ordenar_
quick(vetor,inicial,j);
16 if( i < final) ordenar_
quick(vetor,i,final);
17 }

Fonte: Elaborado pelo autor (2013).

A função de ordenação ordenar_quick() recebe como parâmetros o vetor


de inteiros que será manipulado e as posições de início e final do vetor.
Por exemplo, se o vetor x, de inteiros possui dez elementos, a posição
inicial será 0 e a posição final será 9. A variável “i” tem como objetivo
localizar o valor à esquerda do pivô que seja maior ou igual ao valor do
pivô, por isso, a variável é iniciada com o valor da posição inicial. Já a
variável “j” tem como finalidade localizar o valor menor ou igual ao pivô
nos elementos à direita do pivô, por isso, a variável começa valendo a
posição do final do vetor. O valor do pivô é calculado como sendo o
elemento do meio do vetor, por isso, a posição do pivô corresponde ao
resultado da soma da posição inicial com a posição final, dividido por
dois, conforme a instrução da linha 5, pivo = vetor[(inicial+final)/2].
Mesmo sendo uma divisão por dois, o seu resultado será um inteiro, pois
em C a divisão por um valor inteiro, no nosso caso o valor 2, retorna o
resultado da divisão como um inteiro, descartando as casas decimais.

www.esab.edu.br 355
Saiba mais
Caro aluno, você pode conhecer um pouco mais
sobre as operações aritméticas em C clicando aqui.

Portanto, as variáveis “i” e “j” representam a posição do elemento fora


de ordem à esquerda do pivô (i) e a posição do elemento fora de ordem
à direita do pivô (j). Essas posições são pesquisadas até que a posições i
e j tenham o mesmo valor, pois essa condição indica que não há valores
desordenados nem à esquerda e nem à direita, e o pivô já está na sua
posição correta no vetor. Por esse motivo temos, a condição da linha 06
com a instrução while (i <= j). Na linha 07, a instrução while (vetor[i] <
pivo && i < final) i++ tem como finalidade percorrer os elementos do
vetor, posição por posição, a partir da posição inicial do vetor, até que
seja encontrado um valor maior que o valor do pivô ou até que se chegue
no final do vetor. A Figura 138 apresenta essa regra de funcionamento.
(inicial) (final)
0 1 2 3 4
1 9 5 7 15 vetor[i], que vale 1, é menor ou igual a 5 (pivô)?
(i) (pivô)
SIM
i++;
(inicial) (final)
0 1 2 3 4
1 9 5 7 15 vetor[i], que vale 9, é menor ou igual a 5 (pivô)?
(i) (pivô)
NÃO

Terminar a pesquisa, o maior valor à esquerda está na posição 1 do vetor.

Figura 138 – Pesquisa do maior valor à esquerda.


Fonte: Elaborada pelo autor (2013).

Note que é percorrido o vetor, incrementada cada posição referenciada


pela variável “i”, desde a posição inicial até a posição final do vetor, desde
que o valor acessado seja menor que o pivô. Caso seja encontrado um
valor maior ou igual ao pivô ou o vetor pesquisado termine, a pesquisa
é encerrada com a variável atualizada. Dessa forma, o algoritmo saberá a
posição do maior elemento à esquerda do pivô.

www.esab.edu.br 356
Na linha 08, a instrução while (vetor[j] > pivo && j > inicial) j--; tem
como finalidade percorrer os elementos do vetor, posição por posição,
a partir da posição final do vetor, até que seja encontrado um valor
menor ou igual ao valor do pivô ou até que se chegue no início do vetor.
A instrução tem como objetivo armazenar na variável “j” a posição do
menor valor à direita do pivô.

Na linha 09 é verificado se a posição do maior valor à esquerda do pivô


(i) é menor ou igual à posição do maior valor à direita do pivô (j). Caso
a condição seja verdadeira, é realizada a troca dos valores, por meio
da função trocar. Em seguida, todo processo para encontrar o maior e
menor valor em relação ao pivô é reiniciado, a partir da próxima posição
à esquerda (i++) e da posição anterior à direita (j--).

Estando o pivô na posição correta, o método de ordenação ordenar_


quick() é chamado recursivamente, com a parte da esquerda do pivô,
que vai da posição inicial (inicial) até a posição do pivô -1 (j). Por isso,
a instrução if( j > inicial) ordenar_quick(vetor,inicial,j) na linha 15 do
algoritmo. Já a instrução if( i < final) ordenar_quick(vetor,i,final), na linha
16, faz a chamada recursiva para a parte à direita do pivô, que começa na
posição do pivô+1 (i) e vai até a última posição do vetor (final).

Com as chamadas recursivas cada segmento do vetor, dividido a partir da


posição do pivô, será ordenado utilizando as regras do algoritmo quick
sort, até que cada segmento tenha um único elemento ordenado.

Veremos a seguir como utilizar o método recursivo de ordenação por


quick sort em um programa em C, já que se trata de um algoritmo de
ordenação muito utilizado no desenvolvimento de aplicações (CELES;
CERQUEIRA; RANGEL NETTO, 2004).

www.esab.edu.br 357
42.2 Exercícios comentados
Para a ordenação do vetor utilizando o algoritmo de quick sort, é
necessário que sejam informados o vetor que será manipulado e as
posições de início e final do vetor. O Algoritmo 126 apresenta utilização
da função ordenar_quick para um vetor de inteiros com cinco posições.

Algoritmo 126 – Uso da função ordenar_quick()


01 int main(int argc, char *argv[]){
02 int MAX = 4;
03 int dados[MAX];
04 int i=0;
05 for(i=0; i < MAX;i++){
06 printf("Informe o Valor para insercao
no vetor\n");
07 scanf("%d",&dados[i]);
08 }
09 ordenar_quick(dados,0,MAX-1);
10 printf("VETOR ORDENADO\n");
11 for(i=0; i < MAX;i++){
12 printf("%d,",dados[i]);
13 }
14 printf("\n");
15 system("PAUSE");
16 return 0;
17 }

Fonte: Elaborado pelo autor (2013).

Na linha 02, a variável MAX representa o número de elementos do vetor,


que nesse exemplo valerá 4. Na linha 03 é declarado o vetor dados com a
quantidade “MAX” de elementos. A rotina das linhas 05 a 07 representa
o processo de preenchimento do vetor com valores informados pelo
usuário do programa.

A instrução da linha ordenar_quick (dados,0,MAX-1) é responsável


por iniciar a ordenação do vetor, usando o algoritmo de quick sort
implementado na função ordenar_quick(). São passados como argumentos
para essa função o vetor que será manipulado (dados), a posição inicial do
vetor (0) e a posição final do vetor (MAX-1). A função ordenar_quick()
será executada, definindo internamente um pivô, e os elementos à esquerda

www.esab.edu.br 358
e à direita do elemento pivô serão trocados até que o valor do pivô esteja na
posição correta. A partir da ordenação do pivô, dois novos segmentos do
vetor serão gerados, da posição inicial do vetor até a posição do pivô -1, e
da posição do pivô +1 até a posição final do vetor.

De forma recursiva, todos os novos segmentos serão ordenados


utilizando o algoritmo quick sort, até que cada segmento tenha um valor
e esteja ordenado. Para o usuário do programa, as chamadas recursivas
são abstraídas e a ordenação se dá em um único processo. Nas linhas 11 a
13 são apresentadas as instruções para listagem do vetor após a ordenação
por quick sort.

Você pode baixar o código em C com as operações de inserção, listagem e ordenação do


vetor utilizando o método de ordenação quick sort clicando aqui. Você poderá testar o
algoritmo com qualquer quantidade de elementos do vetor, bastando para isso alterar o
valor da variável MAX. O algoritmo foi desenvolvido na ferramenta DEV C++.

Nesta unidade, você pôde desenvolver o algoritmo de ordenação


quick sort, que utiliza a função trocar (já desenvolvida para o método
de bolha), cuja finalidade é inverter os valores do par que está sendo
ordenado. O algoritmo de quick sort utiliza dois comandos de laço:
um para localizar o valor maior ou igual ao pivô à sua esquerda e outro
para localizar o valor menor ou igual ao pivô à sua direita. As posições
do maior e menor valor são utilizadas na função trocar para inverter os
valores do vetor. Esse processo se repete até que o pivô esteja na posição
correta (menores à sua esquerda e maiores à sua direita).

Após a ordenação do pivô, recursivamente, a função ordenar_quick


é executada para ordenação da parte à esquerda do pivô, que vai da
posição inicial até a posição do pivô -1, e a ordenação da parte à direita
do pivô, que vai da posição do pivô +1 até a posição final do vetor. Cada
segmento (parte esquerda e direita) é ordenado por quick sort até que
todo o vetor esteja ordenado.

Na próxima unidade, vamos estudar um novo algoritmo de ordenação, o


merge sort.

www.esab.edu.br 359
Resumo

Na unidade 37 foram implementadas as rotinas de inserção, consulta e


localização do maior e menor valor da árvore binária, sendo que todas
elas se caracterizam por serem funções recursivas, ou seja, que chamam a
si próprias. Na função de consulta para verificação de um determinado
valor na árvore binária, foi desenvolvida a rotina de descer à esquerda,
quando o valor a ser localizado é menor que o valor do nó pesquisado, e
à direita, quando o valor pesquisado é maior que o valor do nó visitado,
repetindo o processo de descida na árvore até que o valor pesquisado
seja localizado ou até que o nó visitado não tenha mais filhos a serem
visitados. Para pesquisa do maior valor, a rotina é semelhante, porém, a
partir da raiz se desce sempre pelo nó da direita e para localizar o menor
valor, a partir da raiz se desce sempre pelo nó da esquerda, até que não
haja mais nós a serem visitados.

Na unidade 38 foi estudada a definição de pesquisa e ordenação, em que


a pesquisa tem como finalidade descobrir se um determinado elemento
está presente ou não na estrutura de dados. Já a ordenação consiste
na organização dos elementos da estrutura de dados seguindo alguma
ordem, que pode ser crescente ou decrescente. Além disso, pode ocorrer
no momento da inserção do elemento na estrutura de dados, de forma
que a estrutura esteja sempre ordenada ou a partir de um conjunto de
elementos já inseridos na estrutura de dados.

Na unidade 39 você pôde estudar o algoritmo de bubble sort que controla


o número de processos de ordenação e para cada um deles, o número de
comparações a serem executadas. Estando os elementos que compõem cada
par comparado fora de ordem, eles devem ser trocados de posição.

www.esab.edu.br 360
Na unidade 40 foram implementadas duas regras fundamentais para o
funcionamento do algoritmo de bolha: a função trocar, que tem como
finalidade inverter o par de elementos do vetor que estão fora de ordem;
e a função ordenar_bolha(), que controla o número de processos e
comparações que serão realizados para que o vetor fique ordenado.

Na unidade 41, você pôde conhecer as características que definem o


método de ordenação quick sort, cuja ideia principal é particionar
o vetor em duas partes e ordenar os valores de forma que os valores
menores que o valor do pivô fiquem à sua esquerda e os maiores que o
valor do pivô, à sua direita. A implementação do quick sort é realizada
de forma recursiva para tornar mais eficiente a ordenação dos dois
segmentos gerados a partir do vetor original.

Por fim, na unidade 42 foi implementado o algoritmo quick sort com


uso das funções trocar e ordenar_quick().

www.esab.edu.br 361
43 Métodos de ordenação: merge sort

Objetivo
Apresentar o conceito e o funcionamento do algoritmo merge sort.

Na unidade anterior, você desenvolveu o algoritmo de ordenação quick


sort, que se utiliza da função trocar – a mesma já desenvolvida para o
método de bolha –, e cuja finalidade é inverter os valores do par que
está sendo ordenado. O algoritmo de quick sort fundamenta-se em dois
comandos de laços para localizar o valor maior ou igual ao pivô à sua
esquerda e o valor menor ou igual ao pivô à sua direita. As posições do
maior e menor valor são utilizadas na função trocar para inverter os valores
do vetor, e o processo se repete até que o pivô esteja na posição ordenada.
Após a ordenação do pivô, recursivamente, a função de ordenação pelo
método quick sort é executada para a ordenação dos elementos à direita e à
esquerda do pivô, que continuam desordenados. O processo de ordenação
se repete até que todos os elementos estejam ordenados.

Nesta unidade, vamos estudar o algoritmo de ordenação conhecido


como merge sort, cujo funcionamento se dá por dois processos: divisão e
conquista. Esse conteúdo foi desenvolvido com base em Celes, Cerqueira
e Rangel Netto (2004).

Vamos começar conhecendo a definição do algoritmo e, depois,


estudaremos o seu funcionamento. Vamos lá.

www.esab.edu.br 362
43.1 Definição
O algoritmo de ordenação merge sort se fundamenta em três processos:
divisão, conquista e combinação, de forma que a estrutura de dados
a ser ordenada seja dividida em partes menores (segmentos), que são
ordenadas em separado e, posteriormente, unidas, resultando em uma
estrutura de dados ordenada. A Figura 139 representa o processo de
ordenação por merge sort.

Vetor
Merge

Merge Merge

Figura 139 – Ordenação do vetor por merge sort.


Fonte: Elaborada pelo autor (2013).

Observe que a estrutura de dados, no caso um vetor, é dividida (merge)


de forma que cada novo segmento (resultado da divisão) tenha, pelo
menos, dois elementos que serão ordenados. Dessa forma, espera-se que,
quanto menor o número de elementos do segmento, menor o número
de processos executados para a sua ordenação e, consequentemente,
menor será o tempo de processamento para a ordenação. Assim,
vários segmentos serão previamente ordenados, e a união desses vários
segmentos resultará na ordenação de toda a estrutura à qual pertenciam
esses segmentos (CELES; CERQUEIRA; RANGEL NETTO, 2004).

Dessa forma, por definição, o método merge sort divide a estrutura de


dados em várias partes (divisão), como forma de facilitar e acelerar a
ordenação da estrutura de dados, principalmente quando possuem uma
grande quantidade de elementos. Cada parte é ordenada em separado
(conquista) e, ao final, os segmentos previamente ordenados são
combinados (combinação), resultando na estrutura de dados ordenada.

Por isso, o algoritmo é conhecido como algoritmo do tipo “dividir para


conquistar”. Veremos, a seguir, o seu funcionamento.

www.esab.edu.br 363
43.2 Funcionamento do algoritmo
O funcionamento do algoritmo visa garantir os três passos para a
ordenação da estrutura de dados: dividir, conquistar e combinar, e, para
isso, possui algumas regras de funcionamento, apresentadas na Figura 140.

MERGE SORT
Dividir o Vetor
inicial em dois
segmentos.

Divida o
COMBINAR Segmento
AS PARTES no Meio.
SEGMENTADA

Número de Ordenar a
NÃO SIM PRIMEIRA
elementos do
Vetor Segmento > 1 METADE
ORDENADO. recursivamente.

Ordenar a
SEGUNDA
METADE
recursivamente.

Figura 140 – Fluxograma do método merge sort.


Fonte: Elaborada pelo autor (2013).

Observe que o vetor é dividido, inicialmente, em duas partes, se possível


com a mesma quantidade de elementos. Cada parte segmentada é
dividida, novamente, em duas partes, as quais são sucessivamente
divididas em outras duas novas partes até que cada nova parte tenha
dois elementos. Cada parte com dois elementos é ordenada, primeiro a
parte da esquerda (primeira metade), depois a parte da direita (segunda
metade). Após a ordenação, que resulta em vários segmentos com
um único elemento na sua posição correta, todas as partes que foram
previamente ordenadas são combinadas, resultando no vetor ordenado.

www.esab.edu.br 364
Vamos aplicar essas regras para um hipotético vetor com os elementos
38, 16, 27, 39, 12 e 27, representado na Figura 141.

0 1 2 3 4 5
38 16 27 39 12 27

Figura 141 – Vetor com seis elementos.


Fonte: Elaborada pelo autor (2013).

Vamos dividir o vetor em dois segmentos, dividindo-o exatamente ao


meio, na posição 2, gerando um segmento das posições 0 a 2, e outro
segmento das posições 3 a 5, conforme ilustra a Figura 142.

0 1 2 3 4 5
38 16 27 39 12 27

Figura 142 – Primeira segmentação do vetor.


Fonte: Elaborada pelo autor (2013).

Note que ainda há segmentos com mais de um elemento, e que, por isso,
vamos realizar mais uma divisão, conforme consta na Figura 143.

0 1 2 3 4 5
38 16 27 39 12 27

0 1 2 3 4 5
38 16 27 39 12 27
Figura 143 – Segunda segmentação do vetor.
Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 365
Note que ainda há segmentos com mais de um elemento, e que, por isso,
vamos realizar mais uma divisão, conforme a Figura 144.
0 1 2 3 4 5
38 16 27 39 12 27

0 1 2 3 4 5
38 16 27 39 12 27

0 1 3 4
38 16 39 12

Figura 144 – Terceira segmentação do vetor.


Fonte: Elaborada pelo autor (2013).

Note que, agora, todos os segmentos possuem um único elemento.


Portanto, podemos começar a realizar a conquista, ordenando os
elementos de cada segmento, começando pelos últimos segmentos
criados: 38 e 16; 39 e 12. Logo, vamos ordenar esses dois pares, de forma
que o menor valor fique à esquerda, conforme a Figura 145.

0 1 2 3 4 5
16 38 27 12 39 27

0 1 3 4
Ordenação 38 16 39 12 Ordenação

Figura 145 – Ordenando os últimos segmentos criados.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 366
Note que os últimos segmentos criados estão ordenados crescentemente.
Agora, vamos combinar o segmento ordenado com o segmento não
ordenado, de forma que o vetor gerado fique ordenado, conforme a
Figura 146.

0 1 2 3 4 5
16 38 27 12 39 27

16 27 38 12 27 39

Figura 146 – Combinando os elementos ordenados.


Fonte: Elaborada pelo autor (2013).

Note que cada segmento está ordenado, porém, ainda temos dois
segmentos formados pelos elementos das posições 0 a 2 e os elementos
das posições 3 a 5. Vamos combinar os elementos dos dois segmentos
para gerar um único vetor ordenado.

entrada saída
7 5 2 4 1 6 3 0 0 1 2 3 4 5 6 7

7 5 2 4 1 6 3 0 2 4 5 7 0 1 3 6
dividir combinar
7 5 2 4 1 6 3 0 5 7 2 4 1 6 0 3

7 5 2 4 1 6 3 0 7 5 2 4 1 6 3 0

Figura 147 – Combinando os elementos de todos os segmentos ordenados.


Fonte: Elaborada pelo autor (2013).

Observe que são combinados os elementos de cada um dos segmentos,


de forma que o menor valor dos dois segmentos seja transferido para o
vetor final, que estará ordenado. Por exemplo, na primeira transferência o
valor 16 (na posição p1) do primeiro segmento é comparado com o valor
12 (posição p2) do segundo segmento, e, como o valor 12 é menor, é
colocado na posição correta. A próxima comparação será entre o valor 16

www.esab.edu.br 367
do primeiro segmento e o valor 27 (posição p2) do segundo segmento,
sendo, consequentemente, o valor 16 colocado na posição correta. Dessa
forma, sempre que um valor é colocado na posição correta, o elemento
à frente desse segmento será utilizado para a próxima comparação,
representado por p1 no primeiro segmento e por p2 no segundo
segmento. Isso possibilita que se saiba quais valores serão comparados
(elementos da posição p1 com elementos da posição p2), sendo que o
menor valor entre eles será colocado na posição correta.

Nesta unidade, você estudou que o merge sort se fundamenta em três


processos: divisão, conquista e combinação, e que, por isso, o algoritmo é
conhecido como do tipo “dividir para conquistar”.

Basicamente, o algoritmo divide a estrutura de dados em várias partes


(divisão), cada parte é ordenada em separado (conquista) e, ao final,
os segmentos previamente ordenados são combinados (combinação),
resultando na estrutura de dados ordenada.

Em relação ao funcionamento do algoritmo, vimos que cada parte


segmentada é dividida em duas novas partes, que, sucessivamente, são
divididas em outras duas novas partes, até que cada nova parte tenha
dois elementos. Cada parte com dois elementos é ordenada; primeiro a
parte da esquerda (primeira metade), depois a parte da direita (segunda
metade). Após a ordenação, que resulta em vários segmentos com
um único elemento na sua posição correta, todas as partes que foram
previamente ordenadas são combinadas, resultando no vetor ordenado.
Para a ordenação, é utilizada a combinação dos elementos dos segmentos
previamente ordenados, de forma que são comparados dois elementos
dos segmentos, e o menor entre eles é colocado na posição correta, sendo
o seu elemento sucessor utilizado na próxima comparação para saber qual
elemento será ordenado.

Na próxima unidade, vamos estudar como codificar o algoritmo de


merge sort.

www.esab.edu.br 368
44 Métodos de ordenação: merge sort

Objetivo
Apresentar a codificação do algoritmo merge sort.

Na unidade anterior, você estudou que o merge sort se fundamenta


em três processos: divisão, conquista e combinação; e que, por isso, o
algoritmo é conhecido como do tipo “dividir para conquistar”. Esse
algoritmo divide a estrutura de dados em várias partes (divisão), em que
cada parte é ordenada em separado (conquista) e, ao final, os segmentos
previamente ordenados são combinados (combinação), resultando na
estrutura de dados ordenada. Na prática, o algoritmo segmenta o vetor,
sucessivamente, em duas partes, até que cada parte tenha dois elementos.
Cada parte com dois elementos é ordenada, utilizando a combinação dos
elementos, e, ao final do processo de ordenação de cada segmento, todas
as partes estarão ordenadas, resultando no vetor ordenado.

Nesta unidade, vamos implementar o algoritmo de merge sort. O


conteúdo desta unidade foi desenvolvido com base em Celes, Cerqueira e
Rangel Netto (2004).

Vamos apresentar, inicialmente, a codificação do algoritmo.

44.1 Codificação do algoritmo


O algoritmo de merge sort é dividido em duas rotinas principais: a rotina
que realiza a divisão do vetor em duas partes, até que se tenham segmentos
com um único elemento, chamada rotina de segmentação, e a rotina
responsável por combinar os elementos dos dois segmentos, de forma
que o menor valor seja inserido na posição correta, chamada de rotina de
combinação (CELES; CERQUEIRA; RANGEL NETTO, 2004).

www.esab.edu.br 369
Primeiramente, vamos conhecer a codificação da rotina de combinação,
representada no Algoritmo 127.

Algoritmo 127 – Função de combinação


01 void combinacao(int vetor[],int vetoraux[],int
ini1, int ini2,int fim2){
02 int in1=ini1,in2=ini2,fim1=in2-1,au=0,i,j;
03 while(in1<=fim1 && in2<=fim2){
04 if (vetor[in1]<vetor [in2]){
05 vetoraux[au++] = vetor[in1++];
06 }else{
07 vetoraux[au++] = vetor [in2++];
08 }
09 }
10 while(in1<=fim1){
11 vetoraux[au++] = vetor[in1++];
12 }
13 while(in2<=fim2){
14 vetoraux[au++] = vetor[in2++];
15 }
16 for(i=0;i<au;i++){
17 vetor[i+ini1]=vetoraux[i];
18 }
19 }

Fonte: Elaborado pelo autor (2013).

Observe que a função de combinação recebe, como parâmetro, o vetor que


será manipulado (representado pelo parâmetro nomeado vetor). A função
combinação também recebe como parâmetro o vetor auxiliar (nomeado
vetoraux), utilizado para guardar os elementos do vetor principal durante
a ordenação. Há outros três parâmetros: ini1, que é a posição inicial do
primeiro segmento; ini2, que é a posição inicial do segundo segmento; e o
parâmetro fim2, que é a posição final do segundo segmento.

As instruções das linhas 3 a 9 representam o processo de cópia e


ordenação dos elementos do vetor principal para o vetor auxiliar, sendo
que a condição ”if ” da linha 4 verifica se o elemento do primeiro
segmento é menor que o elemento do segundo segmento. Caso a
condição seja verdadeira, copia o valor do primeiro segmento para o
vetor auxiliar; caso contrário, copia o valor do segundo segmento. Esse
processo copia todos os elementos até o meio do vetor.

www.esab.edu.br 370
As instruções das linhas 10 a 12 copiam os elementos ordenados do
primeiro segmento (os quais não precisaram ser trocados, pois já estavam
ordenados) para o vetor auxiliar, assim como as instruções das linhas 13
a 15 copiam os elementos já ordenados do segundo segmento para o
vetor auxiliar. Por fim, as instruções das linhas 16 a 18 copiam todos os
elementos que já estão ordenados no vetor auxiliar para o vetor principal,
deixando-o ordenado.

A função de combinação é utilizada na função de segmentação do vetor,


uma vez que, após a divisão dos elementos do vetor (segmentação), os
dados dos segmentos devem ser ordenados por meio da combinação. O
Algoritmo 128 apresenta a função de segmentação do vetor.

Algoritmo 128 – Função de segmentação


01 void segmentacao(int v[],int aux[],int esq,
int dir){
02 int meio,i;
03 if(esq<dir){
04 meio=(esq+dir)/2;
05 segmentacao(v,aux,esq,meio);
06 segmentacao(v,aux,meio+1,dir);
07 combinacao(v,aux,esq,meio+1,dir);
08 }
09 }

Fonte: Elaborado pelo autor (2013).

Observe que, na linha 7 do algoritmo, a função de combinação é


chamada para que os segmentos criados nas rotinas recursivas das
linhas 5 e 6 sejam ordenados. A instrução da linha 5 é responsável por
segmentar o vetor da posição inicial até o meio do vetor, e a instrução
da linha 6 segmenta o vetor a partir da posição do meio do vetor até o
seu final. Por exemplo, se o vetor tem 10 posições, a instrução da linha 5
representa a ordenação do segmento das posições 0 a 4 e a instrução da
linha 6 a ordenação dos elementos das posições 5 a 9.

Veremos, a seguir, como utilizar essas duas funções para a ordenação do


vetor.

www.esab.edu.br 371
44.2 Exercícios comentados
Vamos desenvolver um exercício no qual o usuário informa um
conjunto de valores inteiros que serão armazenados no vetor, e que,
posteriormente, serão ordenados por meio do algoritmo de merge sort.
Além de cadastrar os valores no vetor e ordená-lo, o usuário da aplicação
também poderá listar os elementos do vetor. Para a interação do
usuário com o sistema, criaremos um menu com as opções de inserção,
ordenação, listagem e sair do sistema.

Primeiramente, vamos declarar o vetor que será utilizado para o


cadastramento dos dados e o vetor auxiliar, que será utilizado para a
ordenação por merge sort. O Algoritmo 129 apresenta a declaração
dessas duas variáveis.

Algoritmo 129 – Declaração dos vetores


01 int dados[10];
02 int aux[10];

Fonte: Elaborado pelo autor (2013).

Observe que os dois vetores devem ter o mesmo tamanho, pois todos os
valores do vetor dados serão copiados para o vetor auxiliar (aux).

Vamos criar a função para o cadastro dos valores no vetor, conforme o


Algoritmo 130.

Algoritmo 130 – Inserção de elementos no vetor


01 void inserir(){
02 int i;
03 for(i=0; i < 10; i++){
04 system("cls");
05 printf("Informe o %d .o elemento de 10
elementos \n",(i+1));
06 scanf("%d",&dados[i]);
07 }
08 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 372
Como o vetor de dados possui tamanho valendo 10, o usuário poderá
cadastrar 10 elementos do tipo inteiro no vetor. Para listar os elementos
do vetor, faremos uma função para percorrer e mostrar, na tela, todos os
10 elementos do vetor dados, conforme o Algoritmo 131.

Algoritmo 131 – Listar os elementos do vetor


01 void listar(){
02 int i;
03 system("cls");
04 for(i=0; i < 10; i++){
05 printf("%d ",dados[i]);
06 }
07 printf("\n");
08 system("pause");
09 }

Fonte: Elaborado pelo autor (2013).

A função listar mostrará os elementos inseridos no vetor chamado dado,


e, se o vetor tiver sido ordenado, os dados serão listados ordenados; caso
contrário, serão listados na mesma sequência na qual foram cadastrados.

Vamos criar uma função de menu para que o usuário possa escolher qual
operação ele deseja utilizar no sistema. O Algoritmo 132 apresenta a
função menu.

www.esab.edu.br 373
Algoritmo 132 – Função menu
01 void menu(){
02 int op=0;
03 do{
04 system("cls");
05 printf("1-Inserir\n2-Listar\n3-Ordenar\n4-
Sair\nEscolha uma das opcoes:\n");
06 scanf("%d",&op);
07 switch(op){
08 case 1:inserir();break;
09 case 2:listar();break;
10 case 3:segmentacao(dados,aux,0,9);
11 printf("\nOrdenado com
Sucesso!\n");
12 system("pause");
13 break;
14 }
15 }while(op!=4);
16 }

Fonte: Elaborado pelo autor (2013).

Note que a opção 3 do menu tem como finalidade ordenar o vetor.


Para isso, é utilizada a função segmentar, que recebe como parâmetro o
vetor que será ordenado (dados), o vetor auxiliar que será utilizado na
combinação dos segmentos (aux), a primeira posição do vetor (0) e a
última posição do vetor (9). Após a execução da função segmentação, o
vetor dados estará ordenado crescentemente.

Para que o programa seja executado, apresentando o menu com as opções


para o usuários na função main do programa, basta chamar a função
menu. O Algoritmo 133 mostra essa rotina.

Algoritmo 133 – Função main


01 int main(int argc, char *argv[]){
02 menu();
03 return 0;
04 }

Fonte: Elaborado pelo autor (2013).

www.esab.edu.br 374
Observe que, na função main, a função menu é chamada para que sejam
apresentadas as opções para o usuário, quem poderá preencher, listar e
ordenar o vetor várias vezes e, para sair do sistema, basta escolher a opção
4 do menu. Não se esqueça de incluir, no algoritmo, nas duas primeiras
linhas, as chamadas para as bibliotecas stdio.h e stdlib.h. Para isso, insira as
instruções, #include “stdio.h” e #include “stdlib.h”, uma em cada linha.

Caro aluno, você pode fazer o download dos códigos desenvolvidos nesta unidade
clicando aqui. Faça o download do código e execute-o no seu computador, usando
a ferramenta de sua preferência, sendo que todo o código foi desenvolvido na
ferramenta DEV C++.

Nesta unidade, você pôde desenvolver o algoritmo de merge sort a partir


de duas funções, combinação e segmentação. A função de combinação
tem como finalidade realizar a cópia dos valores do vetor que será
ordenado para um vetor auxiliar,de forma que cada elemento copiado
para o vetor auxiliar seja o elemento ordenado. O elemento ordenado
é buscado de modo que sejam comparados os elementos do primeiro
segmento com os elementos do segundo segmento, e que o menor valor
entre esses dois elementos sejam inseridos no vetor auxiliar. Após a
inserção dos elementos ordenados no vetor auxiliar, eles serão copiados
novamente para o vetor principal, de forma que ele esteja ordenado.
A função segmentação tem como objetivo dividir o vetor em dois
segmentos, da posição inicial até a posição do meio do vetor, e da posição
do meio + 1 até a posição final do vetor. Cada um desses segmentos será
ordenado por meio da função combinação. Ao final, a ordenação dos
segmentos resultará na ordenação do vetor.

Na próxima unidade, vamos estudar as formas de busca no vetor, o que


pode ocorrer de forma sequencial ou binária.

www.esab.edu.br 375
Métodos de pesquisa: busca em
45 vetor
Objetivo
Apresentar os métodos de pesquisa utilizando vetor.

Na unidade anterior, você pôde desenvolver o algoritmo de merge


sort a partir de duas funções, combinação e segmentação. A função de
combinação tem como finalidade realizar a cópia e a ordenação dos
valores do vetor que está sendo manipulado para um vetor auxiliar.
O elemento ordenado é buscado de forma que sejam comparados
os elementos do primeiro segmento com os elementos do segundo
segmento e o menor valor entre esses dois elementos seja inserido no
vetor auxiliar.

Você viu que a função segmentação tem como objetivo dividir o vetor
em dois segmentos, e que cada um desses segmentos será ordenado por
meio da função combinação.

Ao final, a ordenação dos segmentos resultará na ordenação do vetor.

Nesta unidade, vamos estudar as formas de busca no vetor, que pode se dar
por meio de dois tipos de pesquisa, sequencial ou binária. Esta unidade foi
desenvolvida com base em Celes, Cerqueira e Rangel Netto (2004).

Vamos começar conhecendo a pesquisa sequencial.

www.esab.edu.br 376
45.1 Busca sequencial
A forma mais simples de busca em um vetor consiste em percorrer o
vetor, elemento a elemento, para verificar se o elemento de interesse
existe no vetor, ou seja, se algum dos elementos do vetor é igual ao
elemento desejado (CELES; CERQUEIRA; RANGEL NETTO, 2004).
O Algoritmo 134 apresenta uma rotina de pesquisa sequencial.

Algoritmo 134 – Busca sequencial


01 int pesquisa_sequencial(int vetor[], int
chave, int tamanho){
02 int i;
03 for(i=0; i < tamanho; i++){
04 if(vetor[i] == chave){
05 return 1; //achou
06 }
07 }
08 return 0; //não achou
09 }

Fonte: Elaborado pelo autor (2013).

A função percorre todos os elementos do vetor, da posição 0 até a última


posição (< tamanho), e, caso o valor do vetor (vetor[i]) seja igual ao valor
procurado (chave), a função retornará o valor 1 (verdadeiro), pois o valor
foi localizado, e a função será encerrada, pois não há necessidade de
continuar a busca. Caso o valor não seja localizado, todos as posições do
vetor terão sido visitadas, e, ao sair do comando de laço “for”, a função
retornará um 0 (falso), pois o valor não existe no vetor.

Trata-se de um algoritmo bastante simples, desenvolvido com um


comando de laço e um comando de decisão, o que o torna fácil de ser
implementado e entendido. Porém, a sua eficiência é baixa, já que é
preciso percorrer todos os elementos do vetor, desde a primeira posição,
até que seja localizado o valor desejado ou o vetor se encerre. Logo,
quanto maior o vetor, mais demorada será a pesquisa sequencial.

www.esab.edu.br 377
Uma estratégia para tornar o algoritmo mais eficiente é considerar que
a estrutura de dados já está ordenada. Por exemplo, se temos um vetor
com elemento 3, 8, 15 e 21 e desejamos buscar o elemento 12, não será
preciso pesquisar todos os elementos do vetor. Isso acontece porque,
como o valor 12 é menor que 15, se ele não for achado até a posição do
valor 15, não é preciso continuar a busca – o valor (12) não tem como
existir nas posições à frente em um vetor ordenado. O Algoritmo 135
apresenta essa nova regra para a busca sequencial.

Algoritmo 135 – Busca sequencial ordenada


01 int pesquisa_sequencial_ordenada(int vetor[],
int chave, int tamanho){
02 int i;
03 for(i=0; i < tamanho; i++){
04 if(vetor[i] == chave){
05 return 1; //achou
06 }else{
07 if (vetor[i] > chave){
08 return 0; // não achou
09 }
10 }
11 }
12 return 0; //não achou
13 }

Fonte: Elaborado pelo autor (2013).

Note que, na linha 4, é verificado se o valor do vetor é igual ao valor


pesquisado, e, sendo verdade, a função é encerrada e o valor 1 é
retornado. Mas, se o valor desejado não foi localizado e ele é maior que
o valor atual do vetor, a função é encerrada e um 0 é retornado, pois o
valor não foi localizado. E, como o vetor é ordenado, os próximos valores
não podem ser menores que o valor atual do vetor; sendo assim, o valor
desejado não tem como existir nas demais posições do vetor. A busca se
encerra sem que seja necessário pesquisar todos os elementos restantes.

Com essa alteração no algoritmo, ele ficará mais rápido que a


implementação anterior, porém, o algoritmo continua fazendo uma
busca sequencial, elemento por elemento, o que o torna ineficiente e
demorado. Celes, Cerqueira e Rangel Netto (2004, p. 258) destacam
que esse novo algoritmo sequencial para pesquisa em um vetor ordenado

www.esab.edu.br 378
“[...] apresenta um desempenho ligeiramente superior ao primeiro, mas
a ordem dessa versão do algoritmo continua sendo linear [...]”, o que o
torna ainda demorado.

Para a busca em vetores ordenados há algoritmos mais eficientes, e


veremos o principal deles a seguir: a busca binária.

45. 2 Busca binária


É importante lembrarmos que esse algoritmo destina-se à busca em
vetores ordenados; caso contrário, não poderá ser aplicado. A ideia do
algoritmo é buscar o elemento desejado comparando-o com o elemento
do meio do vetor. Assim, se o valor do meio do vetor for igual ao
elemento desejado, podemos parar a busca. Porém, se o valor do meio do
vetor for maior que o valor desejado, isso significa que o valor só poderá
existir na parte inferior do vetor, da posição inicial até a posição anterior
ao meio do vetor. Se o valor do meio do vetor for menor que o valor
desejado, isso significa que o valor desejado só poderá existir da posição
do meio + 1 até a posição final do vetor. Se o valor não foi localizado,
para cada novo segmento em que continuará a busca será definido um
novo meio para que seja verificado se o valor do vetor (posição do meio)
é ou não igual ao valor desejado. Caso não seja, se o valor do meio for
maior, todas as posições a partir do meio serão desconsideradas para
novas buscas. Caso contrário, todas as posições anteriores ao meio é que
serão desconsideradas para novas buscas, até que o valor seja localizado
ou sinalizado que não existe. A Figura 148 ilustra esse processo.

www.esab.edu.br 379
buscar (99)
10, 15, 21, 44, 66, 77, 88, 99
meio procura
para frente

10, 15, 21, 44, 66, 77, 88, 99


meio procura
para frente
77, 88, 99 (achou)
meio
não será
pesquisado

não será
pesquisado
Figura 148 – Busca sequencial.
Fonte: Elaborada pelo autor (2013).

Note que o valor desejado, 99, encontra-se após o valor do meio (66);
dessa forma, todos os elementos anteriores ao meio, inclusive o meio
(66), não farão parte da nova busca, que será realizada apenas com os
elementos 77, 88 e 99. Será gerado um novo meio, que será o valor 88, e,
como o valor 99 é maior que 88, a pesquisa continuará a partir do valor
99, descartando todos os elementos anteriores ao meio e o próprio meio,
sobrando apenas o valor 99 a ser pesquisado. Como só há o valor 99, ele é
o meio do segmento, e, ao compará-lo com o valor desejado, o resultado
será de valor localizado. Caso o valor desejado não estivesse na posição do
valor 99, isso indicaria que o valor desejado não existe no vetor.

O Algoritmo 136 apresenta a implementação das regras da busca binária.

www.esab.edu.br 380
Algoritmo 136 – Busca binária
01 int pesquisa_binaria(int vetor[],int pi,int
pf,int chave){
02 if (pi > pf) return 0;
03 int meio = (pi+pf) / 2;
04 if (vetor[meio] == chave){
05 return 1; //achou
06 }
07 else{
08 if(vetor[meio] > chave){
09 return pesquisa_
binaria(vetor,0,meio-1, chave);
10 }else{
11 return pesquisa_
binaria(vetor,meio+1,pf,chave);
12 }
13 }
14 }

Fonte: Elaborado pelo autor (2013).

Note que, caso a posição inicial do vetor seja maior que a posição final,
o valor pesquisado (chave) não existe no vetor e a função retornará
0 (falso). Se o valor da posição do meio do vetor for igual ao valor
pesquisado (chave), a função retornará 1 (verdadeiro). Caso contrário,
se o valor do meio do vetor for maior que o valor procurado, a função
pesquisa_binária será chamada, recursivamente, na linha 9 para pesquisar
os elementos anteriores ao meio do vetor. Ou, ainda, se o valor do meio
do vetor for menor que o valor procurado, a função pesquisa_binária
será chamada, recursivamente, na linha 11 para pesquisar os elementos
posteriores ao meio do vetor.

Caro aluno, você pode fazer o download dos códigos desenvolvidos nesta unidade
clicando aqui. Faça o download do código e execute-o no seu computador usando
a ferramenta de sua preferência, sendo que todo o código foi desenvolvido na
ferramenta DEV C++.

www.esab.edu.br 381
Nesta unidade, você pôde estudar a busca sequencial, entendendo que
a forma mais simples de busca em um vetor consiste em percorrê-lo,
elemento a elemento, para verificar se o elemento de interesse existe
no vetor, ou seja, se algum dos elementos do vetor é igual ao elemento
desejado. Trata-se de um algoritmo bastante simples, desenvolvido com
um comando de laço e um comando de decisão, o que o torna fácil de
ser implementado e entendido. Porém, a sua eficiência é baixa, já que é
preciso percorrer todos os elementos do vetor, desde a primeira posição,
até que seja localizado o valor desejado ou o vetor se encerre. Logo,
quanto maior o vetor, mais demorada será a pesquisa sequencial. Uma
estratégia para tornar o algoritmo de pesquisa sequencial mais eficiente é
considerar que a estrutura de dados já está ordenada; assim, o algoritmo
ficará mais rápido. Porém, como o algoritmo continuará fazendo uma
busca sequencial, elemento por elemento, ainda será demorado.

Estudo complementar
Para saber mais sobre pesquisa binária, acesse o
vídeo explicativo.

Você pôde estudar, ainda, que, para vetores ordenados, deve-se usar a busca
binária, que tem como ideia buscar o elemento desejado comparando-o
com o elemento do meio do vetor. Se o valor do meio do vetor for igual
ao elemento desejado, podemos parar a busca, pois o valor foi localizado.
No entanto, se o valor do meio do vetor for maior que o valor desejado,
uma nova procura será realizada da posição inicial até a posição anterior ao
meio do vetor. Se o valor do meio do vetor for menor que o valor desejado,
a busca será da posição do meio + 1 até a posição final do vetor. Se o valor
não for localizado, para cada novo segmento em que se continuará a busca
será definido um novo meio e todo o processo se repete, dividindo o vetor
e descartando uma das partes da divisão até que se ache o valor desejado,
ou seja, sinalizado que ele não existe no vetor.

Na próxima unidade, vamos estudar o método de pesquisa, utilizando


árvore binária de pesquisa.

www.esab.edu.br 382
Métodos de pesquisa: árvore
46 binária de pesquisa
Objetivo
Apresentar o método de pesquisa utilizando árvore binária de
pesquisa.

Na unidade anterior, você pôde estudar a busca sequencial, a forma mais


simples de busca em um vetor, que consiste em percorrê-lo, elemento
a elemento, para verificar se o elemento de interesse existe no vetor. É
um algoritmo bastante simples, porém, a sua eficiência é baixa, já que é
preciso percorrer todos os elementos do vetor, desde a primeira posição,
até que seja localizado o valor desejado ou que o vetor se encerre. Logo,
quanto maior o vetor, mais demorada será a pesquisa sequencial. Mesmo
com a estratégia de ordenar o vetor para tornar o algoritmo de pesquisa
sequencial mais eficiente, o algoritmo continuará fazendo uma busca
sequencial, elemento por elemento, sendo, ainda, demorado. Você
pôde estudar, também, que para vetores ordenados deve-se usar a busca
binária, que tem como ideia buscar o elemento desejado, comparando-o
com o elemento do meio do vetor. Caso o valor não seja localizado, este
será particionado até que se ache o valor desejado, ou seja, sinalizado que
o valor desejado não existe. A cada particionamento, uma das duas partes
gerada será descartada para a continuação da busca, tornando-o mais
eficiente e rápido.

Nesta unidade, vamos estudar as formas de busca na árvore binária. O


conteúdo desta unidade foi desenvolvido com base em Puga (2009).

Vamos começar relembrando o que é uma árvore binária.

www.esab.edu.br 383
46.1 Busca em árvore binária de pesquisa
Na unidade 33, realizamos o estudo da estrutura de dados árvore binária,
assim nomeada por possuir, no máximo, dois filhos para cada nó. Segundo
Puga (2009, p. 234), as árvores binárias “[...] possuem nós com grau
menor ou igual a 2, isto é, nenhum nó possui mais que dois descendentes
diretos (dois filhos)”. A Figura 149 apresenta uma árvore binária.

Figura 149 – Árvore binária.


Fonte: Elaborada pelo autor (2013).

Observe que cada nó da árvore possui, no máximo, dois nós filhos.

A árvore binária é “[...] uma estrutura de dados não linear, que possui
propriedades especiais e admite muitas operações, como; pesquisa,
inserção, remoção, e são úteis para implementação de algoritmos
que necessitam de estruturas hierárquicas” (PUGA, 2009, p. 230). A
árvore é uma estrutura de dados bem peculiar e, por isso, possui uma
terminologia própria para a identificação de seus elementos e grupo de
elementos, por exemplo:

• nó – cada elemento da árvore é chamado de nó;


• nó raiz – nó do topo da árvore, do qual descendem os demais nós.
O primeiro nó da árvore;
• nó folha ou terminal – nó que não possui descendentes;
• grau do nó – número de descendentes de um nó da árvore. Na
árvore binária esse grau pode ser no máximo igual a 2; e
• trajetória – caminho percorrido de um nó origem até um nó destino.

www.esab.edu.br 384
Uma árvore binária pode ser classificada como balanceada ou não
balanceada. Uma árvore binária balanceada se caracteriza por ter,
para cada nó, a seguinte regra: os valores menores que o nó pai ficam
sempre à esquerda, enquanto que os valores maiores que o nó pai ficam
sempre à direita. Consequentemente, na árvore binária não balanceada
os valores não possuem regra de organização e os valores são inseridos
aleatoriamente na estrutura de dados. A Figura 150 apresenta os dois
tipos de árvores binárias.

(-) (+)

8 4

3 10 13 3

1 6 14 1 6 14

4 7 13 10 7 8

Árvore binária Árvore binária


balanceada não balanceada

Figura 150 – Árvore binária balanceada e não balanceada.


Fonte: Elaborada pelo autor (2013).

Note que, na árvore balanceada, todos os nós possuem os valores maiores


que ele à direita e os valores menores à esquerda. Por exemplo, o nó com
valor 6 possui, como filho da esquerda, o nó com valor 4, e, como filho
da direita, o nó com valor 7. Já o nó com valor 6 está à esquerda do nó
com valor 8 (raiz da árvore), pois 6 é menor que 8.

Já a árvore binária não balanceada não possui regra de organização dos


valores. Assim, temos valores maiores que o nó pai à direita e à esquerda,
como é o caso do nó com valor 6, que possui o nó com valor 10 à
esquerda e o nó com valor 7 à direita, ambos os valores maiores que o nó
pai (6).

www.esab.edu.br 385
A forma de se buscar um elemento na árvore balanceada é diferente da
busca na árvore não balanceada. Nesta unidade, abordaremos a busca na
árvore não balanceada e, na próxima unidade, faremos o estudo da busca
em árvores balanceadas. Na árvore binária, a busca é mais inteligente, já
que os valores estão organizados. Já na árvore não balanceada, é preciso
percorrer todos os seus elementos para localizar o valor desejado. A busca
na árvore corresponde à trajetória (caminho percorrido de um nó origem
até um nó destino) do nó raiz até se chegar ao valor desejado ou até que
seja sinalizado que o valor não existe.

Para buscar um elemento na árvore binária não balanceada, deve ser


adotada uma das três possíveis ordens de percurso na árvore (tema que
estudamos na unidade 35). Vale relembrar que os tipos de ordens são:
prefixado ou pré-ordem, central e pós-fixado. O que muda de uma
ordem de percurso para outra é a sequência dos elementos visitados. A
ordem de percurso prefixado trata, primeiro, o elemento raiz, depois
percorre os elementos da esquerda e depois os da direita, ou seja, R,
E, D, onde R é raiz, E, esquerda, e D, direita. A ordem de percurso
central possui, como regra, percorrer os filhos da esquerda, depois tratar
o elemento raiz, e, por último, percorrer os filhos da direita, ou seja, E,
R, D. Por fim, a ordem de percurso pós-fixado busca o elemento mais
à esquerda, depois o elemento mais à direita, e, por último, o nó pai,
resultando na ordem E, D, R.

Como a árvore não está balanceada, qualquer um dos tipos de ordem


de percurso pode ser utilizado, pois, em todos os casos, será preciso
percorrer toda a árvore binária para verificar se o valor desejado existe ou
não na árvore, o que muda é a ordem de verificação dos nós. A Figura
151 apresenta uma árvore binária não balanceada com seis elementos.

www.esab.edu.br 386
K

A L

M P

Figura 151 – Árvore binária não balanceada com seis elementos.


Fonte: Elaborada pelo autor (2013).

Note que a árvore não possui nenhuma regra de organização dos


elementos, os quais foram inseridos aleatoriamente. Ao executar cada
um dos tipos de ordem de percurso, os elementos serão percorridos
sequencialmente, seguindo as regras de cada tipo, como pode ser
visualizado no Quadro 7.

Tipo de Ordem Regras de Percurso Listagem


Pré-ordem raiz, esquerda e direita K, A, M, Z, L e P
Central esquerda, raiz e direita A, Z, M, K, P, e L
Pós-ordem esquerda, direita e raiz Z, M, A, P, L, e K

Quadro 7 – Resultado da listagem dos tipos de ordem.


Fonte: Elaborado pelo autor (2013).

Podemos considerar que cada forma de percurso – pré-ordem, central


e pós-ordem – possui uma sequência de visitação dos elementos,
representada pela coluna chamada listagem. Sendo assim, a busca será
sequencial, pesquisando cada nó até que se localize o valor desejado ou
informando que ele não existe. Por exemplo, para buscar o elemento L
na árvore, usando o tipo de percurso pré-ordem, serão visitados os nós
K, A, M, Z e L, para que o nó com valor L seja localizado. Já na mesma
busca com o tipo central, os nós visitados serão A, Z, M, K, P e L, ou
seja, todos os nós. Por fim, a mesma busca com o tipo pós-ordem visita
os nós Z, M, A, P e L, para localizar o elemento L.

www.esab.edu.br 387
Note que o processo é muito semelhante ao da busca sequencial, pois
todos os elementos são visitados, um a um, até que se localize o elemento
desejado. Dessa forma, quanto mais elementos tivermos na árvore, mais
demorada tende a ser a pesquisa. Portanto, utilizar as formas de percurso
na árvore binária não balanceada não se trata de uma boa estratégia para
a busca de elementos, principalmente se a estrutura tende a armazenar
muitos deles.

Para resolver esse problema de busca na árvore binária, tornando


o processo eficiente mesmo que a estrutura de dados tenha muitos
elementos, é preciso, primeiramente, que a árvore esteja balanceada, para
que, depois, sejam buscados os elementos de forma não linear (como na
pesquisa sequencial). Esse assunto será abordado na próxima unidade.

Nesta unidade, você pôde revisar os conceitos básicos da estrutura de


dados árvore binária, assim nomeada por possuir, no máximo, dois
filhos para cada nó. Lembramos que a árvore binária é uma estrutura de
dados não linear, que possui propriedades especiais e que admite muitas
operações, como pesquisa, inserção e remoção. Além disso, é útil para a
implementação de algoritmos que necessitam de estruturas hierárquicas.
Vimos que a árvore binária pode ser classificada como balanceada ou não
balanceada, sendo que uma árvore binária balanceada se caracteriza por
ter, para cada nó, os filhos com valores menores à esquerda do nó pai e os
filhos com valores maiores à direita do nó pai. Para buscar um elemento
na árvore binária não balanceada, deve ser adotada uma das três
possíveis ordens de percurso na árvore: prefixado ou pré-ordem, central
e pós-fixado. O que muda de uma ordem de percurso para a outra é a
sequência dos elementos visitados.

O processo de busca na árvore binária não balanceada é muito


semelhante ao da busca sequencial, pois todos os elementos são visitados,
um a um, até que se localize o elemento desejado. Dessa forma, quanto
mais elementos tivermos na árvore, mais demorada tende a ser a
pesquisa. Portanto, utilizar as formas de percurso na árvore binária não
balanceada não se trata de uma boa estratégia para busca de elementos,
principalmente se a estrutura tende a armazenar muitos deles.

Na próxima unidade, vamos estudar o método de pesquisa utilizando


árvores balanceadas.

www.esab.edu.br 388
Métodos de pesquisa: árvore
47 balanceada
Objetivo
Apresentar o método de pesquisa utilizando árvores balanceadas.

Na unidade anterior, você pôde revisar os conceitos básicos da estrutura


de dados da árvore binária, que é assim chamada por possuir, no
máximo, dois filhos para cada nó. A árvore binária é uma estrutura de
dados não linear que permite a pesquisa, a inserção e a remoção, e é
útil para a implementação de algoritmos que necessitam de estruturas
hierárquicas. Vimos que a árvore binária pode ser classificada como
balanceada ou não balanceada.

Para buscar um elemento na árvore binária não balanceada, deve ser


adotada uma das três possíveis ordens de percurso na árvore: prefixado
ou pré-ordem, central e pós-fixado. O que muda de uma ordem de
percurso para a outra é a sequência dos elementos visitados. O processo
de busca na árvore binária não balanceada é muito semelhante ao da
busca sequencial, e, dessa forma, quanto mais elementos tivermos na
árvore, mais demorada tende a ser a pesquisa. Portanto, utilizar as formas
de percurso na árvore binária não balanceada não se trata de uma boa
estratégia para a busca de elementos, principalmente se a estrutura tende
a armazenar muitos deles.

Nesta unidade, vamos estudar a forma de busca na árvore binária


balanceada. O conteúdo desta unidade foi desenvolvido com base em
Celes, Cerqueira e Rangel Netto (2004).

Vamos começar relembrando o que é uma árvore binária balanceada.

www.esab.edu.br 389
47.1 Busca em árvores balanceadas
Vamos começar revisando o conceito de árvore binária balanceada e
como essa árvore é gerada. Na unidade 34, abordamos esse assunto
definindo o balanceamento da árvore como sendo uma importante
propriedade na qual os valores menores que o seu pai ficam à sua
esquerda, enquanto que os valores maiores que seu pai ficam à sua direita
(CELES; CERQUEIRA; RANGEL NETTO, 2004). Vamos utilizar
essa propriedade para desenvolver a busca na árvore binária. A Figura
152 apresenta uma árvore binária balanceada que armazena os valores
informados pelo usuário, números inteiros, de forma que os valores
menores fiquem à esquerda e, os maiores, à direita.

Valores a serem inseridos:


[15, 21, 55, 08, 12, 33]

Insere 15 (1) Insere 8 (4) Insere 33 (6)


se for maior -> direita se for maior -> direita
15 (raiz) se for menor -> esquerda se for menor -> esquerda
Insere 21 (2)
se for maior -> direita 15 (raiz) 15 (raiz)
se for menor -> esquerda
08 21 08 21

15 (raiz) 55 12 55

21 33
Insere 55 (3) Insere 12 (5)
se for maior -> direita se for maior -> direita
se for menor -> esquerda se for menor -> esquerda

15 (raiz) 15 (raiz)
21 08 21
55 12 55

Figura 152 – Inserção na árvore binária balanceada.


Fonte: Elaborada pelo autor (2013).

www.esab.edu.br 390
Observe que a primeira inserção define o valor que será a raiz da árvore.
A cada inserção, é verificado se o valor é maior ou menor que o valor da
raiz. Se for maior, o valor é inserido à direita; se for menor, é inserido à
esquerda. Porém, essas posições já podem ter sido ocupadas por outra
inserção anterior, logo, é preciso que o processo de inserção faça uma nova
verificação do valor que está na posição que seria a inserção, avaliando
se o valor a ser inserido é maior ou menor que esse valor já inserido. Se
for maior que o valor já inserido, o novo valor é inserido à direita; se
for menor que o valor já inserido, o novo valor é inserido à esquerda. O
processo se repete para cada nó que já está ocupado até que se ache uma
posição livre para inserção. Por exemplo, a Figura 152 apresenta o processo
para a inserção do valor 33. Como ele é maior que o nó 15 (raiz), deve ser
inserido à direita do nó 15, porém, nessa posição já existe o nó 21. Então,
é perguntado ao nó 21 se o valor 33 é maior ou menor que ele (21). Como
é maior, deve ser inserido à direita. No entanto, já existe o nó 55 nessa
posição, então, é perguntado ao nó 55 se 33 é maior ou menor que ele.
Como é menor, é inserido à esquerda do nó 55.

Assim, podemos concluir que todos os nós possuem os valores menores


que ele à esquerda e maiores que ele à direita. Com essa regra de inserção,
sempre que inserido ou removido um elemento da árvore binária, esta
se manterá organizada e balanceada, com maiores à direita e menores à
esquerda. Na unidade 34, foi exposta, no Algoritmo 108, a rotina para
a inserção balanceada dos elementos na árvore binária, reapresentada no
Algoritmo 137.

www.esab.edu.br 391
Algoritmo 137 – Inserção na árvore binária
01 void inserir(ArvoreBinaria *arvore,int
novovalor){
02 if ((*arvore) = = NULL) {
03 *arvore = (No *) malloc(sizeof(No));
04 (*arvore)->esq = NULL;
05 (*arvore)->dir = NULL;
06 (*arvore)->valor = novovalor;
07 }
08 if (novovalor < (*arvore)->valor) {
09 inserir((&(*arvore)->esq, novovalor);
10 }
11 else{
12 if (novovalor > (*arvore)->valor) {
13 inserir(&((*arvore)->dir, novovalor);
14 }
15 }
16 }

Fonte: Elaborado pelo autor (2013).

Basicamente, o algoritmo aloca um espaço de memória para o novo nó


da árvore binária e verifica se o valor a ser inserido (novo valor) é maior
que o valor do nó atual, faz a inserção do novo elemento à direita e, caso
contrário, insere à esquerda.

Essa mesma regra será utilizada na busca do elemento na árvore binária


balanceada. Primeiramente, será verificado se o valor do nó atual não é
igual ao valor desejado (pesquisado). Se for igual, o processo de busca é
encerrado, pois o valor foi localizado. Caso o valor não seja localizado e
o valor do nó atual seja menor que o valor pesquisado, isso significa que
o valor pesquisado só poderá existir à direita do nó atual. Dessa forma,
todos os nós à esquerda são desconsiderados para a continuidade da busca
pelo elemento desejado. No caso de o valor não ser localizado, porém,
o valor do nó atual ser maior que o valor desejado, isso significa que o
valor desejado só poderá existir à esquerda do nó atual. Assim, todos os
elementos à direita do nó atual serão descartados na continuidade da
busca. A Figura 153 apresenta uma árvore binária balanceada.

www.esab.edu.br 392
15

8 21

12 55

33

Figura 153 – Árvore binária balanceada.


Fonte: Elaborada pelo autor (2013).

Vamos simular a busca do valor 55 dessa árvore. O primeiro passo é


comparar o valor do nó raiz da árvore (15) com o valor desejado (55).
Como os valores não são iguais, a busca continuará; porém, como o
valor 55 é maior que o valor 15, todos os nós à esquerda do nó raiz não
serão pesquisados futuramente. A Figura 154 apresenta a subárvore que
será pesquisada.
15
subárvore

8 21

12 55

33

Figura 154 – Árvore binária balanceada – subárvore.


Fonte: Elaborada pelo autor (2013).

Observe que, na primeira verificação, foi comparado o valor 15 com


o valor 55 (valor desejado). Como o valor não foi localizado, toda a
subárvore da esquerda será descartada, inclusive a raiz, e a pesquisa
continuará na subárvore da direita, pois 55 é maior que 15. A próxima
comparação será com o valor 21, raiz da subárvore à direita. Ao comparar
o valor 21 com 55, como o valor 21 não é igual ao 55 e é menor que
55, o valor 55 só poderá existir a partir do nó da direita do 21. Dessa
forma, uma nova subárvore será gerada para a continuidade da pesquisa,
conforme ilustra a Figura 155.

www.esab.edu.br 393
15

subárvore
8 21

12 55

33

Figura 155 – Árvore binária balanceada – subárvore.


Fonte: Elaborada pelo autor (2013).

Observe que uma nova subárvore será utilizada para a continuidade da


busca, sendo que tem como raiz o valor 55. Dessa forma, se o valor da
raiz (55) é igual ao valor desejado, o resultado será de valor localizado e
a pesquisa não precisará continuar. Caso o valor não fosse encontrado, a
busca continuaria para a esquerda (menores) ou para a direita (maiores),
até que o valor desejado fosse localizado ou até que não se houvessem
mais valores à esquerda ou à direita (valor não existe na árvore binária).

Dessa forma, para buscar o valor 55, a árvore com os elementos 15, 8,
12, 21, 55 e 33 pesquisou os elementos 15, 21 e 55 e obteve sucesso. Se a
pesquisa fosse sequencial, seriam pesquisados os valores 15, 8, 12, 21 e 55
para que fosse obtido o mesmo resultado, ou seja, o valor existe na árvore.

Nesta unidade, você revisou que o balanceamento da árvore é uma


importante propriedade da árvore binária, na qual os valores menores
que o seu pai ficam à sua esquerda e os valores maiores que seu pai, à sua
direita. Para a geração da árvore binária balanceada, a primeira inserção
de um novo valor define esse novo valor como sendo a raiz da árvore. A
cada inserção, é verificado se o valor é maior ou menor que o valor da
raiz. Se for maior, o valor é inserido à direita; se for menor, é inserido à
esquerda. Porém, essas posições já podem ter sido ocupadas por outra
inserção anterior. Logo, é preciso que o processo de inserção faça uma nova
verificação do valor que está na posição que seria a inserção, avaliando se
o valor a ser inserido é maior ou menor que aquele já adicionado. Se o
novo valor for maior que o valor já inserido, este é adicionado à direita; se

www.esab.edu.br 394
for menor, é inserido à esquerda do valor já inserido. O processo se repete
para cada nó que já está ocupado, até que se ache uma posição livre para
a inserção. Assim, podemos concluir que todos os nós possuem os valores
menores que ele à esquerda e maiores à direita.

Essa regra de menores à esquerda e maiores à direita é utilizada na busca


do elemento na árvore binária balanceada. Primeiramente, será verificado
se o valor do nó atual não é igual ao valor desejado (pesquisado). Se
for igual, o processo de busca é encerrado, pois o valor foi localizado.
Caso o valor não seja localizado e o valor do nó atual seja menor que
o valor pesquisado, isso significa que o valor pesquisado só poderá
existir à direita do nó atual. Dessa forma, todos os nós à esquerda são
desconsiderados para a continuidade da busca pelo elemento desejado.
Do mesmo modo, se o valor não foi localizado, porém, o valor do nó
atual é maior que o valor desejado, isso significa que o valor desejado só
poderá existir à esquerda do nó atual. Assim, todos os elementos à direita
do nó atual serão descartados na continuidade da busca.

Na próxima unidade, vamos estudar como implementar a busca em


árvores binárias balanceadas com o desenvolvimento de um exercício
resolvido e comentado.

Tarefa dissertativa
Caro estudante, convidamos você a acessar o
Ambiente Virtual de Aprendizagem e realizar a
tarefa dissertativa.

www.esab.edu.br 395
48 Exercícios de fixação

Objetivo
Abordar exemplos de exercícios resolvidos e comentados.

Na unidade anterior, você estudou como o balanceamento da árvore é


importante para que a busca de um valor desejado seja eficiente e rápida.
Você revisou que a geração da árvore binária balanceada tem, na primeira
inserção do novo valor, a definição do nó raiz da árvore, e que, em cada
inserção, é verificado se o valor é maior ou menor que o valor da raiz. Se
for maior, o valor é inserido à direita; se for menor, à esquerda.

Essa mesma regra de menores à esquerda e maiores à direita é utilizada na


busca do elemento na árvore binária balanceada, na qual, primeiramente,
é verificado se o valor do nó atual é igual ao valor desejado (pesquisado).
Se sim, o processo de busca é encerrado; caso o valor não seja localizado
e o valor do nó atual seja menor que o valor pesquisado, todos os nós à
esquerda são desconsiderados para a continuidade da busca. Da mesma
forma, se o valor não foi localizado, porém, o valor do nó atual é maior
que o valor desejado, todos os elementos à direita do nó atual serão
descartados na continuidade da busca.

Nesta última unidade, vamos estudar como implementar a busca em


árvores binárias balanceadas com o desenvolvimento de um exercício
resolvido e comentado. O conteúdo desta unidade foi desenvolvido com
base em Celes, Cerqueira e Rangel Netto (2004).

Vamos começar definindo a estrutura da árvore binária que será manipulada.

www.esab.edu.br 396
Exercícios de fixação

Vamos desenvolver um algoritmo que permita ao usuário cadastrar,


listar e consultar valores inteiros armazenados em uma árvore binária
balanceada. Não se preocupe em copiar todo o código que será
apresentado, pois, no final da unidade você poderá fazer o download de
todo o código desenvolvido.

No primeiro código, vamos implementar o tipo estruturado, que


representará cada nó da árvore e a função que cria a árvore binária. Veja o
Algoritmo 138.

Algoritmo 138 – Estrutura da árvore binária


01 struct noarvore {
02 struct noarvore* esq;
03 float valor;
04 struct noarvore* dir;
05 };
06 typedef struct noarvore No;
07 typedef No* ArvoreBinaria;
08 void criarArvore(ArvoreBinaria *arvore){
09 *arvore = NULL;
10 }

Fonte: Elaborado pelo autor(2013).

Cada nó da árvore é composto por uma referência para o nó da esquerda


(esq) e uma referência para o nó da direita (dir), além do valor inteiro
que será armazenado. Na linha 6, a instrução typdef define um novo tipo
de dado, chamado No, que representa o tipo estruturado noarvore. Na
linha 7, é definido um novo tipo, chamado ArvoreBinaria, que é uma
referência, um ponteiro, para um tipo No. A função criarArvore recebe,
como parâmetro, a referência (endereço na memória) da árvore binária
que será manipulada, e a inicializa com NULL.

Para a inserção dos elementos na árvore binária, serão passados, como


parâmetros, a referência da árvore que será manipulada e o valor a
ser inserido. Como regra, caso a árvore esteja vazia (vale NULL), será
inserido o valor como sendo a raiz da árvore, e as demais inserções
verificarão se o valor a ser inserido é menor que o valor do nó pesquisado

www.esab.edu.br 397
ou maior, sendo os valores menores inseridos à esquerda e os maiores à
direita do nó pesquisa. Caso a posição de inserção já esteja preenchida,
é preciso “descer” nos níveis da árvore, até localizar uma posição de
inserção disponível. O Algoritmo 139 apresenta a função de inserção na
árvore binária.

Algoritmo 139 – Inserção na árvore binária


01 void inserir(ArvoreBinaria *arvore,int
novovalor){
02 if ((*arvore) = = NULL) {
03 *arvore = (No *) malloc(sizeof(No));
04 (*arvore)->esq = NULL;
05 (*arvore)->dir = NULL;
06 (*arvore)->valor = novovalor;
07 }
08 if (novovalor < (*arvore)->valor) {
09 inserir((&(*arvore)->esq, novovalor);
10 }
11 else{
12 if (novovalor > (*arvore)->valor) {
13 inserir(&((*arvore)->dir, novovalor);
14 }
15 }
16 }

Fonte: Elaborado pelo autor (2013).

Na linha 02, a instrução if ((*arvore) == NULL) verifica se o endereço


alocado na memória para a árvore está disponível, ou seja, ainda não foi
alocado. Logo, não existe um nó nessa posição (raiz, esquerda ou direita).
O nó será alocado na memória na instrução da linha 03 e as referências
da esquerda e da direita desse nó valerão NULL, já que o nó não possui
filhos quando da sua criação. Portanto, as instruções das linhas 03 a
06 criam um nó na árvore, com as referências da esquerda e da direita
valendo NULL e com o campo valor preenchido com o novo valor,
passado como parâmetro. Porém, isso só ocorrerá quando a posição na
árvore estiver vazia ((*arvore) == NULL).

Na linha 08, é verificado se o novo valor a ser inserido é maior que


o valor do nó da árvore, e, caso seja verdadeira a condição verificada,
a função inserir é chamada, recursivamente, na linha 09, recebendo,
como parâmetro, o endereço do filho da esquerda do nó que está sendo

www.esab.edu.br 398
avaliado (&((*arvore)->esq) e também o novo valor a ser inserido. Caso
o novo valor não seja menor que o valor do nó que está sendo verificado,
porém, seja maior (linha 12), função inserir é chamada, recursivamente,
na linha 13, recebendo, como parâmetro, o endereço do filho da direita
do nó que está sendo avaliado (&((*arvore)->dir) e também o novo valor
a ser inserido. Na chamada recursiva, com o endereço da esquerda ou da
direita, junto com o valor a ser inserido, a função verificará, novamente
na linha 02, se essa nova referência está livre (NULL). Se estiver, o nó é
criado, com esquerda e direita valendo NULL e o novo valor gravado no
nó. Caso contrário, o processo recursivo se repete (descendo na árvore)
até que um nó vazio seja localizado.

Vamos desenvolver uma função para listar os nós da árvore, a qual deverá
ser recursiva, de forma que a listagem comece no elemento da esquerda,
passe pelo elemento da raiz e, por fim, pelo elemento da direita de cada nó
que não seja NULL. O Algoritmo 140 apresenta essa forma de listagem.

Algoritmo 140 – Listagem dos elementos da árvore binária


01 void exibirNos(ArvoreBinaria arvore, char
tipo){
02 if (no != NULL){
03 if (no->esq != NULL) exibirNos (no->esq);
04 printf("%d\n",no->valor);
05 if (no->dir != NULL) exibirNos (no->dir);
06 }
07 }

Fonte: Elaborado pelo autor (2013).

Note que, se o nó é diferente de NULL, a função central é chamada,


recursivamente, com a referência do nó da esquerda do nó visitado até
que ele chegue a uma folha (nó valendo NULL), que é o elemento mais
à esquerda. O valor nó é impresso e, recursivamente, a função central é
chamada com o elemento da direita do nó acessado, resultado na ordem
E, R e D (CELES; CERQUEIRA; RANGEL NETTO, 2004).

Para buscar um determinado valor na árvore binária, vamos desenvolver


uma função que recebe, como parâmetro, a árvore binária que está
sendo manipulada e o valor (chave) que será pesquisado na árvore. O
Algoritmo 141 apresenta essa função.

www.esab.edu.br 399
Algoritmo 141 – Função buscar
01 int buscar(ArvoreBinaria no, int chave){
02 if(no == NULL){
03 return 0;
04 }
05 else{
06 if (no->valor == chave){
07 return 1;
08 }else{
09 if (no->valor > chave){
10 return buscar(no->esq,chave);
11 }else{
12 return buscar(no->dir,chave);
13 }
14 }

Fonte: Elaborado pelo autor (2013).

A função verifica se o nó visitado vale NULL, e, caso essa condição seja


verdadeira, a função retorna 0 (falso). Caso contrário, é verificado se
o valor do nó visitado é igual ao valor pesquisado, e se a condição for
verdadeira, a função retorna 1 (verdadeiro), indicando que o valor foi
localizado. Se o valor não foi localizado e o valor do nó for maior que
a chave, a função buscar é chamada, recursivamente, a partir do nó à
esquerda do no atual, pois o valor pesquisado só poderá ser encontrado
na subárvore à esquerda do nó atual. Por fim, se o valor não foi
localizado e o valor do nó atual é menor que o valor pesquisado, a função
buscar é chamada, recursivamente, a partir do nó à direita do nó atual,
pois o valor pesquisado só poderá ser encontrado na subárvore à direita
do nó atual.

Por fim, vamos desenvolver uma função de menu que permita ao usuário
escolher qual função ele deseja executar na aplicação. O Algoritmo 142
apresenta essa função.

www.esab.edu.br 400
Algoritmo 142 – Função menu
01 void menu(){
02 ArvoreBinaria binaria;
03 criarArvore(&binaria);
04 int op = 0,valor;
05 do{
06 system("CLS");
07 printf("1-Inserir\n2-Listar\n3-Buscar\
n4-Sair\nEscolha uma opcao:");
08 scanf("%d",&op);
09 switch(op){
10 case 1:printf("Valor:\n");
11 scanf("%d",&valor);
12 inserir(&binaria,valor);
13 break;
14 case 2:
15 exibirNos(binaria);
16 system("pause");
17 break;
18 case 3:
19 printf("Valor:\n");
20 scanf("%d",&valor);
21 int achou =
buscar(binaria,valor);
22 if(achou){
23 printf("\nValor Localizado na
Arvore\n");
24 }else{
25 printf("\nValor Nao Existe na
Arvore\n");
26 }
27 system("pause");
28 break;
29 }
30 }while(op != 4);
31 }

Fonte: Elaborado pelo autor (2013).

O menu permite que o usuário insira os elementos já ordenando-os. Liste-os


em ordem crescente e permita pesquisar qualquer valor na árvore binária.

Você pode baixar todo o código do exemplo desenvolvido nesta unidade, com
exemplos para a chamada da função listar de forma crescente e as rotinas de inserção
e busca clicando aqui.

www.esab.edu.br 401
Nesta unidade, você pôde desenvolver um algoritmo que permite ao
usuário cadastrar, listar e consultar valores inteiros armazenados em uma
árvore binária balanceada. No primeiro código, você implementou o tipo
estruturado, que representará cada nó da árvore, e a função que cria a
árvore binária. Para a inserção dos elementos na árvore binária, foi criada
a função inserir, que recebe, como parâmetro, a referência da árvore que
será manipulada e o valor a ser inserido. Como regra, caso a árvore esteja
vazia (vale NULL), será inserido o valor como sendo a raiz da árvore.
As demais inserções verificarão se o valor a ser inserido é menor que o
valor do nó pesquisado ou maior, sendo os valores menores inseridos
à esquerda e, os maiores, à direita do nó pesquisa. Caso a posição de
inserção já esteja preenchida, é preciso “descer” nos níveis da árvore, até
localizar uma posição de inserção disponível.

Para listar os elementos da árvore binária, foi criada a função exibirNos,


que lista os nós da árvore e que deverá ser recursiva, de forma que a
listagem comece no elemento da esquerda, depois o elemento da raiz
e, por fim, o elemento da direita de cada nó que não seja NULL. Para
buscar um determinado valor na árvore binária, foi desenvolvia a função
que recebe, como parâmetro, a árvore binária que está sendo manipulada
e o valor (chave) buscar, que verifica se o nó visitado vale NULL. Caso
essa condição seja verdadeira, a função retorna 0 (falso); caso contrário,
é verificado se o valor do nó visitado é igual ao valor pesquisado. Se a
condição for verdadeira, a função retorna 1 (verdadeiro), indicando que
o valor foi localizado. Se o valor não foi localizado e o valor do nó é
maior que a chave, a função buscar é chamada, recursivamente, a partir
do nó à esquerda do no atual, pois o valor pesquisado só poderá ser
encontrado na subárvore à esquerda do nó atual. Por fim, se o valor não
foi localizado e o valor do nó atual é menor que o valor pesquisado, a
função buscar é chamada, recursivamente, a partir do nó à direita do no
atual, pois o valor pesquisado só poderá ser encontrado na subárvore à
direita do nó atual. Para o teste da aplicação, foi desenvolvida a função
menu, que permite que o usuário insira os elementos, ordenando-os,
listando-os em ordem crescente e possibilitando pesquisar qualquer valor
na árvore binária.

www.esab.edu.br 402
Com esse assunto, finalizamos a nossa disciplina, e desejo que o
conhecimento adquirido com o estudo das estruturas de dados,
principalmente no que se refere ao gerenciamento de memória,
implementação de métodos de ordenação e pesquisa, bem como a
construção de algoritmos mais eficientes e seguros, possa representar
um diferencial profissional para você, caro aluno, em um mercado de
Tecnologia da Informação cada vez mais competitivo e desafiador. Meus
parabéns por mais essa vitória!

Atividade
Chegou a hora de você testar seus conhecimentos
em relação às unidades 37 a 48. Para isso, dirija-se ao
Ambiente Virtual de Aprendizagem (AVA) e responda
às questões. Além de revisar o conteúdo, você estará
se preparando para a prova. Bom trabalho!

www.esab.edu.br 403
Resumo

Na unidade 43, você estudou que o merge sort se fundamenta em três


processos: divisão, conquista e combinação, e que, por isso, o algoritmo
é conhecido como do tipo “dividir para conquistar”. Em relação ao
funcionamento do algoritmo, vimos que cada parte segmentada é
dividida em duas novas partes, as quais, sucessivamente, são divididas em
outras duas novas partes, até que cada nova parte tenha dois elementos.
Cada parte com dois elementos é ordenada – primeiro a parte da
esquerda (primeira metade), depois a parte da direita (segunda metade).
Após a ordenação, que resulta em vários segmentos com um único
elemento na sua posição correta, todas as partes que foram previamente
ordenadas são combinadas, resultando no vetor ordenado.

Na unidade 44, você pôde desenvolver o algoritmo de merge sort a


partir de duas funções, a saber, combinação e segmentação. A função de
combinação tem como finalidade realizar a cópia dos valores do vetor, que
será ordenado para um vetor auxiliar, de forma que cada elemento copiado
para o vetor auxiliar seja o elemento ordenado. O elemento ordenado
é buscado de forma que sejam comparados os elementos do primeiro
segmento com os elementos do segundo segmento, e que o menor valor
entre esses dois elementos sejam inseridos no vetor auxiliar. Após a
inserção dos elementos ordenados no vetor auxiliar, estes serão copiados,
novamente, para o vetor principal, de forma que ele esteja ordenado.

Na unidade 45, você pôde estudar a busca sequencial, a forma mais


simples de busca em um vetor, que consiste em percorrer o vetor, elemento
a elemento, para verificar se o elemento de interesse existe no vetor, ou seja,
se algum dos elementos do vetor é igual ao elemento desejado.

www.esab.edu.br 404
Na unidade 46, você pôde revisar os conceitos básicos da estrutura de
dados árvore binária e viu que a árvore binária pode ser classificada
como balanceada ou não balanceada. Uma árvore binária balanceada
se caracteriza por ter, para cada nó, os filhos com valores menores à
esquerda do nó pai e os filhos com valores maiores à direita do nó pai.
Para buscar um elemento na árvore binária não balanceada, deve ser
adotada uma das três possíveis ordens de percurso na árvore prefixado,
central ou pós-fixado. O que muda de uma ordem de percurso para
a outra é a sequência dos elementos visitados. O processo de busca
na árvore binária não balanceada é muito semelhante ao de busca
sequencial, pois todos os elementos são visitados, um a um, até que
se localize o elemento desejado. Dessa forma, quanto mais elementos
tivermos na árvore, mais demorada tende a ser a pesquisa.

Na unidade 47, você revisou que o balanceamento da árvore é uma


importante propriedade da árvore binária, na qual os valores menores
que o seu pai ficam à sua esquerda, enquanto que os valores maiores que
seu pai ficam à sua direita.

Na unidade 48, você pôde desenvolver um algoritmo que permite ao


usuário cadastrar, listar e consultar valores inteiros armazenados em uma
árvore binária balanceada.

www.esab.edu.br 405
Glossário

Arquivo de log
É um arquivo temporário para registro de informações, na forma de
um processo passivo, sem a interferência do usuário de computador, e
que armazena eventos e ações executadas no computador, de forma a
monitorar as operações executadas. R

Arquivo-fonte
Arquivos-texto que contêm o código-fonte dos programas de
computadores, o qual é constituido pelas linhas de programação em
linguagem de programação específica. Algumas linguagens de programação
necessitam converter as linhas de programação em linguagem de máquina,
isto é, aquele que o computador entende, como: C, Pascal, Java. R

Árvore genealógica
É uma representação das pessoas que tiveram participação na existência
de uma pessoa, é o histórico de dados sobre seus ancestrais de forma que
fiquem conhecidas as conexões estabelecidas entre estes. R

Atribuição
Define um valor para a variável, que pode ser definida pelo programador
ou por meio de um comando de entrada de dados, todas as linguagens
de programação possuem um comando de atribuição, que no caso da
linguagem C é representada pelo sinal de igual (variável = valor;). R

www.esab.edu.br 406
Bibliotecas
Conjunto de funções pré-codificadas por outros programadores que
resolvem determinados problemas para que o usuário não precise
implementá-las, nem saber como foram implementadas. São exemplos
de bibliotecas: OpenGL (funções para desenhar), OpenCV (funções para
processamento de imagem). R

Bubble
O termo bubble, ou bolha, foi escolhido para representar esse algoritmo
devido ao fato das bolhas de ar, em um tanque com água, ao subirem
para a superfície, procurarem seu lugar se encaixando uma ao lado da
outra, de forma organizada. R

Cabeçalho
Nas linguagens de programação representa a declaração de uma função
ou método, omitindo a codificação, apenas especificando o nome da
função, seu tipo de retorno e os parâmetros com seus nomes e os tipos de
dados, representando, assim, a especificação da interface da função. R

Combinação
Consiste em gerar um novo conjunto de valores com elementos
combinados de dois ou mais conjuntos diferentes. R

Constantes
Consiste em uma técnica de armazenamento de dados em linguagens
de programação em que a informação não pode ser alterada durante a
execução do programa. R

Contêiner
Um contentor ou contêiner é um recipiente de metal ou madeira,
geralmente de grandes dimensões, destinado ao acondicionamento e
transporte de carga em navios, trens etc. R

www.esab.edu.br 407
Descritor
Variável que armazena endereços de elementos da estrutura de dados,
como o endereço do primeiro e do último elemento da lista. Os
descritores podem armazenar o endereço de qualquer informação da lista,
como o endereço do maior e do menor valor inserido na lista. R

Desempenho computacional
É a avaliação de um algoritmo com relação ao seu custo computacional,
que é o tempo necessário para a sua execução e o consumo de memória
principal que será utilizado durante a execução do algoritmo. Quanto
mais rápido o algoritmo e menos espaço de memória alocado, melhor
será seu desempenho computacional. R

Desfazer
Em computação, a função desfazer é uma operação muito comum em
aplicativos de editores de textos e reverte a última edição ou alteração
realizada no texto. R

Disco rígido
É um periférico do computador cuja utilidade é o armazenamento
de dados. Basicamente todas as informações do computador são
armazenadas no disco rígido, também conhecido como HD (a abreviação
de Hard Disk, disco rígido, em inglês). R

Do-while
É um comando de laço, uma instrução em linguagem de programação,
conhecido como “repita até”. Nessa instrução os processos são executados
pelo menos uma vez, já que primeiramente os processos são executados
para depois ser avaliada a condição para que o laço se repita ou se encerre.
R

Estruturas hierárquicas
Estrutura hierárquica é um sistema com organização de cima para baixo e
normalmente representado em forma de diagrama. R

www.esab.edu.br 408
Fila de arquivos
Também conhecida como fila de impressão ou spool, é um espaço na
memória principal do computador, gerenciado pelo sistema operacional,
onde ficam armazenados em fila os arquivos enviados para impressão. R

Gerenciar
No contexto desta disciplina, o sistema operacional realiza/gerencia
os processos que compartilham os recursos do computador para que
problemas não aconteçam, como falta de memória ou demora no
processamento de dados. R

Heap
Corresponde a uma área na memória principal do computador destinada
à alocação e liberação de espaços para as variáveis locais e globais que
são criadas e manipuladas nos programas de computadores. As variáveis
alocadas dinamicamente ou estaticamente também são armazenadas
nesse espaço de memória. R

Índices
Representam as posições de acesso em uma tabela de banco de dados
ou em um vetor unidimensional (usa apenas um índice) ou vetores
bidimensionais (usam dois índices). R

Inicialização
Define o valor inicial das variáveis declaradas nos programas de
computadores, a maioria das linguagens de programação já define um
valor padrão para as variáveis, de forma que o programador não precise
inicializá-las. Por exemplo, variáveis numéricas em C são inicializadas
com 0 automaticamente. R

Interação
Consiste em criar mecanismos ou interfaces que permitam ao usuário
manipular o sistema computacional, sendo o ponto de contato entre o
usuário e o aplicativo. R

www.esab.edu.br 409
Interface
São arquivos de código-fonte que apresentam as funções, especificando
apenas os cabeçalhos, mas não implementam o código dessas funções.
As regras de negócio serão implementadas nos arquivos que estenderem
as interfaces, os quais, obrigatoriamente, devem usar a mesma estrutura
definida no cabeçalho das funções. R

Inviabilizar a operação
A resposta da pesquisa em uma estrutura de dados, indicando se o valor
pesquisado existe ou não na estrutura de dados, deve ser dada em tempo
hábil, que permita a utilização da informação por parte do usuário do
sistema. É fundamental que durante os processos de pesquisa o usuário
seja informado visualmente de que a busca está em execução. Caso
contrário, o usuário poderá cancelá-la indevidamente acreditando que a
operação estava inativa. R

Jargão
É um tipo de comunicação específica limitada a um determinado grupo
de especialistas ou profissionais, e que tem por objetivo padronizar
a troca de informações. Por exemplo, o jargão jurídico utilizado por
advogados e juízes. R

Main
É a principal função de um programa em C, sendo a primeira função a
ser executada em um programa escrito em linguagem C que pode ou não
chamar outras funções. R

Método
São procedimentos ou funções, também conhecidas como sub-rotinas
– que com a utilização do paradigma orientado a objetos passaram a ser
chamadas de métodos –, pois são executadas por um objeto e determinam
o seu comportamento. Na programação estruturada é chamada de função
ou sub-rotina, na programação orientada a objetos de método. R

www.esab.edu.br 410
Mnemônicos
Termos associados à memória; aquilo que é de memorização fácil devido
à associação da palavra com o objeto ou pessoa. R

Não linear
Os dados que não seguem uma sequência física, em que os elementos
estão um ao lado do outro, sequencialmente. R

Navegador
O termo navegador é utilizado desde o início da internet, já que os
usuários visitavam os sites na forma de uma navegação pelos conteúdos.
Na prática, o termo representa o programa instalado no computador que
possibilita o acesso aos sites. Também é muito conhecido como browser.
R

Offset
É o termo utilizado para representar o deslocamento de um ou vários
valores em uma estrutura de dados. Um offset pode ocorrer para a
direita, quando os valores são deslocados uma posição à frente, ou para
a esquerda, quando os valores são deslocados para uma posição anterior.
R

Overflow
É um erro de execução do programa quando é requisitada mais memória
do que há disponível pelo sistema operacional. Esse erro é considerado
fatal já que encerra inesperadamente a execução do programa. R

Particionado
Consiste em dividir o vetor em, pelo menos, duas partes, ou dois
segmentos, mantendo as caraterísticas do vetor original, como tipo de
dados e posições (índices). R

www.esab.edu.br 411
Percurso
É um caminho constituído por uma partida, uma série de locais visitados
e identificados, unidos por linhas. R

Pilha
A pilha (stack ou heap) é um tipo de vetor dinâmico. Suas operações
são limitadas em inserção e remoção, que são gerenciadas pela mesma
extremidade (topo). Essa estrutura é conhecida como LIFO (Last In -
First Out), pois os itens são inseridos e retirados inversamente. Ou seja, o
primeiro item que entra é o último a sair. R

Processos cíclicos
São processos contínuos que se repetem em uma ordem predefinida,
retornando para o estágio inicial. R

Referência
O termo é utilizado para indicar o endereço da variável na memória
principal. R

Segmentação
É um termo genérico para designar a divisão de algo em partes separadas
ou segmentos. R

Segmentos
São partes da estrutura de dados, formadas por um conjunto de
elementos da estrutura de dados, com posição inicial e final. Um
segmento possui o mesmo tipo de dados e características da estrutura de
dados original. R

Sinalizado
Consiste em enviar algum tipo de aviso ao usuário de que alguma ação
foi executada no sistema. R

www.esab.edu.br 412
Sistema de segurança
Conjunto de mecanismos de monitoramento remoto com uso de
câmeras que possibilitam a gravação e visualização de vídeos em tempo
real e armazenamento em equipamentos para visualização posterior. R

Sistemas Gerenciadores de Banco de Dados (SGDB)


Programa de computador responsável por gerenciar (criar e manipular)
uma base de dados. São exemplos de SGDB, o MYSQL, Oracle,
Firebird, SQL Server etc. R

Subprogramas
São trechos de programas que possuem uma funcionalidade específica
dentro do programa principal. São chamados pelo nome a partir do
programa principal. Quando retornam valor, são chamados de função.
Caso contrário, são conhecidos como procedimento. R

Regras de negócio
Representam as regras de funcionamento do sistema, o que ele deve fazer
seguindo o modo como seus processos são executados no mundo real.
Essas regras de negócio devem ser documentadas na forma de requisitos e
futuramente implementadas no sistema. R

Tipo Básico
As linguagens de programação possuem os tipos de dados predefinidos,
também conhecidos como tipos básicos ou tipos primitivos,
representados pelos inteiros, reais, lógicos e caracteres. R

Tipos de dados
Os tipos de dados são utilizados pelas linguagens de programação para
definição do domínio dos dados (valores que podem ser armazenados) e
o espaço de memória que será alocado. R

www.esab.edu.br 413
Tipos primitivos
São os tipos de dados básicos das linguagens de programação,
normalmente representados pelos tipos: byte, int, float e char. R

Unidimensionais
Termo utilizado para representação de vetores que possuem uma linha
por várias colunas. R

URL
Uniform Resource Locator (Localizador Padrão de Recursos)
é o endereço virtual de um recurso disponível em uma rede de
computadores, como a internet. A URL pode indicar onde está o
arquivo, computador, site ou qualquer outro recurso que o usuário de
computador desejar acessar. R

www.esab.edu.br 414
Referências

ASCENCIO, A. F. G.; ARAÚJO, G. S. Estruturas de dados: algoritmos, análise


da complexidade e implementações em JAVA e C++. São Paulo: Pearson Prentice
Hall, 2010.

CELES, W.; CERQUEIRA, R. F. G.; RANGEL NETTO, J. L. M. Introdução


a estruturas de dados: uma introdução, com técnicas de programação em C. Rio
de Janeiro: Campus, 2004.

CERQUEIRA, R. F. G.; CELES, W.; RANGEL NETTO, J. L. M. Introdução a


estruturas de dados: uma introdução, com técnicas de programação em C. Rio
de Janeiro: Campus, 2004.

FORBELLONE, A. L. V. et al. Lógica de programação: a construção de


algoritmos e estrutura de dados. 3. ed. São Paulo: Makron Books, 2005.

GUIMARÃES, Â. de M. Algoritmos e estruturas de dados. Rio de Janeiro:


LTC, 1994.

PUGA, S. Lógica de programação e estrutura de dados, com aplicações em


java. 2. ed. São Paulo: Pearson Prentice Hall, 2009.

www.esab.edu.br 415

Você também pode gostar