Estruturas e estruturas de dados encadeadas em C

Aprenda a agrupar dados relacionados com estruturas e typedef, dar nomes a conjuntos de valores com enumerações e armazenar alternativas em uniões. Depois, implemente as estruturas de dados encadeadas clássicas com nós alocados com malloc: listas encadeadas, pilhas, filas, árvores binárias de busca e tabelas de espalhamento. Em cada uma, você insere, encontra, remove e percorre itens, trata os casos de estrutura vazia e os casos-limite e libera todos os nós sem vazamentos.

BásicoAula 6 de 8 do curso Curso de Programação em C

O que você vai aprender

  1. Defina tipos de estrutura, dê nomes curtos a eles com typedef, acesse seus membros por meio de variáveis e ponteiros e mantenha vários registros em arranjos de estruturas.
    • Defina uma estrutura e trabalhe com seus membros
      Declare um tipo de estrutura, crie e inicialize variáveis de estrutura, leia e altere membros e compreenda como funcionam a cópia, a comparação e a disposição na memória.
    • Dê nomes a tipos com typedef e passe estruturas para funções
      Crie nomes alternativos para tipos com typedef, passe estruturas por valor ou por ponteiros, use o operador seta e aloquel estruturas no heap.
    • Armazene e processe registros em arranjos de estruturas
      Mantenha vários registros em um arranjo de estruturas, percorra-os com laços, encontre e ordene registros, amplie o arranjo no heap e manipule estruturas responsáveis por outras áreas de memória.
  2. Substitua números mágicos por enumerações e use uniões e uniões etiquetadas para armazenar um entre vários tipos de valor na mesma memória.
    • Use enumerações para conjuntos de valores nomeados
      Defina enumerações, controle seus valores, use-as em condições e instruções switch e saiba o que C verifica e o que não verifica sobre elas.
    • Sobreponha alternativas de dados com uniões e uniões etiquetadas
      Declare uniões, saiba o que acontece ao ler e escrever seus membros, combine uma união com uma etiqueta de enumeração para armazenar valores de tipos diferentes com segurança e agrupe pequenos indicadores em campos de bits.
  3. Encadeie nós alocados dinamicamente em listas, percorra-as, insira e remova nós em qualquer posição, libere-os completamente e decida quando uma lista é mais adequada que um arranjo para uma tarefa.
    • Crie, percorra e libere uma lista simplesmente encadeada
      Defina um tipo de nó, adicione nós ao início e ao fim de uma lista, visite todos os nós, escreva funções que alterem o ponteiro para o início da lista e libere todos os nós.
    • Busque nós, insira-os em ordem e remova-os
      Encontre um valor em uma lista, insira um nó na posição correta de uma lista ordenada e desencadeie e libere um nó, tratando o primeiro nó, o último nó e valores ausentes.
    • Compare listas com arranjos e amplie as listas
      Avalie listas encadeadas em relação a arranjos, mantenha um ponteiro para o fim e uma contagem, implemente listas duplamente encadeadas e libere nós responsáveis por outras áreas de memória.
  4. Implemente pilhas em que o último a entrar é o primeiro a sair e filas em que o primeiro a entrar é o primeiro a sair, usando arranjos e nós encadeados, previna operações em estruturas vazias ou cheias e aplique-as a tarefas reais.
    • Implemente uma pilha e utilize-a
      Descreva as operações de pilha, implemente uma pilha com um arranjo e com nós encadeados, trate pilhas vazias e cheias e use pilhas para verificar pares de delimitadores e avaliar expressões pós-fixadas.
    • Implemente uma fila com nós encadeados ou um buffer circular
      Descreva as operações de fila, implemente uma fila com ponteiros para o início e o fim e com um arranjo circular de tamanho fixo, e diferencie uma fila vazia de uma cheia.
  5. Represente árvores binárias com nós encadeados, busque, insira e remova chaves em uma árvore binária de busca, visite os nós em ordens diferentes e libere uma árvore inteira.
    • Represente uma árvore binária e faça buscas em uma árvore binária de busca
      Use o vocabulário das árvores binárias, enuncie a propriedade da árvore binária de busca e faça buscas em uma árvore por recursão e com um laço.
    • Insira, percorra, remova e libere nós de árvores
      Insira chaves em uma BST, visite nós em ordem, em pré-ordem e em pós-ordem, calcule tamanho e altura, remova chaves e libere todos os nós.
  6. Transforme chaves em índices de baldes com uma função de espalhamento, trate colisões com cadeias de nós e implemente, use e libere uma tabela de espalhamento em C.
    • Mapeie chaves para baldes com uma função de espalhamento
      Explique o que uma função de espalhamento faz, calcule índices de baldes para chaves que são cadeias de caracteres, reconheça colisões e distribuições desiguais e use o fator de carga para decidir quando ampliar uma tabela.
    • Implemente uma tabela de espalhamento com encadeamento em C
      Implemente uma tabela de espalhamento a partir de um arranjo de baldes com listas encadeadas: busque, insira e conte chaves, copie com segurança chaves que são cadeias de caracteres e libere todas as alocações.

Guia de estudo de Estruturas de dados em C

6 capítulos

1

Agrupe dados relacionados em estruturas

Valores relacionados são mais fáceis de manipular quando seguem juntos como um registro. Estruturas dão nomes a esses valores, e nomes alternativos para tipos tornam as declarações mais fáceis de ler. As chamadas de função trazem então uma escolha prática: passar uma cópia ou um endereço que dá acesso ao original. Arranjos estendem essa escolha a vários registros, em que ordenação, redimensionamento e cópia de membros ponteiro exigem clareza sobre quais objetos ainda existem e qual código é responsável por liberá-los.

1.1 Membros, cópias e disposição de estruturas

Uma estrutura agrupa valores relacionados em um registro. Seus membros têm nomes e podem ter tipos diferentes. Uma leitura de sensor, por exemplo, precisa de um identificador e de uma temperatura.

Defina e inicialize um registro
struct reading {
    int id;
    double temp_c;
};
struct reading r = {.temp_c = 21.5, .id = 7};
r.temp_c += 0.5;

A definição apresenta o tipo struct reading; reading é sua etiqueta. O ponto e vírgula final fica depois da chave de fechamento. Definir o tipo não aloca um objeto. A declaração de r cria um objeto, e . seleciona seus membros. Aqui a temperatura passa a 22,0 °C. Quando apenas uma etiqueta foi declarada, o nome do tipo inclui struct: reading r; precisa de um typedef separado.

Um inicializador como {7, 21.5} segue a ordem de declaração. Designadores como .id = 7 permitem nomear membros em outra ordem. Membros omitidos de um inicializador recebem valores zero, então {0} inicializa todo o registro. Uma declaração local struct reading r; sem inicializador deixa seus membros indeterminados até que recebam valores.

Um membro tem seu próprio tipo e endereço. Para ler uma temperatura com scanf, use scanf("%lf", &r.temp_c) e verifique se o valor de retorno é 1. O ponto tem precedência sobre &, então isso passa o endereço do membro double.

Copie e compare os valores
struct reading s = r;
s.temp_c = -4.0;
int same = r.id == s.id && r.temp_c == s.temp_c;

A atribuição copia todos os membros. Alterar s.temp_c deixa r.temp_c em 22,0. Arranjos fixos dentro de estruturas também são copiados: um membro char name[32] pertence ao registro. Isso funciona embora arranjos independentes em C não possam ser atribuídos com =. Membros ponteiro exigem cuidado extra, pois o ponteiro copiado continua apontando para o mesmo objeto.

C não define == para estruturas inteiras. Compare os membros que definem a igualdade para sua tarefa. memcmp compara os bytes diretamente, incluindo o preenchimento, e pode indicar uma diferença entre registros cujos valores de membros são iguais.

Entenda a disposição na memória

Os membros aparecem na ordem de declaração, sem preenchimento antes do primeiro membro. O compilador pode adicionar bytes não usados entre os membros e no fim para atender aos requisitos de alinhamento. Em uma plataforma comum de 64 bits com int de 4 bytes e alinhamento de 4 bytes, struct { char c; int i; char d; } ocupa 12 bytes. Seus membros começam nos deslocamentos 0, 4 e 8. Reordenar para int i; char c; char d; normalmente reduz o tamanho para 8 bytes. Use sizeof para o tamanho real e offsetof de <stddef.h> para um deslocamento.

Estruturas podem ser aninhadas. Com struct point { double x_m, y_m; }; e struct rect { struct point corner; double width_m, height_m; };, box.corner.x_m acessa a coordenada x do canto em metros. Uma estrutura não pode conter um membro completo de seu próprio tipo: isso exigiria um objeto infinitamente grande. Um ponteiro para seu próprio tipo tem tamanho fixo e torna possíveis os nós encadeados.

Principais conclusões

  • Use . para selecionar um membro de um objeto de estrutura.
  • Um inicializador zera os membros omitidos; uma estrutura local não inicializada tem membros indeterminados.
  • A atribuição de uma estrutura inteira copia seus membros, incluindo arranjos incorporados.
  • Compare valores dos membros e use sizeof para incluir o preenchimento.
  • Uma estrutura pode conter um ponteiro para seu próprio tipo.

1.2 Nomes de tipos, chamadas de função e ponteiros para estruturas

Uma declaração typedef dá outro nome a um tipo existente. Ela muda como você escreve o tipo, não como seus objetos se comportam. typedef double meters; torna meters outro nome para double.

Dê nome a uma estrutura e passe-a por valor
typedef struct {
    int id;
    double temp_c;
} reading;

reading make_reading(int id, double t) {
    reading r = {id, t};
    return r;
}
void warm_copy(reading r) {
    r.temp_c += 1.0;
}

Agora você pode declarar reading r = make_reading(7, 21.5);. Retornar uma estrutura retorna seu valor. Chamar warm_copy(r) fornece uma cópia à função, então a temperatura da função chamadora permanece em 21,5 °C. Uma cópia de um registro pequeno costuma ser a interface mais clara quando o objeto da função chamadora deve permanecer inalterado.

Um tipo autorreferencial precisa de sua etiqueta dentro da definição:

typedef struct node {
    int number;
    struct node *next;
} node;

O nome alternativo node fica disponível após a declaração. Dentro dessas chaves, struct node já nomeia o tipo incompleto e pode ser usado para um ponteiro. A etiqueta e o nome alternativo podem ter a mesma grafia porque etiquetas usam seu próprio espaço de nomes.

Acesse o objeto da função chamadora por um ponteiro
void warm(reading *p) {
    p->temp_c += 1.0;
}
reading r = {7, 21.5};
reading *p = &r;
p->temp_c -= 1.5;
warm(&r);

A subtração altera r.temp_c para 20,0; warm então o altera para 21,0. A função recebe uma cópia do endereço, e os dois ponteiros continuam acessando o mesmo registro. Use -> com um ponteiro e . com um objeto de estrutura. p->temp_c significa (*p).temp_c. Os parênteses são necessários porque o acesso a membro tem precedência maior que a desreferenciação: *p.temp_c tenta selecionar um membro de p antes de aplicar *.

Para ler sem copiar a estrutura, receba const reading *p. Uma atribuição a p->temp_c por meio desse ponteiro é rejeitada. A variável ponteiro ainda pode mudar, por exemplo, ao percorrer um arranjo. Já reading *const p torna fixo o próprio ponteiro. Para estruturas com membros ponteiro, um ponteiro para dados constantes não torna somente para leitura os dados apontados que foram alocados separadamente.

Atribua a alguém a responsabilidade por um registro no heap

Inclua <stdlib.h> para alocação e <stdbool.h> para este resultado de sucesso:

bool make_heap_reading(reading **out, int id, double t) {
    reading *p = malloc(sizeof *p);
    if (p == NULL) return false;
    *p = (reading){.id = id, .temp_c = t};
    *out = p;
    return true;
}

A função escreve em *out somente após a alocação ser bem-sucedida. A função chamadora pode declarar reading *item = NULL;, chamar make_heap_reading(&item, 8, -2.0), tratar um resultado falso e depois chamar free(item). sizeof *p mede o tipo apontado mesmo antes de p apontar para um registro inicializado; essa expressão não lê o registro.

Um ponteiro para uma estrutura local automática fica pendente quando a função retorna. Retorne o valor da estrutura ou uma alocação no heap com responsabilidade de liberação bem definida. Retornar &r de uma função cuja variável local é r não pode estender seu tempo de vida.

Principais conclusões

  • typedef cria um nome alternativo, e uma definição autorreferencial usa sua etiqueta de estrutura.
  • Passe uma estrutura por valor para obter uma cópia ou passe seu endereço para alterar o original.
  • p->member é equivalente a (*p).member.
  • Um ponteiro para dados constantes impede alterações dos membros por meio desse ponteiro.
  • Verifique a alocação antes da inicialização e atribua a responsabilidade pela liberação de cada registro no heap.
  • Retornar uma estrutura local por valor é válido; retornar seu endereço deixa um ponteiro pendente.

1.3 Arranjos de registros e cadeias sob responsabilidade dos registros

Um arranjo de estruturas mantém registros lado a lado. Use um índice para selecionar um registro e depois um ponto para selecionar um membro. Isso faz com que percursos, buscas e ordenações funcionem de forma semelhante ao que ocorre com um arranjo de números.

Percorra registros com uma contagem explícita
reading readings[4] = {
    {1, 21.5}, {2, -3.0}, {3, 18.25}, {4, 30.0}
};
size_t n = sizeof readings / sizeof readings[0];
size_t best = 0;
for (size_t i = 1; i < n; ++i) {
    if (readings[i].temp_c > readings[best].temp_c)
        best = i;
}

Aqui, reading é o nome alternativo da estrutura com identificador e temperatura. O melhor índice passa de 0 para 3, então o registro com a maior temperatura tem id 4 e temperatura de 30,0 °C. Começar com um elemento real também funciona quando todas as temperaturas são negativas; começar com um suposto máximo de zero pode falhar. Esse percurso exige pelo menos um elemento. Uma busca reutilizável deve informar que o arranjo está vazio antes de ler o elemento 0.

A contagem com sizeof funciona onde readings é um arranjo. Em um parâmetro de função, uma declaração de arranjo se torna um ponteiro, então passe n separadamente. (items + i)->temp_c e items[i].temp_c acessam o mesmo membro.

Forneça uma função de comparação para qsort

Inclua <stdlib.h>. Para valores finitos de temperatura, esta função de comparação retorna um sinal sem converter uma pequena diferença de temperatura em inteiro:

int cmp_temp(const void *a, const void *b) {
    const reading *x = a;
    const reading *y = b;
    return (x->temp_c > y->temp_c)
         - (x->temp_c < y->temp_c);
}
/* With the array above: */
qsort(readings, n, sizeof readings[0], cmp_temp);

A função de comparação recebe endereços de elementos. Negativo significa que o primeiro vem antes, zero significa equivalência e positivo significa que vem depois. A ordem dos ids após a ordenação é 2, 3, 1, 4. Retornar a diferença double diretamente trunca uma diferença como −0,25 para zero. Subtrair chaves inteiras pode causar estouro. Comparar primeiro evita esses erros. qsort não preserva a ordem original dos elementos equivalentes.

