<aside>

Conhecimentos necessários:

Alocação de memória (ponteiros)

Struct, Union e Enum

</aside>

<aside>

Navegação

</aside>

Já imaginou fazer um programa que receba uma lista de compras? Tipo, parece simples, né? Basta usar um array e dá certo… bom, sim, pode dar dar certo, mas concorda comigo que nunca será quantos itens a pessoa colocará, então você teria que criar um array muito grande para que ela possa adicionar os itens que ela pretende comprar… concorda comigo que isso não parece eficiente? Imagina criar um array de 100 itens para a pessoa adicionar apenas 5? É perda de memória! Ou simplesmente fazer um array de 100 itens e a pessoa querer adicionar 300… também não dá, né? Isso não é eficiente e menos ainda prático… Isso porque eu nem mencionei a retirada fora de ordens de itens…

Para resolver esse problema, usaremos listas!

O que são as listas simplesmente ligadas?

Listas são estruturas de dados que não permitem adicionar quantos itens quisermos em qualquer lugar dela! Podemos adicionar no final, no início ou no meio e retirar tudo de uma forma bem fácil e prática.

Isso porque ela funciona a partir de TADs (tipo abstrato de dados)

Tipo abstrato de dados

Nada mais é do que uma struct que contém os dados que precisamos e - aí que entra o ponto principal - um ponteiro para outra struct idêntica e do mesmo tipo.

Imagino que talvez já tenha sacado o que vai acontecer aqui… caso não, vou te explicar: esse ponteiro nas structs vamos usar para conectar uma na outra e, quanto todas estiverem ligadas, teremos uma lista!

Algo parecido com isso:

image.png

Está vendo? Há um ponteiro indicando o início do primeiro elemento, que mostra onde fica o próximo, que mostra onde fica o próximo e assim vai… até chegar no último elemento, que não aponta pra ninguém, pois ele é o último, então seu ponteiro é nulo.

Para criar cada um desses itens em quadradinho que apontam para o próximo, precisamos criá-los dinamicamente, ou seja, usando alocação de memória… então como bem sabe, se perdermos o ponteiro no início (ou qualquer outro) em algum momento, perdemos toda a lista (ou parte dela, no caso de perdermos onde aponta o ponteiro de algum dos elementos).

Adicionando elementos na lista

Na lista, você pode adicionar elementos em três lugares diferentes:

Adicionar no início

Para adicionar um elemento no início, basta criá-lo, apontar para o primeiro elemento e fazer o ponteiro do início apontar para ele. Tipo assim:

#include <stdlib.h>

typedef struct elem {
	int valor;
	struct elem *proximo;
} elem;

void AdicionarNoInicio(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    elemento_criado->proximo = *inicio; //atribuindo o ex-primeiro elemento como o próximo
    *inicio = elemento_criado; //mudando o início para o elemento que acabamos de criar
}

int main()
{
	elem *inicio = NULL; //inicializando o ponteiro
	AdicionarNoInicio(&inicio, 18); //colocando o elemento 18 no início da lista

	return 0;
}

Observe que temos que modificar o valor original do ponteiro, por isso criamos um ponteiro de ponteiro nos parâmetros da função

Adicionar no meio

Para adicionar um elemento no meio de outros dois elementos (que chamarei de A e B para facilitar), basta criá-lo e apontar para o elemento B, depois disso fazer o elemento A apontar para o que acabamos de criar. Tipo assim:

#include <stdlib.h>

typedef struct elem {
	int valor;
	struct elem *proximo;
} elem;

void AdicionarNoMeio(elem **inicio, int value, int local){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    
    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior ao local onde vamos colocar
            if(auxiliar->proximo) //verifica se existe um próximo elemento. Caso não exista, esse é o último elemento
                auxiliar = auxiliar->proximo;
            else
                break;
        }
        if(auxiliar->proximo){ //se não for o último elemento...
            elemento_criado->proximo = auxiliar->proximo;
            auxiliar->proximo = elemento_criado;
        }
        else{ //se for o último elemento...
            auxiliar->proximo = elemento_criado;
        }
    }
}

