Lista Encadeada Dinâmica

Conceitos de Ponteiro e implementação de Ponteiros em Pascal

As linguagens de programação modernas tornaram possível explicitar não apenas o acesso aos dados, mas também aos endereços desses dados. Isso significa que ficou possível a utilização de ponteiros explicitamente. Assim, existe uma distinção de notação entre um dado e a sua referência (ou endereço). Um ponteiro armazena o endereço de memória onde um dado 9de um determinado tipo) está armazenado. A notação introduzida por Wirth, com a introdução de Pascal, é:
Type Dado = Seu Tipo de Dado; TPPonteiro = ^Dado;

Esta declaração expressa que valores do tipo TPPonteiro são ponteiros para dados do tipo Dado. Ele armazena um endereço de memória. Esse endereço deve ser a posição de início do armazenamento de um dado do tipo Dado. Portanto, lemos o simbolo ^ como sendo ponteiro para... , e na declaração acima lemos 'TPPonteiro é um ponteiro para variáveis do tipo Dado'. O fato de que o tipo dos elementos apontados ser evidente na declaração do ponteiro tem importância fundamental. Isso distingue ponteiros de linguagens de alto nível de endereços em Assembly. Valores para ponteiros são gerados quando dados correspondentes a seus tipos são alocados/desalocados dinamicamente. Em Pascal, os procedimentos dispose existem com esse propósito. Portanto, deixa-se a cargo do programa (via linguagem de programação), e não do programador, prover e devolver espaço para inserções e eliminações em tempo de execução (Observe mais abaixo os procedimentos de manutenção da Lista Dinâmica). Custo: Tempo de execução comprometido. Idéia: O programador declara apenas o tipo de registro que contém um elemento da lista, e avisa que a alocação será dinâmica. Sempre que requisitar um registro novo para a inserção, ou liberar um registro a ser eliminado, o programador lança mão de duas rotinas pré-definidas para este fim. Registros com ponteiros Dizemos que uma variável do tipo TpPonteiro aponta para, ou é um ponteiro para um registro do tipo TpDado . Ponteiro é o único tipo que pré-referencia outro em Pascal.
Type TPDado = Record Codigo : Integer; Nome : String[30]; Salario : Real; End; TPPonteiro = ^TPDado;

Var P

: TPPonteiro;

Neste caso, a variável P é um ponteiro para um registro do tipo TPDado. P armazena o endereço de memória onde o registro foi alocado. Se mandarmo apresentar o valor de P, ocorrerá um erro do tipo: "Error 64: Cannot read or write variables of this type.". Isto significa que não é possivel apresentar o endereço do ponteiro. O Pascal não permite que se leia nem escreva variáveis do tipo ponteiro. Quem fornece o valor do ponteiro é a própria linguagem de programação. Em Pascal, para criarmos uma um novo ponteiro, utilizamos o procedimento New ();. Veja o Exemplo:
Type TPDado = Record Codigo : Integer; Nome : String[30]; Salario : Real; End; TPPonteiro = ^TPDado; Var P : TPPonteiro; Begin New(P); {cria um novo registro do tipo TPDado que será "apontado" por P} End.

No exemplo acima, a instrução New(P), cria (aloca espaço) um novo registro apontado pelo ponteiro P. Como acessar os dados armazenados em uma variável do tipo ponteiro? Entendemos então que P é um ponteiro que não permite que seja apresentado nem lido o seu valor. Quem fornece o valor do ponteiro é a própria linguagem de programação. Em Pascal, para criarmos uma um novo ponteiro, utilizamos o procedimento New();. Assim, para acessarmos os dados armazenados, precisamos designar o ponteiro, o objeto e os campos. Notação Objeto (Registro) Campos P^.Codigo P^ P^.Nome P^.Salario

Ponteiro P

Endereço nulo (terra) Pascal provê uma constante pré-definida para denotar o endereço nulo nil. Podemos utilizá-la para atribuições e testes, como nos exemplos abaixo:
1: P := nil; 2: If (P = nil) Then ...

Ponteiro x Objeto Apontado Nos exemplos abaixo, é ilustrada a diferença entre as operações de atribuição entre ponteiros (por exemplo, p : = q ) e a atribuição entre o conteúdo dos registros apontados pelos ponteiros (isto é: p^ : = q^).

Type TPDado = Record Codigo : Integer; Nome : String[30]; Salario : Real; End; TPPonteiro = ^TPDado; Var P, Q : TPPonteiro;

Dada a situação abaixo, chamada de (a):

Dada a situação (a), após a atribuição p : = q temos a representação abaixo (b). Esta atribuição só é válida para ponteiros do mesmo tipo e é a única operação entre ponteiros. Isso significa que o ponteiro P recebe o ponteiro Q. Como os ponteiros armazenam endereços de memória, entende-se que a partir desta instrução, o ponteiro P passou a apontar para o mesmo endereço de memória apontado pelo ponteiro Q. Assim, P e Q apontam para a mesma informação.

Dada a situação (a), após a atribuição p^ : = q^ temos a representação abaixo (c), onde o conteúdo do ponteiro Q é atribuido ao pontiero P. Assim, P e Q possuem o mesmo conteúdo, mas endereços diferentes.

