[C] Listas Simplesmente Ligadas - Exemplo de Aplicação (inteiros)

mauro1855

I'm cool cuz I Fold
Uma dúvida que aparece frequentemente por aqui pelo fórum: O que são, ou como funcionam, listas?

Bem... Imaginem um vector. Quando declaram o vector têm de dizer o tamanho dele à partida... Mas e se não souberem isso? Se eu for o vosso patrão ou professor e vos pedisse um programa que armazene números, mas sem saber quantos números vou armazenar? 5 números? 100 números? 100000 números? Não sei.
Quer dizer, posso continuar a usar vectores se estiver constantemente a realocar o espaço do vector, mas isso não é propriamente elegante, e mesmo tirando a questão da elegância, normalmente recorrem-se sempre a outro tipo de estruturas em detrimento dos vectores nestes casos...
As listas é um caso de uma estrutura dinâmica usada nesses casos... Mas atenção que uma lista não serve simplesmente para substituir um vector, tem muitas outras aplicações, umas mais complexas que outras, em que algumas dessas aplicações dependem mesmo da existência de uma lista para funcionarem correctamente... De qualquer forma, aprender o conceito básico deste tipo de estruturas é essencial na vossa passagem pelo C.

Então mas, muito basicamente, o que é uma lista simplesmente ligada?
É uma estrutura que armazena um ou mais valores (pode ser um inteiro, um char, três floats, ou qualquer coisa) e ainda um ponteiro que aponta para um próximo elemento dessa estrutura...
É como eu ter um quadrado que guarda um valor, aponta para outro quadrado que guarda outro valor, que aponta para outro quadrado e por aí a fora:

lista.png


O final da lista aponta para NULL.
No que respeita ao código, uma lista tem várias funções auxiliares, que permitem eliminar elementos da lista, inserir valores, eliminar a lista, etc etc...
E pronto, está explicado. Fica aqui o código de um exemplo de aplicação de uma lista simplesmente ligada de inteiros.
Este programa permite:
- Inserir um inteiro (no fim da lista - a lista inicialmente está vazia).
- Procurar um inteiro e dizer se ele faz parte da lista.
- Imprimir todos os inteiros armazenados na lista.
- Eliminar inteiros/blocos da lista.

O que devem observar neste programa é o código, sendo que o funcionamento do programa serve apenas como demonstração das funções usadas...


Código:
/* Função: Exemplo de Aplicação de Listas Simplesmente Ligadas de Inteiros
 * Propósito: Educacional
 * Autor: mauro1855 (Zwame.pt)
 */

#include <stdio.h>
#include <stdlib.h>


typedef struct _lista{
	int valor;
	struct _lista * next;
}lista;


lista * cria_lista(){

	lista * novo = (lista*)malloc(sizeof(lista));
	novo->next = NULL;
	return novo;
}


int procura(lista * original, int valor){

	lista * aux = original->next;

	while(aux != NULL && valor != aux->valor){
		aux=aux->next;
	}

	if(aux != NULL)
		return 1;
	else
		return 0;
}



void inserir(lista * original, int inteiro){
	
	lista * base = original;
	lista * aux = original->next;
	lista * novo;


	if(procura(original, inteiro)){
		printf("O inteiro já se encontra na lista. Nenhuma operação efectuada\n");
	}else{
		while(aux!= NULL){
			aux=aux->next;
			base=base->next;
		}

		novo=(lista*)malloc(sizeof(lista));
		novo->valor = inteiro;
		novo->next = aux;
		base->next = novo;
		printf("O número foi inserido\n");
	}
}


void imprimir(lista * original){
	lista * aux = original->next;
	if(aux == NULL){
		printf("A lista está vazia\n");
	}else{
		printf("Os valores da lista, por ordem com que foram introduzidos, são: \n");
	        while(aux!= NULL){
		       printf("%d\n", aux->valor);
		       aux=aux->next;
	       }
        }
}


void eliminar(lista * original, int inteiro){
	lista * aux = original->next;
	lista * base = original;

	while(aux!= NULL && aux->valor!=inteiro){
		aux=aux->next;
		base = base->next;
	}

	if(aux!= NULL){
		base->next=aux->next;
		free(aux);
		printf("Valor removido\n");
	}else{
		printf("O Valor não pode ser removido pois não se encontra na lista\n");
	}

}