int main()
{
	elem *inicio = NULL; //inicializando o ponteiro
	AdicionarNoMeio(&inicio, 18, 3); //colocando o elemento 18 no terceiro lugar

	return 0;
}

Basicamente o que está acontecendo aqui é o seguinte… pensa comigo: para adicionar o elemento no terceiro lugar, precisamos colocar na frente do segundo, então basta pegarmos o segundo elemento, apontar o elemento que criamos para o ex-terceiro e o apontar o segundo para o elemento que acabamos de criar. Assim, adicionamos um novo elemento entre o segundo e o terceiro. O problema é que ele começa logo pegando o primeiro elemento, então para resolver isso, basta colocar local-1 dentro do for.

Para não dar problema caso a função esteja vazia, há uma verificação no início para verificar se o ponteiro do início é nulo.

Para também evitar de dar problema caso a pessoa digite um local maior do que a lista, há uma verificação para garantir que o próximo elemento existe e, caso não exista, ele adicione no final normalmente.

Adicionar no final

Para adicionar no final, basta percorrer a lista até chegar no final e adicionar o elemento logo em seguida. Tipo assim:

#include <stdlib.h>

typedef struct elem {
	int valor;
	struct elem *proximo;
} elem;

void AdicionarNoFinal(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos

    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        while(auxiliar->proximo){ //vai percorrendo a lista até achar um elemento que não tem um próximo, ou seja, é o último.
            auxiliar = auxiliar->proximo;
        }
        auxiliar->proximo = elemento_criado;
    }
}

int main()
{
	elem *inicio = NULL; //inicializando o ponteiro
	AdicionarNoFinal(&inicio, 18); //colocando o elemento 18 no final da lista

	return 0;
}

Removendo elementos na lista

Remover no início

Para remover o primeiro elemento é bem tranquilo! Bastante salvar o primeiro elemento em um ponteiro, fazer o início apontar para o segundo elemento e limpar da memória o ex-primeiro elemento que queremos apagar. Prontinho! Só não esqueça de verificar se a lista não está vazia!

Fazendo isso em uma função, ficará assim:

void RemoverNoInicio(elem **inicio){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem *aux = *inicio; //salvamos o primeiro elemento para depois limpá-lo da memória
        *inicio = (*inicio)->proximo; //pega o segundo elemento da lista
        free(aux); //limpa o primeiro elemento da memória para excluí-lo
    }
}

Remover no meio

Para remover no meio, vamos usar uma lógica parecida para adicionar um elemento no meio: se queremos remover um elemento, vamos pegar o anterior a ele e fazer 3 coisas:

  1. Salvar o elemento que queremos apagar
  2. Fazer o elemento anterior apontar para o próximo
  3. Apagar o elemento que precisamos (o que salvamos)

Por exemplo: suponha que tenho a seguinte lista:

  1. 11
  2. 22
  3. 33
  4. 44
  5. 55

Se eu quero apagar o terceiro elemento, eu vou pegar o elemento 22, salvar o elemento 33, e fazer o próximo do 22 ser o 44, ficando assim:

Salvo: 33

  1. 11
  2. 22
  3. 44
  4. 55

Como esse 33 ainda está ocupando memória, basta apagar ele usando o free();

Colocando isso em prática em uma função, fica assim:

void RemoverNoMeio(elem **inicio, int local){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem* aux = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior do elemento que vamos apagar
            if(aux->proximo) //se existir um próximo elemento ainda...
                aux = aux->proximo; //...vá para esse próximo...
            else //...caso não exista...
                break; //...pare
        }
        if(aux->proximo) { //se existir um próximo elemento...
            elem *apagar = aux->proximo; //...salve-o em uma variável para depois apagar...
            aux->proximo = aux->proximo->proximo; //..., coloque que o próximo será o elemento seguinte do que vamos apagar...
            free(apagar); //...e apague o elemento
        }
    }
}

