Aula 36 – Revisão Completa: listaSequencial.c

Percorrendo cada bloco do programa completo — do cabeçalho ao main(), função por função, linha por linha

Visão Geral do Programa

Esta aula revisa o arquivo listaSequencial.c — o programa completo que implementa todos os conceitos das Aulas 29 a 35. Em vez de ver os conceitos isoladamente, aqui você lê o código como um todo e entende como cada parte se encaixa na outra.

Um programa de lista sequencial completo tem três camadas: a definição dos tipos (o que é uma lista), as funções de operação (o que podemos fazer com ela) e o programa principal (main) que orquestra tudo. Ler o código nessa ordem é a melhor forma de dominar o projeto.

O programa implementa as seguintes operações, nesta ordem no arquivo:

#FunçãoO que faz
1inicializarListaZera o contador — prepara a lista para uso
2exibirListaImprime todos os elementos válidos
3tamanhoRetorna quantos elementos estão na lista
4tamanhoEmBytesRetorna o tamanho físico da struct em bytes
5primeiroElemRetorna a chave do primeiro elemento
6ultimoElemRetorna a chave do último elemento
7enesimoElemRetorna a chave do elemento na posição n
8reinicializarListaEsvazia a lista sem liberar memória
9inserirElemListaInsere um registro na posição i com deslocamento
10buscaSequencialBusca linear sem sentinela — O(n)
11excluirElemListaRemove elemento por chave com deslocamento
12buscaSentinelaBusca linear otimizada com sentinela — O(n)
13mainPrograma principal — exercita todas as funções

Bloco 1 — Cabeçalho, Includes e Defines

As primeiras linhas do arquivo estabelecem o contexto e as dependências do programa:

/******************************************************************************
    listaSequencial.c
  Este programa gerencia listas lineares sequenciais.
  As listas gerenciadas podem ter MAX elementos (posicoes de 0 a MAX-1 no
    arranjo A). Alocamos MAX+1 posicoes no arranjo para possibilitar a
    criacao de um elemento sentinela.
******************************************************************************/
#include <stdio.h>
#include <stdbool.h>
#define MAX  50
#define ERRO -1
#include <stdio.h> — Biblioteca padrão de entrada e saída. Necessária para printf e scanf.

#include <stdbool.h> — Disponível desde C99. Define o tipo bool e as constantes true (1) e false (0). Sem esse include, funções que retornam bool — como inserirElemLista e excluirElemLista — não compilariam.

#define MAX 50 — Capacidade máxima da lista. Uma constante simbólica: o pré-processador substitui toda ocorrência de MAX por 50 antes da compilação.

#define ERRO -1 — Código sentinela de erro. Quando uma função de busca não encontra o elemento, retorna -1. Usando o nome ERRO em vez do número, o código comunica a intenção claramente.
Por que -1 como código de erro? Índices válidos de array vão de 0 a MAX-1 — todos não-negativos. O valor -1 é impossível como índice, então funciona perfeitamente como "sentinela de erro" sem ambiguidade.

Bloco 2 — Definição dos Tipos

A fundação de tudo: os três typedefs que definem o que é uma lista neste programa.

typedef int TIPOCHAVE;

typedef struct {
  TIPOCHAVE chave;
  // outros campos...
} REGISTRO;

typedef struct {
  REGISTRO A[MAX+1];
  int nroElem;
} LISTA;

Leia de baixo para cima para entender a dependência entre os tipos:

TipoO que éDepende de
TIPOCHAVE Apelido para int. Representa o identificador de um registro. Se a chave mudar de tipo (ex.: CPF como string), só esta linha precisa mudar. —
REGISTRO A unidade mínima de dados. Contém pelo menos uma chave. Outros campos de dados (nome, data, valor...) seriam adicionados aqui. TIPOCHAVE
LISTA O contêiner completo: um array de até MAX+1 registros + um contador nroElem de quantos estão em uso. REGISTRO, MAX
Por que A[MAX+1] e não A[MAX]? A posição extra A[MAX] é reservada exclusivamente para a técnica de busca com sentinela (função buscaSentinela): colocamos a chave procurada nessa posição para garantir que o loop sempre termine sem precisar checar o limite. Sem o +1, esse acesso seria fora dos limites do array — comportamento indefinido em C.
O campo nroElem na LISTA é o equivalente ao size() de um ArrayList em Java. A capacidade física é sempre MAX; o tamanho lógico (em uso) é nroElem, que varia de 0 a MAX conforme inserções e exclusões.

