Вы находитесь на странице: 1из 8

______________________________________________________________________________________

APOSTILA DE PROGRAMAÇÃO BÁSICA EM C++


______________________________________________________________________________________

3. ESTRUTURAS DE DADOS COM ALOCAÇÃO DINÂMICA


3.1. Estruturas de Dados Estáticas e Dinâmicas
O termo estrutura de dados é comumente empregado para identificar modelos de armazenamento de dados
na memória. Os vetores, por exemplo, definem uma estrutura onde os elementos possuem um mesmo tipo
básico e estão armazenados de forma indexada. No caso das estruturas heterogêneas (structs em C++), ou
registros, os elementos, chamados de campos, possuem um identificador e podem ser de tipos distintos.

Vetores e registros são estruturas de dados estáticas. Isto porque o número de elementos em cada uma
destas estruturas é determinado no momento de sua alocação na memória; normalmente quando as
variáveis são declaradas.

Em muitos casos, entretanto, o número de elementos em uma estrutura não é conhecido inicialmente; ou
seja, no início de um algoritmo não é possível determinar quantos elementos serão necessários na
resolução de um determinado problema. Nestes casos, é necessário utilizar o conceito de estruturas de
dados dinâmicas, onde a quantidade de elementos pode ser frequentemente alterada, para mais ou para
menos.

3.1.1. Aplicações das Estruturas de Dados Dinâmicas


Alguns problemas que requerem o uso de estruturas de dados dinâmicas são apresentados a seguir:

Exemplo 1: Considere a realização de um concurso onde deve-se registrar os candidatos, determinar a


quantidade de salas necessárias à realização das provas e determinar a relação de candidatos por sala. Se
o número de candidatos não for conhecido a priori, é impossível determinar a quantidade de memória
necessária para armazenar os seus dados; bem como, a quantidade de salas suficientes à realização do
concurso e, consequentemente, o número de listas de candidatos por sala.

Exemplo 2: Para localizar um arquivo no disco, por exemplo, é necessário explorar cada uma das pastas de
armazenamento. Se a busca iniciar na pasta raiz, é preciso identificar que pastas estão contidas nela. Mas,
dentro de cada uma destas pastas existem possivelmente outras, e dentro destas outras, mais outras, até
que um limite é atingido. Nestes casos, também é impossível determinar inicialmente o número de pastas a
pesquisar, assim como o número de ocorrências do arquivo procurado.

Exemplo 3: Considere, finalmente, o seguinte problema: viajar de Natal para Tóquio. Qual a melhor solução
para o problema considerando que o passageiro quer realizar a viagem no tempo mais curto, ou gastando o
menos possível, ou efetuando o menor número de decolagens? Aqui, é necessário mapear todos os
caminhos possíveis entre as cidades e quantificar todas as variáveis pertinentes ao problema: tempo, custo
e decolagens. Certamente, a quantidade de percursos possíveis é desconhecida no início do problema.

Portanto, em problemas mais complexos, é frequentemente necessário utilizar alguma estrutura de dados
que possa aumentar ou diminuir de tamanho de acordo com a solução requisitada para o problema.

3.1.2. Tipos Mais Comuns de Estruturas de Dados Dinâmicas


Dentre as estruturas dinâmicas mais comuns usadas, estão as Listas, as Árvores e os Grafos.

As listas são coleções de elementos, em geral de um mesmo tipo, semelhantes aos vetores. A diferença
está no fato delas não terem um tamanho fixo e não serem indexadas. Comumente, uma lista inicia vazia e
os elementos vão sendo adicionados e removidos quando necessário. O acesso aos elementos é realizado
através de apontadores que são variáveis do tipo ponteiro. Em geral, o primeiro elemento da lista possui o
endereço do segundo elemento; o segundo do terceiro e assim por diante. O exemplo 1, apresentado
anteriormente, pode ser resolvido com o auxílio de duas listas: uma para representar a relação de
candidatos e outra para a relação de salas.