Remover no final

Para remover o último elemento, basta a gente pegar o penúltimo e limpar o próximo elemento. Pegamos o penúltimo porque se pegássemos o último elemento, quando limparmos ele, não teríamos como colocar o ponteiro de próximo do elemento anterior como nulo. Mas não podemos esquecer de verificar se a lista está vazia ou se possui apenas um elemento.

Escrevendo isso em uma função, fica assim:

void RemoverNoFinal(elem **inicio){
    if(*inicio) { //verifica se início não é nulo, ou seja, se a lista não é vazia
        if(!(*inicio)->proximo) { //se o primeiro elemento não tiver um próximo...
            free(*inicio); //...apague o primeiro e único elemento...
            *inicio = NULL; //...e coloque o inicio como NULL...
        }
        else{ //...caso contrário...
            elem *aux = *inicio; //...comece no início...
            while (aux->proximo->proximo){ //...e percorra até pegar o penúltimo elemento... 
                aux = aux->proximo;
            }
            free(aux->proximo); //...limpe o elemento seguite do penúltimo...
            aux->proximo = NULL; //...e defina-o como nulo

        }
    }
}

Todas as adições e remoções

Adicionando tudo isso em um código simples, temos:

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

typedef struct elem {
	int valor;
	struct elem *proximo;
} elem;

void AdicionarNoInicio(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    elemento_criado->proximo = *inicio; //atribuindo o ex-primeiro elemento como o próximo
    *inicio = elemento_criado; //mudando o início para o elemento que acabamos de criar
}

void AdicionarNoMeio(elem **inicio, int value, int local){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    
    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior ao local onde vamos colocar
            if(auxiliar->proximo) //verifica se existe um próximo elemento. Caso não exista, esse é o último elemento
                auxiliar = auxiliar->proximo;
            else
                break;
        }
        if(auxiliar->proximo){ //se não for o último elemento...
            elemento_criado->proximo = auxiliar->proximo;
            auxiliar->proximo = elemento_criado;
        }
        else{ //se for o último elemento...
            auxiliar->proximo = elemento_criado;
        }
    }
}

void AdicionarNoFinal(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos

    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        while(auxiliar->proximo){ //vai percorrendo a lista até achar um elemento que não tem um próximo, ou seja, é o último.
            auxiliar = auxiliar->proximo;
        }
        auxiliar->proximo = elemento_criado;
    }
}

void RemoverNoInicio(elem **inicio){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem *aux = *inicio; //salvamos o primeiro elemento para depois limpá-lo da memória
        *inicio = (*inicio)->proximo; //pega o segundo elemento da lista
        free(aux); //limpa o primeiro elemento da memória para excluí-lo
    }
}

void RemoverNoMeio(elem **inicio, int local){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem* aux = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior do elemento que vamos apagar
            if(aux->proximo) //se existir um próximo elemento ainda...
                aux = aux->proximo; //...vá para esse próximo...
            else //...caso não exista...
                break; //...pare
        }
        if(aux->proximo) { //se existir um próximo elemento...
            elem *apagar = aux->proximo; //...salve-o em uma variável para depois apagar...
            aux->proximo = aux->proximo->proximo; //..., coloque que o próximo será o elemento seguinte do que vamos apagar...
            free(apagar); //...e apague o elemento
        }
    }
}