Bloco 3 — inicializarLista

/* Inicialização da lista sequencial (a lista já está criada e é apontada
   pelo endereço em l) */
void inicializarLista(LISTA* l) {
  l->nroElem = 0;
} /* inicializarLista */
A função mais simples — e a mais importante. Em C, variáveis locais não são zeradas automaticamente — elas contêm o "lixo" que estava naquele endereço de memória antes. Uma LISTA declarada com LISTA lista; terá nroElem com valor imprevisível. Esta função corrige isso, zerando o contador antes de qualquer uso.

Três pontos para fixar sobre esta função:

1. Parâmetro LISTA* l

Recebe o endereço da lista, não uma cópia. Sem o *, a função zeraria uma cópia local e a lista original permaneceria com lixo.
2. Operador ->

Como l é um ponteiro, acessamos campos com -> em vez de .. l->nroElem é idêntico a (*l).nroElem.
3. Array não é zerado

Só nroElem recebe 0. O array A ainda contém lixo — mas as funções ignoram qualquer posição além de nroElem, então isso não causa problema.
Chamada correta no main: inicializarLista(&lista); — o & obtém o endereço de lista e o passa para o ponteiro l.

Bloco 4 — exibirLista

/* Exibição da lista sequencial */
void exibirLista(LISTA* l) {
  int i;
  printf("Lista: \" ");
  for (i = 0; i < l->nroElem; i++)
    printf("%i ", l->A[i].chave); // só lembrando TIPOCHAVE = int
  printf("\"\n");
} /* exibirLista */

O for percorre exatamente os nroElem primeiros índices do array — de 0 até nroElem - 1. A condição i < l->nroElem garante que nunca acessamos posições além dos elementos válidos.

Formato da saída: A função imprime Lista: " 9 3 12 4 1 " — as aspas fazem parte da saída formatada com \" (aspas escapadas dentro de string). O especificador %i é equivalente a %d para inteiros.
Acesso a l->A[i].chave: Esta expressão percorre dois níveis de acesso. Primeiro l->A[i] obtém o REGISTRO na posição i. Depois .chave acessa o campo chave desse registro. O -> é usado porque l é ponteiro; o . é usado porque A[i] é um valor (não um ponteiro).

Bloco 5 — tamanho e tamanhoEmBytes

/* Retornar o tamanho da lista (numero de elementos "validos") */
int tamanho(LISTA* l) {
  return l->nroElem;
} /* tamanho */

/* Retornar o tamanho em bytes da lista. Neste caso, isto nao depende do numero
   de elementos que estao sendo usados, pois a alocacao de memoria eh estatica.
   A priori, nao precisariamos do ponteiro para a lista, vamos utiliza-lo apenas
   porque teremos as mesmas funcoes para listas ligadas. */
int tamanhoEmBytes(LISTA* l) {
  return sizeof(LISTA);
} /* tamanhoEmBytes */
FunçãoO que medeVaria com inserções?Equivalente Java
tamanho() Elementos em uso (nroElem) ✅ Sim — cresce e decresce arrayList.size()
tamanhoEmBytes() Bytes ocupados pela struct LISTA na memória ❌ Não — alocação estática fixa Sem equivalente direto
Por que tamanhoEmBytes recebe LISTA* l se não usa l? O comentário original explica: "A priori, não precisaríamos do ponteiro para a lista, vamos utilizá-lo apenas porque teremos as mesmas funções para listas ligadas." É uma decisão de design para manter a assinatura consistente entre os dois tipos de lista. sizeof(LISTA) é avaliado em tempo de compilação — não depende de l.
Saída real do programa: Tamanho da lista (em bytes): 208. — com MAX=50 e REGISTRO contendo apenas um int (4 bytes), temos 51 × 4 + 4 = 208 bytes: 51 registros (MAX+1) × 4 bytes cada, mais 4 bytes do campo nroElem.

Bloco 6 — primeiroElem, ultimoElem e enesimoElem

/* Retornar a chave do primeiro elemento (caso haja) e ERRO caso vazia */
TIPOCHAVE primeiroElem(LISTA* l) {
  if (l->nroElem > 0) return l->A[0].chave;
  else return ERRO; // lista vazia
} /* primeiroElem */

/* Retornar a chave do ultimo elemento (caso haja) e ERRO caso vazia */
TIPOCHAVE ultimoElem(LISTA* l) {
  if (l->nroElem > 0) return l->A[l->nroElem - 1].chave;
  else return ERRO; // lista vazia
} /* ultimoElem */