1
As árvores são coleções de elementos atraledos a uma estrutura hierárquica onde um elemento possui um
determinado número de subordinados. Em uma árvore, os elementos são comumente chamados de nós. O
nó principal é denominado de nó raiz e pode possuir uma quantidade qualquer de subordinados: os nós-
filhos. Cada nó-filho pode possuir também um determinado número de subordinados, mas possui apenas
um nó-pai. A estrutura de pastas ou diretórios em um disco é um exemplo clássico de árvore e certamente
pode ser utilizada para solucionar o exemplo 2, citado acima.
As árvores são na verdade um tipo particular de grafos, onde um nó possui muitos filhos mas um único pai.
Em um grafo, um nó pode ter vários pais e vários filhos, gerando uma estrutura um pouco mais complexa.
Em um grafo é possível partir de um nó-pai, passar por vários filhos e, de repente, voltar ao nó pai.

A figura abaixo mostra em um digrama os três tipos de estruturas. Na lista, o primeiro elemento possui um
apontador para o segundo e o segundo para o terceiro. Na árvore, o nó pai (1) possui apontadores para os
nós filhos (2) e (3). Finalmente, no grafo, cada nó possui apontadores para os nós interligados a ele.

Os grafos são usados na solução de muitos problemas práticos, como por exemplo, nas áreas de Redes de
Computadores, Telecomunicações, Engenharia Elétrica, Tráfego Aéreo e Computação Gráfica. O exemplo
da viagem Natal-Tóquio, visto anteriormente, é um tipo de problema onde os grafos são comumente
empregados. Certamente, uma das aplicações mais comuns para os grafos é o circuito elétrico; onde os nós
representam os pontos do circuito com mesma tensão e os elementos físicos, as ligações entre eles.

1 1 1

3 2 3 2 3

A) Lista B) Árvore C) Grafo

Figura 3.1. Diagramas de Estruturas de Dados

3.2. Listas Simplesmente Encadeadas


Como dito anteriormente, as listas lineares, ou simplesmente listas, são estruturas que agrupam uma
quantidade de elementos indefinida inicialmente. Cada elemento da lista está ligado a outro elemento
através de um apontador que, certamente, é uma variável do tipo ponteiro. Isto significa que cada elemento
da lista armazena o local de memória onde se encontra outro elemento.

Na lista simplesmente encadeada, cada elemento possui um apontador para o elemento seguinte. Ou seja,
cada elemento tem um ponteiro que armazena o endereço de memória do próximo elemento. Desta forma,
para que todos os elementos da lista sejam acessíveis, é necessário possuir apenas o endereço do primeiro
elemento.

3.2.1. Declaração de uma Lista


Os elementos de uma lista são sempre variáveis do tipo registro (structs), de forma que cada um deles
armazena seus dados e o endereço do próximo elemento. Portanto, para listas simplesmente encadeadas, é
sempre necessário definir uma estrutura semelhante a apresenta abaixo:

struct Tipo_Elemento
{
// Declaração dos Campos do Elemento
Tipo_Elemento *Prox;
};

Tipo_Elemento *Inicio = NULL;

2
Onde Tipo_Elemento é o tipo de cada elemento da lista; Prox é um ponteiro que cada elemento possui
para armazenar o endereço de memória do elemento seguinte e Inicio é um ponteiro para o primeiro
elemento. Como a lista inicia vazia, o ponteiro Inicio deve receber o valor NULL, indicando que nenhum
endereço válido é armazenado. Finalmente, dentro do registro devem ser listados os campos que cada
elemento da estrutura deve armazenar.

As declarações abaixo, por exemplo, são o primeiro passo para criar uma lista com os dados dos veículos
de uma concessionária. Cada elemento da lista armazena o modelo, o ano e o preço de um veículo e o local
de memória onde estão os dados do veículo seguinte.

struct Veiculo
{
AnsiString Modelo;
int Ano;
double Preco;
Veiculo *Prox;
};

Veiculo *Inicio = NULL;

Neste instante, a memória do computador possui a seguinte configuração:

Inicio NULL

3.2.2. Inserção do Primeiro Elemento na Lista