int main(){

	int opcao, numero;
	lista * estrutura;

	estrutura = cria_lista();
	printf("*** Funcionamento de Listas de Inteiros ***\n");
	printf("Seleccione uma operação: \n");
	printf("1 - Inserir inteiro na lista \n");
	printf("2 - Imprimir valores da lista \n");
	printf("3 - Procurar valor na lista \n");
	printf("4 - Eliminar valor da lista\n");
	printf("5 - Sair\n");

	while(true){
		printf("\nIndique o número da operação: ");
		scanf("%d", &opcao);

		switch(opcao){

			case 1:  
				printf("Introduza o inteiro a inserir: ");
				scanf("%d", &numero);
				inserir(estrutura, numero);
				break;

			case 2:
				imprimir(estrutura);
				break;

			case 3:
				printf("Qual o valor que deseja procurar? ");
				scanf("%d", &numero);
				if(procura(estrutura, numero))
					printf("O valor encontra-se na lista\n");
				else
					printf("O valor não se encontra na lista\n");
				break;

			case 4:
				printf("Indique o valor da lista a eliminar: ");
				scanf("%d", &numero);
				eliminar(estrutura, numero);
				break;

			case 5:
				return 0;

			default: 
				printf("Opção Inválida\n");

		}
	}

}
 
Última edição:
Podias ter feito um "procura" que te encontrava o nó anterior e depois todas as outras (imprimir, apagar, etc) podiam utilizar o procura, poupando código :P
 
Podias ter feito um "procura" que te encontrava o nó anterior e depois todas as outras (imprimir, apagar, etc) podiam utilizar o procura, poupando código :P

Dizes fazer uma função procura que invés de retornar 1 se achar e 0 se não achar, retornava NULL se não achar ou então o elemento anterior da lista se achar? Acho que não percebi bem, mas:
Só existe uma função que tem o código mais ou menos repetido da função procura: é a função eliminar. As outras não beneficiam disso, repara:
- A função inserir primeiro verifica se o elemento existe. Se não existir, como queremos inserir o elemento no fim (é o mais usual, mas em termos de complexidade computacional era mais fácil inserir no início), tem de percorrer a lista toda até ao fim. Portanto, uma função procura nesses moldes não ajuda muito... Nem ajuda é nada. Aquilo que neste caso ajudava era um ponteiro no início para o fim. Mas isso já não era uma lista, era um anel (penso, nunca trabalhei com anéis). Complexidade que não quis adicionar.
- A função "procura" apenas pretende ver se o numero está lá, pelo que devolver o elemento é irrelevante.

A função eliminar talvez já fosse de considerar, fazia mais sentido, sim. Se bem que em termos de código não poupavas muito, e já tínhamos que andar com mais ponteiros...
Enfim, este tópico é mesmo só o básico das listas, porque se uma pessoa que não perceber muito bem o assunto perceber isto, facilmente desenvolve... Eu ainda me lembro perfeitamente quando comecei a dar isto que não percebia nada disto e fazia o código com montes de erros e com muito cuidadinho. Agora demorei 10 minutos a fazer este código, mais coisa menos coisa (mais 5 a testar)...

Cumps
 
não vou fazer o código, mas vou tentar explicar.

Para evitar teres que percorrer a lista TODA sempre, podes tentar inserir ordenado. Assim tinhas:

procura:

while (curr->next->valor < no->valor) curr = curr->next;
return curr;

insere:

node tmp = procura (no);
if(tmp->next->valor == no-> valor) ERRO;
else
no->next = tmp->next;
tmp->next = no;

elimina:
node tmp = procura (valor);
if(tmp->next->valor == valor) tmp->next = tmp->next->next; (nao esquecer dos frees)

encontra:
node tmp = procura(valor);
if(tmp->next->valor == valor) tmp = tmp->next;
return tmp;


acho que não me enganei em nada, mas era só para dar uma ideia. Usando só um procura e ordenando os nós consegue-se diminuir o tamanho do programa.
 
errandum, isto é uma lista simplesmente ligada para ficar como referência para os que estão a estudar C e precisem de saber como funciona, basicamente, uma lista. Portanto, isto quere-se o mais simples possível, e normalmente nas cadeiras básicas de Programação, é isto que pedem. Pelo menos comigo foi assim: Quando dei isto, os professores não estavam minimamente interessados na complexidade computacional das operações com listas. Eu sei perfeitamente que a operação de inserir na lista, neste código, tem uma complexidade 2N, que não é o melhor... No entanto, quando se está a aprender o básico, não é preciso estar com muita atenção para a gestão da complexidade, isso é coisas que se fala mais à frente. Por isso é que, se fores a ver, eu neste código nem me dei ao trabalho de fazer uma boa gestão de memória e fazer free da lista toda no fim do programa...
Para diminuir a complexidade podia fazer uma lista ordenada, podia ter feito essa função procurar dessa forma que, diga-se de passagem, servia para a generalidade do programa, mas depois não era tão simples de identificar se um nó estava na lista ou não... Já agora, essa função de procura não está correcta. Repara que tu vais avançando (curr=curr->next), mas depois se curr == NULL, quando tentares aceder a curr->valor, o programa vai crashar, uma vez que aparentemente nao tens condição para parar quando curr == NULL..
Mas OK, eu agora vou para um campoenato de AED, mas logo quando chegar vou fazer um código com a tua solução, a gente discute isso ver se está bem como dizes, e eu adiciono à página... Afinal de contas é outra solução que, para os mais curiosos, pode ter vantagens em relação à minha implementação.