Manipulação de Registros com ponteiros 1) Declaração de Variável
Type TPDado = Record Codigo : Integer; Nome : String[30]; Salario : Real; End; TPPonteiro = ^TPDado; Var P : TPPonteiro;

2) Criação de um registro Substitui o procedimento ObterNo(j) dada uma variável ponteiro do tipo TPPonteiro (^TDDado).
new(p);

a) efetivamente aloca uma variável do tipo TPDado b) gera um ponteiro do ^TPDado apontando para aquela variável c) atribui o ponteiro à variável P A partir daí: a) o ponteiro pode ser referenciado como P b) variável referenciada por p é denotada por P^ 3) Atribuição de conteúdo ao registro:
p^.codigo := valor;

4) Liberação de um registro Substitui o DevolverNo(j) - dispose(p)
dispose(p);

a) operação libera o espaço apontado por P b) P passa a ter valor indefinido
Lista Encadeada Dinâmica

Anteriormente, estudamos algumas estruturas de dados estáticas (lista, pilha e fila circular). Elas tinham esta denominação porque os dados eram armazenados em um array. Assim, tinhamos de definir um tamanho máximo de registros que seriam armazenados, tornando assim, as estruturas limitadas. A partir de agora, os dados serão alocados dinamicamente na memória. Isso significa dizer que a medida em que for necessária a inclusão de um novo dado, será criado um novo nó na memoria, atraves de um ponteiro e este novo nó será associado a lista encadeada. Da mesma, quando da necessidade de se excluir um nó, a memória será liberada.

Em uma implementação de lista através de ponteiros, cada item (nó) da lista é encadeado com o seguinte através de uma variável do tipo ponteiro. Isso significa dizer que cada nó da lista, contém o registro de dados (informações) e uma variável que armazena o endereço do próximo nó. Este tipo de implementação permite utilizar posições não contíguas de memória, sendo possível inserir e retirar elementos sem haver a necessidade de deslocar os nós seguintes da lista. A Figura 1 ilustra uma lista representada desta forma. Observe que existe um nó inicial denominado "Nó Cabeça". Apesar no nó cabeça não conter dados válidos é conveniente fazê-lo com a mesma estrutura que um outro nó qualquer para simplificar as operações sobre a lista. A lista é constituída de "nós", onde cada nó contém um dado da lista (registro) e um ponteiro (prox) para o proximo elemento da lista. O registro do tipo TPLista contém um apontado para o nócabeça (prim) e um apontador para o último nó da lista (ult). O último nó da lista não tem próximo. O ponteiro próx do último nó possui valo NIL (nulo). A implementação através de ponteiros permite inserir ou remover dados em qualquer posição da lista a um custo constante, aspecto importante quando a lista tem que ser mantida ordenada (no nosso caso a lista estará ordenada pelo campo chave primária). Em aplicações em que não existe previsão sobre o crescimento da lista é conveniente usar listas encadeadas por ponteiros (lista encadeada dinâmica), porque neste caso o tamanho máximo da lista não precisa ser definido. A maior desvantagem deste tipo de implementação é a utilização de memória extra para armazenar os apontadores.

Figura 1 - Representação de uma lista encadeada dinâmica.

Program Estoque; Uses Crt, Dos; Type TpChave = integer; TpDado = record Codigo : TpChave; Nome : String[50]; Preco : Real; QtdeEst : Real; QtdeMin : Real; End; TpPonteiro=^TpNo; TpNo = record Dado : TpDado;

Figura 2 - Representação de um Nó da lista

Prox : TpPonteiro; End; TpLista = record Prim : TpPonteiro; Ult : TpPonteiro; End;

Var {Declaração das variáveis globais} Produto : TpLista; Dado : TpDado; P : TpPonteiro; Op : Char;

Criando uma lista dinâmica vazia

Este procedimento recebe via prâmetro por referência uma lista dinâmica indefinida, cria o ponteiro

Procedure IniciaLista (Var L:TpLista); Begin New(L.Prim); L.Ult:=L.Prim; L.Prim^.Prox:=Nil; End; Funtion ListaVazia (L : TpLista): Boolean; Begin ListaVazia:=L.prim=L.ult; End;

Procedure Insere (Var L: TpLista; Dado: Tpdado; P: TpPonteiro); Var Aux : TpPonteiro; Begin New(Aux); Aux^.Dado:=Dado; Aux^.Prox:=P^.Prox; P^.Prox:=Aux; If Aux^.Prox=nil Then Begin L.ult := Aux; End; End; Procedure Remove (Var L : TpLista; P: TpPonteiro; Var D ado : TpDado); Var Aux : TpPonteiro;

Begin If P^.Prox = L.Ult Then Begin L.Ult := P; End; Aux:=P^.Prox; P^.Prox:=Aux^.Prox; Dado:=Aux^.Dado; Dispose(Aux); End; Function Busca(L: TpLista; Cod: TpChave; Var P: TpPonteiro): Boolean; Begin P:=L.Prim; While (P^.prox <> Nil) And (P^.prox^.Dado.Codigo < Cod) Do P:=P^.prox; BuscaDin:=(P^.Prox <> Nil)And (P^.Prox^.Dado.código=Cod); End;