void RemoverNoFinal(elem **inicio){
    if(*inicio) { //verifica se início não é nulo, ou seja, se a lista não é vazia
        if(!(*inicio)->proximo) { //se o primeiro elemento não tiver um próximo...
            free(*inicio); //...apague o primeiro e único elemento...
            *inicio = NULL; //...e coloque o inicio como NULL...
        }
        else{ //...caso contrário...
            elem *aux = *inicio; //...comece no início...
            while (aux->proximo->proximo){ //...e percorra até pegar o penúltimo elemento... 
                aux = aux->proximo;
            } 
            free(aux->proximo); //...limpe o elemento seguite do penúltimo...
            aux->proximo = NULL; //...e defina-o como nulo

        }
    }
}

void VerLista(elem *inicio){
    elem *aux = inicio;
    printf("\\n");
    for(int i = 1; aux; i++){
        printf("%d: %d\\n", i, aux->valor);
        aux = aux->proximo;
    }
    printf("\\n");
}

int main()
{
	elem *inicio = NULL; //inicializando o ponteiro
    int escolha, num, local;
	while(1){
        printf("1 - Ver lista\\n2 - Adicionar no inicio\\n3 - Adicionar no meio\\n4 - Adicionar no fim\\n5 - Remover do inicio\\n6 - Remover do meio\\n7 - Remover no final\\n");
        scanf("%d", &escolha);
        switch (escolha) {
            case 1:
                VerLista(inicio);
                break;
            
            case 2:
                scanf("%d", &num);
                AdicionarNoInicio(&inicio, num);
                break;

            case 3:
                scanf("%d %d", &num, &local);
                AdicionarNoMeio(&inicio, num, local);
                break;

            case 4:
                scanf("%d", &num);
                AdicionarNoFinal(&inicio, num);
                break;
            
            case 5:
                RemoverNoInicio(&inicio);
                break;
            
            case 6:
                scanf("%d", &local);
                RemoverNoMeio(&inicio, local);
                break;
            
            case 7:
                RemoverNoFinal(&inicio);
                break;
            
            default:
            printf("Opcao invalida.\\n");
                break;
        }
    }

	return 0;
}

Lista duplamente ligada

Consiste no mesmo conceito de uma lista simplesmente ligada, mas, ao invés dos elementos só serem ligados com o próximo, eles também são ligados com os elementos anteriores. Assim, a struct que usaremos como TAD ficará mais ou menos assim:

typedef struct elem {
	//o que está dentro da TAD
	struct elem *proximo;
	struct elem *anterior;
} elem;

Então, ao invés de fazer apenas uma ligação para cada elemento, basta fazer uma ligação a mais.

Fazendo as devidas mudanças em cada uma das funções, o código completo ficaria assim:

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

typedef struct elem {
	int valor;
	struct elem *proximo;
    struct elem *anterior;
} elem;

void AdicionarNoInicio(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    elemento_criado->proximo = *inicio; //atribuindo o ex-primeiro elemento como o próximo
    if(*inicio){
        elemento_criado->proximo->anterior = elemento_criado; //colocando que o anterior do elemento seguinte é ele mesmo
    }
    *inicio = elemento_criado; //mudando o início para o elemento que acabamos de criar
}

void AdicionarNoMeio(elem **inicio, int value, int local){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos
    
    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior ao local onde vamos colocar
            if(auxiliar->proximo) //verifica se existe um próximo elemento. Caso não exista, esse é o último elemento
                auxiliar = auxiliar->proximo;
            else
                break;
        }
        if(auxiliar->proximo){ //se não for o último elemento...
            elemento_criado->proximo = auxiliar->proximo;
            elemento_criado->anterior = auxiliar;
            auxiliar->proximo = elemento_criado;
            elemento_criado->proximo->anterior = elemento_criado;
        }
        else{ //se for o último elemento...
            auxiliar->proximo = elemento_criado;
            elemento_criado->anterior = auxiliar;
        }
    }
}