Cumps
 
Última edição:
aquilo era "pseudo-código".

falta ali muita coisa, era só para te dar uma ideia, tanto me faz como a fazes ou não :P. Eu aprendi assim.

Boa sorte para o campeonato.
 
Aprendeste isso assim? Bolas... Aprendeste da forma complicada... Eu para evitar a complexidade que fiz no primeiro exemplo, e para evitar a tua complexidade intuitiva (ponteiros para um lado, ponteiros para outro), simplesmente mandava a função procura com os porcos... Não é uma função essencial, fazia-se mais código, mas fazia-se logo tudo directo nas funções inserir, eliminar, etc...

Cumps
 
Com desenhos e num projector fica muito fácil de perceber.

Basicamente a função encontrava o bloco anterior e depois era só "manipular" esse bloco para o que queríamos. Em vez de começar a explicação do insere "e agora fazemos um while ate ter X" ou a do apaga "e agora fazemos um while até ter Y" era simplesmente "chamamos o procura e mudamos o ponteiro de A para B ou C". Eu habituei-me assim, mas não há, penso eu, maneiras certas ou erradas de fazer isso... Se funciona, está feito... E além do uso numa cadeira ou noutra que especificamente pede listas, não estou a ver ninguém a pegar nisto :P. Em java tens ArrayLists e HashTables, em C++ Vectors e maps... Em C... Bom, duvido que estejas a fazer isso em C se não for para a dita cadeira ^^.

É um bom artigo, provavelmente não devia ter dito nada heh
 
Com desenhos e num projector fica muito fácil de perceber.

Basicamente a função encontrava o bloco anterior e depois era só "manipular" esse bloco para o que queríamos. Em vez de começar a explicação do insere "e agora fazemos um while ate ter X" ou a do apaga "e agora fazemos um while até ter Y" era simplesmente "chamamos o procura e mudamos o ponteiro de A para B ou C". Eu habituei-me assim, mas não há, penso eu, maneiras certas ou erradas de fazer isso... Se funciona, está feito... E além do uso numa cadeira ou noutra que especificamente pede listas, não estou a ver ninguém a pegar nisto :P. Em java tens ArrayLists e HashTables, em C++ Vectors e maps... Em C... Bom, duvido que estejas a fazer isso em C se não for para a dita cadeira ^^.

É um bom artigo, provavelmente não devia ter dito nada heh

LOOL, claro que pode ser feito da maneira que quiseres... Eu percebi pk já tenho facilidade nisto, mas sinceramente, se tivesse a aprender isto agora, até ficava baralhado com tanto ponteiro... Normalmente os Newbies em programação em C, quando lhes mostram ponteiros, ficam meio parvos a olhar para aquilo e acredito que uma boa parte dos iniciados em C use ponteiros mas nem sequer sabe o que são....

E aproveito e faço aqui um parêntesis para os iniciados que estão a ler: Não fiquem com a ideia de que só se pode fazer assim. Desde que percebam o código de uma lista, podem fazê-lo da forma que quiserem, desde que funcione e, principalmente, desde que se adeque ao vosso projecto. Isto é muito importante: escolham a implementação que melhor se adeque às necessidades do vosso programa.

Voltando ao quote:
Vou discordar quando dizes que, tirando uma cadeira ou outra, não tás a ver ninguém pegar nisto. A verdade é que existem uma série de Algoritmos em C que necessitam da utilização de uma pilha, ou de uma fila de prioridades que é, essencialmente, uma lista com uma implementação mais específica. Exemplo disso é o Algoritmo BFS para procura em Árvores e Grafos, ou o Algoritmo de Dijkstra, para achar a Shortest Path Tree num Grafo, ou o Algoritmo de Prim para achar a Minimal Suport Tree...Até a própria implementação dum grafo pode ser feito por Matriz de Adjacências ou Lista de Adjacências, sendo que cada uma das implementações tem vantagens em certos casos.
Já as HashTables que falas que existe em Java, também existe em C (talvez de forma diferente, não sei), mas geralmente quando se fala de HashTables implementado por matrizes, também falas de HashTables implementado por Listas... São usadas mesmo em muitos lados, pelo menos em C.

Cumps
 
Última edição:
Back
Topo