Estrutura De Dados C - Para Que Serve Estrutura De Dados - Várias Estruturas
Para Que Serve Estrutura De Dados - Várias Estruturas

Por que estrutura de dados em C ainda é importante

Você vai encontrar muita gente dizendo que aprender estrutura de dados só faz sentido em linguagens mais modernas. Isso não é verdade. O ponto de aprender estrutura de dados em C é que você vê exatamente o que acontece na memória. Quando você trabalha com vetores dinâmicos em Python ou Listas em Java, o runtime cuida do gerenciamento. Em C, você é obrigado a pensar em tudo. Isso pode parecer frustrante no começo. Mas depois de passar por alguns projetos, você percebe que a maior parte dos problemas que encontro em sistemas de produção tem a ver com alocação de memória e acesso indevido a endereços. Saber C te dá uma intuição que outras linguagens não desenvolvem tão rapidamente.

O que você precisa saber antes de começar com estrutura de dados c

O conhecimento mínimo necessário são ponteiros, arrays, structs e funções. Se você ainda não entende como um ponteiro funciona, não vá para lista encadeada. Vá estudar ponteiros primeiro. A diferença entre & e * não é algo que se aprende de cabeça para baixo. Outro ponto importante: você precisa entender o funcionamento da pilha e do heap. Variáveis locais vivem na pilha. Memória alocada com malloc fica no heap. Quando você passa um ponteiro para uma função, está passando um endereço, não uma cópia dos dados. Isso é fundamental para qualquer estrutura que você for implementar.

A biblioteca padrão do C oferece algumas coisas úteis. stdlib.h tem malloc, free, calloc e realloc. stdio.h para entrada e saída. Se precisar de strings, string.h tem strlen, strcpy e memcpy. Mas para estruturas propriamente ditas, você vai ter que construir tudo do zero. Não há lista, fila ou pilha na biblioteca padrão.

Vetores e arrays dinâmicos

Array em C tem tamanho fixo. Isso é uma limitação importante. Quando você declara int arr[100], o compilador reserva 100 inteiros na pilha. Se precisar de mais, tem que usar malloc. Um vetor dinâmico em C é basicamente um array alocado no heap com realloc quando cresce. A implementação mais simples fica assim:

typedef struct {
    int *data;
    int size;
    int capacity;
} VetorDinamico; Quando o tamanho excede a capacidade, você chama realloc com o dobro do espaço. A lógica é simples, mas existe um detalhe que muitas pessoas ignoram. Se realloc não conseguir expandir o bloco atual, ele alocará um novo bloco e copiará os dados. Isso significa que você precisa manter o ponteiro original e liberá-lo com free depois. Se você simplesmente sobrescrever o ponteiro, perde a referência ao bloco antigo e vaza memória.

No meu primeiro projeto sério, escrevi um vetor dinâmico para um sistema de processamento de logs. O programa funcionava bem com arquivos pequenos. Quando começou a processar arquivos de mais de 500MB, o realloc foi chamando o sistema operacional diversas vezes e o desempenho despencou. A solução foi pré-alocar com capacidade maior desde o início. Em vez de começar com capacity igual a 16 e dobrar a cada crescimento, comecei com 4096 e usei um fator de crescimento menor, tipo 1.5 ao invés de 2.0. Isso reduziu o tempo de processamento de cerca de 8 minutos para aproximadamente 45 segundos no caso dos arquivos grandes.

Listas encadeadas

Lista encadeada é uma das primeiras estruturas que todo mundo implementa. O conceito é simples: cada nó aponta para o próximo. A vantagem em relação ao vetor é que a inserção no início é constante, O(1). A desvantagem é que o acesso aleatório é linear, O(n), porque você precisa percorrer da cabeça até o índice desejado. O erro mais comum que eu vejo em código de lista encadeada é esquecer de tratar o caso de inserção no início, no meio e no final como casos separados. Quando você insere no início, o novo nó se torna a cabeça. Quando insere no final, o penúltimo nó passa a apontar para o novo. Quando insere no meio, o nó anterior aponta para o novo e o novo aponta para o próximo. Três casos, três linhas diferentes de código. Muitos programadores tentam generalizar e acabam introduzindo bugs.

Outra coisa que merece atenção é a liberação de memória. Uma lista encadeada mal implementada pode vazar memória facilmente. Sempre percorra a lista inteira e chame free em cada nó. Não confie no garbage collector. C não tem isso.

Pilhas e filas

Pilha segue o princípio LIFO. Último a entrar é o primeiro a sair. Fila segue FIFO. Primeiro a entrar é o primeiro a sair. Ambas podem ser implementadas com vetor ou com lista encadeada. A escolha depende do caso de uso. Para pilha, uma implementação baseada em lista encadeada é eficiente porque a inserção e remoção acontecem sempre na cabeça. Isso evita movimentos de elementos e mantém a operação em O(1) constante. Para fila, uma lista encadeada com ponteiro para o último elemento também garante O(1) tanto na inserção quanto na remoção.

Um problema real que enfrentei foi com uma fila de prioridade implementada como vetor simples. O sistema processava pedidos em um servidor web. Nos primeiros dias, tudo funcionava normalmente. Após uma semana em produção, notei um aumento gradual no tempo de resposta. A causa era a ordenação da fila. Como o vetor era pequeno e os pedidos eram inseridos aleatoriamente, a operação de ordenação consome cada vez mais tempo conforme a fila cresce. A solução foi trocar para uma heap, que mantém a operação de extração do mínimo em O(log n) ao invés de O(n).