Amplie um arranjo sem perder seu armazenamento

Mantenha separadamente a quantidade de elementos em uso e a capacidade. Para um arranjo alocado com cap positivo, use um ponteiro temporário ao dobrar a capacidade. Inclua <stdint.h> para SIZE_MAX:

if (cap > SIZE_MAX / 2 / sizeof *items) return false;
size_t new_cap = 2 * cap;
reading *tmp = realloc(items, new_cap * sizeof *items);
if (tmp == NULL) return false;
items = tmp;
cap = new_cap;

Essas instruções pertencem a uma função que retorna bool; items é seu ponteiro para o arranjo e cap é sua capacidade positiva. Em caso de falha, a alocação antiga permanece disponível. Em caso de sucesso, a alocação pode mudar de lugar, então ponteiros antigos para seus elementos precisam ser substituídos. Uma capacidade inicial zero exige sua própria alocação inicial.

Determine a responsabilidade de uma cópia

Com typedef struct { char *name; int age; } person;, person b = a; copia o endereço em name. Os dois registros acessam a mesma cadeia de caracteres. Liberar as duas cópias como responsáveis pela memória liberaria a mesma alocação duas vezes. Para uma cópia independente, aloquel strlen(a.name) + 1 bytes, verifique se a alocação teve sucesso e use strcpy de <string.h>. Um membro fixo char name[32] é copiado com a estrutura. strdup faz a alocação e a cópia, mas pertence a POSIX e C23, não à biblioteca padrão C17.

Principais conclusões

  • Use items[i].member e passe uma contagem para funções que recebem arranjos.
  • Inicialize uma busca pelo máximo com um elemento existente após verificar se o arranjo está vazio.
  • Uma função de comparação de qsort retorna um sinal; subtrair chaves pode causar truncamento ou estouro.
  • Verifique a aritmética do tamanho de alocação e armazene o resultado de realloc em um ponteiro temporário.
  • Um membro ponteiro copiado continua se referindo à alocação original.
2

Dê nomes às opções com enumerações e compartilhe armazenamento com uniões

Um programa frequentemente precisa de uma opção com um nome compreensível ou de um valor que possa assumir várias formas diferentes. Enumerações nomeiam as opções; uniões compartilham armazenamento entre alternativas. Combiná-las faz com que cada valor armazenado descreva seu próprio tipo. O essencial é manter o nome e os dados associados consistentes, reconhecendo que C deixa a validação da entrada e as escolhas de representação sob controle do programa.

2.1 Valores de enumeração e opções verificadas

Uma enumeração dá nomes compreensíveis a opções inteiras. Nomes como HIGH tornam visível o propósito de uma condição sem exigir que a pessoa lembre o que o número 10 representa.

Acompanhe a numeração
enum weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
enum level { LOW = 1, MEDIUM, HIGH = 10, CRITICAL };
enum level alert = HIGH;

Um primeiro valor omitido é zero. Valores omitidos posteriores aumentam o valor anterior em um. Assim, MON é 0 e SUN é 6; os níveis são 1, 2, 10 e 11. Um valor explícito reinicia a contagem naquele ponto. Constantes diferentes podem compartilhar um valor, mas valores idênticos não podem aparecer como rótulos case separados no mesmo switch.

Um nome alternativo pode encurtar uma declaração: typedef enum { RED, YELLOW, GREEN } light; permite light signal = RED;. Constantes de enumeração compartilham o espaço de nomes dos identificadores comuns em seu escopo. Duas enumerações diferentes não podem ambas declarar RED no mesmo escopo. Prefixos como em LIGHT_RED e PAINT_RED mantêm os nomes distintos.

Trate cada opção
const char *level_name(enum level value) {
    switch (value) {
        case LOW: return "low";
        case MEDIUM: return "medium";
        case HIGH: return "high";
        case CRITICAL: return "critical";
    }
    return "unknown";
}

Cada return encerra seu caso. Se um caso realizar uma ação e depois precisar parar, inclua um break; chegar ao próximo rótulo faz a execução continuar nele. Com GCC, -Wall ativa avisos para casos de enumeração ausentes quando não há default; o Clang também diagnostica essas omissões por padrão. Um aviso pode revelar um caso ausente quando você adicionar uma constante posteriormente.

Valide a entrada como um conjunto de opções

O tipo de enumeração não obriga o valor a pertencer ao conjunto nomeado. C17 permite atribuição a partir de um inteiro, então enum level x = 42; pode compilar. Uma verificação de que o número está entre 1 e 11 ainda admite valores sem nome, como 6. Teste os valores realmente permitidos:

#include <stdbool.h>
bool valid_level(int value) {
    return value == LOW || value == MEDIUM
        || value == HIGH || value == CRITICAL;
}

Somente após essa verificação a entrada deve se tornar um nível. Para os dias consecutivos da semana, uma verificação de faixa de MON a SUN é suficiente. Em uma enumeração comum de C17, as constantes enumeradoras têm tipo int. C23 também permite especificar um tipo subjacente, como em enum color : unsigned char; essa sintaxe não pertence a C17.

Imprima nomes e use contagens

printf("%d\n", SUN) imprime “6”, não “SUN”. Para valores consecutivos que começam em zero, um arranjo de nomes fornece o texto: const char *day_names[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};. Após validar um índice, day_names[SUN] é “Sun”. Usar um valor de nível como HIGH para indexar esse arranjo exigiria outro mapeamento, pois os valores de nível têm lacunas.

Adicionar DAY_COUNT imediatamente após SUN lhe dá o valor 7, útil como limite de laço ou tamanho de arranjo. Ele conta os dias, mas não é um dia válido. Nomes de enumeração seguem as regras de escopo de C e podem ser exibidos por depuradores; já #define faz substituição de texto pelo pré-processador.

Principais conclusões

  • A numeração de uma enumeração começa em zero, a menos que um valor seja fornecido, e continua após cada valor explícito.
  • Uma variável de enumeração pode conter um valor sem nome, então valide entradas externas.
  • Use break ou return para encerrar um caso quando não quiser continuar no caso seguinte.
  • Um arranjo que associa índices a nomes precisa de índices validados e consecutivos começando em zero.

2.2 Uniões com uma etiqueta correspondente

Uma união dá a vários membros o mesmo armazenamento. Ela é adequada a um valor que pode ser de um entre vários tipos. Uma estrutura dá a cada membro seu próprio armazenamento e é adequada a um registro que precisa de todos esses valores juntos.

Armazene uma alternativa
union value {
    int i;
    double d;
    char c;
};
union value v = {.d = 2.5};

Todos os membros começam no mesmo endereço. A união deve comportar seu maior membro e atender às necessidades de alinhamento; com um double de 8 bytes em plataformas comuns, essa união ocupa 8 bytes. Um inicializador simples {42} inicializa seu primeiro membro, i; .d = 2.5 escolhe d explicitamente.

Escrever v.i = 42 após o inicializador substitui a alternativa armazenada. Ler v.d depois disso não converte 42 em um double: interpreta os bytes armazenados usando outra representação. Essa representação pode ser inválida. Leia o membro correspondente ao valor que você armazenou.

Registre qual membro é válido

Combine a união com uma enumeração. A enumeração é a etiqueta, e a união contém os dados associados:

enum kind { K_INT, K_DOUBLE, K_CHAR };
struct tagged {
    enum kind kind;
    union value as;
};
struct tagged make_double(double d) {
    return (struct tagged){.kind = K_DOUBLE, .as = {.d = d}};
}

Uma função auxiliar define as duas partes juntas. O código que escreve mantém os dois campos sincronizados. Se o código escrever em t.as.i, mas deixar t.kind como K_DOUBLE, uma leitura posterior escolherá a representação errada.

#include <stdio.h>
void print_cell(struct tagged t) {
    switch (t.kind) {
        case K_INT: printf("%d", t.as.i); break;
        case K_DOUBLE: printf("%.1f", t.as.d); break;
        case K_CHAR: printf("%c", t.as.c); break;
    }
}

Chamar essa função para células contendo 42, 2,5 e 'x', com espaços impressos entre as chamadas, produz “42 2.5 x”. Cada caso escolhe tanto o membro quanto o formato correspondente. Um token de calculadora pode, de forma semelhante, conter um número ou um operador.

Em plataformas comuns de 64 bits, uma enumeração de 4 bytes, preenchimento e essa união de 8 bytes fazem struct tagged ocupar 16 bytes. Uma estrutura que armazena a enumeração e as três alternativas separadamente costuma ocupar 24 bytes. Esses são exemplos de disposição; consulte os tipos reais com sizeof.

Agrupe pequenos indicadores em campos de bits
struct flags {
    unsigned int ready : 1;
    unsigned int mode : 3;
};

A largura vem depois dos dois-pontos. Um campo sem sinal de 1 bit contém 0 ou 1; um campo de 3 bits contém valores de 0 a 7. Armazenar 9 em mode mantém os três bits menos significativos e armazena 1. Isso perde informação, então valide um modo antes de armazená-lo quando valores maiores indicarem erro.

Um campo de bits não tem uma posição endereçável separadamente, então &flags.mode é inválido para uma variável chamada flags. Sua ordem de alocação e seu empacotamento dependem da implementação. Não use uma declaração de campo de bits como garantia portável de uma disposição exata em arquivo ou dispositivo. Uma largura declarada especifica a faixa de valores do campo, não a disposição de cada byte ao seu redor.

Principais conclusões

  • Membros de uniões se sobrepõem, enquanto membros de estruturas têm armazenamento separado.
  • Uma etiqueta deve corresponder ao membro da união que contém o valor armazenado.
  • Um switch seleciona o membro e o formato de saída correspondentes.
  • A largura de campos de bits sem sinal limita os valores armazenados; bits mais significativos excedentes são perdidos.
  • A disposição de campos de bits é definida pela implementação, e os campos não têm endereço próprio.
3

Implemente e mantenha listas encadeadas

Estruturas encadeadas crescem conectando alocações separadas. Uma lista torna essas conexões visíveis: um ponteiro inicia a cadeia, e cada nó leva ao seguinte. O trabalho interessante acontece quando as ligações mudam, especialmente no primeiro ou no último nó. Manter a ligação de entrada correta permite que a mesma operação trate várias posições, enquanto ponteiros para o fim e ligações nos dois sentidos alteram quais operações precisam de uma busca.

3.1 Crie uma lista e mantenha todos os nós acessíveis

Uma lista simplesmente encadeada armazena cada item em um nó alocado separadamente. Cada nó contém seu valor e o endereço do próximo nó. O ponteiro para o início da lista aponta para o primeiro nó; NULL marca tanto uma lista vazia quanto o fim de uma cadeia.

Defina a ligação e insira no início
#include <stdbool.h>
#include <stdlib.h>
typedef struct node {
    int number;
    struct node *next;
} node;

bool prepend(node **head, int value) {
    node *n = malloc(sizeof *n);
    if (n == NULL) return false;
    n->number = value;
    n->next = *head;
    *head = n;
    return true;
}

Comece com node *list = NULL;. Chame prepend(&list, value) e trate um resultado falso. A função recebe o endereço do ponteiro para o início da lista da função chamadora. Portanto, *head designa o ponteiro que deve mudar. Receber apenas node *head e atribuir um valor a esse ponteiro local deixaria inalterado o ponteiro da função chamadora.

A ordem das atribuições finais preserva a lista antiga. Primeiro conecte o novo nó ao início antigo; depois substitua o ponteiro para o início. Inverter essas instruções faz n->next apontar para o próprio n e perde a cadeia antiga. Para as chegadas 1, 2 e 3, inserções sucessivas no início produzem 1, depois 2 → 1 e depois 3 → 2 → 1 → NULL. A inserção no início inverte a ordem de chegada.

Percorra sem mover o ponteiro que mantém a lista acessível
#include <stdio.h>
void print_list(const node *head) {
    for (const node *p = head; p != NULL; p = p->next)
        printf("%d ", p->number);
}

O ponteiro de percurso p se move enquanto head permanece disponível. O exemplo imprime “3 2 1 ” com um espaço ao final. Para uma lista vazia, o corpo do laço é executado zero vezes. Não há índice de arranjo: cada passo lê uma ligação para encontrar o próximo nó. As alocações podem estar espalhadas pela memória, e inserir um nó não move os nós existentes.

Adicione no fim
bool append(node **head, int value) {
    node *n = malloc(sizeof *n);
    if (n == NULL) return false;
    *n = (node){.number = value, .next = NULL};
    node **link = head;
    while (*link != NULL) link = &(*link)->next;
    *link = n;
    return true;
}

link começa no ponteiro para o início da lista da função chamadora. Depois passa ao endereço de cada membro next até designar a ligação nula final. Fazer a atribuição ali também funciona para uma lista vazia. Sem um ponteiro para o fim da lista armazenado, cada inserção no fim precisa primeiro seguir a cadeia existente até o final. Uma função também pode retornar um novo ponteiro para o início, desde que a função chamadora o armazene e trate falhas de alocação sem perder a lista antiga.

Libere os nós em uma ordem segura
void clear_list(node **head) {
    while (*head != NULL) {
        node *old = *head;
        *head = old->next;
        free(old);
    }
}

Leia o sucessor enquanto old ainda existe, mova o ponteiro para o início e então libere o nó antigo. Após o laço, o ponteiro para o início da função chamadora é NULL. Ler old->next após liberar old é uso após a liberação, mesmo que a memória pareça inalterada. Uma ferramenta de verificação de memória pode ajudar a revelar leituras inválidas e alocações esquecidas; a liberação também deve ocorrer após a falha de uma operação posterior.

Principais conclusões

  • Uma lista vazia tem um ponteiro nulo para o início, e o último nó tem uma ligação nula para o próximo.
  • Aloquel e inicialize um novo nó antes de alterar a cadeia existente.
  • Forneça o endereço do ponteiro para o início a funções que o alteram ou armazene o ponteiro retornado por elas.
  • Percorra com um ponteiro separado para manter a lista acessível.
  • Sem um ponteiro para o fim da lista, inserir no fim exige percorrê-la até lá.
  • Leia o sucessor antes de liberar um nó.

3.2 Altere a ligação de entrada para inserir e remover

Buscar em uma lista significa seguir uma ligação por vez. A inserção e a remoção precisam de algo mais: o endereço da ligação que deve mudar. Essa ligação pode ser o ponteiro para o início da lista ou um membro next em um nó anterior.

Busque por valor

Usando o tipo node com membros number e next:

node *find(node *head, int value) {
    for (node *p = head; p != NULL; p = p->next)
        if (p->number == value) return p;
    return NULL;
}

Uma correspondência retorna o endereço do próprio nó. Um valor ausente retorna NULL. Chegar ao 1.000º nó exige seguir 999 ligações a partir do início, mesmo quando você conhece sua posição de antemão.

Mantenha um ponteiro para a ligação que mudará

Um node ** pode apontar para o ponteiro para o início da lista ou para o campo next de um nó. Chame-o de link: *link é o nó atualmente alcançado por essa ligação. Avançar com link = &(*link)->next muda a ligação examinada; isso não altera a lista.