void AdicionarNoFinal(elem **inicio, int value){
    elem* elemento_criado = malloc(sizeof(elem)); //criando um novo elemento
    elemento_criado->valor = value; //atribuindo o valor que pedimos

    if(!*inicio){ //verifica se a lista está vazia ao colocar um elemento.
        *inicio = elemento_criado;
    }
    else{
        elem* auxiliar = *inicio;
        while(auxiliar->proximo){ //vai percorrendo a lista até achar um elemento que não tem um próximo, ou seja, é o último.
            auxiliar = auxiliar->proximo;
        }
        auxiliar->proximo = elemento_criado;
        elemento_criado->anterior = auxiliar;
    }
}

void RemoverNoInicio(elem **inicio){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem *aux = *inicio; //salvamos o primeiro elemento para depois limpá-lo da memória
        *inicio = (*inicio)->proximo; //pega o segundo elemento da lista
        if(*inicio){ //para caso a lista não for ficar vazia
            (*inicio)->anterior = NULL;
        }
        free(aux); //limpa o primeiro elemento da memória para excluí-lo
    }
}

void RemoverNoMeio(elem **inicio, int local){
    if(*inicio){ //verifica se início não é nulo, ou seja, se a lista não é vazia
        elem* aux = *inicio;
        for(int i = 1; i < local-1; i++){ //pegando o elemento anterior do elemento que vamos apagar
            if(aux->proximo) //se existir um próximo elemento ainda...
                aux = aux->proximo; //...vá para esse próximo...
            else //...caso não exista...
                break; //...pare
        }
        if(aux->proximo) { //se existir um próximo elemento...
            elem *apagar = aux->proximo; //...salve-o em uma variável para depois apagar...
            aux->proximo = aux->proximo->proximo; //..., coloque que o próximo será o elemento seguinte do que vamos apagar...
            if(aux->proximo){ //se houver um proximo
                aux->proximo->anterior = aux;
            }
            free(apagar); //...e apague o elemento
        }
    }
}

void RemoverNoFinal(elem **inicio){
    if(*inicio) { //verifica se início não é nulo, ou seja, se a lista não é vazia
        if(!(*inicio)->proximo) { //se o primeiro elemento não tiver um próximo...
            free(*inicio); //...apague o primeiro e único elemento...
            *inicio = NULL; //...e coloque o inicio como NULL...
        }
        else{ //...caso contrário...
            elem *aux = *inicio; //...comece no início...
            while (aux->proximo->proximo){ //...e percorra até pegar o penúltimo elemento... 
                aux = aux->proximo;
            } 
            free(aux->proximo); //...limpe o elemento seguite do penúltimo...
            aux->proximo = NULL; //...e defina-o como nulo

        }
    }
}

void VerLista(elem *inicio){
    elem *aux = inicio;
    printf("\\n");
    for(int i = 1; aux; i++){
        printf("%d: %d\\n", i, aux->valor);
        aux = aux->proximo;
    }
    printf("\\n");
}

int main()
{
	elem *inicio = NULL; //inicializando o ponteiro
    int escolha, num, local;
	while(1){
        printf("1 - Ver lista\\n2 - Adicionar no inicio\\n3 - Adicionar no meio\\n4 - Adicionar no fim\\n5 - Remover do inicio\\n6 - Remover do meio\\n7 - Remover no final\\n");
        scanf("%d", &escolha);
        switch (escolha) {
            case 1:
                VerLista(inicio);
                break;
            
            case 2:
                scanf("%d", &num);
                AdicionarNoInicio(&inicio, num);
                break;

            case 3:
                scanf("%d %d", &num, &local);
                AdicionarNoMeio(&inicio, num, local);
                break;

            case 4:
                scanf("%d", &num);
                AdicionarNoFinal(&inicio, num);
                break;
            
            case 5:
                RemoverNoInicio(&inicio);
                break;
            
            case 6:
                scanf("%d", &local);
                RemoverNoMeio(&inicio, local);
                break;
            
            case 7:
                RemoverNoFinal(&inicio);
                break;
            
            default:
            printf("Opcao invalida.\\n");
                break;
        }
    }

	return 0;
}