/* Retornar a chave do elemento na posição n (0 a MAX-1) */
TIPOCHAVE enesimoElem(LISTA* l, int n) {
  if ((n >= 0) && (n < l->nroElem)) return l->A[n].chave;
  else return ERRO;
} /* enesimoElem */

As três funções seguem o mesmo padrão de proteção: verificam se o acesso é válido antes de acessar o array. Compare os três casos de acesso:

FunçãoÍndice acessadoCondição de validade
primeiroElemA[0]nroElem > 0 (lista não vazia)
ultimoElemA[nroElem - 1]nroElem > 0 (lista não vazia)
enesimoElemA[n]n >= 0 e n < nroElem
Por que nroElem - 1 em ultimoElem? Com 5 elementos, eles ocupam os índices 0, 1, 2, 3 e 4. O último está em nroElem - 1 = 4. Usar A[nroElem] acessaria a posição da sentinela — fora dos elementos válidos.

Bloco 7 — reinicializarLista

/* Reinicializar a estrutura */
void reinicializarLista(LISTA* l) {
  l->nroElem = 0;
} /* reinicializarLista */

O código é idêntico ao de inicializarLista. A diferença está na intenção semântica: inicializarLista é chamada na primeira vez, quando a lista acaba de ser criada; reinicializarLista é chamada para esvaziar uma lista que já estava em uso.

Nenhum dado é apagado de verdade. Os elementos ainda existem no array A, mas como nroElem voltou a 0, todas as funções os ignoram completamente. Na próxima inserção, a posição 0 será sobrescrita — os dados antigos desaparecem naturalmente. Em Java, arrayList.clear() faz o equivalente (e adicionalmente permite que o garbage collector libere os objetos).

Bloco 8 — inserirElemLista

/* Inserção "direta" na iésima posição (posicao i do arranjo A).
   Funciona da mesma maneira de um insertionSort: deve-se deslocar todos os
   elementos a partir da iesima posicao e entao se insere o novo elemento. */
bool inserirElemLista(LISTA* l, REGISTRO reg, int i) {
  int j;
  if ((l->nroElem >= MAX) || (i < 0) || (i > l->nroElem))
    return false; // lista cheia ou índice inválido
  for (j = l->nroElem; j > i; j--) l->A[j] = l->A[j-1];
  l->A[i] = reg;
  l->nroElem++;
  return true;
} /* inserirElemLista */

Esta é a função mais complexa do programa. Disseque cada parte:

Guarda de entrada (linha 3)

Três condições combinadas com ||:
• nroElem >= MAX → lista cheia
• i < 0 → índice negativo inválido
• i > nroElem → índice além do último elemento+1

Se qualquer uma for verdadeira, retorna false imediatamente.
Loop de deslocamento (linha 4)

for (j = nroElem; j > i; j--)
Começa no fim e anda para a esquerda. Cada iteração copia A[j-1] para A[j] — abrindo espaço na posição i. Obrigatoriamente da direita para a esquerda para não sobrescrever dados antes de copiá-los.
Inserção e incremento (linhas 5–6)

A[i] = reg — coloca o novo registro na posição liberada.
nroElem++ — atualiza o contador. Sem isso, a lista "não saberia" que ganhou um elemento.

Passo a passo para inserir chave 12 na posição i=2 da lista [9, 3, 4, 1] (nroElem=4):

jOperaçãoA[0]A[1]A[2]A[3]A[4]
—Estado inicial9341—
4A[4] ← A[3]93411
3A[3] ← A[2]93441
2j > i falso — para93441
—A[2] = 12, nroElem=5931241

Bloco 9 — buscaSequencial

/* Busca sequencial em lista ordenada ou não SEM SENTINELA */
int buscaSequencial(LISTA* l, TIPOCHAVE ch) {
  int i = 0;
  while (i < l->nroElem) {
    if (ch == l->A[i].chave) return i; // achou
    else i++;
  }
  return ERRO; // não achou
} /* buscaSequencial */

A lógica é direta: percorre do índice 0 ao nroElem-1, comparando a chave procurada ch com a chave de cada elemento. Dois caminhos de saída:

✅ Encontrou

return i — retorna imediatamente o índice onde a chave foi encontrada, sem continuar percorrendo o restante da lista.
❌ Não encontrou

