Skip to content

Latest commit

 

History

History
348 lines (279 loc) · 10.2 KB

4-listas-encadeadas.org

File metadata and controls

348 lines (279 loc) · 10.2 KB

Estrutura de Dados @@latex:\@@ 4 - Listas Encadeadas

Sumário

Descrição

Esta nota descreve uma implementação estilo Tipo Abstrato de Dado para listas encadeadas em C. As notas são baseadas nas aulas ministradas pelo Dr. Jarbas no semestre de 2017.2 na UFC - Sobral na disciplina de Estrutura de Dados.

Lista simplesmente encadeada

Uma lista simplesmente encadeada só possuí um ponteiro para o próximo. Um nó só descreve quem é o próprio elemento e quem será o próximo. É a lista encadeada mais simples que tem. Outras versões de listas encadeadas podem ser implementadas como duplamente encadeada e circular. Em outro tópico será comentado sobre isso.

Definição formal

Definição formal de lista pode ser vista de uma maneira recursiva. Existe um primeiro nó, que é a cabeça, então esse aponta para o próximo: a cauda. O que consequentemente, o último elemento apontará para o nil [fn:nil]

Head :: List
Tail :: List
Head -> data
        prox -> Tail -> nil

Outra maneira de se ver uma lista é como uma árvore em que cada nó só possui um filho e um pai.

    (1)
      \
      (2)
        \
        (3)
          \
          (4)
            \
            nil

=> [1,2,3,4]

Observe que nesse exemplo só existe o ponteiro de pai->filho. Quando existir um ponteiro a mais que aponta filho->pai isto será uma lista duplamente encadeada.

Em Lisp, por exemplo, listas são definidas através de pares chamado de cons.

CL-USER> (cons 1 (cons 2 (cons 3 (cons 4 nil))))
(1 2 3 4)

De uma maneira similar, embora não tão elegante como Lisp, seguindo essas ideias podemos definir uma lista através de um struct na linguagem C usando ponteiros.

[fn:nil] nil é uma keyword muito comum no mundo de Lisp pra descrever uma lista vazia, em C poderíamos entender isso grosseiramente como NULL.

Estrutura

Uma estrutura de lista simplesmente encadeada, armazenando um simples int pode ser implementada da seguinte maneira:

struct node {
    int data;
    struct node *next;
};

Ou seja, essa estrutura possuí apenas os campos data e o ponteiro next para o próximo nó. O nó por si mesmo é uma lista (com apenas um elemento).

Métodos

Para definir nosso tipo abstrato de dados List, devemos implementar um conjunto de operações definidos num cabeçalho. Essas operações serão usadas pra manipular completamente a lista.

/* Tipo lista publico */
typedef struct node List;

/* Cria uma nova lista */
List* list_create(void);

/* Insere no começo da lista */
List* list_insert(List *l, int data);

/* Insere ordenadamente um elemento na lista */
List* list_insert_ord(List *l, int data);

/* Insere no fim da lista */
List* list_append(List *l, int data);

/* Busca na lista e retorna nó */
List* list_search(List *l, int data);

/* Imprime lista */
void list_print(List *l, int data);

/* Remove elemento específico da lista */
List* list_remove(List *l, int data);

/* Libera memória de toda lista */
void list_free(List *l);

/* Verifica se a lista está vazia */
int list_empty(List *l);

Implementação

Podemos implementar estes métodos acima de duas maneiras: iterative e recursiva. A iterativa é geralmente a mais usada em C pois costumam ser menos custosas em memória. Você precisa em geral de estruturas de repetição como laços: while e for.

Implementações recursivas, definidas em termo de seus próprios métodos, geralmente são muito mais concisas e elegantes, mas C não garante Tail Call Optimization, então sua execução no geral pode ser mais custosa em memória por precisar de lembrar mais variáveis nas sucessivas criações de escopos nas chamadas recursivas.

É importante destacar que procedimentos iterativos podem ser definidos através de recursão por Tail Call (chamada por cauda) quando a linguagem ou compilador garante que no processo de compilação essa chamada de cauda irá descartar o escopo da pilha anterior (iterativo). Isto é conhecido como Tail Call Optimization ou Tail Call Eliminitation. Uma chamada de cauda é quando um função recursiva só chama a si mesmo quando não há mais nada pra lembrar do escopo atual.

Uma resposta que escrevi no StackOverflow descreve essa diferença no contexto de Scheme (LISP-1).

Observe que nem todas funções precisam ser definidas diferentemente, apenas são necessárias aquelas que percorrem a lista de alguma maneira. Por esse motivo, um arquivo separado chamado list-common.c é definido os métodos comuns da lista. Isso evita reescrevê-los nos arquivos individuais que contêm as implementações específicas dos métodos recursivos e iterativos. O código fonte para esses métodos você encontra em ./../src/list/single.

Métodos em Comum

Os métodos em comum para essa TAD entre as implementações recursiva e iterativa são as seguintes: list_create, list_empty e list_insert.

List* list_create(void) {
    return NULL;
}

int list_empty(List *l) {
    return l == EMPTY_LIST;
}

List* list_insert(List *l, int data) {
    List *head = (List *) malloc(sizeof(List));
    check_alloc(head);
    head->data = data;
    head->next = list_create();

    if (list_empty(l)) {
        l = head;
    } else {
        head->next = l;
        l = head;

    }
    return l;
}

Como inserção na cabeça é \(Θ(1)\) então não é necessário percorrer a lista de nenhuma maneira.

Recursiva

As seguintes funções são criadas para auxiliar a definição dos métodos:

// util function
static inline List* new_node(int data) {
    List* l = (List *) malloc(sizeof(List));
    check_alloc(l);
    l->data = data;
    l->next = list_create();
    return l;
}

// auxiliar print recursively list (without squared brackets)
void aux_list_print(List *l) {
    if(!list_empty(l)) {
        printf("%d", l->data);
        if (!list_empty(l->next)) {
            printf(", ");
        }
        aux_list_print(l->next);
    }
}

A função a seguir insere o elemento no fim.

List* list_append(List *l, int data) {
    if (list_empty(l)) {
        l = new_node(data);
    } else {
        l->next = list_append(l->next, data);
    }

    return l;
}

Função definida recursivamente para inserir um elemento na lista de maneira ordenada.

List* list_insert_ord(List *l, int data) {
    if(list_empty(l) || data <= l->data) {
        l = list_insert(l, data);
    } else {
        l->next = list_insert_ord(l->next, data);
    }
    return l;
}

Função de busca linear na lista.

List* list_search(List *l, int data) {
    if(!list_empty(l)) {
        if (l->data != data) {
            l = list_search(l->next, data);
        }
    }
    return l;
}

Procedimento de impressão da lista, observe que está definida através da função recursiva auxiliar aux_list_print.

void list_print(List *l) {
    printf("[");
    aux_list_print(l);
    printf("]\n");
}

Procedimento para remoção ordenada de um elemento específico da lista.

List* list_remove(List *l, int data) {
    if (!list_empty(l)) {
        if (l->data == data) {
            List* next = l->next;
            free(l);
            l = next;
        } else {
            l->next = list_remove(l->next, data);
        }
    }
    return l;
}

Procedimento para liberar memória ocupada pelos nós da lista.

void list_free(List *l) {
    if (!list_empty(l)) {
        list_free(l->next);
        free(l);
    }
}

Iterativa

A iterativa é a maneira mais verbosa de ser feita. Vou procastina um pouco até escrever. Mas enquanto isso olhe a recursiva.