bool insert_sorted(node **head, int value) {
    node **link = head;
    while (*link != NULL && (*link)->number < value)
        link = &(*link)->next;
    node *n = malloc(sizeof *n);
    if (n == NULL) return false;
    n->number = value;
    n->next = *link;
    *link = n;
    return true;
}

Inclua <stdbool.h> e <stdlib.h>. A lista de entrada já deve estar em ordem crescente. Pare antes do primeiro valor que não seja menor que o novo, assim os valores duplicados entram antes dos valores iguais. O lado esquerdo de && verifica se há NULL antes de o lado direito desreferenciar o nó. Inverter esses testes desreferenciaria um ponteiro nulo no fim.

Para as inserções 5, 2, 8 e 4, a lista final é 2 → 4 → 5 → 8. Ao inserir 4 em 2 → 5 → 8, link passa do ponteiro para o início da lista para 2.next. O novo nó aponta para 5, e 2.next é redirecionado para o novo nó. Se o novo item for o menor, link ainda designa o ponteiro para o início da lista. No fim, ele designa a ligação nula final.

Desencadeie antes de liberar
bool remove_first(node **head, int value) {
    node **link = head;
    while (*link != NULL && (*link)->number != value)
        link = &(*link)->next;
    if (*link == NULL) return false;
    node *old = *link;
    *link = old->next;
    free(old);
    return true;
}

O laço encontra a ligação de entrada. Salvar seu destino em old mantém o nó disponível enquanto *link é alterado para apontar para o sucessor. Só então o nó removido é liberado. Remover 5 de 2 → 4 → 5 → 8 produz 2 → 4 → 8. Remover 2 em seguida altera o início da lista e deixa 4 → 8. Se o valor estiver ausente, a função retorna falso sem alterar a lista.

Uma versão com dois ponteiros mantém prev e cur. Quando prev é nulo, a correspondência está no início e o ponteiro para o início deve mudar; caso contrário, atualize prev->next. Esquecer o caso do início deixa um ponteiro para memória liberada. A versão com o endereço da ligação trata ambos com a mesma atribuição.

Verifique a entrada vazia, uma lista com um nó, correspondências no primeiro e no último nó e um valor ausente. Para cada alteração, identifique a ligação de entrada, o nó afetado e seu sucessor antes de escrever uma atribuição. Nenhum nó restante deve ficar inacessível, e nenhuma ligação deve apontar para um nó liberado.

Principais conclusões

  • A busca retorna um nó correspondente ou NULL ao chegar ao fim.
  • Um ponteiro para uma ligação pode designar o ponteiro para o início da lista ou um campo next.
  • A inserção ordenada liga o novo nó a seu sucessor antes de redirecionar a ligação de entrada.
  • A remoção redireciona a ligação de entrada antes de liberar o nó removido.
  • Teste os casos de início, fim, lista vazia, lista com um nó e valor ausente.

3.3 Escolha entre um arranjo, um ponteiro para o fim ou ligações nos dois sentidos

A pergunta útil é quais operações seu programa realiza. Arranjos e listas podem conter os mesmos valores, mas acessam e movem esses valores de formas diferentes.

Compare as operações
TarefaArranjoLista simplesmente encadeada
Acessar a posição 500Usar a[500] diretamenteSeguir as ligações a partir do início
Inserir após uma posição conhecidaDeslocar os elementos posterioresAlterar duas ligações após a alocação
Percorrer todos os valoresLer elementos contíguosSeguir nós possivelmente espalhados
Remover um nó conhecidoDeslocar os elementos posterioresTambém obter sua ligação de entrada

Uma lista não elimina o trabalho de encontrar uma posição. Inserir após um nó para o qual você já tem um ponteiro é direto, mas a remoção também exige o predecessor ou a ligação que aponta para o nó. Com apenas o endereço do nó, uma lista simplesmente encadeada geralmente precisa ser percorrida desde o início para encontrar essa ligação de entrada.

O armazenamento também importa. Em uma plataforma típica de 64 bits, um nó com um int de 4 bytes e um ponteiro de 8 bytes ocupa 16 bytes após o preenchimento. Para 1.000 inteiros, os nós usam 16.000 bytes mais os dados de controle das alocações; um arranjo contíguo de inteiros usa 4.000 bytes. Percorrer o arranjo costuma ser mais rápido porque valores próximos são armazenados juntos.

Mantenha os metadados da lista juntos
typedef struct {
    node *head;
    node *tail;
    size_t count;
} list_t;

Inicialize uma estrutura que envolve a lista com list_t list = {0};. As condições do estado vazio são head == NULL, tail == NULL e count == 0. Cada inserção ou remoção bem-sucedida deve atualizar a contagem e as ligações. Uma falha de alocação não altera nenhuma delas.

Com um ponteiro para o fim da lista, a inserção no fim após uma alocação verificada usa estas atribuições:

n->next = NULL;
if (list.tail == NULL) list.head = n;
else list.tail->next = n;
list.tail = n;
++list.count;

Aqui, n é um nó inicializado. Para o primeiro nó, os ponteiros para o início e o fim se tornam iguais. Remover o último nó exige recuar o ponteiro para o fim; em uma lista simplesmente encadeada, isso exige o predecessor. Remover o único nó define as duas extremidades como NULL e a contagem como zero.

Faça ligações nos dois sentidos
typedef struct dnode {
    int value;
    struct dnode *prev, *next;
} dnode;
typedef struct {
    dnode *head, *tail;
} dlist;

void unlink_node(dlist *list, dnode *d) {
    if (d->prev != NULL) d->prev->next = d->next;
    else list->head = d->next;
    if (d->next != NULL) d->next->prev = d->prev;
    else list->tail = d->prev;
    free(d);
}

Essa função recebe um nó ainda existente que pertence à lista. As ligações dos dois vizinhos são corrigidas antes de ele ser liberado. Nos casos de extremidade, altera-se o ponteiro para o início ou para o fim em vez de um vizinho ausente. Uma lista duplamente encadeada pode ser percorrida de trás para a frente e permite remover um nó conhecido sem buscar seu predecessor, ao custo de mais um ponteiro por nó e mais ligações para manter.

Libere os dados sob responsabilidade do nó antes do próprio nó

Um word_node pode ser responsável tanto pela alocação do nó quanto pela alocação de uma cadeia de caracteres acessada por word. Salve next, chame free(n->word), depois free(n) e avance para o sucessor salvo. Liberar apenas o nó causa um vazamento da cadeia. Ler n->word após liberar o nó é inválido. A responsabilidade pela liberação indica qual código deve liberar cada alocação, mesmo quando vários ponteiros podem acessá-la.

Principais conclusões

  • Arranjos oferecem acesso direto por índice e armazenamento contíguo e compacto.
  • Listas permitem alterações locais de ligações quando a posição necessária ou a ligação de entrada é conhecida.
  • Um ponteiro para o fim torna direta a inserção nessa posição, mas toda operação deve manter as duas extremidades atualizadas.
  • Nós duplamente encadeados armazenam o predecessor e o sucessor.
  • Libere os dados associados sob responsabilidade do nó antes de liberar o nó que armazena seu endereço.
  • Uma contagem armazenada deve mudar a cada inserção e remoção bem-sucedida.
4

Implemente pilhas e filas

Pilhas e filas descrevem a ordem em que o trabalho é tratado. Uma pilha mantém pronto o item inacabado mais recente, o que é adequado para aninhamento e avaliação de expressões. Uma fila mantém pronto o item que espera há mais tempo, o que é adequado à ordem de chegada. Ambas podem usar arranjos ou nós encadeados. Suas implementações devem tornar explícitos os estados vazio e cheio e manter contagens, extremidades e índices consistentes após uma alteração.

4.1 Operações de pilha e trabalho aninhado

Uma pilha remove primeiro o item adicionado mais recentemente: último a entrar, primeiro a sair (LIFO). Suas operações definem um tipo abstrato de dados independentemente de os valores serem armazenados em um arranjo ou em nós encadeados. push adiciona no topo, pop remove o topo e peek lê o topo sem removê-lo.

Use um arranjo e uma contagem
#include <stdbool.h>
#define STACK_CAP 100
typedef struct {
    int items[STACK_CAP];
    int top;
} stack;

bool push(stack *s, int value) {
    if (s->top == STACK_CAP) return false;
    s->items[s->top++] = value;
    return true;
}
bool pop(stack *s, int *out) {
    if (s->top == 0) return false;
    *out = s->items[--s->top];
    return true;
}

Inicialize com stack s = {0};. Aqui, top é o número de itens armazenados, então o índice ocupado no topo é top - 1. Uma inserção bem-sucedida escreve na próxima posição livre e aumenta a contagem. Uma remoção bem-sucedida primeiro diminui a contagem e depois lê esse índice. A função chamadora fornece um ponteiro out válido e verifica o resultado antes de usar seu valor.

Empilhar 4, 7 e 1 e depois remover duas vezes retorna 1 e 7, deixando 4. Sem uma verificação de capacidade, uma inserção na pilha cheia escreve em items[STACK_CAP] após o fim. Sem a verificação de pilha vazia, o top com sinal passa de 0 para −1 e a remoção lê items[-1]. Ambos os acessos têm comportamento indefinido.

Use o início de uma lista como topo

Uma pilha encadeada insere no topo adicionando uma alocação verificada ao início da lista. A remoção retira o primeiro nó:

bool pop_linked(node **top, int *out) {
    if (*top == NULL) return false;
    node *old = *top;
    *out = old->number;
    *top = old->next;
    free(old);
    return true;
}

node tem um number inteiro e um ponteiro next. Uma pilha encadeada não tem a capacidade fixa de um arranjo, mas uma inserção pode falhar quando a alocação falha. Ela ainda precisa de verificações de estado vazio e da liberação dos nós restantes ao final.

Verifique pares de delimitadores aninhados

Percorra a entrada da esquerda para a direita. Empilhe cada parêntese, colchete ou chave de abertura. Para cada delimitador de fechamento, retire um da pilha e verifique se os tipos correspondem. Um fechamento sem abertura falha imediatamente. Termine verificando se a pilha está vazia: aberturas restantes também indicam uma falta de correspondência.