O while termina quando i == nroElem. A execução sai do loop e cai no return ERRO, que vale -1.
Duas verificações por iteração: O while checa i < nroElem (limite) e o if checa a igualdade da chave. Para n elementos não encontrados, são feitas 2n comparações no total. A buscaSentinela reduz isso a n+1.
Parâmetro TIPOCHAVE ch: ch é o valor que se quer encontrar — a "chave alvo". Dentro do while, l->A[i].chave é a chave do elemento na posição i. A comparação ch == l->A[i].chave verifica se são iguais.

Bloco 10 — excluirElemLista

/* Exclusão do elemento cuja chave seja igual a ch */
bool excluirElemLista(LISTA* l, TIPOCHAVE ch) {
  int pos, j;
  pos = buscaSequencial(l, ch);
  if (pos == ERRO) return false; // não existe
  for (j = pos; j < l->nroElem - 1; j++) l->A[j] = l->A[j+1];
  l->nroElem--;
  return true;
} /* excluirElemLista */

A função tem três etapas encadeadas:

Etapa 1 — Localizar

Reutiliza buscaSequencial para encontrar a posição do elemento. Se retornar ERRO, o elemento não existe e a função retorna false sem modificar nada.
Etapa 2 — Deslocar para a esquerda

for (j = pos; j < nroElem-1; j++) A[j] = A[j+1]
O oposto da inserção: agora deslocamos da posição removida até o final, copiando cada elemento uma casa para a esquerda. Isso "fecha o buraco".
Etapa 3 — Decrementar

nroElem-- — reduz o contador. O dado ainda existe no array mas é ignorado pelas funções a partir de agora.

Visualizando a exclusão do elemento com chave 4 (posição 3) da lista [9, 3, 12, 4, 1]:

jOperaçãoA[0]A[1]A[2]A[3]A[4]
—Estado inicial931241
3A[3] ← A[4]931211
4j < nroElem-1 falso — para931211
—nroElem-- (4)93121ignorado
Reutilização de código: excluirElemLista não reimplementa a busca — ela chama buscaSequencial. Esse é um princípio importante de programação: funções devem se construir sobre funções já existentes. O resultado é código mais curto, mais legível e mais fácil de corrigir.

Bloco 11 — buscaSentinela

/* Busca sequencial em lista COM SENTINELA (vetor criado com MAX+1 posições) */
int buscaSentinela(LISTA* l, TIPOCHAVE ch) {
  int i = 0;
  l->A[l->nroElem].chave = ch; // sentinela
  while (l->A[i].chave != ch) i++;
  if (i > l->nroElem - 1) return ERRO; // não achou
  else return i;
} /* buscaSentinela */

Compare com a buscaSequencial: o while original tinha duas condições. Esta versão tem apenas uma:

Sem sentinelaCom sentinela
Condição do loopi < nroElem && A[i] != chA[i].chave != ch
Verificações/iteração21
Como terminaLimite atingido ou chave encontradaSempre encontra (real ou sentinela)
Confirmação finalNão precisaif (i > nroElem-1) — parou na sentinela?
O truque da sentinela passo a passo:

1. l->A[l->nroElem].chave = ch — coloca a chave procurada na posição A[MAX] (a posição extra reservada).
2. O while avança até encontrar ch. Como a sentinela garante que ch existe em algum ponto, o loop sempre para — sem risco de ultrapassar os limites.
3. Após o loop, verificamos se parou numa posição válida (i <= nroElem-1) ou na sentinela (i == nroElem).
Sentinela modifica a lista! A linha l->A[l->nroElem].chave = ch escreve na posição A[nroElem] — que existe graças ao MAX+1, mas está além dos elementos válidos. Isso é intencional e seguro, pois nunca sobrescreve dados reais. Porém é uma escrita na memória da lista, não apenas uma leitura.

Bloco 12 — main(): o Programa em Ação

O main exercita todas as funções em sequência. Vamos percorrê-lo em seções:

Seção A — Inicialização e estado inicial

LISTA lista;
inicializarLista(&lista);
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n",  tamanhoEmBytes(&lista));

Lista: " "

Numero de elementos na lista: 0.

Tamanho da lista (em bytes): 208.

LISTA lista; declara a struct na stack. inicializarLista(&lista) zera nroElem. A lista exibida está vazia e já ocupa 208 bytes — a memória é reservada integralmente desde a declaração.

Seção B — Inserções

REGISTRO reg;
reg.chave = 9;
inserirElemLista(&lista, reg, 0);  // lista: [9]
exibirLista(&lista);