👉 Clique no botão abaixo para saber mais sobre o assunto!

Tabelas hash

Tabela hash mapeia chaves para índices de um array usando uma função de hash. O objetivo é ter acesso em O(1) na média. Na prática, existem colisão e degradação de performance que precisam ser consideradas. Quando duas chaves diferentes produzem o mesmo hash, ocorre colisão. A forma mais simples de resolver é encadeamento, onde cada posição da tabela armazena uma lista encadeada. Outra forma é endereçamento aberto, onde você procura pela próxima posição disponível. O encadeamento é mais simples de implementar. O endereçamento aberto tende a ter melhor localidade de cache.

A função de hash é o componente mais crítico. Se a função for ruim, todas as chaves vão para poucas posições e a tabela se comporta como uma lista simples. Para strings, uma função comum é somar o valor de cada caractere multiplicado por potências de um número primo. Mas há funções mais robustas como FNV-1a e MurmurHash, que distribuem melhor os dados. Em um projeto de cache para um serviço interno, implementamos uma tabela hash com encadeamento e carga máxima de 0.75. Quando a carga ultrapassava esse limite, Redimensionávamos a tabela para o dobro do tamanho. No início, usamos uma função de hash baseada apenas nos primeiros caracteres da string. Isso causava muitas colisões para chaves com prefixos similares. Trocamos por FNV-1a e o número de colisões caiu drasticamente. O throughput do serviço melhorou cerca de 30%

Árvores binárias de busca

Árvore binária de busca organiza dados de forma que o nó esquerdo é menor que o pai e o nó direito é maior. Busca, inserção e remoção são O(log n) na média. No pior caso, quando a árvore fica desbalanceada, todas as operações viram O(n). Um AVL ou Red-Black resolve o problema do desbalanceamento, mas a complexidade de implementação aumenta significativamente. Se você está começando, uma árvore binária simples já é suficiente para entender o conceito. Quando precisar de garantia de performance, aí sim considere estruturas balanceadas.

O problema mais difícil em árvores binárias é a remoção. Remover uma folha é trivial. Remover um nó com um filho também. Mas remover um nó com dois filhos exige encontrar o sucessor inorder, que é o menor nó na subárvore direita, e substituir o valor do nó removido por esse sucessor. Depois você remove o sucessor, que no máximo terá um filho. É aqui que muitos bug surgem. A lógica parece simples no papel, mas na implementação, é fácil perder referências. Já depurei uma árvore binária por horas porque o ponteiro do pai não estava sendo atualizado corretamente durante a remoção. O sintoma era uma fuga de memória e nós órfãos na estrutura. A correção foi adicionar uma função separada para atualizar os pais e testar com casos unitários antes de integrar com o resto do sistema.

Grafos

Grafo é uma estrutura que representa pares de vértices conectados por arestas. Pode ser representado por matriz de adjacência ou lista de adjacência. Matriz é mais simples para grafos densos. Lista de adjacência é mais eficiente para grafos esparsos. Algoritmos clássicos como DFS e BFS funcionam em ambas as representações. DFS usa pilha. BFS usa fila. A escolha da representação afeta a complexidade de espaço e tempo. Matriz de adjacência consome O(V²) de espaço. Lista de adjacência consome O(V + E).

Em um projeto de roteamento de rede, usamos lista de adjacência com Dijkstra para encontrar o caminho mais curto. O grafo tinha cerca de 10 mil vértices e 50 mil arestas. A matriz de adjacência seria inviável em termos de memória. Com lista de adjacência, a solução cabia confortavelmente em 200MB de RAM e as consultas de caminho ficavam abaixo de 50ms.

Questões de performance e memory management

A maior armadilha de estrutura de dados em C é o gerenciamento manual de memória. Cada malloc precisa ter um free correspondente. Se você pular algum, o programa vaza memória. Em processos longos, como servidores, isso pode levar a problemas sérios de desempenho e instabilidade. Valgrind é a ferramenta padrão para detectar vazamentos e erros de memória. Ele analisa o programa e reporta cada alocação que não foi liberada. Recomendo usar Valgrind em todos os testes. Outros tools como Address Sanitizer são ainda mais rápidos e podem ser habilitados com flags de compilação.

Outro problema comum é uso de ponteiros pendentes. Se você libera memória e continua acessando o ponteiro, o comportamento é indefinido. O programa pode funcionar, crashar, ou produzir resultados errados sem avisar. Sempre coloque o ponteiro como NULL após free. Isso faz com que acessos subsequentes causem crash imediato em vez de corrupção silenciosa.

Alternativas e quando não usar C

Se o seu projeto não exige controle fino de memória ou performance extrema, considere usar linguagens com coleções na biblioteca padrão. Go tem slices e maps. Rust tem Vec e HashMap. Essas abstrações economizam tempo de desenvolvimento e reduzem bugs. Porém, se você está trabalhando em embedded systems, drivers, kernels ou aplicações que precisam de baixo consumo de memória e baixo overhead, C ainda é a escolha certa. A estrutura de dados em C não é sobre convenience. É sobre controle total.

Resumindo o que eu aprendi: comece com vetores dinâmicos. Domine ponteiros antes de partir para listas. Use Valgrind desde o primeiro programa. E não tenha pressa. Estrutura de dados em C se aprende implementando, não lendo. Pegue um papel e tente implementar uma lista encadeada do zero. Depois tente uma pilha. Só então parta para estruturas mais complexas.