O texto “( [ ] { } )” está balanceado. Em “( ]”, o fechamento não corresponde à abertura retirada. Em “( ( )”, uma abertura permanece após o percurso. Uma simples contagem de aberturas e fechamentos não detectaria um aninhamento incorreto como “( [ ) ]”.

Avalie expressões pós-fixadas

Empilhe os números conforme chegam. Para um operador binário, retire primeiro o operando da direita, depois o da esquerda, calcule left operator right e empilhe o resultado. Para “3 4 + 2 *”, + combina 3 e 4 em 7, depois * combina 7 e 2 em 14. Para “9 4 -”, retire 4 como operando da direita e 9 como o da esquerda, produzindo 5. Invertê-los produz −5. Rejeite uma quantidade insuficiente de operandos e exija um único resultado final; a divisão também precisa de um operando da direita diferente de zero.

Chamadas de função usam tempos de vida organizados como uma pilha: uma função chamada retorna antes que sua função chamadora continue. Cada chamada ativa precisa de um registro de ativação, então uma recursão muito profunda pode esgotar a pilha de chamadas mesmo quando as alocações no heap são bem-sucedidas.

Principais conclusões

  • LIFO significa que o item mais recente é removido primeiro.
  • Na implementação com arranjo, top conta os itens; verifique os estados vazio e cheio antes de acessar.
  • Uma pilha encadeada insere e remove no início da lista.
  • A verificação de delimitadores exige tipos correspondentes e uma pilha vazia no fim.
  • Na subtração ou divisão pós-fixada, o primeiro valor retirado é o operando da direita.

4.2 Extremidades da fila e armazenamento circular

Uma fila remove primeiro o item mais antigo: primeiro a entrar, primeiro a sair (FIFO). Enfileirar adiciona ao fim; desenfileirar remove do início. Após enfileirar A, B e C, as duas próximas remoções retornam A e B. Isso é adequado a trabalhos que devem ser tratados na ordem de chegada.

Mantenha as duas extremidades de uma fila encadeada

Usando nós com membros number e next e incluindo <stdbool.h> e <stdlib.h>:

typedef struct { node *head, *tail; } queue;

bool enqueue(queue *q, int value) {
    node *n = malloc(sizeof *n);
    if (n == NULL) return false;
    *n = (node){.number = value, .next = NULL};
    if (q->tail == NULL) q->head = n;
    else q->tail->next = n;
    q->tail = n;
    return true;
}
bool dequeue(queue *q, int *out) {
    if (q->head == NULL) return false;
    node *old = q->head;
    *out = old->number;
    q->head = old->next;
    if (q->head == NULL) q->tail = NULL;
    free(old);
    return true;
}

Inicialize com queue q = {0};. A inserção define a ligação do antigo último nó antes de mover o ponteiro para o fim. Para o primeiro nó, também define o ponteiro para o início. A remoção avança o ponteiro para o início. Quando isso remove o único nó, o ponteiro para o fim também deve se tornar NULL. Caso contrário, a próxima inserção escreve por meio de um ponteiro pendente para o fim. Nenhuma das operações precisa percorrer a lista.

Reutilize um arranjo fixo como anel

Uma fila em arranjo simples que sempre remove o índice 0 precisa deslocar seus valores restantes. Um buffer circular mantém um índice para o início e uma contagem de itens:

#define CAP 4
typedef struct {
    char items[CAP];
    unsigned head, count;
} ring;

bool ring_put(ring *q, char value) {
    if (q->count == CAP) return false;
    q->items[(q->head + q->count) % CAP] = value;
    ++q->count;
    return true;
}
bool ring_get(ring *q, char *out) {
    if (q->count == 0) return false;
    *out = q->items[q->head];
    q->head = (q->head + 1) % CAP;
    --q->count;
    return true;
}

Inicialize um anel com {0}. head sempre identifica o primeiro item quando a fila não está vazia. O próximo índice de inserção é o início mais o número de itens, com retorno ao começo por meio de % CAP. Leia o primeiro item antes de mover head. A contagem distingue o estado vazio do cheio sem abrir mão de uma posição.

Rastreie o retorno ao início

Enfileire A, B e C nas posições 0, 1 e 2. Desenfileire A: head passa a 1 e count passa a 2. D entra na posição 3; E entra na posição 0. O arranjo físico agora contém E, B, C, D, mas a ordem lógica de remoção é B, C, D, E. O anel está cheio porque count é 4, embora o início esteja no índice 1.

Outro projeto mantém o índice head e o índice tail da próxima posição livre sem uma contagem. Nesse projeto, índices iguais poderiam significar vazio ou cheio, então uma regra comum deixa uma posição sem uso. Vazio é head == tail; cheio é (tail + 1) % CAP == head. Não misture essa regra com as regras de capacidade da versão baseada em contagem. Filas também permitem visitas a árvores nível por nível, mantendo os nós que aguardam sua vez.

Principais conclusões

  • FIFO remove o valor que está há mais tempo na fila.
  • Uma fila encadeada insere no fim e remove no início.
  • Remover o único nó exige redefinir os ponteiros para o início e o fim.
  • Uma fila circular faz os índices retornarem ao início e mantém a ordem lógica independente da ordem física.
  • Use uma contagem ou uma regra de posição não usada para distinguir vazio de cheio.
  • Leia o primeiro item antes de avançar o ponteiro para o início.
5

Implemente e percorra árvores binárias

Árvores substituem uma cadeia única por ramificações. Uma árvore de busca usa a ordem das chaves para escolher um ramo, mas seu formato determina quantos nós a busca alcança. A mesma ideia de subárvore orienta a inserção, o percurso, a contagem e a remoção. Cada operação precisa de um caso vazio, uma regra para tratar o nó atual e uma decisão clara sobre qual ponteiro do pai deve receber uma raiz de subárvore alterada.

5.1 Busque em uma árvore seguindo a regra de ordenação

Uma árvore binária dá a cada nó no máximo dois filhos, chamados esquerdo e direito. A raiz é o nó no topo. Uma folha não tem filhos, e um nó junto com todos os seus descendentes é uma subárvore. Um filho ausente e uma árvore vazia são representados por NULL.

Represente os ramos
typedef struct tnode {
    int key;
    struct tnode *left;
    struct tnode *right;
} tnode;

Cada ponteiro para filho leva à raiz de outra árvore. A mesma definição, portanto, descreve uma folha, uma grande árvore ramificada ou uma longa cadeia. A altura conta as arestas no caminho mais longo da raiz até uma folha: uma raiz sozinha tem altura 0. Use −1 para uma árvore vazia, de modo que as duas subárvores vazias de uma folha levem naturalmente à altura 0. Contar níveis daria um a mais.

Aplique a regra a subárvores inteiras

Uma árvore binária de busca, ou BST, ordena as chaves. Para cada nó, todas as chaves em sua subárvore esquerda são menores e todas as chaves em sua subárvore direita são maiores. Armazene cada chave uma vez. Essa condição inclui descendentes distantes. Uma raiz 50 com filho esquerdo 30 e um filho direito 60 abaixo desse 30 é inválida: 60 ainda está na subárvore esquerda de 50.

Insira 50, 30, 70, 20, 40, 60 e 80 em uma BST vazia. A raiz é 50; 30 e 70 são seus filhos. Os filhos de 30 são 20 e 40, e os de 70 são 60 e 80. Cada escolha de ramo preserva os limites herdados dos ancestrais.

Escolha um ramo a cada passo
#include <stddef.h>
tnode *search(tnode *t, int key) {
    while (t != NULL) {
        if (key == t->key) return t;
        if (key < t->key) t = t->left;
        else t = t->right;
    }
    return NULL;
}

Buscar 60 no exemplo visita 50, vai à direita até 70 e depois à esquerda até 60: três nós. Buscar 65 segue o mesmo caminho e depois tenta o filho direito de 60. Esse filho é NULL, então 65 está ausente. Você não precisa examinar a subárvore esquerda de 50 porque todas as chaves nela são menores que 50.

A forma recursiva trata primeiro t == NULL e uma chave correspondente, depois busca no filho escolhido. O teste de subárvore vazia é o caso-base. Sem ele, uma busca malsucedida acaba desreferenciando um ponteiro nulo. A versão com laço para no mesmo limite sem adicionar chamadas recursivas.

Use o formato para entender o trabalho

Uma árvore perfeitamente balanceada com 20 níveis contém 1.048.575 chaves, e uma busca visita no máximo 20 nós. Inserir as chaves já ordenadas 10, 20, 30, 40, 50, por outro lado, cria uma cadeia à direita. Sua altura é 4, e encontrar 50 visita todos os cinco nós. A regra de ordenação de uma BST, por si só, não mantém a árvore baixa. Árvores autobalanceadas, como árvores AVL, reorganizam os ramos para controlar a altura.

Para encontrar um mínimo, siga os filhos à esquerda até que a próxima ligação à esquerda seja NULL. Para encontrar um máximo, siga os filhos à direita. No exemplo com sete nós, essas chaves são 20 e 80. As mesmas escolhas de direção funcionam na raiz de qualquer subárvore porque a regra de ordenação também se aplica ali.

Principais conclusões

  • Um nó binário tem no máximo dois filhos; filhos ausentes são ponteiros nulos.
  • A ordenação de uma BST se aplica a todos os descendentes em cada subárvore.
  • A busca segue à esquerda para uma chave menor e à direita para uma chave maior.
  • Aqui a altura conta arestas, com altura −1 para uma árvore vazia.
  • Uma árvore de pequena altura reduz as visitas; uma BST ainda pode se tornar uma cadeia.

5.2 Atualize, percorra e libere uma árvore de busca

Funções de árvore trabalham em uma subárvore e conectam seu resultado ao pai. Mantenha explícito o caso de subárvore vazia e diferencie operações que alteram ligações de operações que apenas visitam nós.

Insira por meio da ligação da subárvore

Com tnode contendo key, left e right, inclua <stdbool.h> e <stdlib.h>:

bool insert(tnode **link, int key) {
    if (*link == NULL) {
        tnode *n = malloc(sizeof *n);
        if (n == NULL) return false;
        *n = (tnode){.key = key, .left = NULL, .right = NULL};
        *link = n;
        return true;
    }
    if (key < (*link)->key) return insert(&(*link)->left, key);
    if (key > (*link)->key) return insert(&(*link)->right, key);
    return true;
}

Chame com insert(&root, key) e verifique o resultado. Uma chave existente deixa a árvore inalterada e conta como sucesso. O endereço da ligação permite que a inserção atualize a raiz da função chamadora ou um ponteiro para filho. Em uma interface que retorna a raiz, armazene cada resultado: t->left = insert(t->left, key) e root = insert(root, key). Ignorar uma alocação retornada quando a subárvore original estava vazia deixa esse nó sem ligação e causa um vazamento.

Escolha quando visitar a raiz
PercursoOrdem de visitaFinalidade típica
Em ordemEsquerda, raiz, direitaImprimir chaves de uma BST em ordem
Em pré-ordemRaiz, esquerda, direitaVisitar pais antes dos filhos
Em pós-ordemEsquerda, direita, raizConcluir os filhos antes de liberar um pai

Para as inserções 50, 30, 70, 20, 40, 60, 80, a saída em ordem é “20 30 40 50 60 70 80”. Em pré-ordem produz “50 30 20 40 70 60 80”; em pós-ordem produz “20 40 30 60 80 70 50”.

void free_tree(tnode *t) {
    if (t == NULL) return;
    free_tree(t->left);
    free_tree(t->right);
    free(t);
}

Após free_tree(root), o código responsável deve definir root = NULL. A função libera primeiro os filhos para poder ler seus endereços a partir do pai ainda existente. O tamanho é zero para uma subárvore vazia, caso contrário é um mais os tamanhos dos dois filhos. A altura é −1 para uma subárvore vazia, caso contrário é um mais a maior altura dos filhos. O exemplo tem tamanho 7 e altura 2.

Remova sem quebrar a ordem

Uma folha se torna NULL. Um nó com um filho é substituído por esse filho. Para dois filhos, copie a menor chave da subárvore direita para o nó e depois remova esse sucessor de sua posição original:

tnode *erase(tnode *t, int key) {
    if (t == NULL) return NULL;
    if (key < t->key) t->left = erase(t->left, key);
    else if (key > t->key) t->right = erase(t->right, key);
    else {
        if (t->left == NULL || t->right == NULL) {
            tnode *child = t->left != NULL ? t->left : t->right;
            free(t);
            return child;
        }
        tnode *s = t->right;
        while (s->left != NULL) s = s->left;
        t->key = s->key;
        t->right = erase(t->right, s->key);
    }
    return t;
}

Chame como root = erase(root, key). Remover 50 do exemplo coloca 60 na raiz e remove o antigo nó 60. A saída em ordem passa a ser “20 30 40 60 70 80”; em pré-ordem passa a ser “60 30 20 40 70 80”. Um sucessor pode ter um filho direito, então sua remoção deve reconectar esse filho.

Para chaves que são palavras, use o sinal de strcmp, não a igualdade de ponteiros, e dê a cada palavra sob responsabilidade da árvore sua própria alocação. Libere a palavra antes de seu nó. A remoção de registros assim com dois filhos também precisa de um plano de responsabilidade pela palavra movida e por qualquer valor associado; copiar um ponteiro responsável pela memória e depois liberar a palavra do nó original deixaria uma chave pendente.

Principais conclusões

  • Uma inserção deve instalar uma nova raiz de subárvore e informar falhas de alocação.
  • O percurso em ordem imprime as chaves de uma BST ordenadas; o percurso em pré-ordem começa pelo pai.
  • A liberação em pós-ordem libera os dois filhos antes do pai.
  • O tamanho soma os tamanhos dos dois filhos; a altura usa a maior altura dos filhos.
  • A remoção reconecta zero ou um filho ou substitui a chave de um nó com dois filhos pela de seu sucessor.
  • Armazene a raiz retornada após a remoção, inclusive ao remover a raiz original.
  • Árvores com chaves que são cadeias de caracteres comparam o texto e gerenciam cada alocação sob sua responsabilidade.
6

Armazene e busque chaves em tabelas de espalhamento

Tabelas de espalhamento transformam uma chave em uma posição inicial e resolvem as colisões ali encontradas. A função de espalhamento e o teste de igualdade devem concordar sobre quais chaves têm o mesmo significado. Uma implementação com encadeamento reúne arranjos, inserção em listas, cópias de cadeias de caracteres e liberação de recursos. Sua utilidade depende de distribuir as chaves entre os baldes e reconstruir essas posições quando a quantidade de baldes muda.

6.1 Distribua chaves em baldes e trate colisões

Uma tabela de espalhamento escolhe uma posição inicial a partir de uma chave. Uma função de espalhamento produz um número; reduzir esse número módulo uma quantidade positiva de baldes fornece um índice. O encadeamento separado mantém uma lista encadeada em cada balde, então a busca percorre a cadeia selecionada.

Separe o cálculo do espalhamento da igualdade

Uma função de espalhamento útil é determinística, rápida de calcular e distribui as chaves esperadas entre os baldes. Chaves iguais devem produzir valores de espalhamento iguais. Chaves diferentes ainda podem compartilhar um índice: isso é uma colisão, e toda tabela deve tratá-la. Com mais chaves possíveis que baldes, algumas colisões são inevitáveis.

Para palavras iniciadas por letras em um sistema compatível com ASCII, toupper((unsigned char)word[0]) - 'A' atribui 26 baldes de letras; “Luigi” começa no balde 11. Muitas palavras em inglês começam com “s” e poucas com “x”, então essa regra pode deixar cadeias muito desiguais. A conversão explícita é necessária porque as funções de <ctype.h> exigem um valor de caractere sem sinal ou EOF. Valide a letra inicial e a suposição sobre o conjunto de caracteres antes de usar essa regra de indexação.

Uma função de espalhamento por soma de caracteres também tem pontos fracos. Usando códigos ASCII e dez baldes, “cat” soma 99 + 97 + 116 = 312 e chega ao balde 2; “dog” soma 314 e chega ao balde 4. “act” também soma 312. A adição perde a ordem das letras, então todo rearranjo causa colisão.

Incorpore a ordem dos caracteres ao espalhamento
unsigned long hash_word(const char *word) {
    unsigned long h = 5381;
    const unsigned char *p = (const unsigned char *)word;
    while (*p != '\0') {
        h = h * 33 + *p;
        ++p;
    }
    return h;
}

Esse padrão djb2 usa aritmética sem sinal, cujo estouro sofre redução modular de forma definida. Um estouro com sinal teria comportamento indefinido. Para as cadeias ASCII “cat” e “act”, os valores são 193488125 e 193486013. Reduza com hash_word(word) % N, em que N é a quantidade positiva de baldes. A ordem das letras altera esses valores de espalhamento, embora reduzi-los ainda possa produzir uma colisão.

Se uma tabela trata “Cat” e “cat” como iguais, tanto o cálculo do espalhamento quanto a comparação devem usar a mesma regra para maiúsculas e minúsculas. Ignorar essa diferença apenas durante a comparação pode enviar chaves iguais a baldes diferentes, onde a busca não as comparará. Um resto negativo de um cálculo com sinal também não é adequado como índice de arranjo.

Escolha uma estratégia para colisões

O encadeamento separado armazena todas as colisões na lista encadeada de um balde. A busca compara chaves até encontrar uma correspondência ou chegar ao fim. O endereçamento aberto armazena as entradas no próprio arranjo. Com sondagem linear, uma colisão leva à tentativa da posição seguinte, com retorno ao início no fim. A busca segue a mesma sequência de sondagem, então pode examinar várias posições do arranjo.

Reconstrua os índices quando a tabela crescer

O fator de carga é o número de chaves armazenadas dividido pela quantidade de baldes. Doze chaves em oito baldes resultam em uma média de 1,5 entrada por balde. Dezesseis baldes reduzem essa média para 0,75. Essa média não garante que toda cadeia seja curta; uma função de espalhamento ruim pode colocar todas as chaves juntas.

Um índice de balde depende da quantidade de baldes. Após alocar e inicializar um arranjo maior de baldes, recalcule o índice de cada chave armazenada para a nova quantidade e reconecte-a ali. Copiar os ponteiros antigos para o início das listas nos novos baldes de mesmo número deixa chaves em posições nas quais a nova busca não procurará. Uma tabela com encadeamento pode conter mais entradas que baldes; o endereçamento aberto precisa de posições livres para novas entradas e sondagens confiáveis.

Principais conclusões

  • Reduza um valor de espalhamento determinístico módulo uma quantidade positiva de baldes para obter um índice.
  • Chaves iguais precisam de valores de espalhamento iguais; chaves distintas podem colidir.
  • A aritmética sem sinal evita estouro indefinido com sinal no cálculo do espalhamento.
  • O encadeamento percorre a lista de um balde; o endereçamento aberto segue uma sequência de sondagem.
  • O fator de carga mede entradas por balde, não a cadeia mais longa.
  • Recalcule o espalhamento de cada chave quando a quantidade de baldes mudar.

6.2 Implemente uma tabela de contagem de palavras responsável por suas chaves

Uma tabela com encadeamento combina um arranjo, ligações de lista, comparação de cadeias de caracteres e responsabilidade pela liberação. Cada entrada é responsável por sua chave copiada. O arranjo de baldes é responsável pelas cadeias, e cada cadeia termina em NULL.

Inicialize os baldes e busque texto
#include <stdbool.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#define N 26
typedef struct entry {
    char *key;
    int count;
    struct entry *next;
} entry;
static entry *table[N];

O armazenamento estático começa com ponteiros nulos em todos os baldes. Um arranjo local, por sua vez, precisa de um inicializador como entry *table[N] = {NULL};. Use a função sem sinal hash_word, que começa em 5381 e incorpora cada byte com o multiplicador 33.

entry *find_word(const char *key) {
    size_t i = hash_word(key) % N;
    for (entry *e = table[i]; e != NULL; e = e->next)
        if (strcmp(e->key, key) == 0) return e;
    return NULL;
}

strcmp testa o texto. e->key == key testaria se os ponteiros designam a mesma posição. Dois buffers separados podem conter palavras iguais, então a igualdade de ponteiros não faz uma busca por palavra.

Conte uma chave existente ou insira uma nova
bool add_word(const char *key) {
    entry *e = find_word(key);
    if (e != NULL) {
        if (e->count == INT_MAX) return false;
        ++e->count;
        return true;
    }
    e = malloc(sizeof *e);
    if (e == NULL) return false;
    e->key = malloc(strlen(key) + 1);
    if (e->key == NULL) {
        free(e);
        return false;
    }
    strcpy(e->key, key);
    e->count = 1;
    size_t i = hash_word(key) % N;
    e->next = table[i];
    table[i] = e;
    return true;
}

A função chamadora fornece uma cadeia de caracteres terminada em nulo. A verificação de INT_MAX informa uma falha antes que uma contagem cause estouro em seu armazenamento int. O byte extra na alocação da cadeia contém '\0'. Se a segunda alocação falhar, libere a entrada ainda não vinculada. O ponteiro para o início da lista do balde muda somente depois que as duas alocações e a inicialização são bem-sucedidas.

Salvar e->key = key manteria o armazenamento da função chamadora. Se a entrada reutilizar repetidamente um buffer, todos os ponteiros salvos poderão mostrar a palavra mais recente. Copiar o texto dá a cada entrada um tempo de vida independente desse buffer. strdup fornece essa operação em POSIX e C23; alocação explícita e strcpy funcionam em C17.

Em um exemplo com dez baldes e soma de caracteres, inserir “cat”, “dog”, “act” deixa “act” → “cat” no balde 2 e “dog” no balde 4. Buscar “tac” percorre o balde 2, compara as duas palavras e chega a NULL. Um balde correspondente é apenas uma posição candidata, não uma prova de que há uma chave igual.

Libere dados associados, nós e ponteiros para o início dos baldes
void clear_table(void) {
    for (size_t i = 0; i < N; ++i) {
        entry *e = table[i];
        while (e != NULL) {
            entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
        table[i] = NULL;
    }
}

Salvar next e liberar a chave acontecem enquanto a entrada ainda existe. Redefinir cada balde permite sua reutilização. Ferramentas de verificação de memória ajudam a verificar os caminhos executados; caminhos de falha de alocação também precisam liberar recursos. A ordem de espalhamento não é uma ordem classificada. Para imprimir em ordem alfabética, reúna as chaves para ordenação ou armazene-as em uma BST.

No endereçamento aberto, a remoção precisa de um marcador de remoção para que a busca continue através de uma posição removida até chaves posteriores que colidiram. Uma posição realmente nunca usada pode encerrar uma busca malsucedida. As sequências de sondagem devem ter limite, e a tabela deve crescer antes que todas as posições estejam ocupadas.

Principais conclusões

  • Inicialize como NULL cada ponteiro para o início da lista de um balde.
  • O espalhamento seleciona uma cadeia; strcmp confirma se uma chave corresponde.
  • Incremente a contagem de uma chave existente em vez de adicionar uma entrada duplicada.
  • Copie as cadeias de entrada e libere a entrada se a alocação da chave falhar.
  • Libere a chave antes de sua entrada, salve o sucessor e redefina cada balde.
  • A remoção em endereçamento aberto preserva caminhos de busca com marcadores de remoção.

Exercícios resolvidos de Estruturas de dados em C

6 Lições Interativas

1

Acompanhe registros por cópias e ponteiros

15 passos
Crie um registro de leitura, passe-o para funções, percorra um arranjo e diferencie dados incorporados de ponteiros responsáveis por memória.
  1. 1
    IntroduçãoMantenha uma leitura reunida

    Uma leitura combina um identificador e uma temperatura. Mantê-los em um registro, uma struct de C, permite que o código passe ou armazene o par como um único valor.

    Acompanhe esse registro desde sua definição até uma chamada de função e depois até um arranjo. Em cada passo, diferencie o objeto dos ponteiros que o acessam: essa distinção determina se uma alteração afeta uma cópia ou o original, e se uma cópia rasa de um membro ponteiro compartilha armazenamento.

  2. 2
    ExemploDefina o registro

    A definição apresenta um tipo; a declaração seguinte cria um objeto desse tipo.

    struct reading { int id; double temp_c; };
    struct reading r = {.id = 24, .temp_c = 16.5};
    r.temp_c += 0.5;
    

    O ponto seleciona um membro de r, então sua temperatura agora é 17,0 °C. Cada membro tem seu próprio tipo, e o ponto e vírgula após a chave de fechamento encerra a definição do tipo.

  3. 3
    Ideia-chaveCopie os valores dos membros

    Agora crie um segundo registro com struct reading s = r;. Essa atribuição copia os valores dos dois membros. Alterar s.id ou s.temp_c depois disso deixa r inalterado.

    Um arranjo armazenado dentro de uma estrutura também é copiado. O preenchimento ainda pode ser diferente entre objetos, então testar a igualdade significa comparar os membros relevantes para a tarefa. C não fornece um operador == para estruturas inteiras.

  4. 4
    DesafioLeia um membro omitido

    Que valor struct reading a = {.id = 31}; coloca em a.temp_c, e o código pode ler esse valor?

    ---reveal---

    O membro omitido é inicializado com zero, então a temperatura é 0.0 e pode ser lida. Há um inicializador, embora ele nomeie apenas id. Uma declaração de a local sem inicializador exigiria atribuições antes da leitura de seus membros.

  5. 5
    ExplicaçãoDê um nome curto ao tipo

    Um typedef fornece um nome alternativo sem alterar o comportamento dos objetos.

    typedef struct { int id; double temp_c; } reading;
    reading make(int id, double t) {
        return (reading){.id = id, .temp_c = t};
    }
    

    Uma chamada pode retornar o registro completo por valor. Se um nó precisar se referir ao próprio tipo dentro de uma definição, dê a ele uma etiqueta e escreva essa ligação para o próprio tipo com struct node *next.

  6. 6
    ExemploEscolha uma cópia ou um endereço

    Compare duas interfaces de função:

    void warm_copy(reading x) { x.temp_c += 1.0; }
    void warm(reading *p) { p->temp_c += 1.0; }
    

    warm_copy(r) altera uma cópia local. warm(&r) acessa o registro da função chamadora e altera seu membro. A seta significa (*p).temp_c; seus parênteses fazem a desreferenciação acontecer antes que o ponto selecione o membro.

  7. 7
    DicaAloque um registro com responsabilidade de liberação definida

    Inclua <stdlib.h> e aloque reading *p = malloc(sizeof *p);. Antes de escrever por meio de p, verifique se ele é NULL e trate a falha.

    Após o sucesso, inicialize os campos e mantenha um responsável por free(p). Retornar um valor de estrutura é seguro; retornar o endereço de uma estrutura local deixa um ponteiro pendente porque esse objeto local deixa de existir quando sua função retorna.

  8. 8
    DesafioPreveja qual temperatura muda

    Uma função chamadora começa com reading r = {24, 17.0};, chama warm_copy(r) e depois warm(&r) usando as duas funções mostradas. Que temperatura r contém?

    ---reveal---

    Ele contém 18,0 °C. A primeira chamada altera apenas sua cópia particular, então a função chamadora ainda tem 17,0 quando a segunda chamada começa. A segunda recebe o endereço do registro da função chamadora e soma um por meio desse ponteiro.

  9. 9
    ExemploPercorra um conjunto de registros

    Suponha que um arranjo contenha as temperaturas −5,5; −1,0 e −7,5 °C. Após verificar que a contagem é diferente de zero, comece best no índice 0 e compare cada registro posterior com o melhor até então.

    O índice 1 substitui o índice 0 porque −1,0 é mais quente que −5,5. O índice 2 não o substitui. Começar com um elemento real funciona quando todos os valores estão abaixo de zero; inventar um máximo inicial de zero faria com que eles não fossem encontrados.

  10. 10
    DicaRetorne um sinal da função de comparação

    Para temperaturas finitas, uma função de comparação pode retornar (x->temp_c > y->temp_c) - (x->temp_c < y->temp_c). O resultado é negativo, zero ou positivo.

    Retornar a diferença de temperatura como int pode transformar uma pequena diferença em zero. A subtração de inteiros pode causar estouro para chaves extremas. Um sinal baseado em comparações evita os dois erros, e qsort ainda não promete preservar a ordem dos elementos equivalentes.

  11. 11
    ExplicaçãoPermita a recuperação após uma falha de redimensionamento

    Para um arranjo no heap, mantenha a quantidade em uso separada da capacidade. Verifique se a quantidade de bytes solicitada cabe no tipo e atribua o resultado de realloc para um tamanho diferente de zero a um ponteiro temporário.

    Em caso de falha, o arranjo original permanece disponível. Em caso de sucesso, atualize juntos o novo endereço inicial e a capacidade. Reconstrua os ponteiros para elementos a partir de seus índices salvos, pois a alocação pode ter se movido ou substituído seu armazenamento anterior.

  12. 12
    ConexãoVeja o que uma cópia de ponteiro compartilha

    Um registro de pessoa com char *name contém um endereço, então person b = a; deixa ambos os nomes apontando para uma única cadeia de caracteres. Essa é uma cópia superficial.

    Se cada registro precisar ser responsável por uma cadeia independente, dê a b.name sua própria alocação verificada e copie o texto, incluindo o byte nulo terminador. Cada responsável pode então liberar sua própria cadeia uma vez. Copiar um arranjo de caracteres incorporado já fornece caracteres separados.

  13. 13
    DesafioCorrija a cópia do nome

    O name de um registro é uma cadeia de caracteres no heap sob sua responsabilidade. O código copia o registro e depois pretende liberar os dois nomes independentemente. O que deve acontecer antes dessa liberação?

    ---reveal---

    Aloque um nome separado para a cópia, verifique se a alocação teve sucesso e copie o texto original antes de liberar qualquer uma das cadeias. Os dois registros compartilham atualmente um endereço. Liberar os dois ponteiros compartilhados liberaria o mesmo bloco duas vezes; copiar o ponteiro novamente não ajudaria.

  14. 14
    DesafioRetorne um registro com uma etiqueta incorporada

    Uma função retorna por valor uma estrutura que contém char label[12]. A etiqueta fica pendente porque a estrutura original era local? Explique usando tempo de vida e cópia.

    ---reveal---

    Não. Retornar por valor copia os valores dos membros da estrutura, incluindo o arranjo incorporado, para o valor retornado. A função chamadora é responsável pelo registro resultante. Retornar um ponteiro para a estrutura local, por outro lado, faria a função chamadora acessar um objeto cujo tempo de vida terminou.

  15. 15
    RecapitulaçãoAcompanhe valores e responsabilidades

    Um objeto de estrutura armazena valores de membros. Um ponteiro designa um objeto, e um ponteiro copiado continua designando o mesmo alvo. Parâmetros e valores de retorno de funções seguem essa distinção.

    Para arranjos, mantenha contagens explícitas e preserve o responsável pela memória durante o redimensionamento. Para registros com ponteiros responsáveis por memória, decida se uma cópia compartilha o armazenamento ou recebe alocações independentes. Essas decisões determinam tanto as atualizações quanto a liberação de recursos.

2

Mantenha opções e dados de uniões consistentes

11 passos
Acompanhe valores de enumeração, rejeite lacunas, construa uma célula etiquetada e escolha entre campos simultâneos e alternativos.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoNomeie a opção e identifique seus dados associados
  2. 2
    ExemploAcompanhe valores explícitos de enumeração
  3. 3
    Ideia-chaveVerifique o conjunto permitido
  4. 4
    DicaImpeça a continuação entre casos
  5. 5
    DesafioValide um valor de etapa
  6. 6
    ExplicaçãoSobreponha os valores alternativos
  7. 7
    ExemploDefina as duas partes de um valor etiquetado
  8. 8
    DesafioIdentifique a atualização inconsistente
  9. 9
    DicaArmazene uma pequena faixa de valores de indicador
  10. 10
    DesafioEscolha o armazenamento para um registro
  11. 11
    RecapitulaçãoMantenha cada descrição correspondente a seus dados
3

Rastreie a criação, a inserção e a remoção em listas

16 passos
Use ligações de entrada para preservar uma cadeia e depois mantenha extremidades, contagens e dados associados sob responsabilidade dos nós.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoAltere uma cadeia uma ligação por vez
  2. 2
    ExplicaçãoComece com um ponteiro nulo para o início
  3. 3
    ExemploInsira no início sem perder a cadeia
  4. 4
    Ideia-chavePermita que a função chamadora mantenha o novo início
  5. 5
    DesafioPreveja a ordem das inserções no início
  6. 6
    ExplicaçãoAcompanhe o endereço de uma ligação
  7. 7
    ExemploEncontre uma posição de inserção ordenada
  8. 8
    DesafioInsira antes do início da lista
  9. 9
    ExemploRemova um nó do meio com segurança
  10. 10
    DicaPare com segurança quando um valor estiver ausente
  11. 11
    ExplicaçãoAdicione um ponteiro para o fim e uma contagem
  12. 12
    ConexãoUse uma ligação para trás quando a remoção precisar dela
  13. 13
    DesafioEscolha o armazenamento para leituras repetidas por índice
  14. 14
    DicaLibere os dados associados antes do nó
  15. 15
    DesafioCombine remoção e metadados
  16. 16
    RecapitulaçãoMantenha válido o caminho de entrada
4

Rastreie operações de pilha e o retorno ao início em filas

14 passos
Avalie operações aninhadas, mantenha as extremidades de uma fila encadeada e acompanhe a ordem lógica de uma fila circular.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoEscolha qual item em espera vem a seguir
  2. 2
    ExplicaçãoUse top como contagem de itens
  3. 3
    ExemploRastreie a ordem da pilha
  4. 4
    ExemploCompare o fechamento com a abertura no topo
  5. 5
    ExemploMantenha os operandos pós-fixados em ordem
  6. 6
    DesafioVerifique uma expressão pós-fixada alterada
  7. 7
    ExplicaçãoSepare o início do fim
  8. 8
    DicaCorrija o caso de um único nó
  9. 9
    ExplicaçãoUse um anel em vez de deslocar
  10. 10
    ExemploRastreie uma disposição com retorno ao início
  11. 11
    DesafioEncontre a próxima posição de inserção
  12. 12
    Ideia-chaveMantenha uma regra para o estado cheio
  13. 13
    DesafioSelecione a ordem de trabalho
  14. 14
    RecapitulaçãoFaça a ordem e o estado concordarem
5

Implemente, busque, percorra e remova nós de uma BST

15 passos
Acompanhe uma árvore durante a escolha de ramos, inserção, ordens de percurso, substituição pelo sucessor e liberação.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoAcompanhe uma árvore da raiz à subárvore
  2. 2
    Ideia-chaveAplique a regra abaixo de cada ancestral
  3. 3
    ExemploConstrua a árvore do exemplo
  4. 4
    ExemploBusque um ramo por vez
  5. 5
    DesafioDiagnostique uma violação em um descendente
  6. 6
    ConexãoMeça o formato
  7. 7
    ExemploConecte uma inserção onde a busca para
  8. 8
    ExemploEscolha uma ordem de visita
  9. 9
    ExplicaçãoConte ramos e o comprimento do caminho
  10. 10
    ExplicaçãoTrate zero ou um filho
  11. 11
    ExemploSubstitua a chave de um nó com dois filhos
  12. 12
    DesafioPreserve o filho do sucessor
  13. 13
    DicaLibere os descendentes antes do pai
  14. 14
    DesafioVerifique juntos o percurso e a altura
  15. 15
    RecapitulaçãoReconecte toda subárvore alterada
6

Implemente um contador de palavras com tabela de espalhamento

14 passos
Rastreie colisões, igualdade, chaves copiadas, crescimento, falhas de alocação e liberação completa.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoUse uma chave para reduzir o espaço de busca
  2. 2
    ExemploCalcule um balde inicial
  3. 3
    Ideia-chaveMantenha a igualdade e o espalhamento consistentes
  4. 4
    DesafioDiferencie uma colisão de uma correspondência
  5. 5
    ExplicaçãoUse o fator de carga sem perder de vista a distribuição
  6. 6
    ExemploRecalcule as posições após o crescimento
  7. 7
    ExplicaçãoBusque texto em uma cadeia
  8. 8
    ExemploInsira uma nova chave sob responsabilidade da tabela
  9. 9
    DesafioTrate uma alocação parcial
  10. 10
    DicaNão mantenha um buffer de entrada reutilizado
  11. 11
    ExplicaçãoPreserve caminhos de sondagem após a remoção
  12. 12
    DesafioCombine tratamento de colisões e contagem de palavras
  13. 13
    ConexãoLibere cada chave e nó
  14. 14
    RecapitulaçãoFaça localização, igualdade e tempo de vida concordarem

Questões de Estruturas de dados em C

50 Questões de Prática em 5 conjuntos

Conjunto 1: Leia registros e rastreie operações básicas10 Questões de Prática

Questão 1 de 10

Uma BST é construída inserindo 42, 18, 63, 27 e 21. Quais nós uma busca por 21 visita?
  1. 42 → 18 → 21
  2. 42 → 63 → 27 → 21
  3. 18 → 27 → 21
  4. 42 → 18 → 27 → 21
Mostrar respostaOcultar resposta
Resposta: D
Em 42, 21 é menor, então a busca vai à esquerda até 18; depois vai à direita até 27 e à esquerda até 21. A inserção de 21 o colocou abaixo de 27, não diretamente abaixo de 18. Uma busca segue as ligações existentes para filhos e não pode saltar para uma chave menor apenas porque se sabe que ela está presente.

Questão 2 de 10

Um reading *p válido aponta para um registro de temperatura. Qual expressão acessa seu membro temp_c?
  1. p.temp_c
  2. *p.temp_c
  3. &p.temp_c
  4. p->temp_c
Mostrar respostaOcultar resposta
Resposta: D
O operador seta seleciona um membro por meio de um ponteiro e é equivalente a (*p).temp_c. Um ponto exige um objeto de estrutura à sua esquerda, então p.temp_c é inválido aqui. Escrever *p.temp_c não corrige o problema porque o ponto é aplicado antes da desreferenciação.

Questão 3 de 10

Registros têm temperaturas finitas de −8,0; −2,5 e −6,0 °C nos índices 0, 1 e 2. Uma busca pelo máximo começa com best = 0 e o atualiza para um registro com temperatura estritamente maior. Qual índice permanece?
  1. 1
  2. 0
  3. 2
  4. Nenhum índice; o máximo continua zero
Mostrar respostaOcultar resposta
Resposta: A
A busca começa com o registro no índice 0 e depois o substitui pelo índice 1 porque −2,5 é mais quente que −8,0. A temperatura final de −6,0 não é mais quente que −2,5, então o índice 1 permanece. Temperaturas negativas não causam problema especial quando o melhor valor inicial é um registro real.

Questão 4 de 10

Que valor PAUSED tem em enum state { IDLE = 3, RUNNING, FAILED = 9, PAUSED };?
  1. 5
  2. 4
  3. 10
  4. 9
Mostrar respostaOcultar resposta
Resposta: C
Um valor implícito de enumeração é um a mais que o enumerador imediatamente anterior. FAILED define explicitamente a sequência em 9, então PAUSED é 10. Contar PAUSED a partir do início da declaração ignoraria a reinicialização introduzida pelo valor explícito.

Questão 5 de 10

Uma união etiquetada tem a etiqueta de enumeração kind e a união as com membros int i e double d. Um valor existente está etiquetado como K_DOUBLE. Qual atualização armazena o inteiro 18 de forma consistente?
  1. t.kind = K_DOUBLE; t.as.i = 18;
  2. t.kind = K_INT; t.as.d = 18.0;
  3. t.kind = K_INT; t.as.i = 18;
  4. t.kind = K_DOUBLE; t.as.d = 18.0;
Mostrar respostaOcultar resposta
Resposta: C
A etiqueta deve informar qual membro contém o valor armazenado. Atribuir K_INT e escrever em as.i faz as duas partes descreverem o inteiro 18. Escrever no membro inteiro e manter a etiqueta de double levaria o código posterior a ler a representação errada; a etiqueta não muda automaticamente.

Questão 6 de 10

Uma lista vazia recebe inserções de 6, 11 e depois 3 no início. Quais valores aparecem quando ela é percorrida a partir do início?
  1. 6 → 11 → 3
  2. 3 → 11 → 6
  3. 11 → 6 → 3
  4. 3 → 6 → 11
Mostrar respostaOcultar resposta
Resposta: B
Cada inserção no início coloca o novo nó antes do início atual. Os inícios são, portanto, 6, depois 11 e depois 3, com os nós anteriores ligados atrás deles. A ordem de chegada 6 → 11 → 3 resultaria de inserções no fim, não das inserções no início descritas.

Questão 7 de 10

Uma lista ordenada contém 1 → 7 → 12. Um laço de inserção avança enquanto o valor atual é menor que 9. Em qual ligação de entrada ele para?
  1. No ponteiro para o início, que aponta para o nó com 1
  2. No campo next do nó que contém 1
  3. No campo next do nó que contém 12
  4. No campo next do nó que contém 7
Mostrar respostaOcultar resposta
Resposta: D
O laço passa por 1 e 7 e depois para antes de 12 porque 12 não é menor que 9. A ligação que atualmente alcança 12 é o membro next do nó 7. Parar na ligação após 12 colocaria 9 no fim e violaria a ordem crescente da lista.

Questão 8 de 10

Uma pilha vazia executa push 8, push 2, pop, push 5, pop, pop. Quais valores as três remoções retornam?
  1. 2, 5, 8
  2. 8, 2, 5
  3. 2, 8, 5
  4. 5, 2, 8
Mostrar respostaOcultar resposta
Resposta: A
A primeira remoção retira 2, o valor mais recente naquele momento. Empilhar 5 o coloca acima do 8 restante, então as próximas remoções retornam 5 e 8. Retornar 8 primeiro seguiria a ordem de uma fila e não corresponderia ao comportamento da pilha de último a entrar, primeiro a sair.

Questão 9 de 10

Que valor r.temp_c tem após struct reading { int id; double temp_c; }; struct reading r = {.id = 12};?
  1. Um valor indeterminado
  2. 0.0
  3. 12.0
  4. Um erro de inicialização
Mostrar respostaOcultar resposta
Resposta: B
O inicializador designado define id e inicializa com zero o membro omitido temp_c, então seu valor é 0.0. Isso difere de declarar uma estrutura local sem inicializador algum. O inicializador fornecido torna o membro omitido utilizável sem uma atribuição posterior.

Questão 10 de 10

Uma tabela com entradas trata palavras ASCII sem distinguir maiúsculas de minúsculas, mas calcula o espalhamento de seus bytes originais. Uma busca por “mint” não encontra uma entrada existente “Mint”, sem visitá-la. Qual correção completa atende às entradas existentes e futuras?
  1. Manter o espalhamento dos bytes originais, ignorar maiúsculas na igualdade e reconstruir as mesmas posições.
  2. Ignorar maiúsculas no espalhamento, manter essa regra na igualdade e reconstruir as posições.
  3. Ignorar maiúsculas no espalhamento, manter essa regra na igualdade e conservar as posições antigas.
  4. Manter o espalhamento dos bytes originais, comparar endereços e ampliar o arranjo de baldes.
Mostrar respostaOcultar resposta
Resposta: B
O cálculo do espalhamento deve tratar grafias iguais da mesma forma, então normalize maiúsculas e minúsculas antes de calculá-lo e mantenha a comparação que ignora essa diferença. As entradas existentes também precisam ser posicionadas segundo a regra corrigida. Alterar apenas os cálculos futuros deixa entradas antigas em posições que suas buscas corrigidas talvez nunca examinem.
Conjunto 2: Aplique regras de estado e armazenamento10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

struct tag { char name[8]; int id; }; é usado para a e b. Após b = a; b.name[0] = 'Z';, o que acontece com a.name[0]?
  1. Ele muda para o caractere Z.
  2. Ele passa a ter um valor indeterminado.
  3. Ele mantém seu caractere original.
  4. A atribuição da estrutura não compila.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Dada void cool(reading r) { r.temp_c -= 2.0; }, uma função chamadora define reading a = {9, 14.0}; e chama cool(a);. Qual é o valor de a.temp_c da função chamadora depois disso?
  1. 14.0
  2. 12.0
  3. 16.0
  4. Um valor indeterminado
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Que texto printf("%d", WED); imprime para enum day { MON, TUE, WED, THU };?
  1. “WED”
  2. “3”
  3. “Wednesday”
  4. “2”
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma união etiquetada armazena um double sob a etiqueta K_DOUBLE. Depois, o código altera apenas a etiqueta para K_INT e uma função de impressão lê as.i. Qual é o defeito?
  1. Alterar a etiqueta converte o valor armazenado em inteiro.
  2. A nova etiqueta seleciona um membro que não foi armazenado.
  3. A união mantém os dois valores tipados até ser liberada.
  4. A função deve manter a etiqueta antiga e alterar seu formato.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Uma lista simplesmente encadeada armazena ponteiros para o início e o fim e uma contagem. Seu único nó é removido e liberado. Qual estado deve permanecer?
  1. head = NULL; tail = NULL; count = 0;
  2. head = NULL; tail = old; count = 0;
  3. head = old; tail = NULL; count = 0;
  4. head = NULL; tail = NULL; count = 1;
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Que resultado a expressão pós-fixada “15 6 - 3 /” produz com operandos inteiros?
  1. -3
  2. 7
  3. -7
  4. 3
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma fila circular baseada em contagem tem capacidade 5, head = 4 e count = 2. Em qual posição a próxima inserção escreverá?
  1. Posição 0
  2. Posição 2
  3. Posição 1
  4. Posição 4
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Qual percurso libera cada subárvore filha antes de liberar seu pai?
  1. Em pré-ordem
  2. Em pós-ordem
  3. Em ordem
  4. Apenas a raiz
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma tabela com encadeamento e chaves inteiras usa a chave módulo a quantidade de baldes. Ela insere 13, 25 e 23 no início das cadeias de 6 baldes vazios. Depois cresce para 10 baldes, percorrendo os baldes antigos em ordem crescente de índice e cada cadeia a partir do início, inserindo cada entrada no início de seu novo balde. Quais cadeias resultam?
  1. Balde 3: 13 → 23; balde 5: 25
  2. Balde 1: 25 → 13; balde 5: 23
  3. Balde 3: 23 → 13; balde 5: 25
  4. Balde 3: 23; balde 5: 25 → 13
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um contador de palavras com encadeamento, inicialmente vazio, usa uma função de espalhamento que envia “ash” e “elm” ao mesmo balde. Ele lê para um buffer reutilizado, depois busca esse texto e incrementa uma correspondência ou aloca um nó com contagem 1. Novos nós armazenam incorretamente key = buffer. Ele lê “ash” e depois “elm”. O que permanece antes da liberação?
  1. Uma entrada mostrando “elm” com contagem 2
  2. Duas entradas, “ash” e “elm”, cada uma com contagem 1
  3. Uma entrada mostrando “ash” com contagem 2
  4. Duas entradas mostrando “elm”, cada uma com contagem 1
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 3: Rastreie ligações, retorno ao início e colisões10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma função de comparação de temperaturas finitas retorna x->temp_c - y->temp_c como int. Dois registros distintos têm temperaturas de 6,20 e 6,45 °C. Por que a ordenação pode tratá-los como iguais?
  1. Os argumentos const void * forçam as duas entradas a serem inteiros.
  2. A função de comparação deve retornar exatamente a diferença de temperatura.
  3. Os dois valores finitos se tornam iguais depois que qsort os copia.
  4. A diferença fracionária se torna zero durante a conversão.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Qual faixa cabe em unsigned int mode : 4?
  1. 0 a 4
  2. 0 a 15
  3. 1 a 16
  4. −8 a 7
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um nó é inserido no início de uma lista não vazia com head = n; n->next = head; após a alocação de n. Para onde o next do novo nó aponta?
  1. Para o antigo primeiro nó
  2. Para o antigo último nó
  3. Para o próprio novo nó
  4. Para o marcador nulo de fim
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma lista crescente começa como 3 → 8 → 12. Insira 6, remova o primeiro nó e depois insira 8 usando um laço que avança enquanto o valor atual é estritamente menor que o valor inserido. Onde o novo 8 é ligado?
  1. Após 6 e antes do 8 existente
  2. Após o 8 existente e antes de 12
  3. Antes de 6 e após o 3 removido
  4. Após 12 e antes da ligação nula
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um programa lê repetidamente registros por índice numérico e percorre todos os registros, com poucas inserções. Qual armazenamento é mais adequado a essas operações?
  1. Uma lista simplesmente encadeada de registros
  2. Uma lista duplamente encadeada de registros
  3. Uma fila encadeada de nós de registros
  4. Um arranjo contíguo de registros
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma fila encadeada vazia enfileira 12 e 30 com sucesso, desenfileira uma vez, falha na alocação ao enfileirar 41, desenfileira novamente e depois enfileira 44 com sucesso. Operações corretas preservam o estado em caso de falha. Quais são as extremidades finais e os valores em espera?
  1. O início alcança 30, o fim 44; os valores 30 → 44.
  2. O início alcança 44 enquanto o fim é nulo.
  3. As duas extremidades alcançam o único nó, 44.
  4. O início alcança 12, o fim 44; os valores 12 → 44.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma árvore tem raiz 36, filho esquerdo 17 e 41 como filho direito de 17. Por que ela não atende à regra de BST?
  1. 17 é menor que seu pai e está à esquerda.
  2. 41 está na subárvore esquerda de 36, mas é maior que 36.
  3. 41 é maior que seu pai e está à direita.
  4. 36 tem apenas um filho direto em vez de dois.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma BST é construída a partir de 48, 24, 75, 62, 68, 90. Remova 48 usando seu sucessor em ordem. Qual é a nova raiz e qual é o filho esquerdo restante de 75?
  1. Raiz 62; filho esquerdo 68
  2. Raiz 62; filho esquerdo NULL
  3. Raiz 75; filho esquerdo 62
  4. Raiz 68; filho esquerdo 62
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma tabela com encadeamento tem 21 chaves armazenadas e 14 baldes. Qual é seu fator de carga?
  1. 1.5
  2. 0.67
  3. 7
  4. 35
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma busca em uma tabela de espalhamento chega a uma cadeia que contém “elm” e “ash” enquanto procura “oak”. As duas comparações de texto falham. O que ela deve retornar após o fim da cadeia?
  1. A primeira entrada de “elm”
  2. A última entrada de “ash”
  3. NULL
  4. Uma nova entrada para “oak”
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 4: Diagnostique erros de cópia e de limites10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma lista ordenada 2 → 6 → 6 → 14 usa um laço que avança enquanto number < value. Um novo 6 é inserido. Onde ele entra?
  1. Entre 2 e o primeiro 6 existente
  2. Entre os dois nós 6 existentes
  3. Entre o segundo 6 e 14
  4. Após 14, no fim da lista
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma pilha em arranjo tem top = 0 com sinal, e a remoção lê items[--top] sem verificação. Qual índice ela tenta ler?
  1. 0
  2. 1
  3. -1
  4. O último índice válido do arranjo
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Em uma plataforma com char de 1 byte, int de 4 bytes alinhado a 4 bytes e tamanhos de estruturas arredondados para um múltiplo de 4, A tem membros char, int, char nessa ordem; B tem int, char, char. Quanto armazenamento total usa um arranjo de cinco registros A em comparação com cinco registros B?
  1. A usa 60 bytes; B usa 40 bytes.
  2. A usa 30 bytes; B usa 30 bytes.
  3. A usa 45 bytes; B usa 30 bytes.
  4. A usa 60 bytes; B usa 30 bytes.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um parâmetro de função é const reading *p. Qual atribuição pode alterar o ponteiro local sem alterar um registro?
  1. p->id = 19;
  2. (*p).temp_c = 8.0;
  3. p = another_reading_pointer;
  4. p->temp_c += 0.5;
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

a.name é responsável por uma cadeia de caracteres no heap. Após person b = a;, o código aloca com sucesso uma cadeia temporária copy, mas ainda não a preencheu nem alterou qualquer um dos registros. Ambos devem terminar responsáveis por cadeias independentes. Qual sequência consegue isso sem perder dados ou uma alocação?
  1. Definir a.name = copy; copiar b.name para a.name; liberar a.name duas vezes.
  2. Copiar a.name para copy; definir b.name = copy; liberar cada nome uma vez.
  3. Liberar a.name; copiar b.name para copy; definir b.name = copy.
  4. Copiar a.name para copy; manter b.name compartilhado; liberar ambos os nomes.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma entrada destina-se a enum mode { M_IDLE = 0, M_READ = 4, M_WRITE = 5 };. Qual teste aceita exatamente seus valores nomeados?
  1. x >= 0 && x <= 5
  2. x >= 4 && x <= 5
  3. x == 0 || x == 4 || x == 6
  4. x == 0 || x == 4 || x == 5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Durante a liberação de uma lista, p aponta para um nó ainda existente e o código executa free(p); p = p->next;. Qual alteração preserva o percurso pretendido com segurança?
  1. Atribuir p = p->next primeiro e depois liberar o novo p.
  2. Salvar next primeiro, liberar p e depois atribuir p = next.
  3. Liberar p->next primeiro e depois atribuir p = p->next.
  4. Definir p->next = NULL primeiro e depois continuar após liberar p.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma fila circular de capacidade 6 usa uma contagem. Ela começa com head = 5, count = 3, com os valores P, Q, R na ordem da fila. Ela desenfileira duas vezes e depois enfileira S e T. Quais são o índice inicial, a contagem e o primeiro valor finais?
  1. Início 0; contagem 3; primeiro Q
  2. Início 1; contagem 5; primeiro R
  3. Início 3; contagem 3; primeiro T
  4. Início 1; contagem 3; primeiro R
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma BST contém a raiz 31, o filho esquerdo 12 com filho direito 19 e o filho direito 54. Qual saída em pré-ordem permanece após remover 12?
  1. 19, 31, 54
  2. 31, 54, 19
  3. 31, 19, 54
  4. 31, 12, 19, 54
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma nova entrada de tabela é alocada, mas a alocação da cadeia de caracteres de sua chave falha antes de ela ser ligada. Qual liberação de recursos preserva a tabela e evita um vazamento?
  1. Liberar a chave da função chamadora e retornar sucesso.
  2. Liberar a nova entrada e retornar falha.
  3. Ligá-la com uma chave nula e retornar sucesso.
  4. Retornar falha mas manter a nova entrada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 5: Combine regras em casos desconhecidos10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Um laço tenta remover todos os valores 5 de 5 → 5 → 9:

node **link = &head;
while (*link != NULL) {
    if ((*link)->number == 5) {
        node *old = *link;
        *link = old->next;
        free(old);
    }
    if (*link != NULL) link = &(*link)->next;
}

Por que um 5 permanece e qual alteração corrige o laço?

  1. Ele pula um nó liberado; libere o sucessor antes de avançar.
  2. Ele pula uma correspondência mantida; avance apenas após uma não correspondência.
  3. Ele perde a ligação para o início; reinicie o laço após cada não correspondência.
  4. Ele remove o nó errado; libere antes de ler o sucessor.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Um arranjo no heap tem três registros em uso, cada um responsável por uma cadeia de nome distinta, e um ponteiro salvo para o registro 1. Um redimensionamento para capacidade 6 é bem-sucedido. O programa pretende remover o registro 0 deslocando os outros dois registros para a esquerda e reduzindo a quantidade em uso para 2. Qual plano mantém o acesso ao registro 1 original e libera todo o armazenamento sob responsabilidade dos registros?
  1. Liberar o nome removido; deslocar; reconstruir o ponteiro no índice 0; depois liberar os nomes ativos e o arranjo.
  2. Liberar o nome removido; deslocar; manter o ponteiro antigo; depois liberar todas as seis posições da capacidade e o arranjo.
  3. Deslocar; reconstruir o ponteiro no índice 1; depois liberar apenas os dois nomes ativos e o arranjo.
  4. Deslocar; reconstruir o ponteiro no índice 0; depois liberar os nomes das três posições antigas e o arranjo.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um campo de 3 bits sem sinal recebe o valor 13 em tempo de execução. Qual valor é armazenado?
  1. 13
  2. 7
  3. 1
  4. 5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma lista é 4 → 9 → 16. O código salva node **link = &head->next;, remove e libera o primeiro nó 4 e depois usa *link para remover 9. Por que essa segunda operação é inválida e qual ligação de entrada ela deve usar agora?
  1. link aponta para o nó 9 ainda existente; use o endereço do campo next de 9.
  2. link aponta para dentro do nó 16 ainda existente; use o endereço do campo next de 16.
  3. link aponta para dentro do nó 4 liberado; use o endereço do atual ponteiro para o início.
  4. link é o endereço do atual ponteiro para o início; use o endereço do antigo nó 4.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Uma lista duplamente encadeada é A ↔ B ↔ C, com A no início e C no fim. Remova B, insira D no fim e depois remova C, mantendo as duas direções. Qual é o nó final e seu predecessor?
  1. Nó final D; seu predecessor A
  2. Nó final C; seu predecessor A
  3. Nó final D; seu predecessor B
  4. Nó final A; seu predecessor D
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um avaliador de expressões pós-fixadas trata a divisão corretamente, mas inverte os dois operandos de toda subtração. Para “20 8 - 3 / 2 -”, quais são o resultado correto e o resultado do avaliador com defeito? Todas as divisões aqui são divisões inteiras exatas.
  1. Resultado correto 6; resultado com defeito 2
  2. Resultado correto 2; resultado com defeito −6
  3. Resultado correto 2; resultado com defeito 6
  4. Resultado correto −2; resultado com defeito 6
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma fila vazia enfileira 22, depois 7 e depois 15. O que sua primeira remoção retorna?
  1. 15
  2. 7
  3. Uma falha por fila vazia
  4. 22
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Insira as chaves distintas 9, 16, 23, 30 em uma BST vazia nessa ordem. Com a altura medida em arestas, qual é sua altura?
  1. 4
  2. 3
  3. 2
  4. 1
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma BST inicialmente tem apenas a raiz 40. Em uma interface que retorna a raiz, o código chama insert(root->left, 20) e ignora a alocação retornada, depois armazena root = insert(root, 60) e, por fim, root = erase(root, 40). Uma inserção bem-sucedida liga internamente os resultados dos filhos; erase reconecta os filhos mantidos. O que permanece?
  1. Raiz 60 com filho esquerdo 20; nenhuma alocação é perdida.
  2. Raiz 20 com filho direito 60; nenhuma alocação é perdida.
  3. Raiz 40 com filho direito 60; o nó 20 é perdido.
  4. Apenas a raiz 60; o nó 20 alocado é perdido.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma tabela de tamanho 7 com endereçamento aberto usa sondagem linear. Três chaves mapeadas para a posição 5 ocupam as posições 5, 6 e 0. A chave da posição 6 é removida. Por que a posição 6 deve manter um marcador de remoção em vez de se tornar uma posição não usada?
  1. Uma busca pela chave da posição 0 deve continuar após a posição 6.
  2. Uma busca pela chave da posição 5 deve movê-la para a posição 6.
  3. Uma busca deve tratar a chave removida como ainda presente.
  4. Uma busca deve reiniciar sempre sua sondagem na posição 0.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

72 Termos-Chave, cada um com sua definição

A

Alinhamento
Alinhamento é um requisito de posicionamento de endereço para o tipo de um objeto que pode causar espaços entre membros ou no fim de uma estrutura.
Um membro inteiro pode começar após um espaço que segue um membro caractere para que seu endereço fique adequadamente alinhado.
Altura
A altura de uma árvore não vazia é o número de arestas em seu caminho mais longo da raiz até uma folha; a altura usada aqui para uma árvore vazia é −1.
Uma cadeia com cinco nós tem quatro arestas em seu caminho mais longo, então sua altura é 4.
Armazenamento contíguo
O armazenamento contíguo coloca elementos de arranjo lado a lado em uma sequência de memória, permitindo acesso direto por índice e percursos por posições próximas.
Um arranjo de inteiros evita os ponteiros de ligação em cada nó e os custos de alocações separadas de uma lista de inteiros.
Arranjo de estruturas
Um arranjo de estruturas armazena registros de um mesmo tipo de estrutura de forma contígua, permitindo a seleção direta de um registro por seu índice numérico.
A expressão readings[3].temp_c acessa a temperatura no quarto registro do arranjo.

Á

Árvore binária
Uma árvore binária é uma estrutura encadeada em que cada nó tem no máximo dois filhos, diferenciados como filho esquerdo e filho direito.
Um nó pode não ter filhos, ter apenas um filho ou ter um filho esquerdo e um direito.
Árvore binária de busca
Uma árvore binária de busca ordena chaves distintas de modo que cada nó tenha chaves menores em toda a subárvore esquerda e chaves maiores em toda a subárvore direita.
Comparar a chave procurada com a chave atual seleciona a única subárvore que pode conter uma correspondência.

B

Balde (bucket)
Um balde é uma posição selecionada por um índice de tabela de espalhamento; no encadeamento separado, ele armazena o ponteiro para o início de uma lista de entradas atribuídas a ele.
Uma tabela de dez baldes que usa somas de caracteres ASCII coloca “dog” no balde 4 porque sua soma é 314.
Buffer circular
Um buffer circular reutiliza um arranjo fixo fazendo suas posições retornarem da última para a primeira, em vez de deslocar os itens restantes.
Após uma remoção no início, uma inserção posterior pode reutilizar a posição liberada quando seu índice retornar ao começo.

C

Campo de bits
Um campo de bits é um membro de estrutura declarado com uma largura em bits, limitando seus valores inteiros armazenados enquanto seu empacotamento é definido pela implementação.
Um campo de modo sem sinal de 3 bits armazena valores de 0 a 7 e não pode ter seu endereço obtido.
Capacidade
Capacidade é o número de elementos que um arranjo alocado pode conter, que pode ser maior que o número de elementos atualmente em uso.
Um arranjo com capacidade 16 e 10 registros em uso pode aceitar mais seis registros antes de outra etapa de crescimento.
Caso-base
Um caso-base trata a menor entrada de uma função recursiva sem fazer outra chamada recursiva, interrompendo a descida para subproblemas menores.
Uma função de liberação de árvore retorna imediatamente para uma subárvore nula antes de tentar ler qualquer ponteiro para filho.
Colisão
Uma colisão ocorre quando chaves distintas recebem o mesmo índice de balde, exigindo que uma tabela mantenha e distinga suas entradas separadas.
Uma busca deve comparar as chaves dentro de um balde compartilhado em vez de supor que sua primeira entrada corresponda à chave procurada.
Comportamento definido pela implementação
Comportamento definido pela implementação é uma escolha que a implementação de C deve documentar, de modo que código portável não pode supor que toda implementação faça a mesma escolha.
A ordem documentada usada para empacotar campos de bits pode diferir entre implementações da mesma declaração em C.
Continuação entre casos (fall-through)
A continuação entre casos ocorre quando a execução passa de um caso de switch para o seguinte porque nenhum break, return ou outra transferência a interrompe.
Um caso que imprime um estado, mas omite break, também pode executar a ação escrita para o caso seguinte.
Cópia profunda
Uma cópia profunda cria cópias independentes dos dados apontados sob responsabilidade do registro, além do registro externo, com responsabilidades de liberação para as novas alocações.
Copiar um nome para uma nova alocação permite que cada registro de pessoa libere seu próprio nome sem uma liberação dupla.
Cópia superficial
Uma cópia superficial duplica os valores dos membros de um registro, incluindo endereços de ponteiros, mantendo compartilhados os dados apontados que foram alocados separadamente.
Após copiar um registro de pessoa, os dois ponteiros para nome ainda acessam a mesma cadeia de caracteres, a menos que uma nova cópia seja alocada.

D

Dados associados (payload)
Dados associados são os dados armazenados selecionados pelo tipo de um valor etiquetado, mantidos separadamente da etiqueta que descreve como interpretá-los.
Em um token de calculadora, os dados associados contêm o operando numérico ou o operador identificado por sua etiqueta.
Declaração typedef
Uma declaração typedef introduz um nome alternativo para um tipo existente, permitindo que o código use um nome mais curto sem criar um tipo distinto.
Após typedef struct reading reading;, uma declaração pode usar reading *p para um ponteiro para essa estrutura.
Desencadeamento
O desencadeamento remove um nó de uma estrutura reconectando as referências ao seu redor antes de liberar qualquer armazenamento sob responsabilidade do nó.
Redirecionar o next de um predecessor para o sucessor do nó removido mantém acessível a cadeia restante.
Desenfileirar
Desenfileirar é remover o item do início de uma fila, informando que ela está vazia antes de alterar o estado ou ler um valor.
Uma remoção circular lê o primeiro item, avança o índice inicial com retorno ao começo e diminui a contagem.

E

Encadeamento separado
O encadeamento separado trata colisões de espalhamento mantendo uma lista encadeada de entradas em cada balde e comparando chaves dentro da lista escolhida.
Novas entradas de palavras podem ser inseridas no início porque sua ordem dentro de um balde não afeta a busca exata.
Endereçamento aberto
O endereçamento aberto armazena entradas diretamente em posições de arranjo e resolve colisões buscando em uma sequência definida de posições alternativas.
Uma busca pode examinar várias posições mesmo começando em um índice de espalhamento derivado de uma chave.
Enfileirar
Enfileirar é adicionar um item ao fim de uma fila enquanto se preserva a posição inicial de quaisquer valores que já estejam em espera.
Um anel baseado em contagem escreve o novo item em (head + count) % CAP e depois aumenta sua contagem.
Entrada da tabela
Uma entrada da tabela é um registro armazenado em uma tabela de espalhamento que contém uma chave e dados associados, além de um ponteiro next quando o encadeamento separado conecta entradas.
Uma entrada de contagem de palavras é responsável por uma cadeia de chave, armazena a contagem da palavra e se liga à próxima entrada em seu balde.
Enumeração
Uma enumeração define constantes inteiras nomeadas e um tipo para valores associados a essas opções, sem exigir que os valores pertençam ao conjunto nomeado.
Uma enumeração de alerta pode nomear os níveis baixo, médio, alto e crítico, mas a entrada ainda precisa ser validada.
Enumerador
Um enumerador é uma constante inteira nomeada em uma definição de enumeração, com um valor explícito ou determinado a partir de seu predecessor.
Em enum state { IDLE = 3, RUNNING };, o enumerador RUNNING recebe o valor 4.
Estrutura (struct)
Uma estrutura é um tipo de C que agrupa membros nomeados, que podem ter tipos diferentes, em um registro com armazenamento separado para cada membro.
Um registro de temperatura combina um identificador inteiro de sensor e uma temperatura em um objeto de estrutura.
Etiqueta de estrutura
Uma etiqueta de estrutura nomeia um tipo de estrutura após a palavra-chave struct e pode identificar o tipo dentro de sua própria definição.
A etiqueta em struct node permite que a definição declare uma ligação para o próprio tipo com struct node *next.
Expressão pós-fixada
Uma expressão pós-fixada escreve cada operador após seus operandos, permitindo que uma pilha mantenha valores intermediários até que o operador os combine.
Em “9 4 -”, o primeiro valor retirado é o operando da direita 4 e o resultado é 5.

F

Fator de carga
O fator de carga é o número de chaves armazenadas dividido pela quantidade de baldes, medindo a ocupação média sem fixar o comprimento de cada cadeia.
Doze chaves em oito baldes dão um fator de carga de 1,5, mesmo que alguns baldes permaneçam vazios.
Fila
Uma fila é uma coleção que adiciona no fim e remove no início, preservando a ordem de chegada entre os valores ainda em espera.
Uma fila encadeada adiciona um novo nó após seu último nó e remove o nó atualmente alcançado pelo ponteiro para o início.
Folha
Uma folha é um nó de árvore sem filhos, de modo que os dois ponteiros para filhos são nulos e sua altura é zero quando a altura conta arestas.
A remoção de uma folha libera sua alocação e substitui sua ligação de entrada por um ponteiro nulo.
Função de comparação
Uma função de comparação informa a ordem relativa de dois valores usando um resultado negativo, zero para equivalência ou um resultado positivo.
Uma função de comparação de temperaturas retorna um sinal após comparar os valores, evitando o truncamento de uma pequena diferença.
Função de espalhamento (hash)
Uma função de espalhamento transforma deterministicamente uma chave em um número que é reduzido a um índice de balde, com o objetivo de distribuir uniformemente as chaves esperadas.
Uma função de espalhamento por soma de caracteres envia rearranjos como “cat” e “act” para o mesmo balde.

I

Igualdade entre cadeias de caracteres
A igualdade entre cadeias de caracteres significa que duas cadeias terminadas em nulo contêm sequências de caracteres correspondentes, independentemente de seus endereços de armazenamento serem diferentes.
Uma busca em tabela de palavras usa strcmp(e->key, key) == 0 para reconhecer texto igual em alocações separadas.
Inicializador designado
Um inicializador designado nomeia o membro ou elemento que recebe um valor, permitindo inicializar membros de estrutura sem seguir a ordem de declaração.
Para uma struct declarada com id antes de temp_c, o inicializador {.temp_c = 21.5, .id = 7} nomeia seus membros na ordem inversa à de declaração.
Inserção ordenada
A inserção ordenada coloca um novo valor onde ele preserva a ordem existente, conectando o novo nó entre seu predecessor e seu sucessor.
Um laço em uma lista crescente para antes do primeiro valor que não seja menor que o novo valor.
Inserir no início
Inserir no início é adicionar um item antes do primeiro item atual de uma sequência, alterando qual item é alcançado pelo ponteiro para o início.
Inserir 1, depois 2 e depois 3 no início cria uma lista cujo percurso visita 3 antes de 2 e depois 1.
Instrução switch
Uma instrução switch seleciona um caso inicial a partir de uma expressão inteira e continua até que o controle saia da instrução ou da função.
Um switch sobre um valor etiquetado escolhe a operação de saída para o tipo de dados associados armazenado no momento.

L

Ligação de entrada
Uma ligação de entrada é o ponteiro que alcança um nó, seja o ponteiro para o início da lista ou o campo next de um predecessor, e deve mudar quando esse nó é contornado.
Remover o primeiro nó altera o ponteiro para o início porque nenhum nó predecessor fornece sua ligação de entrada.
Lista duplamente encadeada
Uma lista duplamente encadeada armazena ligações para o sucessor e o predecessor em cada nó, permitindo percurso de trás para a frente e ajuste por meio de qualquer vizinho.
Remover um nó interno atualiza a ligação next do nó anterior e a ligação prev do nó seguinte.
Lista simplesmente encadeada
Uma lista simplesmente encadeada é uma sequência de nós conectados por ponteiros next, com um ponteiro para o início e uma ligação nula no fim.
Inserir um nó no início faz dele o novo início e define o início antigo como seu sucessor.

M

Marcador de remoção (tombstone)
Um marcador de remoção marca uma posição removida em endereçamento aberto sem fazê-la parecer nunca usada, permitindo que a busca continue até entradas posteriores que colidiram.
Uma busca passa por um marcador de remoção em vez de parar antes de uma chave correspondente armazenada mais adiante no caminho de sondagem.
Membro
Um membro é um componente nomeado de uma estrutura ou união, selecionado em um objeto com um ponto ou por meio de um ponteiro com uma seta.
A expressão r.temp_c seleciona o membro de temperatura da leitura armazenada em r.

N

Nó
Um nó é um registro que contém dados e os ponteiros que o conectam a outras partes de uma estrutura encadeada.
Um nó de lista contém um inteiro e seu ponteiro para o sucessor; um nó de árvore, por sua vez, tem dois ponteiros para filhos.

O

Operador seta
O operador seta seleciona um membro de estrutura ou união por meio de um ponteiro e combina desreferenciação com seleção de membro.
Para um ponteiro p para um registro ainda existente, p->temp_c acessa o mesmo membro que (*p).temp_c.

P

Passagem por valor
Passagem por valor significa que um parâmetro de função recebe uma cópia do valor fornecido, de modo que alterar esse parâmetro não substitui o valor da função chamadora.
Uma alteração de temperatura em um parâmetro de registro passado por valor deixa inalterado o membro de temperatura da função chamadora.
Percurso
Um percurso visita nós seguindo as ligações de uma estrutura em uma ordem definida, parando no fim apropriado ou no caso de subárvore vazia.
Um ponteiro separado de percurso de lista move-se pelas ligações next enquanto o responsável mantém o ponteiro original para o início.
Percurso em ordem
Um percurso em ordem visita cada subárvore esquerda antes de sua raiz e depois sua subárvore direita, produzindo chaves ordenadas em uma árvore binária de busca.
Imprimir uma BST de contagem de palavras em ordem produz as palavras armazenadas na ordem usada por suas comparações de texto.
Percurso em pós-ordem
Um percurso em pós-ordem visita as subárvores esquerda e direita antes de sua raiz, permitindo que a liberação de recursos libere os filhos antes do pai.
Uma função de liberação de árvore lê as ligações para filhos enquanto o pai ainda existe e depois libera esse pai por último.
Percurso em pré-ordem
Um percurso em pré-ordem visita a raiz de uma subárvore antes de visitar suas subárvores esquerda e direita, colocando cada pai antes de seus descendentes.
Uma saída em pré-ordem começa com a chave da raiz mesmo quando ela não é a menor nem a maior.
Pilha
Uma pilha é uma coleção em que a inserção e a remoção usam o topo, de modo que o valor inserido mais recentemente é o próximo a ser removido.
Um verificador de delimitadores armazena a abertura sem par mais recente no topo de sua pilha.
Ponteiro para dados constantes
Um ponteiro para dados constantes dá acesso a um objeto que não pode ser modificado por meio desse ponteiro, enquanto o próprio ponteiro ainda pode mudar.
Uma função de impressão pode ler por meio de const reading *p, mas não pode atribuir um valor a p->id.
Ponteiro para o fim da lista
O ponteiro para o fim da lista designa o último nó de uma sequência encadeada e é mantido para que a inserção no fim dispense um percurso completo.
Uma fila encadeada define como nulos os ponteiros para o início e o fim quando seu único nó é removido e liberado.
Ponteiro para o início da lista
O ponteiro para o início da lista designa o primeiro nó de uma sequência encadeada; quando é nulo, representa uma sequência sem nós.
Remover o primeiro nó da lista exige atualizar o ponteiro para o início mantido pelo responsável antes de liberar a alocação do nó antigo.
Ponteiro para ponteiro
Um ponteiro para ponteiro contém o endereço de um objeto ponteiro, permitindo que uma função leia ou substitua esse ponteiro por meio do nível adicional.
Um node **link pode passar do endereço do ponteiro para o início ao endereço do campo next de um nó.
Ponteiro pendente (dangling pointer)
Um ponteiro pendente refere-se a um objeto cujo tempo de vida terminou, de modo que desreferenciá-lo não fornece mais acesso válido a esse objeto.
Retornar o endereço de um registro local deixa a função chamadora com um ponteiro para um objeto que deixou de existir.
Preenchimento de alinhamento (padding)
O preenchimento de alinhamento consiste em bytes não usados que uma implementação pode colocar entre membros de uma estrutura ou após o último membro para atender aos requisitos de disposição.
Uma comparação de bytes pode detectar preenchimentos diferentes mesmo quando dois registros contêm valores de membros iguais.
Primeiro a entrar, primeiro a sair (FIFO)
FIFO significa primeiro a entrar, primeiro a sair e descreve a ordem em que o item armazenado há mais tempo é tratado antes dos itens posteriores ainda em espera.
Após enfileirar A, B e C, uma remoção FIFO retorna A antes de qualquer uma das letras posteriores.

R

Raiz
A raiz é o nó onde uma árvore ou subárvore começa, alcançado pelo ponteiro para a raiz mantido pelo responsável ou por uma ligação para filho de um pai.
Substituir uma raiz removida com um filho exige armazenar o filho sobrevivente como nova raiz da árvore.
Realocação
A realocação altera o tamanho de um bloco alocado e pode movê-lo, exigindo que o responsável use o endereço inicial retornado após o sucesso.
Um ponteiro temporário para o resultado preserva os registros antigos quando o redimensionamento de um arranjo para um tamanho diferente de zero falha.
Reespalhamento (rehashing)
O reespalhamento recalcula as posições das chaves armazenadas para uma nova quantidade de baldes e move ou reconecta as entradas para que buscas posteriores usem as posições corretas.
Dobrar o arranjo de baldes exige reduzir o valor de espalhamento de cada chave com a nova quantidade antes de posicioná-la.
Responsabilidade pela liberação (ownership)
A responsabilidade pela liberação atribui a obrigação de liberar uma alocação, mesmo quando vários ponteiros podem acessar o mesmo objeto durante seu tempo de vida.
Um nó de palavra é responsável por sua cadeia de caracteres, então a liberação deve ocorrer antes de liberar o nó que contém seu endereço.

S

Sondagem linear
A sondagem linear resolve uma colisão tentando posições consecutivas do arranjo com retorno ao início, usando a mesma sequência para inserção e busca.
Uma sondagem que começa na última posição continua na posição zero quando outra posição ocupada precisa ser verificada.
Subárvore
Uma subárvore é um nó junto com todos os seus descendentes, formando uma árvore menor na qual as mesmas regras de busca e atualização podem operar.
A restrição da subárvore esquerda de uma BST se aplica tanto a descendentes distantes quanto ao filho esquerdo direto.
Sucessor em ordem
O sucessor em ordem de um nó com subárvore direita é a menor chave dessa subárvore, usada para substituir a chave de um nó com dois filhos durante a remoção.
Siga o filho direito e depois as ligações à esquerda para encontrar a chave de substituição na remoção de um nó com dois filhos.

T

Tamanho da árvore
O tamanho da árvore é seu número de nós, encontrado somando um para sua raiz aos tamanhos das duas subárvores filhas, com zero para uma árvore vazia.
Uma raiz com dois filhos que são folhas tem tamanho 3, embora sua altura medida em arestas seja apenas 1.
Tentativa de remoção em coleção vazia (underflow)
Uma tentativa de remoção em coleção vazia ocorre quando um programa tenta remover um item de uma coleção vazia, exigindo um resultado de falha antes de um acesso inválido.
Uma remoção de pilha em arranjo deve verificar se a contagem de itens é zero antes de decrementá-la e ler o arranjo.
Tipo abstrato de dados (TAD)
Um tipo abstrato de dados descreve uma coleção por meio de suas operações e comportamentos permitidos, independentemente do armazenamento escolhido para implementá-la.
Uma pilha mantém o mesmo comportamento de inserção e remoção no topo, quer seus valores estejam em um arranjo, quer em nós encadeados.

U

União (union)
Uma união é um tipo de C cujos membros compartilham armazenamento, permitindo que esse espaço contenha uma alternativa armazenada entre diferentes tipos de membros.
Em uma union com um membro inteiro e um membro double, escrever no double substitui os dados inteiros anteriores em vez de preservar dois valores.
União etiquetada
Uma união etiquetada combina armazenamento compartilhado de dados associados com uma etiqueta separada que identifica qual membro da união deve ser acessado na leitura.
Um valor etiquetado como K_CHAR é impresso usando o membro caractere e uma conversão de formato para caractere.

Ú

Último a entrar, primeiro a sair (LIFO)
LIFO significa último a entrar, primeiro a sair e descreve a ordem em que o item armazenado mais recentemente é tratado antes dos itens anteriores ainda presentes.
Após uma pilha receber 4, 7 e 1, suas duas primeiras remoções retornam 1 e 7, deixando 4.

Sobre esta aula

Por StudeliRelatar um erro

Crie seus próprios Materiais de Estudo

Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.

Começar