reg.chave = 3;  inserirElemLista(&lista, reg, 1);  // [9, 3]
reg.chave = 4;  inserirElemLista(&lista, reg, 2);  // [9, 3, 4]
reg.chave = 1;  inserirElemLista(&lista, reg, 3);  // [9, 3, 4, 1]
reg.chave = 12; inserirElemLista(&lista, reg, 2);  // [9, 3, 12, 4, 1] ← inserção no meio!
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n",  tamanhoEmBytes(&lista));

Lista: " 9 "

Lista: " 9 3 12 4 1 "

Numero de elementos na lista: 5.

Tamanho da lista (em bytes): 208.

Atenção à inserção do 12 na posição 2: Nesse ponto a lista era [9, 3, 4, 1]. Inserir 12 na posição 2 desloca 4 e 1 para a direita, resultando em [9, 3, 12, 4, 1]. Note que o tamanho em bytes continua 208 — a alocação nunca muda.

Seção C — Buscas

printf("Chave 4 encontrada na posicao: %i do arranjo A.\n", buscaSequencial(&lista, 4));
printf("Chave 4 encontrada na posicao: %i do arranjo A.\n", buscaSentinela(&lista, 4));

Chave 4 encontrada na posicao: 3 do arranjo A.

Chave 4 encontrada na posicao: 3 do arranjo A.

Ambas as funções encontram o mesmo elemento e retornam o mesmo índice (3). A lista nesse ponto é [9, 3, 12, 4, 1] — o elemento 4 está no índice 3.

Seção D — Exclusões

if (excluirElemLista(&lista, 4)) printf("Exclusao bem sucedida: 4.\n");
if (excluirElemLista(&lista, 8)) printf("Exclusao bem sucedida: 8.\n"); // 8 não existe!
if (excluirElemLista(&lista, 9)) printf("Exclusao bem sucedida: 9.\n");
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n",  tamanhoEmBytes(&lista));

Exclusao bem sucedida: 4.

Exclusao bem sucedida: 9.

Lista: " 3 12 1 "

Numero de elementos na lista: 3.

Tamanho da lista (em bytes): 208.

A exclusão de 8 não gera saída porque excluirElemLista retorna false quando o elemento não existe. O if avalia false e a mensagem não é impressa. Esse é o uso inteligente do valor de retorno bool — o chamador pode tomar decisões baseado no sucesso ou falha da operação.

Seção E — Reinicialização

reinicializarLista(&lista);
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n",  tamanhoEmBytes(&lista));
return 0;

Lista: " "

Numero de elementos na lista: 0.

Tamanho da lista (em bytes): 208.

A lista volta ao estado inicial. Os dados [3, 12, 1] ainda existem fisicamente no array, mas nroElem = 0 faz com que todas as funções os ignorem completamente.

Saída Completa do Programa

O comentário ao final do arquivo registra a saída real de uma execução:

Lista: " "

Numero de elementos na lista: 0.

Tamanho da lista (em bytes): 208.

Lista: " 9 "

Lista: " 9 3 12 4 1 "

Numero de elementos na lista: 5.

Tamanho da lista (em bytes): 208.

Chave 4 encontrada na posicao: 3 do arranjo A.

Chave 4 encontrada na posicao: 3 do arranjo A.

Exclusao bem sucedida: 4.

Exclusao bem sucedida: 9.

Lista: " 3 12 1 "

Numero de elementos na lista: 3.

Tamanho da lista (em bytes): 208.

Lista: " "

Numero de elementos na lista: 0.

Tamanho da lista (em bytes): 208.

Exercícios — Aula 36

Questões sobre o programa completo listaSequencial.c:

1. Por que o programa inclui <stdbool.h>?

2. O programa registra: Tamanho da lista (em bytes): 208. Com MAX=50 e REGISTRO contendo apenas um int (4 bytes), como esse valor é calculado?

3. No main, a exclusão do elemento 8 não imprime mensagem. Por quê?

4. Qual é o estado da lista após as 5 inserções do main e por que o 12 está na posição 2?

5. Por que tamanhoEmBytes recebe LISTA* l se não usa o ponteiro internamente?

6. Em buscaSentinela, o que acontece se a chave procurada já existir na lista?

7. Por que o loop de deslocamento em inserirElemLista percorre de j = nroElem até j = i+1 (direita para esquerda), e não ao contrário?

8. Após reinicializarLista(&lista), o que acontece com os dados [3, 12, 1] que estavam no array?