Para inserir o primeiro elemento na lista, é necessário alocar memória para o elemento, através do operador
new, e colocar o ponteiro início apontando para este elemento. Esta operação é apresentada abaixo:

Inicio = new Veiculo;

Neste momento, um elemento é inserido na lista e a variável Inicio armazena o endereço do primeiro
elemento.

Inicio Modelo
Ano
Preco
Prox

Os dados do primeiro elemento devem ser então iniciados. Os comandos abaixo podem ser utilizados para
realizar esta tarefa.

Inicio->Modelo = "Gol Power";


Inicio->Ano = 2006;
Inicio->Preco = 38000;
Inicio->Prox = NULL;

Observe a notação usada para acessar os campos do elemento. Como a variável Inicio é um ponteiro de
struct, o operador -> é utilizado para acessar os campos do endereço de memória apontado por Inicio.
Neste caso, não é necessário utilizar o operador indireto *.

Observe também a iniciação do campo Prox com NULL, uma vez que não existe ainda um segundo
elemento na lista. De fato, o campo Prox do último elemento deve sempre NULL.

Agora, a memória do computador possui a seguinte configuração:

3
Inicio Modelo Gol Power
Ano 2006
Preco 38000
Prox NULL

3.2.3. Inserção de Elementos no Fim da Lista


Para inserir elementos no final da lista – a partir do segundo, por exemplo – é necessário um ponteiro
auxiliar para acessar o último elemento existente até o momento. Para isso, é necessário realizar uma busca
a partir do primeiro elemento até o último. Para o exemplo atual, esta operação pode ser realizada pelos
comandos abaixo:

Veiculo *Aux = Inicio;


while (Aux->Prox != NULL) Aux = Aux.Prox;

Com isto, a memória do computador apresenta a seguinte configuração:

Inicio Modelo Gol Power


Ano 2006
Aux
Preco 38000
Prox NULL

A inserção do novo veículo pode ser realizada com o comando abaixo:

Aux->Prox = new Veiculo;

De forma que a memória do computador é alterada para a seguinte situação:

Inicio Modelo Gol Power Modelo


Ano 2006 Ano
Aux
Preco 38000 Preco
Prox Prox

Observe que o campo Prox de Aux, que apontava para o último elemento, é quem possui acesso ao novo
elemento. Para facilitar o acesso ao novo último elemento, podemos alterar o valor de Aux, de acordo com o
comando abaixo, facilitando a definição dos seus dados.

Aux = Aux->Prox;

Aux

Inicio Modelo Gol Power Modelo


Ano 2006 Ano
Preco 38000 Preco
Prox Prox

Como Aux aponta agora para o último elemento da lista, podemos alterar seus campos através dele como
mostra os comandos a seguir:

4
Aux->Modelo = "Corsa";
Aux->Ano = 2005;
Aux->Preco = 25000;
Aux->Prox = NULL;

A nova situação da memória é apresentada em seguida.

Aux

Inicio Modelo Gol Power Modelo Corsa


Ano 2006 Ano 2005
Preco 38000 Preco 25000
Prox Prox NULL

3.2.4. Acesso aos Elementos da Lista


O acesso aos elementos da lista é sempre realizado a partir do início. Para listar todos os elementos, por
exemplo, pode-se utilizar os comandos abaixo:

Veiculo *Aux = Inicio;


while (Aux != NULL)
{
cout << Aux->Modelo << " " << Aux->Ano << " " << Aux->Preço << endl;
Aux = Aux->Prox;
}

Observe que é sempre conveniente utilizar um ponteiro auxiliar. Isto acontece porque o ponteiro Inicio não
deve ter ser valor alterado visto que é a única variável que armazena o local de memória onde a lista inicia.
É preciso ser prudente ao alterar o valor armazenado em Inicio para evitar que os elementos da lista fiquem
perdidos na memória.

3.2.5. Destruição da Lista


Uma vez que a lista é totalmente formada com os recursos de alocação dinâmica, é necessário destruir os
seus elementos quando não mais necessários ou no término do programa. Os comandos abaixo podem
realizar esta tarefa.

Veiculo *Aux = Inicio;


while (Aux != NULL)
{
Inicio = Inicio->Prox;
delete Aux;
Aux = Inicio;
}

Neste caso, a lista é destruída do início para o fim. Por isso, o ponteiro Inicio vai sendo alterado sempre
para apontar para o elemento seguinte da lista o qual está prestes a ser destruído.

3.3. Exemplo
O programa apresentado a seguir realiza o cadastro de veículos de uma concessionária (na memória)
utilizando uma lista simplesmente encadeada.

5
Programa 11: Lista Simplesmente Encadeada.

A tela principal do programa é apresentada abaixo. São realizadas as operações de cadastro, listagem total
e parcial. Na parcial, devem ser listados os veículos até um determinado preço.

A declaração da lista é a mesma apresentada anteriormente e pode ser feita no cabeçalho do programa.

struct Veiculo
{
AnsiString Modelo;
int Ano;
double Preco;
Veiculo *Prox;
};

Veiculo *Inicio = NULL;

O código para o botão Cadastrar é apresentado abaixo. Verifique que é necessário testar se é ou não o
primeiro veículo da lista.

void __fastcall TForm1::Button1Click(TObject *Sender)


{
Veiculo *Novo;
Novo = new Veiculo;
Novo->Modelo = Edit1->Text;
Novo->Ano = Edit2->Text.ToInt();
Novo->Preco = Edit3->Text.ToDouble();
Novo->Prox = NULL;

if (Inicio == NULL) Inicio = Novo;


else
{
Veiculo *Aux = Inicio;
while (Aux->Prox != NULL) Aux = Aux->Prox;
Aux->Prox = Novo;
}
}

6
Para listar os veículos, é necessário percorrer a lista desde o início. O código para o botão Listar Todos é
apresentado a seguir:

void __fastcall TForm1::Button2Click(TObject *Sender)


{
Memo1->Clear();

Veiculo *Aux = Inicio;


while (Aux != NULL)
{
sprintf(s, "%s Ano: %d Preço: %.2f", Aux->Modelo, Aux->Ano, Aux->Preco);
Memo1->Lines->Add(s);
Aux = Aux->Prox;
}
}

Para listar até um determinado valor, é necessário também percorrer toda a lista. Neste caso, temos que
verificar se cada elemento satisfaz ao critério de busca. O código abaixo mostra a programação do botão
Listar Até.

void __fastcall TForm1::Button3Click(TObject *Sender)


{
char s[100];

Memo1->Clear();
double Preco = Edit4->Text.ToDouble();

Veiculo *Aux = Inicio;


while (Aux != NULL)
{
if (Aux->Preco <= Preco)
{
sprintf(s, "%s Ano: %d Preço: %.2f", Aux->Modelo, Aux->Ano, Aux->Preco);
Memo1->Lines->Add(s);
}
Aux = Aux->Prox;
}
}

Finalmente, é necessário destruir a lista no final do programa. Neste caso, podemos utilizar o evento
OnClose do formulário é realizar esta tarefa, conforme visto abaixo:

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)


{
Veiculo *Aux = Inicio;
while (Aux != NULL)
{
Inicio = Inicio->Prox;
delete Aux;
Aux = Inicio;
}
}

7
3.4. Exercícios

1. Definir uma lista cujos elementos representem os componentes de um computador, armazenando o nome
do componente e o preço. Implementar uma interface gráfica que permita cadastrar os componentes de um
micro, listar os componentes cadastrados e totalizar o custo total do equipamento.

2. Definir uma lista cujos elementos representam os dados das disciplinas do seu curso, armazenando
semestre, nome da disciplina, média final e situação (aprovado ou reprovado). Implementar uma interface
gráfica que permita cadastrar um vetor com até 50 disciplinas, listar todas as disciplinas cadastradas, obter
as disciplinas com a maior e a menor média e calcular a média das médias de todas as disciplinas em que
tenha sido aprovado.

3. Modificar o programa 11 (Lista de Veículos) de forma que os veículos sejam incluídos em ordem
alfabética de nome. Permitir também que um veículo possa ser excluído da lista.

Вам также может понравиться