Structs and Linked Data Structures in C

Learn to group related data with structs and typedef, name sets of values with enums and store alternatives in unions. Then build the classic linked data structures from nodes allocated with malloc: linked lists, stacks, queues, binary search trees and hash tables. For each one you insert, find, remove and traverse items, handle the empty and edge cases, and free every node without leaks.

IntermediateLesson 6 of 8 in Programming in C Course

What you will learn

  1. Define struct types, give them short names with typedef, reach their members through variables and pointers, and keep many records in arrays of structs.
    • Define a struct and work with its members
      Declare a struct type, create and initialize struct variables, read and change members, and understand how copying, comparison and memory layout work.
    • Name types with typedef and pass structs to functions
      Create type aliases with typedef, pass structs by value or through pointers, use the arrow operator, and allocate structs on the heap.
    • Store and process records in arrays of structs
      Keep many records in an array of structs, loop over them, find and sort records, grow the array on the heap, and handle structs that own other memory.
  2. Replace magic numbers with enumerations, and use unions and tagged unions to store one of several kinds of value in the same memory.
    • Use enums for named sets of values
      Define enumerations, control their values, use them in conditions and switch statements, and know what C does and does not check about them.
    • Overlay alternatives with unions and tagged unions
      Declare unions, know what reading and writing their members does, pair a union with an enum tag to store values of different kinds safely, and pack small flags into bit-fields.
  3. Link dynamically allocated nodes into lists, walk them, insert and delete nodes in any position, free them completely, and decide when a list suits a task better than an array.
    • Create, traverse and free a singly linked list
      Define a node type, add nodes to the front and back of a list, visit every node, write functions that change the head, and release all nodes.
    • Search, insert in order and delete nodes
      Find a value in a list, insert a node at the right place in a sorted list, and unlink and free a node, handling the first node, the last node and missing values.
    • Compare lists with arrays and extend them
      Weigh linked lists against arrays, keep a tail pointer and a count, build doubly linked lists, and free nodes that own other memory.
  4. Implement last-in-first-out stacks and first-in-first-out queues with arrays and with linked nodes, guard against empty and full structures, and apply them to real tasks.
    • Build a stack and apply it
      Describe the stack operations, implement a stack with an array and with linked nodes, handle empty and full stacks, and use stacks to check brackets and evaluate postfix expressions.
    • Build a queue with linked nodes or a circular buffer
      Describe the queue operations, implement a queue with head and tail pointers and with a fixed-size circular array, and tell an empty queue from a full one.
  5. Represent binary trees with linked nodes, search, insert and delete keys in a binary search tree, visit the nodes in different orders, and free a whole tree.
    • Represent a binary tree and search a binary search tree
      Use the vocabulary of binary trees, state the binary search tree property, and search a tree recursively and with a loop.
    • Insert, traverse, delete and free tree nodes
      Insert keys into a BST, visit nodes in in-order, pre-order and post-order, compute size and height, delete keys and free every node.
  6. Turn keys into bucket indexes with a hash function, handle collisions with chains of nodes, and implement, use and free a hash table in C.
    • Map keys to buckets with a hash function
      Explain what a hash function does, compute bucket indexes for string keys, recognize collisions and uneven spreads, and use the load factor to decide when to grow a table.
    • Implement a chained hash table in C
      Build a hash table from an array of linked-list buckets: look up, insert and count keys, copy string keys safely, and free every allocation.

Structs and Linked Data Structures in C study guide

6 chapters

1

Group related data with structs

Related values are easier to handle when they travel as one record. Structs give those values names, and type aliases make the declarations easier to read. Function calls then raise a practical choice: pass a copy or pass an address that reaches the original. Arrays extend that choice to many records, where sorting, resizing, and copied pointer members each require a clear account of which objects still exist and which code owns them.

1.1 Struct members, copies, and layout

A struct groups related values into one record. Its members have names and can have different types. A sensor reading, for example, needs both an identifier and a temperature.

Define and initialize a record
struct reading {
    int id;
    double temp_c;
};
struct reading r = {.temp_c = 21.5, .id = 7};
r.temp_c += 0.5;

The definition introduces the type struct reading; reading is its tag. The final semicolon belongs after the closing brace. Defining the type allocates no object. The declaration of r creates one object, and . selects its members. Here the temperature becomes 22.0 °C. With only a tag declared, the type name includes struct: reading r; needs a separate typedef.

An initializer such as {7, 21.5} follows declaration order. Designators such as .id = 7 let you name members in another order. Members omitted from an initializer receive zero values, so {0} initializes the whole record. A local declaration struct reading r; without an initializer leaves its members indeterminate until assigned.

A member has its own type and address. To read a temperature with scanf, use scanf("%lf", &r.temp_c) and check that the return value is 1. The dot binds before &, so this passes the address of the double member.

Copy and compare the values
struct reading s = r;
s.temp_c = -4.0;
int same = r.id == s.id && r.temp_c == s.temp_c;

Assignment copies all members. Changing s.temp_c leaves r.temp_c at 22.0. Fixed arrays inside structs are copied too: a char name[32] member belongs to the record. This works even though standalone C arrays cannot be assigned with =. Pointer members need extra care because the copied pointer still points at the same object.

C does not define == for whole structs. Compare the members that define equality for your task. memcmp compares raw bytes, including padding, and can report a difference between records whose member values are equal.

Follow the memory layout

Members appear in declaration order, with no padding before the first member. The compiler can add unused bytes between members and at the end to meet alignment requirements. On a common 64-bit platform with 4-byte int and 4-byte alignment, struct { char c; int i; char d; } occupies 12 bytes. Its members start at offsets 0, 4, and 8. Reordering to int i; char c; char d; commonly reduces the size to 8 bytes. Use sizeof for the actual size and offsetof from <stddef.h> for an offset.

Structs can nest. With struct point { double x_m, y_m; }; and struct rect { struct point corner; double width_m, height_m; };, box.corner.x_m reaches the corner's x-coordinate in meters. A struct cannot contain a complete member of its own type: that would require an endlessly large object. A pointer to its own type has a fixed size and makes linked nodes possible.

Key Takeaways

  • Use . to select a member of a struct object.
  • An initializer zeroes omitted members; an uninitialized local struct has indeterminate members.
  • Whole-struct assignment copies members, including embedded arrays.
  • Compare member values, and use sizeof to include padding.
  • A struct can contain a pointer to its own type.

1.2 Type names, function calls, and struct pointers

A typedef gives an existing type another name. It changes how you write the type, not how its objects behave. typedef double meters; makes meters another name for double.

Name a struct and pass it by value
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;
}

You can now declare reading r = make_reading(7, 21.5);. Returning a struct returns its value. Calling warm_copy(r) gives the function a copy, so the caller's temperature stays 21.5 °C. A copy of a small record is often the clearest interface when the caller's object should stay unchanged.

A self-referential type needs its tag inside the definition:

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

The alias node becomes available after the declaration. Inside these braces, struct node already names the incomplete type and can be used for a pointer. The tag and the alias can have the same spelling because tags use their own namespace.

Reach the caller's object through a pointer
void warm(reading *p) {
    p->temp_c += 1.0;
}
reading r = {7, 21.5};
reading *p = &r;
p->temp_c -= 1.5;
warm(&r);

The subtraction changes r.temp_c to 20.0; warm then changes it to 21.0. The function receives a copy of the address, and both pointers still reach the same record. Use -> with a pointer and . with a struct object. p->temp_c means (*p).temp_c. The parentheses are necessary because member access binds more tightly than dereferencing: *p.temp_c tries to select a member from p before applying *.

For reading without a struct copy, accept const reading *p. Assigning to p->temp_c through that pointer is rejected. The pointer variable can still change, for example when it moves through an array. reading *const p instead makes the pointer itself fixed. For structs with pointer members, pointer-to-const does not make separately allocated pointed-to data read-only.

Give a heap record an owner

Include <stdlib.h> for allocation and <stdbool.h> for this success result:

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;
}

The function writes *out only after allocation succeeds. The caller can declare reading *item = NULL;, call make_heap_reading(&item, 8, -2.0), handle a false result, and later call free(item). sizeof *p measures the pointed-to type even before p points at an initialized record; this expression does not read that record.

A pointer to an automatic local struct becomes dangling when the function returns. Return the struct value or a heap allocation with a clear owner. Returning &r from a function whose local variable is r cannot extend its lifetime.

Key Takeaways

  • typedef makes an alias, and a self-referential definition uses its struct tag.
  • Pass a struct by value for a copy or pass its address to change the original.
  • p->member is equivalent to (*p).member.
  • Pointer-to-const prevents member changes through that pointer.
  • Check allocation before initialization and give each heap record an owner.
  • Returning a local struct by value is valid; returning its address leaves a dangling pointer.

1.3 Arrays of records and owned strings

An array of structs keeps records side by side. Use an index to select a record and then a dot to select a member. This makes scans, searches, and sorting work much like they do with an array of numbers.

Scan records with an explicit count
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;
}

Here reading is the alias for the identifier-and-temperature struct. The best index progresses from 0 to 3, so the warmest record has id 4 and temperature 30.0 °C. Starting with a real element also works when every temperature is negative; starting with a supposed maximum of zero can fail. This scan requires at least one element. A reusable search must report an empty array before reading element 0.

The sizeof count works where readings is an array. In a function parameter, an array declaration becomes a pointer, so pass n separately. (items + i)->temp_c and items[i].temp_c reach the same member.

Supply a comparator for qsort

Include <stdlib.h>. For finite temperature values, this comparator returns a sign without converting a small temperature difference to an integer:

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);

The comparator receives addresses of elements. Negative means the first comes earlier, zero means a tie, and positive means later. The sorted id order is 2, 3, 1, 4. Returning the raw double difference truncates a difference such as −0.25 to zero. Subtracting integer keys can overflow. Comparing first avoids those errors. qsort does not preserve the original order of ties.

Grow an array without losing its storage

Keep its used count and capacity separately. For an allocated array with positive cap, use a temporary pointer when doubling capacity. Include <stdint.h> for 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;

These statements belong inside a function returning bool; items is its array pointer and cap its positive capacity. On failure the old allocation remains available. On success the allocation may move, so old pointers to its elements must be replaced. A zero starting capacity needs its own initial allocation.

Decide what a copy owns

With typedef struct { char *name; int age; } person;, person b = a; copies the address in name. Both records reach the same string. Freeing both copies as owners would free the same allocation twice. For an independent copy, allocate strlen(a.name) + 1 bytes, check success, and use strcpy from <string.h>. A fixed member char name[32] is copied with the struct. strdup performs allocation and copying, but it belongs to POSIX and C23, not the C17 standard library.

Key Takeaways

  • Use items[i].member and pass a count to functions that receive arrays.
  • Initialize a maximum search from an existing element after checking for an empty array.
  • A qsort comparator returns a sign; subtracting keys can truncate or overflow.
  • Check allocation-size arithmetic and store realloc in a temporary pointer.
  • A copied pointer member still refers to the original allocation.
2

Name choices with enums and share storage with unions

A program often needs a choice with a readable name, or a value that can take several different forms. Enums name the choices; unions share storage between alternatives. Pairing them makes each stored value describe its own kind. The key is to keep the name and payload consistent while recognizing that C leaves input validation and representation choices under the program’s control.

2.1 Enum values and checked choices

An enumeration gives readable names to integer choices. Names such as HIGH make a condition's purpose visible without requiring the reader to remember what the number 10 represents.

Follow the numbering
enum weekday { MON, TUE, WED, THU, FRI, SAT, SUN };
enum level { LOW = 1, MEDIUM, HIGH = 10, CRITICAL };
enum level alert = HIGH;

An omitted first value is zero. Later omitted values increase the previous value by one. Thus MON is 0 and SUN is 6; the levels are 1, 2, 10, and 11. An explicit value restarts the counting at that point. Different constants can share a value, but identical values cannot appear as separate case labels in the same switch.

An alias can shorten a declaration: typedef enum { RED, YELLOW, GREEN } light; allows light signal = RED;. Enumeration constants share the ordinary identifier namespace in their scope. Two different enums cannot both declare RED in the same scope. Prefixes such as LIGHT_RED and PAINT_RED keep the names distinct.

Handle each choice
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";
}

Each return ends its case. If a case instead performs an action and should then stop, give it a break; reaching the next label continues execution there. With GCC, -Wall enables warnings for missing enum cases when there is no default; Clang also diagnoses such omissions by default. A warning can reveal a missing case when you later add a constant.

Validate input as a set of choices

The enum type does not enforce membership in the named set. C17 permits assignment from an integer, so enum level x = 42; can compile. A check that the number lies from 1 through 11 still admits unnamed values such as 6. Test the actual allowed values:

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

Only after that check should input become a level. For the consecutive weekdays, a range check from MON through SUN is sufficient. In an ordinary C17 enum, the enumerator constants have type int. C23 additionally supports a specified underlying type, such as enum color : unsigned char; that syntax is not C17.

Print names and use counts

printf("%d\n", SUN) prints "6", not "SUN". For consecutive values starting at zero, a name array gives the text: const char *day_names[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};. After validating an index, day_names[SUN] is "Sun". Indexing such an array with a level value such as HIGH would need a different mapping because the level values have gaps.

Adding DAY_COUNT immediately after SUN gives it value 7, useful as a loop bound or array size. It counts days but is not itself a valid day. Enum names follow C scope rules and can be shown by debuggers; a #define instead performs preprocessor text substitution.

Key Takeaways

  • Enum numbering begins at zero unless a value is supplied, and resumes after each explicit value.
  • An enum variable can hold an unnamed value, so validate external input.
  • Use break or return to end a case when fallthrough is unwanted.
  • An index-to-name array needs validated, consecutive indexes starting at zero.

2.2 Unions with a matching tag

A union gives several members the same storage. It fits a value that can be one of several kinds. A struct gives each member its own storage and fits a record that needs all those values together.

Store one alternative
union value {
    int i;
    double d;
    char c;
};
union value v = {.d = 2.5};

All members begin at the same address. The union must hold its largest member and meet alignment needs; with an 8-byte double on common platforms, this union occupies 8 bytes. A plain initializer {42} initializes its first member, i; .d = 2.5 chooses d explicitly.

Writing v.i = 42 after the initializer replaces the stored alternative. Reading v.d afterward does not convert 42 into a double: it interprets the stored bytes using another representation. That representation can be invalid. Read the member belonging to the value you stored.

Record which member is valid

Pair the union with an enum. The enum is the tag, and the union is the payload:

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}};
}

A helper sets both pieces together. Writers keep the two fields synchronized. If code writes t.as.i but leaves t.kind as K_DOUBLE, a later reader chooses the wrong representation.

#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;
    }
}

Calling this function for cells holding 42, 2.5, and 'x', with spaces printed between calls, produces "42 2.5 x". Each case chooses both the matching member and the matching format. A calculator token can similarly hold either a number or an operator.

On common 64-bit platforms, a 4-byte enum, padding, and this 8-byte union make struct tagged occupy 16 bytes. A struct storing the enum plus all three alternatives separately commonly takes 24 bytes. These are layout examples; query the actual types with sizeof.

Pack small flags into bit-fields
struct flags {
    unsigned int ready : 1;
    unsigned int mode : 3;
};

The width follows the colon. A 1-bit unsigned field holds 0 or 1; a 3-bit field holds 0 through 7. Storing 9 in mode keeps the low three bits and stores 1. This loses information, so validate a mode before storing it when larger values indicate an error.

A bit-field has no separately addressable location, so &flags.mode is invalid for a variable named flags. Its allocation order and packing depend on the implementation. Do not use a bit-field declaration as a portable promise about an exact file or device layout. A named width specifies the field's value range, not the layout of every byte around it.

Key Takeaways

  • Union members overlap, while struct members have separate storage.
  • A tag must match the union member containing the stored value.
  • A switch selects the matching member and output format.
  • Unsigned bit-field widths limit stored values; excess high bits are lost.
  • Bit-field layout is implementation-defined and fields have no address of their own.
3

Build and maintain linked lists

Linked structures grow by connecting separate allocations. A list makes those connections visible: one pointer starts the chain, and each node leads to the next. The interesting work happens when links change, especially at the first or last node. Keeping the right incoming link lets the same operation handle several positions, while tail pointers and two-way links change which operations need a search.

3.1 Create a list and keep every node reachable

A singly linked list stores each item in a separately allocated node. Each node holds its value and the address of the next node. The head points at the first node; NULL marks both an empty list and the end of a chain.

Define the link and prepend
#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;
}

Start with node *list = NULL;. Call prepend(&list, value) and handle a false result. The function takes the address of the caller's head pointer. *head therefore names the pointer that must change. Taking only node *head and assigning to that local pointer would leave the caller's head unchanged.

The order of the final assignments preserves the old list. First connect the new node to the old head; then replace the head. Reversing those statements sets n->next to n itself and loses the old chain. For arrivals 1, 2, and 3, successive prepends produce 1, then 2 → 1, then 3 → 2 → 1 → NULL. Front insertion reverses arrival order.

Walk without moving the owner
#include <stdio.h>
void print_list(const node *head) {
    for (const node *p = head; p != NULL; p = p->next)
        printf("%d ", p->number);
}

The traversal pointer p moves while head stays available. The example prints "3 2 1 " with a trailing space. For an empty list, the loop body runs zero times. There is no array index: each step reads one link to find the next node. The allocations can be scattered throughout memory, and inserting a node does not move existing nodes.

Add at the back
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 starts at the caller's head. It then moves to the address of each next member until it names the terminating null link. Assigning there works for an empty list too. Without a saved tail pointer, every append must first follow the existing chain to its end. A function can also return a new head, provided the caller stores it and handles allocation failure without losing the old list.

Release nodes in a safe order
void clear_list(node **head) {
    while (*head != NULL) {
        node *old = *head;
        *head = old->next;
        free(old);
    }
}

Read the successor while old is still alive, move the head, and then free the old node. After the loop the caller's head is NULL. Reading old->next after freeing old is a use-after-free even if the memory appears unchanged. A memory checker can help reveal invalid reads and forgotten allocations; cleanup must also run after a later operation fails.

Key Takeaways

  • An empty list has a null head, and the last node has a null next link.
  • Allocate and initialize a new node before changing the existing chain.
  • Give head-changing functions the head address or store their returned head.
  • Traverse through a separate pointer so the list stays reachable.
  • Without a tail pointer, appending requires a walk to the end.
  • Read the successor before freeing a node.

Searching a list follows one link at a time. Insertion and deletion need something more: the address of the link that must change. That link may be the head pointer or a next member inside an earlier node.

Search by value

Using the node type with number and next members:

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

A match returns the actual node's address. A missing value returns NULL. Reaching the 1,000th node requires following 999 links from the head, even when you know its position in advance.

Keep a pointer to the link that will change

A node ** can point at either the head or a node's next field. Call it link: *link is the node currently reached by that link. Advancing with link = &(*link)->next changes which link you inspect; it does not change the list.

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;
}

Include <stdbool.h> and <stdlib.h>. The input list must already be in ascending order. Stop before the first value that is not smaller than the new one, so duplicates go before equal values. The left side of && checks for NULL before the right side dereferences the node. Swapping those tests would dereference a null pointer at the end.

For insertions 5, 2, 8, and 4, the final list is 2 → 4 → 5 → 8. When inserting 4 into 2 → 5 → 8, link moves from the head to 2.next. The new node points to 5, and 2.next is redirected to the new node. If the new item is smallest, link still names the head. At the end, it names the final null link.

Unlink before freeing
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;
}

The loop finds the incoming link. Saving its target as old keeps the node available while *link is changed to its successor. Only then is the removed node freed. Removing 5 from 2 → 4 → 5 → 8 gives 2 → 4 → 8. Removing 2 next changes the head and leaves 4 → 8. If the value is absent, the function returns false without changing the list.

A two-pointer version keeps prev and cur. When prev is null, the match is the head and the head must change; otherwise update prev->next. Forgetting the head case leaves a pointer to freed memory. The link-address version handles both through the same assignment.

Check empty input, a one-node list, first and last matches, and a missing value. For each change, identify the incoming link, the affected node, and its successor before writing an assignment. No remaining node should become unreachable, and no link should point at a freed node.

Key Takeaways

  • Search returns a matching node or NULL after reaching the end.
  • A pointer to a link can name either the head or a next field.
  • Sorted insertion links the new node to its successor before redirecting the incoming link.
  • Deletion redirects the incoming link before freeing the removed node.
  • Test the head, tail, empty, single-node, and missing-value cases.

The useful question is which operations your program performs. Arrays and lists can hold the same values, but they reach and move those values differently.

Compare the operations
TaskArraySingly linked list
Reach position 500Use a[500] directlyFollow links from the head
Insert after a held positionShift later elementsChange two links after allocation
Scan all valuesRead contiguous elementsFollow potentially scattered nodes
Delete a held nodeShift later elementsAlso obtain its incoming link

A list does not make finding a position free. Insertion after a node you already hold is direct, but deletion also needs the predecessor or the link that points at the node. With only the node's address, a singly linked list generally needs a walk from the head to find that incoming link.

Storage also matters. On a typical 64-bit platform, a node with a 4-byte int and an 8-byte pointer occupies 16 bytes after padding. For 1,000 integers, nodes use 16,000 bytes plus allocation bookkeeping; a contiguous integer array uses 4,000 bytes. Scanning the array is usually faster because nearby values are stored together.

Keep list metadata together
typedef struct {
    node *head;
    node *tail;
    size_t count;
} list_t;

Initialize a wrapper with list_t list = {0};. Its empty-state rules are head == NULL, tail == NULL, and count == 0. Each successful insertion or deletion must update the count as well as the links. A failed allocation changes none of them.

With a tail pointer, appending after a checked allocation takes these assignments:

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

Here n is an initialized node. For the first node, head and tail become equal. Removing the last node must move the tail back; for a singly linked list this requires the predecessor. Removing the only node sets both ends to NULL and the count to zero.

Link both directions
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);
}

This function receives a live node belonging to the list. Both neighbors are repaired before it is freed. End cases change head or tail instead of a missing neighbor. A doubly linked list can walk backward and delete a held node without searching for its predecessor, at the cost of another pointer per node and more links to maintain.

Release owned data before its node

A word_node may own both a node allocation and a string allocation through word. Save next, call free(n->word), then free(n), and move to the saved successor. Freeing only the node leaks the string. Reading n->word after freeing the node is invalid. Ownership describes which code must release each allocation, even when several pointers can reach it.

Key Takeaways

  • Arrays give direct indexed access and compact contiguous storage.
  • Lists support local link changes once the required position or incoming link is known.
  • A tail pointer makes append direct, but every operation must maintain both endpoints.
  • Doubly linked nodes store the predecessor and successor.
  • Free an owned payload before freeing the node that stores its address.
  • A stored count must change with every successful insertion and deletion.
4

Implement stacks and queues

Stacks and queues describe the order in which work is handled. A stack keeps the newest unfinished item ready, which suits nesting and expression evaluation. A queue keeps the earliest waiting item ready, which suits arrival order. Both can use arrays or linked nodes. Their implementations must make empty and full states explicit and keep every count, endpoint, and index consistent after a change.

4.1 Stack operations and nested work

A stack removes the most recently added item first: last in, first out (LIFO). Its operations define an abstract data type independently of whether an array or linked nodes hold the values. push adds at the top, pop removes the top, and peek reads the top without removing it.

Use an array and a count
#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;
}

Initialize with stack s = {0};. Here top is the number of stored items, so the top occupied index is top - 1. A successful push writes the next free slot and increases the count. A successful pop first lowers the count and then reads that index. The caller supplies a valid out pointer and checks the result before using its value.

Pushing 4, 7, and 1 then popping twice returns 1 and 7, leaving 4. Without a capacity check, a full push writes items[STACK_CAP] past the end. Without the empty check, the signed top drops from 0 to −1 and pop reads items[-1]. Both accesses have undefined behavior.

Use a list head as the top

A linked stack pushes by prepending a checked allocation. Pop removes the head:

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 has an integer number and a next pointer. A linked stack has no fixed array capacity, but a push can fail when allocation fails. It still needs empty checks and cleanup of nodes left at the end.

Match nested delimiters

Scan input from left to right. Push each opening parenthesis, square bracket, or brace. For each closing delimiter, pop and check that the types match. A closer with no opener fails immediately. Finish by checking that the stack is empty: leftover openers also mean a mismatch.

The text "( [ ] { } )" balances. For "( ]", the closer does not match the popped opener. For "( ( )", one opener remains after scanning. A simple count of openers and closers would miss incorrect nesting such as "( [ ) ]".

Evaluate postfix expressions

Push numbers as they arrive. For a binary operator, pop the right operand first, then the left operand, compute left operator right, and push the result. For "3 4 + 2 *", + combines 3 and 4 into 7, then * combines 7 and 2 into 14. For "9 4 -", pop 4 as the right operand and 9 as the left, producing 5. Reversing them produces −5. Reject insufficient operands and require one final result; division also needs a nonzero right operand.

Function calls use stack-shaped lifetimes: a called function returns before its caller continues. Each active call needs a frame, so very deep recursion can exhaust the call stack even when heap allocations succeed.

Key Takeaways

  • LIFO means the newest item is removed first.
  • In the array implementation, top counts items; check empty and full before accessing.
  • A linked stack pushes and pops at the list head.
  • Delimiter matching needs both matching types and an empty stack at the end.
  • For postfix subtraction or division, the first popped value is the right operand.

4.2 Queue endpoints and circular storage

A queue removes the earliest item first: first in, first out (FIFO). Enqueue adds at the back; dequeue removes from the front. After enqueuing A, B, and C, the next two removals return A and B. This fits work that must be handled in arrival order.

Keep both ends of a linked queue

Using nodes with number and next members, and including <stdbool.h> and <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;
}

Initialize with queue q = {0};. Enqueue sets the old tail's link before moving the tail. For the first node, it also sets the head. Dequeue moves the head forward. When that removes the only node, the tail must become NULL too. Otherwise the next enqueue writes through a dangling tail pointer. Neither operation needs to walk the list.

Reuse a fixed array as a ring

A plain array queue that always removes index 0 must shift its remaining values. A circular buffer instead tracks a front index and an item count:

#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;
}

Initialize a ring with {0}. head always identifies the front item when the queue is nonempty. The next insertion index is the front plus the number of items, wrapped by % CAP. Read the front before moving head. The count distinguishes empty from full without giving up a slot.

Trace the wrap

Enqueue A, B, and C into slots 0, 1, and 2. Dequeue A: head becomes 1 and count becomes 2. D enters slot 3; E enters slot 0. The physical array now holds E, B, C, D, but logical removal order is B, C, D, E. The ring is full because count is 4, even though the front is at index 1.

Another design tracks head and the next free tail index without a count. In that design equal indexes could mean empty or full, so a common policy leaves one slot unused. Empty is head == tail; full is (tail + 1) % CAP == head. Do not mix that policy with the count-based version's capacity rules. Queues also support level-by-level tree visits by holding nodes awaiting their turn.

Key Takeaways

  • FIFO removes the oldest queued value.
  • A linked queue appends at the tail and removes at the head.
  • Removing the only node must reset both head and tail.
  • A circular queue wraps indexes and stores logical order independently of physical order.
  • Use a count or an unused-slot policy to distinguish empty and full.
  • Read the front before advancing the head.
5

Build and traverse binary trees

Trees replace a single chain with branches. A search tree uses key order to choose a branch, but its shape determines how many nodes that search reaches. The same subtree idea guides insertion, traversal, counting, and deletion. Each operation needs an empty case, a rule for handling the current node, and a clear decision about which parent pointer must receive a changed subtree root.

5.1 Search a tree by following the ordering rule

A binary tree gives each node at most two children, called left and right. The root is the top node. A leaf has no children, and a node together with all its descendants is a subtree. A missing child and an empty tree are represented by NULL.

Represent the branches
typedef struct tnode {
    int key;
    struct tnode *left;
    struct tnode *right;
} tnode;

Each child pointer leads to the root of another tree. The same definition therefore describes a leaf, a large branching tree, or a long chain. Height counts edges on the longest root-to-leaf path: a root alone has height 0. Use −1 for an empty tree so a leaf's two empty subtrees lead naturally to height 0. Counting levels instead would give one more.

Apply the rule to whole subtrees

A binary search tree, or BST, orders the keys. For every node, all keys in its left subtree are smaller and all keys in its right subtree are larger. Store each key once. This condition includes distant descendants. A root 50 with left child 30 and a right child 60 under that 30 is invalid: 60 is still in the left subtree of 50.

Insert 50, 30, 70, 20, 40, 60, and 80 into an empty BST. The root is 50; 30 and 70 are its children. The children of 30 are 20 and 40, and those of 70 are 60 and 80. Each branching choice preserves bounds inherited from the ancestors.

Choose one branch at each step
#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;
}

Searching for 60 in the example visits 50, goes right to 70, then left to 60: three nodes. Searching for 65 takes the same path, then tries the right child of 60. That child is NULL, so 65 is absent. You do not need to inspect the left subtree of 50 because every key there is smaller than 50.

The recursive form first handles t == NULL and a matching key, then searches the selected child. The empty-subtree test is the base case. Without it, an unsuccessful search eventually dereferences a null pointer. The loop version stops at the same boundary without adding recursive calls.

Let shape explain the work

A perfectly balanced tree with 20 levels holds 1,048,575 keys, and a search visits at most 20 nodes. Inserting already sorted keys 10, 20, 30, 40, 50 instead makes a rightward chain. Its height is 4, and finding 50 visits all five nodes. A BST's ordering rule alone does not keep the tree short. Self-balancing trees such as AVL trees rearrange branches to control height.

To find a minimum, follow left children until the next left link is NULL. To find a maximum, follow right children. In the seven-node example these keys are 20 and 80. The same direction choices work at any subtree root because the ordering rule applies there too.

Key Takeaways

  • A binary node has at most two children; missing children are null pointers.
  • BST ordering applies to every descendant in each subtree.
  • Search follows left for a smaller key and right for a larger key.
  • Height counts edges here, with an empty height of −1.
  • A short tree reduces visits; a BST can still become a chain.

5.2 Update, traverse, and release a search tree

Tree functions work on a subtree and connect their result to its parent. Keep the empty-subtree case explicit, and distinguish operations that change links from operations that only visit nodes.

Insert through the subtree link

With tnode containing key, left, and right, include <stdbool.h> and <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;
}

Call with insert(&root, key) and check its result. An existing key leaves the tree unchanged and counts as success. The link address lets insertion update either the caller's root or a child pointer. In a return-root interface, store every result: t->left = insert(t->left, key) and root = insert(root, key). Ignoring a returned allocation when the original subtree was empty leaves that node unlinked and leaks it.

Choose when to visit the root
TraversalVisit orderTypical purpose
In-orderLeft, root, rightPrint BST keys sorted
Pre-orderRoot, left, rightVisit parents before children
Post-orderLeft, right, rootFinish children before freeing a parent

For insertions 50, 30, 70, 20, 40, 60, 80, the in-order output is "20 30 40 50 60 70 80". Pre-order gives "50 30 20 40 70 60 80"; post-order gives "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);
}

After free_tree(root), set the owner's root = NULL. The function frees children first so it can read their addresses from the live parent. Size is zero for an empty subtree, otherwise one plus both child sizes. Height is −1 for empty, otherwise one plus the larger child height. The example has size 7 and height 2.

Delete without breaking the order

A leaf becomes NULL. A node with one child is replaced by that child. For two children, copy the smallest key from the right subtree into the node, then delete that successor from its original position:

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;
}

Call as root = erase(root, key). Deleting 50 from the example puts 60 in the root and removes the old 60 node. The in-order output becomes "20 30 40 60 70 80"; pre-order becomes "60 30 20 40 70 80". A successor can have a right child, so its removal must reconnect that child.

For word keys, use the sign of strcmp, not pointer equality, and give each owned word its own allocation. Free the word before its node. A two-child deletion of such records also needs an ownership plan for the moved word and any associated value; copying an owning pointer and then freeing its original node's word would leave a dangling key.

Key Takeaways

  • An insertion must install a new subtree root and report allocation failure.
  • In-order prints BST keys sorted; pre-order starts with the parent.
  • Post-order cleanup frees both children before their parent.
  • Size adds both child sizes; height takes the larger child height.
  • Deletion reconnects zero or one child, or replaces a two-child key with its successor.
  • Store the returned root after deletion, including deletion of the original root.
  • String-key trees compare text and manage each owned string allocation.
6

Store and look up keys with hash tables

Hash tables turn a key into a starting location and then resolve any collisions there. The hash and the equality test must agree about which keys mean the same thing. A chained implementation brings together arrays, list insertion, string copies, and cleanup. Its usefulness depends on spreading keys across buckets and rebuilding those locations when the bucket count changes.

6.1 Hash keys into buckets and handle collisions

A hash table chooses a starting location from a key. A hash function produces a number; reducing that number modulo a positive bucket count gives an index. Separate chaining keeps a linked list at each bucket, so lookup searches the selected chain.

Separate hashing from equality

A useful hash is deterministic, quick to calculate, and spreads the expected keys across buckets. Equal keys must produce equal hashes. Different keys may still share an index: this is a collision, and every table must handle it. With more possible keys than buckets, some collisions are unavoidable.

For letter-initial words on an ASCII-compatible system, toupper((unsigned char)word[0]) - 'A' assigns 26 letter buckets; "Luigi" starts in bucket 11. Many English words start with "s" and few with "x", so this rule can leave very uneven chains. The cast is needed because <ctype.h> functions require an unsigned-character value or EOF. Validate the initial letter and the character-set assumption before using this indexing rule.

A character-sum hash also has weaknesses. Using ASCII codes and ten buckets, "cat" sums to 99 + 97 + 116 = 312 and reaches bucket 2; "dog" sums to 314 and reaches bucket 4. "act" also sums to 312. Addition loses letter order, so every rearrangement collides.

Mix character order into the hash
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;
}

This djb2 pattern uses unsigned arithmetic, whose overflow wraps in a defined way. Signed overflow would have undefined behavior. For the ASCII strings "cat" and "act", the values are 193488125 and 193486013. Reduce with hash_word(word) % N, where N is the positive bucket count. Letter order changes these hashes, although reducing them can still produce a collision.

If a table treats "Cat" and "cat" as equal, both hashing and comparison must use the same case rule. Ignoring case only during comparison can send equal keys into different buckets, where lookup will not compare them. A negative signed hash remainder is also unsuitable as an array index.

Choose a collision strategy

Separate chaining stores every collision in a bucket's linked list. Lookup compares keys until it finds a match or reaches the end. Open addressing stores entries in the array itself. With linear probing, a collision tries the following slot and wraps at the end. Lookup follows the same probe sequence, so it can inspect several array slots.

Rebuild indexes when the table grows

The load factor is the number of stored keys divided by the bucket count. Twelve keys in eight buckets gives 1.5 entries per bucket on average. Sixteen buckets reduces that average to 0.75. This average does not guarantee that every chain is short; a poor hash may put all keys together.

A bucket index depends on the bucket count. After allocating and initializing a larger bucket array, recompute each stored key's index for the new count and relink it there. Copying the old head pointers into the same numbered new buckets leaves keys in places where the new lookup will not search. A chained table can hold more entries than buckets; open addressing needs unused slots for new entries and reliable probing.

Key Takeaways

  • Reduce a deterministic hash modulo a positive bucket count to get an index.
  • Equal keys need equal hashes; distinct keys may collide.
  • Unsigned arithmetic avoids undefined signed overflow in the hash calculation.
  • Chaining walks one bucket list; open addressing follows a probe sequence.
  • Load factor measures entries per bucket, not the longest chain.
  • Rehash every key when the bucket count changes.

6.2 Build a word-count table that owns its keys

A chained table combines an array, list links, string comparison, and ownership. Each entry owns its copied key. The bucket array owns the chains, and each chain ends at NULL.

Initialize buckets and look up text
#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];

Static storage starts with null pointers in every bucket. A local array instead needs an initializer such as entry *table[N] = {NULL};. Use the unsigned hash_word function that starts at 5381 and mixes each byte with multiplier 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 tests text. e->key == key would test whether the pointers name the same location. Two separate buffers can hold equal words, so pointer equality is not a word lookup.

Count an existing key or install a new one
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;
}

The caller supplies a null-terminated string. The INT_MAX check reports failure before a count would overflow its int storage. The extra byte in the string allocation holds '\0'. If the second allocation fails, free the unattached entry. The bucket head changes only after both allocations and initialization succeed.

Saving e->key = key instead would retain the caller's storage. If input repeatedly reuses one buffer, every saved pointer could show the newest word. Copying the text gives each entry a lifetime independent of that buffer. strdup provides that operation in POSIX and C23; explicit allocation and strcpy work in C17.

With a ten-bucket character-sum example, inserting "cat", "dog", "act" leaves "act" → "cat" in bucket 2 and "dog" in bucket 4. Looking up "tac" searches bucket 2, compares both words, and reaches NULL. A matching bucket is only a candidate location, not evidence of an equal key.

Free payloads, nodes, and bucket heads
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;
    }
}

Saving next and freeing the key happen while the entry is still live. Resetting each bucket permits reuse. Memory-checking tools help verify exercised paths; allocation-failure paths need cleanup too. Hash order is not sorted order. To print alphabetically, collect keys for sorting or store them in a BST.

In open addressing, deletion needs a tombstone marker so lookup continues through a removed slot to later colliding keys. A truly unused slot can end an unsuccessful lookup. Probe sequences must be bounded, and the table must grow before every slot is occupied.

Key Takeaways

  • Initialize every bucket head to NULL.
  • Hash selects a chain; strcmp confirms whether a key matches.
  • Increment an existing key instead of adding a duplicate entry.
  • Copy input strings and clean up the entry if the key allocation fails.
  • Free the key before its entry, save the successor, and reset each bucket.
  • Open-addressing deletion preserves search paths with tombstones.

Structs and Linked Data Structures in C examples

6 Walkthroughs

1

Follow records through copies and pointers

15 steps
Build a reading record, pass it to functions, scan an array, and distinguish embedded data from owned pointers.
  1. 1
    IntroductionKeep one reading together

    A reading combines an identifier and a temperature. Keeping them in one record, a C struct, lets code pass or store the pair as one value.

    Follow that record from its definition to a function call and then into an array. At each step, distinguish the object from the pointers that reach it: that distinction determines whether a change affects a copy or the original, and whether a shallow copy of a pointer member shares storage.

  2. 2
    ExampleDefine the record

    The definition introduces a type; the next declaration creates an object of that type.

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

    The dot selects a member of r, so its temperature is now 17.0 °C. Each member has its own type, and the semicolon after the closing brace ends the type definition.

  3. 3
    Key InsightCopy the member values

    Now make a second record with struct reading s = r;. That assignment copies both member values. Changing s.id or s.temp_c afterward leaves r unchanged.

    An array stored inside a struct is copied too. Padding may still differ between objects, so testing equality means comparing the members relevant to the task. C does not supply a whole-struct == operator.

  4. 4
    ChallengeRead an omitted member

    What does struct reading a = {.id = 31}; put in a.temp_c, and may code read that value?

    ---reveal---

    The omitted member is initialized to zero, so the temperature is 0.0 and can be read. An initializer is present even though it names only id. A declaration of a local a with no initializer would instead require assignments before its members were read.

  5. 5
    ExplanationGive the type a short name

    A typedef provides an alias without changing object behavior.

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

    A call can return the complete record by value. If a node must refer to its own type inside a definition, give it a tag and write that self-link with struct node *next.

  6. 6
    ExampleChoose a copy or an address

    Compare two function interfaces:

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

    warm_copy(r) changes a local copy. warm(&r) reaches the caller’s record and changes its member. The arrow means (*p).temp_c; its parentheses make the dereference happen before the dot selects the member.

  7. 7
    TipAllocate a record with a clear owner

    Include <stdlib.h> and allocate reading *p = malloc(sizeof *p);. Before writing through p, check whether it is NULL and handle failure.

    After success, initialize the fields and keep an owner responsible for free(p). Returning a struct value is safe; returning the address of a local struct leaves a dangling pointer because that local object stops existing when its function returns.

  8. 8
    ChallengePredict which temperature changes

    A caller starts with reading r = {24, 17.0};, calls warm_copy(r), then warm(&r) using the two functions shown. What temperature does r hold?

    ---reveal---

    It holds 18.0 °C. The first call changes only its private copy, so the caller still has 17.0 when the second call begins. The second receives the address of the caller’s record and adds one through that pointer.

  9. 9
    ExampleScan a set of records

    Suppose an array holds temperatures −5.5, −1.0, and −7.5 °C. After checking that the count is nonzero, start best at index 0 and compare each later record with the current best.

    Index 1 replaces index 0 because −1.0 is warmer than −5.5. Index 2 does not replace it. Starting from a real element works when all values are below zero; inventing an initial maximum of zero would miss them.

  10. 10
    TipReturn a sign from the comparator

    For finite temperatures, a comparator can return (x->temp_c > y->temp_c) - (x->temp_c < y->temp_c). The result is negative, zero, or positive.

    Returning the temperature difference as int can turn a small difference into zero. Integer subtraction can overflow for extreme keys. A sign based on comparisons avoids both errors, and qsort still makes no promise to preserve the order of ties.

  11. 11
    ExplanationKeep a failed resize recoverable

    For a heap array, retain the used count separately from capacity. Check that the requested byte count fits, then assign the nonzero-size realloc result to a temporary pointer.

    On failure, the original array remains available. On success, install the new base and capacity together. Rebuild element pointers from their saved indexes because the allocation may have moved or replaced its previous storage.

  12. 12
    ConnectionSee what a pointer copy shares

    A person record with char *name holds an address, so person b = a; leaves both names pointing at one string. That is a shallow copy.

    If each record must own an independent string, give b.name its own checked allocation and copy the text, including the terminating null byte. Each owner can then free its own string once. Copying an embedded character array already gives separate characters.

  13. 13
    ChallengeRepair the name copy

    A record’s name is an owned heap string. Code copies the record, then plans to free both names independently. What must happen before that cleanup?

    ---reveal---

    Allocate a separate name for the copy, check success, and copy the original text before either string is freed. The two records currently share an address. Freeing both shared pointers would release the same block twice; copying the pointer again would not help.

  14. 14
    ChallengeReturn a record with an embedded label

    A function returns a struct containing char label[12] by value. Does the label dangle because the original struct was local? Explain using both lifetime and copying.

    ---reveal---

    No. Returning by value copies the struct’s member values, including the embedded array, into the returned value. The caller owns its resulting record. Returning a pointer to the local struct would instead ask the caller to access an object whose lifetime had ended.

  15. 15
    RecapTrack values and owners

    A struct object stores member values. A pointer names an object, and a copied pointer still names the same target. Function parameters and return values follow that distinction.

    For arrays, keep counts explicit and preserve the owner during resize. For records with owned pointers, decide whether a copy shares storage or receives independent allocations. Those decisions determine both updates and cleanup.

2

Keep choices and union payloads consistent

11 steps
Trace enum values, reject gaps, construct a tagged cell, and choose between simultaneous and alternative fields.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionName the choice and identify its payload
  2. 2
    ExampleFollow explicit enum values
  3. 3
    Key InsightCheck the allowed set
  4. 4
    TipKeep cases from falling through
  5. 5
    ChallengeValidate a stage value
  6. 6
    ExplanationOverlay the alternative values
  7. 7
    ExampleSet both parts of a tagged value
  8. 8
    ChallengeCatch the mismatched update
  9. 9
    TipStore a small flag range
  10. 10
    ChallengeChoose storage for a record
  11. 11
    RecapKeep each description matched to its data
3

Trace list creation, insertion, and deletion

16 steps
Use incoming links to preserve a chain, then maintain endpoints, counts, and owned payloads.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionChange a chain one link at a time
  2. 2
    ExplanationStart with an empty head
  3. 3
    ExamplePrepend without losing the chain
  4. 4
    Key InsightLet the caller keep the new head
  5. 5
    ChallengePredict front insertion order
  6. 6
    ExplanationFollow the address of a link
  7. 7
    ExampleFind a sorted insertion point
  8. 8
    ChallengeInsert before the head
  9. 9
    ExampleRemove a middle node safely
  10. 10
    TipStop safely when a value is absent
  11. 11
    ExplanationAdd a tail and count
  12. 12
    ConnectionUse a backward link when deletion needs it
  13. 13
    ChallengeChoose storage for repeated indexed reads
  14. 14
    TipRelease a payload before its node
  15. 15
    ChallengeCombine deletion and metadata
  16. 16
    RecapKeep the incoming path valid
4

Trace stack work and queue wraparound

14 steps
Evaluate nested operations, maintain linked queue endpoints, and follow a circular queue’s logical order.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionChoose which waiting item comes next
  2. 2
    ExplanationUse top as an item count
  3. 3
    ExampleTrace the stack order
  4. 4
    ExampleMatch the closer to the top opener
  5. 5
    ExampleKeep postfix operands in order
  6. 6
    ChallengeCheck a changed postfix expression
  7. 7
    ExplanationSeparate front from back
  8. 8
    TipRepair the one-node case
  9. 9
    ExplanationUse a ring instead of shifting
  10. 10
    ExampleTrace a wrapped layout
  11. 11
    ChallengeFind the next insertion position
  12. 12
    Key InsightKeep one full-state policy
  13. 13
    ChallengeSelect the work order
  14. 14
    RecapMake order and state agree
5

Build, search, traverse, and delete a BST

15 steps
Follow one tree through branch selection, insertion, traversal orders, successor replacement, and cleanup.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFollow a tree from root to subtree
  2. 2
    Key InsightApply the rule below every ancestor
  3. 3
    ExampleBuild the example tree
  4. 4
    ExampleSearch one branch at a time
  5. 5
    ChallengeDiagnose a descendant violation
  6. 6
    ConnectionMeasure the shape
  7. 7
    ExampleLink an insertion where the search stops
  8. 8
    ExampleChoose a visit order
  9. 9
    ExplanationCount branches and path length
  10. 10
    ExplanationHandle zero or one child
  11. 11
    ExampleReplace a two-child key
  12. 12
    ChallengePreserve the successor’s child
  13. 13
    TipFree descendants before their parent
  14. 14
    ChallengeCheck traversal and height together
  15. 15
    RecapReconnect every changed subtree
6

Build a hash-table word counter

14 steps
Trace collisions, equality, copied keys, growth, failed allocations, and complete cleanup.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionUse a key to narrow the search
  2. 2
    ExampleCompute a starting bucket
  3. 3
    Key InsightKeep equality and hashing consistent
  4. 4
    ChallengeDistinguish a collision from a match
  5. 5
    ExplanationUse load factor without losing the distribution
  6. 6
    ExampleRecompute placement after growth
  7. 7
    ExplanationLook up text in a chain
  8. 8
    ExampleInstall a new owned key
  9. 9
    ChallengeHandle a partial allocation
  10. 10
    TipDo not retain a reused input buffer
  11. 11
    ExplanationPreserve probe paths after deletion
  12. 12
    ChallengeCombine collision handling and word counts
  13. 13
    ConnectionRelease every key and node
  14. 14
    RecapMake location, equality, and lifetime agree

Structs and Linked Data Structures in C practice questions

50 Practice Questions in 5 sets

Set 1: Read records and trace basic operations10 Practice Questions

Question 1 of 10

What value does PAUSED have in enum state { IDLE = 3, RUNNING, FAILED = 9, PAUSED };?
  1. 5
  2. 4
  3. 10
  4. 9
Show answerHide answer
Answer: C
An implicit enum value is one more than the immediately preceding enumerator. FAILED explicitly sets the sequence to 9, so PAUSED is 10. Counting PAUSED from the start of the declaration would ignore the reset introduced by the explicit value.

Question 2 of 10

What value does r.temp_c have after struct reading { int id; double temp_c; }; struct reading r = {.id = 12};?
  1. An indeterminate value
  2. 0.0
  3. 12.0
  4. An initialization error
Show answerHide answer
Answer: B
The designated initializer sets id and zero-initializes the omitted temp_c member, so its value is 0.0. This differs from declaring a local struct with no initializer at all. The supplied initializer makes the omitted member usable without a later assignment.

Question 3 of 10

A valid reading *p points at a temperature record. What expression accesses its temp_c member?
  1. p.temp_c
  2. *p.temp_c
  3. &p.temp_c
  4. p->temp_c
Show answerHide answer
Answer: D
The arrow operator selects a member through a pointer and is equivalent to (*p).temp_c. A dot needs a struct object on its left, so p.temp_c is invalid here. Writing *p.temp_c does not fix that problem because the dot is applied before the dereference.

Question 4 of 10

Records have finite temperatures −8.0, −2.5, and −6.0 °C at indexes 0, 1, and 2. A maximum scan starts with best = 0 and updates for a strictly warmer record. What index remains?
  1. 1
  2. 0
  3. 2
  4. No index; the maximum stays zero
Show answerHide answer
Answer: A
The scan starts with the record at index 0, then replaces it with index 1 because −2.5 is warmer than −8.0. The final −6.0 is not warmer than −2.5, so index 1 remains. Negative temperatures cause no special problem when the initial best is an actual record.

Question 5 of 10

A tagged union has enum tag kind and union as with members int i and double d. An existing value is tagged K_DOUBLE. What update stores integer 18 consistently?
  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;
Show answerHide answer
Answer: C
The tag must say which member contains the stored value. Assigning K_INT and writing as.i makes both parts describe integer 18. Writing the integer member while leaving the double tag would direct later code to read the wrong representation; the tag does not change automatically.

Question 6 of 10

An empty list receives prepends of 6, 11, then 3. What values appear when it is traversed from its head?
  1. 6 → 11 → 3
  2. 3 → 11 → 6
  3. 11 → 6 → 3
  4. 3 → 6 → 11
Show answerHide answer
Answer: B
Each prepend places the new node before the current head. The three heads are therefore 6, then 11, then 3, with earlier nodes linked behind them. Arrival order 6 → 11 → 3 would result from appending at the back, not from the stated front insertions.

Question 7 of 10

A sorted list holds 1 → 7 → 12. An insertion loop advances while the current value is smaller than 9. Which incoming link does it stop at?
  1. The head pointer pointing at the node holding 1
  2. The next field of the node holding 1
  3. The next field of the node holding 12
  4. The next field of the node holding 7
Show answerHide answer
Answer: D
The loop passes 1 and 7, then stops before 12 because 12 is not smaller than 9. The link currently reaching 12 is the next member of the 7 node. Stopping at the link after 12 would append 9 and violate the list’s ascending order.

Question 8 of 10

An empty stack executes push 8, push 2, pop, push 5, pop, pop. What values do the three pops return?
  1. 2, 5, 8
  2. 8, 2, 5
  3. 2, 8, 5
  4. 5, 2, 8
Show answerHide answer
Answer: A
The first pop removes 2, the most recent value at that point. Pushing 5 puts it above the remaining 8, so the next pops return 5 and 8. Returning 8 first would follow queue order and would not match the stack’s last-in-first-out behavior.

Question 9 of 10

A BST is built by inserting 42, 18, 63, 27, and 21. What nodes does a search for 21 visit?
  1. 42 → 18 → 21
  2. 42 → 63 → 27 → 21
  3. 18 → 27 → 21
  4. 42 → 18 → 27 → 21
Show answerHide answer
Answer: D
At 42, 21 is smaller, so search goes left to 18; it then goes right to 27 and left to 21. The insertion of 21 placed it under 27, not directly under 18. A search follows existing child links and cannot jump to a smaller key merely because that key is known to be present.

Question 10 of 10

A populated table treats ASCII words case-insensitively but hashes their original bytes. A lookup for "mint" misses an existing "Mint" entry without visiting it. What complete repair supports both existing and future entries?
  1. Retain raw-byte hashing, fold equality, and rebuild the same placements.
  2. Fold case in hashing, retain folded equality, and rebuild placements.
  3. Fold case in hashing, retain folded equality, and keep old placements.
  4. Retain raw-byte hashing, compare addresses, and grow the bucket array.
Show answerHide answer
Answer: B
Hashing must treat equal spellings alike, so normalize case before hashing and retain the case-insensitive comparison. Existing entries also need placement under the corrected rule. Changing only future hashes leaves old entries in locations their corrected lookups may never inspect.
Set 2: Apply state and storage rules10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

struct tag { char name[8]; int id; }; is used for a and b. After b = a; b.name[0] = 'Z';, what happens to a.name[0]?
  1. It changes to the character Z.
  2. It becomes an indeterminate value.
  3. It keeps its original character.
  4. The struct assignment cannot compile.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

Given void cool(reading r) { r.temp_c -= 2.0; }, a caller sets reading a = {9, 14.0}; and calls cool(a);. What is the caller’s a.temp_c afterward?
  1. 14.0
  2. 12.0
  3. 16.0
  4. An indeterminate value
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

What text does printf("%d", WED); print for enum day { MON, TUE, WED, THU };?
  1. "WED"
  2. "3"
  3. "Wednesday"
  4. "2"
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A tagged union stores a double under tag K_DOUBLE. Later code changes only the tag to K_INT and a printer reads as.i. What is the defect?
  1. Changing the tag converts the stored value to an integer.
  2. The new tag selects a member that was not stored.
  3. The union retains both typed values until it is freed.
  4. The printer must keep the old tag and change its format.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A singly linked list stores head, tail, and count. Its only node is removed and freed. What state must remain?
  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;
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What result does the postfix expression "15 6 - 3 /" produce with integer operands?
  1. -3
  2. 7
  3. -7
  4. 3
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A count-based circular queue has capacity 5, head = 4, and count = 2. At what slot will its next enqueue write?
  1. Slot 0
  2. Slot 2
  3. Slot 1
  4. Slot 4
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

What traversal frees each child subtree before freeing its parent?
  1. Pre-order
  2. Post-order
  3. In-order
  4. Root-only order
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A chained integer-key table uses key modulo bucket count. Into 6 empty buckets it inserts 13, 25, and 23 by prepending. It then grows to 10 buckets, scanning old buckets in increasing index order and each chain head-first, prepending each entry into its new bucket. What chains result?
  1. Bucket 3: 13 → 23; bucket 5: 25
  2. Bucket 1: 25 → 13; bucket 5: 23
  3. Bucket 3: 23 → 13; bucket 5: 25
  4. Bucket 3: 23; bucket 5: 25 → 13
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

An initially empty chained word counter uses a hash that sends "ash" and "elm" to the same bucket. It reads into one reused buffer, then looks up that text and increments a match or allocates a node with count 1. New nodes incorrectly store key = buffer. It reads "ash", then "elm". What remains before cleanup?
  1. One entry showing "elm" with count 2
  2. Two entries, "ash" and "elm", each with count 1
  3. One entry showing "ash" with count 2
  4. Two entries both showing "elm", each with count 1
Show answerHide answer

Save this lesson to see this answer.

Set 3: Trace links, wraparound, and collisions10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A finite-temperature comparator returns x->temp_c - y->temp_c as int. Two distinct records have temperatures 6.20 and 6.45 °C. Why can sorting treat them as equal?
  1. The const void * arguments force both inputs to integers.
  2. The comparator must return exactly their temperature difference.
  3. The two finite values are equal after qsort copies them.
  4. The fractional difference becomes zero during conversion.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What range fits unsigned int mode : 4?
  1. 0 through 4
  2. 0 through 15
  3. 1 through 16
  4. −8 through 7
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A nonempty list is prepended with head = n; n->next = head; after n is allocated. What does the new node’s next point to?
  1. The old head node
  2. The old final node
  3. The new node itself
  4. The null end marker
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An ascending list begins 3 → 8 → 12. Insert 6, delete the first node, then insert 8 using a loop that advances while the current value is strictly smaller than the inserted value. Where is the new 8 linked?
  1. After 6 and before the existing 8
  2. After the existing 8 and before 12
  3. Before 6 and after the removed 3
  4. After 12 and before the null link
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A program repeatedly reads records by numeric index and scans all records, with few insertions. Which storage best fits these operations?
  1. A singly linked list of records
  2. A doubly linked list of records
  3. A linked queue of record nodes
  4. A contiguous array of records
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

An empty linked queue successfully enqueues 12 and 30, dequeues once, fails to allocate during enqueue of 41, dequeues again, then successfully enqueues 44. Correct operations preserve state on failure. What are the final endpoints and waiting values?
  1. Head reaches 30, tail 44; values 30 → 44.
  2. Head reaches 44 while the tail is null.
  3. Both endpoints reach the only node, 44.
  4. Head reaches 12, tail 44; values 12 → 44.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A tree has root 36, left child 17, and 41 as the right child of 17. Why does it fail the BST rule?
  1. 17 is smaller than its parent and lies on the left.
  2. 41 lies in the left subtree of 36 but exceeds 36.
  3. 41 is larger than its parent and lies on the right.
  4. 36 has only one direct child instead of two children.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A BST is built from 48, 24, 75, 62, 68, 90. Delete 48 using its in-order successor. What is the new root and the remaining left child of 75?
  1. Root 62; left child 68
  2. Root 62; left child NULL
  3. Root 75; left child 62
  4. Root 68; left child 62
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A chained table has 21 stored keys and 14 buckets. What is its load factor?
  1. 1.5
  2. 0.67
  3. 7
  4. 35
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A hash lookup reaches a chain containing "elm" and "ash" while searching for "oak". Both text comparisons fail. What must it return after the chain ends?
  1. The head entry for "elm"
  2. The last entry for "ash"
  3. NULL
  4. A new entry for "oak"
Show answerHide answer

Save this lesson to see this answer.

Set 4: Diagnose copying and boundary errors10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

On a platform with 1-byte char, 4-byte int aligned to 4 bytes, and struct sizes rounded to a multiple of 4, A has members char, int, char in that order; B has int, char, char. How much total storage does an array of five A records use compared with five B records?
  1. A uses 60 bytes; B uses 40 bytes.
  2. A uses 30 bytes; B uses 30 bytes.
  3. A uses 45 bytes; B uses 30 bytes.
  4. A uses 60 bytes; B uses 30 bytes.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A function parameter is const reading *p. Which assignment can change the local pointer without changing a record?
  1. p->id = 19;
  2. (*p).temp_c = 8.0;
  3. p = another_reading_pointer;
  4. p->temp_c += 0.5;
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

a.name owns a heap string. After person b = a;, code allocates a temporary string copy successfully, but has not filled it or changed either record. Both records must end as independent owners. What sequence achieves that without losing data or an allocation?
  1. Set a.name = copy; copy b.name into a.name; free a.name twice.
  2. Copy a.name into copy; set b.name = copy; free each name once.
  3. Free a.name; copy b.name into copy; set b.name = copy.
  4. Copy a.name into copy; leave b.name shared; free both names.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

Input is intended for enum mode { M_IDLE = 0, M_READ = 4, M_WRITE = 5 };. What test accepts exactly its named values?
  1. x >= 0 && x <= 5
  2. x >= 4 && x <= 5
  3. x == 0 || x == 4 || x == 6
  4. x == 0 || x == 4 || x == 5
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

During list cleanup, p is live and the code runs free(p); p = p->next;. What change preserves the intended traversal safely?
  1. Assign p = p->next first, then free the new p.
  2. Save next first, free p, then assign p = next.
  3. Free p->next first, then assign p = p->next.
  4. Set p->next = NULL first, then continue after freeing p.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A sorted list 2 → 6 → 6 → 14 uses a loop that advances while number < value. A new 6 is inserted. Where does it go?
  1. Between 2 and the first existing 6
  2. Between the two existing 6 nodes
  3. Between the second 6 and 14
  4. After 14 at the end of the list
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

An array stack has signed top = 0, and pop reads items[--top] without a check. What index does it try to read?
  1. 0
  2. 1
  3. -1
  4. The last valid array index
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A capacity-6 circular queue uses a count. It starts at head = 5, count = 3, with values P, Q, R in queue order. It dequeues twice, then enqueues S and T. What are the final head, count, and front value?
  1. Head 0; count 3; front Q
  2. Head 1; count 5; front R
  3. Head 3; count 3; front T
  4. Head 1; count 3; front R
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A BST contains root 31, left child 12 with right child 19, and right child 54. What pre-order output remains after deleting 12?
  1. 19, 31, 54
  2. 31, 54, 19
  3. 31, 19, 54
  4. 31, 12, 19, 54
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A new table entry is allocated, but its key-string allocation fails before it is linked. What cleanup preserves the table and avoids a leak?
  1. Free the caller’s key and return success.
  2. Free the new entry and return failure.
  3. Link it with a null key and return success.
  4. Return failure but keep the new entry.
Show answerHide answer

Save this lesson to see this answer.

Set 5: Combine rules in unfamiliar cases10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A loop tries to delete every 5 from 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;
}

Why does a 5 remain, and what change fixes the loop?

  1. It skips a freed node; free the successor before advancing.
  2. It skips a retained match; advance only after a nonmatch.
  3. It loses the head link; restart the loop after every nonmatch.
  4. It deletes the wrong node; free before reading the successor.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A heap array has three used records, each owning a distinct name string, and a saved pointer to record 1. A resize to capacity 6 succeeds. The program plans to remove record 0 by shifting the other two records left and lowering the used count to 2. What plan retains access to the original record 1 and releases all owned storage?
  1. Free the removed name; shift; rebuild the pointer at index 0; later free active names and the array.
  2. Free the removed name; shift; keep the old pointer; later free all six capacity slots and the array.
  3. Shift; rebuild the pointer at index 1; later free only the two active names and the array.
  4. Shift; rebuild the pointer at index 0; later free names from all three old slots and the array.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

An unsigned 3-bit field is assigned the runtime value 13. What value is stored?
  1. 13
  2. 7
  3. 1
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A list is 4 → 9 → 16. Code saves node **link = &head->next;, removes and frees the head node 4, then uses *link to delete 9. Why is that second operation invalid, and what incoming link should it use now?
  1. link points at live node 9; use the address of 9’s next field.
  2. link points inside live node 16; use the address of 16’s next field.
  3. link points inside freed node 4; use the current head’s address.
  4. link is the current head’s address; use the old node 4’s address.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A doubly linked list is A ↔ B ↔ C, with A as head and C as tail. Remove B, append D, then remove C, maintaining both directions. What is the final tail and its predecessor?
  1. Tail D; its predecessor A
  2. Tail C; its predecessor A
  3. Tail D; its predecessor B
  4. Tail A; its predecessor D
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A postfix evaluator handles division correctly but reverses the two operands of every subtraction. For "20 8 - 3 / 2 -", what are the correct result and the flawed evaluator’s result? All division here is exact integer division.
  1. Correct result 6; flawed result 2
  2. Correct result 2; flawed result −6
  3. Correct result 2; flawed result 6
  4. Correct result −2; flawed result 6
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

An empty queue enqueues 22, then 7, then 15. What does its first dequeue return?
  1. 15
  2. 7
  3. An empty-queue failure
  4. 22
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Insert distinct keys 9, 16, 23, 30 into an empty BST in that order. With height measured in edges, what is its height?
  1. 4
  2. 3
  3. 2
  4. 1
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A BST initially has only root 40. In a return-root interface, code calls insert(root->left, 20) and ignores its returned allocation, then stores root = insert(root, 60) and finally root = erase(root, 40). Successful insertion links child results internally; erase reconnects retained children. What remains?
  1. Root 60 with left child 20; no allocation is leaked.
  2. Root 20 with right child 60; no allocation is leaked.
  3. Root 40 with right child 60; node 20 is leaked.
  4. Root 60 only; the allocated node 20 is leaked.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A size-7 open-addressing table uses linear probing. Three keys hashing to slot 5 occupy slots 5, 6, and 0. The slot-6 key is deleted. Why must slot 6 retain a tombstone instead of becoming an unused slot?
  1. A lookup for the slot-0 key must continue past slot 6.
  2. A lookup for the slot-5 key must move its key to slot 6.
  3. A lookup must treat the deleted key as still present.
  4. A lookup must restart its probing from slot 0 every time.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

72 Key Terms, each with its definition

A

Abstract data type
An abstract data type describes a collection through its allowed operations and behavior, independently of the storage chosen to implement it.
A stack keeps the same push-and-pop behavior whether its values live in an array or linked nodes.
Alignment
Alignment is an address-placement requirement for an object’s type that can cause gaps between members or at the end of a struct.
An integer member may start after a gap following a character member so its address is suitably aligned.
Array of structs
An array of structs stores records of one struct type contiguously, supporting direct selection of a record by its numeric index.
The expression readings[3].temp_c reaches the temperature in the fourth record of the array.
Arrow operator
The arrow operator selects a struct or union member through a pointer and combines dereferencing with member selection.
For a live record pointer p, p->temp_c reaches the same member as (*p).temp_c.

B

Base case
A base case handles a recursive function’s smallest input without making another recursive call, stopping the descent into smaller subproblems.
A tree cleanup returns immediately for a null subtree before attempting to read any child pointers.
Binary search tree
A binary search tree orders distinct keys so every node has smaller keys throughout its left subtree and larger keys throughout its right subtree.
Comparing the target with the current key selects the only subtree that can contain a match.
Binary tree
A binary tree is a linked structure in which each node has at most two children, distinguished as its left and right child.
A node can have no children, only one child, or both a left and a right child.
Bit-field
A bit-field is a struct member declared with a width in bits, limiting its stored integer values while leaving its packing implementation-defined.
An unsigned 3-bit mode field stores values from 0 through 7 and cannot have its address taken.
Bucket
A bucket is a location selected by a hash-table index; in separate chaining it stores the head of a list of entries assigned there.
A ten-bucket table using ASCII character sums places "dog" in bucket 4 because its sum is 314.

C

Capacity
Capacity is the number of elements an allocated array can hold, which can be larger than the number of elements currently in use.
An array with capacity 16 and 10 used records can accept six more records before another growth step.
Circular buffer
A circular buffer reuses a fixed array by wrapping its positions from the final slot back to the first instead of shifting remaining items.
After a front removal, a later insertion can reuse the freed slot once its index wraps around.
Collision
A collision occurs when distinct keys receive the same bucket index, requiring a table to retain and distinguish their separate entries.
A lookup must compare the keys inside a shared bucket instead of assuming its first entry matches.
Comparator
A comparator is a function that reports the relative order of two values using a negative result, zero for a tie, or a positive result.
A temperature comparator returns a sign after comparing values, avoiding truncation of a small difference.
Contiguous storage
Contiguous storage places array elements side by side in one sequence of memory, supporting direct indexing and scans through nearby locations.
An integer array avoids the per-node link pointers and separate allocation overhead of an integer list.

D

Dangling pointer
A dangling pointer refers to an object whose lifetime has ended, so dereferencing it no longer provides valid access to that object.
Returning the address of a local record leaves the caller with a pointer to an object that has stopped existing.
Deep copy
A deep copy creates independent copies of owned pointed-to data as well as the outer record, with cleanup responsibilities for the new allocations.
Copying a name into a fresh allocation lets each person record release its own name without a double free.
Dequeue
To dequeue is to remove the item at a queue’s front, reporting empty input before changing state or reading a value.
A circular dequeue reads the front, advances the head with wraparound, and decreases the count.
Designated initializer
A designated initializer names the member or element receiving a value, allowing struct members to be initialized without declaration order.
For a struct declared with id before temp_c, the initializer {.temp_c = 21.5, .id = 7} names its members in reverse declaration order.
Doubly linked list
A doubly linked list stores both successor and predecessor links in each node, allowing backward traversal and repair through either neighbor.
Removing an interior node updates the previous node’s next link and the following node’s prev link.

E

Enqueue
To enqueue is to add an item at a queue’s back while preserving the front position of any values already waiting.
A count-based ring writes the new item at (head + count) % CAP, then increases its count.
Entry
An entry is a stored hash-table record containing a key and associated data, plus a next pointer when separate chaining connects entries.
A word-count entry owns a key string, stores the word’s count, and links to the next entry in its bucket.
Enumeration
An enumeration defines named integer constants and a type for values associated with those choices, without enforcing membership in the named set.
An alert enum can name the levels low, medium, high, and critical while input still needs validation.
Enumerator
An enumerator is a named integer constant in an enum definition, with an explicit value or one determined from its predecessor.
In enum state { IDLE = 3, RUNNING };, the enumerator RUNNING receives value 4.

F

Fallthrough
Fallthrough is execution that continues from one switch case into the next because no break, return, or other transfer stops it.
A case that prints a state but omits break can also execute the action written for the following case.
FIFO
FIFO means first in, first out, describing the order where the earliest stored item is handled before later items still waiting.
After enqueuing A, B, and C, a FIFO removal returns A before either of the later letters.

H

Hash function
A hash function deterministically turns a key into a number that is reduced to a bucket index, aiming to spread expected keys evenly.
A character-sum hash sends rearrangements such as "cat" and "act" to the same bucket.
Head
The head is the pointer naming the first node of a linked sequence, with a null head representing a sequence that contains no nodes.
Removing the first list node must update the owner’s head before the old node’s allocation is freed.
Height
The height of a nonempty tree is the number of edges on its longest root-to-leaf path; the empty-tree height used here is −1.
A chain containing five nodes has four edges along its longest path, so its height is 4.

I

Implementation-defined behavior
Implementation-defined behavior is a choice the C implementation must document, so portable code cannot assume every implementation makes the same choice.
The documented order used to pack bit-fields can differ between implementations of the same C declaration.
In-order successor
The in-order successor of a node with a right subtree is the smallest key in that subtree, used to replace a two-child node’s key during deletion.
Follow the right child and then left links to find the replacement key for a two-child deletion.
In-order traversal
An in-order traversal visits each left subtree before its root and then its right subtree, producing sorted keys for a binary search tree.
Printing a word-count BST in-order produces its stored words in the order used by its text comparisons.
Incoming link
An incoming link is the pointer that reaches a node, either the list head or a predecessor’s next field, and must change when that node is bypassed.
Deleting the first node changes the head because no predecessor node supplies its incoming link.

L

Leaf
A leaf is a tree node with no children, so both child pointers are null and its height is zero when height counts edges.
A leaf’s deletion frees its allocation and replaces its incoming child link with a null pointer.
LIFO
LIFO means last in, first out, describing the order where the newest stored item is handled before any earlier items still present.
After a stack receives 4, 7, and 1, its first two pops return 1 and 7, leaving 4.
Linear probing
Linear probing resolves a collision by trying consecutive array slots with wraparound, using the same sequence for insertion and lookup.
A probe starting at the final slot continues at slot zero when another occupied position must be checked.
Load factor
The load factor is the number of stored keys divided by the number of buckets, measuring average occupancy without fixing each chain’s length.
Twelve keys in eight buckets give a load factor of 1.5, even if some buckets remain empty.

M

Member
A member is a named component of a struct or union, selected from an object with a dot or through a pointer with an arrow.
The expression r.temp_c selects the temperature member of the reading stored in r.

N

Node
A node is a record holding data and the pointers that connect that record to other parts of a linked structure.
A list node holds one integer and its successor pointer; a tree node instead has two child pointers.

O

Open addressing
Open addressing stores entries directly in array slots and resolves collisions by searching a defined sequence of alternative slots.
A lookup may inspect several slots even though it starts at one key-derived hash index.
Ownership
Ownership assigns responsibility for releasing an allocation, even when several pointers can reach the same object during its lifetime.
A word node owns its string, so cleanup frees that string before freeing the node holding its address.

P

Padding
Padding consists of unused bytes an implementation can place between struct members or after the final member to satisfy layout requirements.
A byte comparison may detect different padding even when two records hold equal member values.
Pass by value
Pass by value means a function parameter receives a copy of the supplied value, so changing that parameter does not replace the caller’s value.
A temperature change to a by-value record parameter leaves the caller’s temperature member unchanged.
Payload
A payload is the stored data selected by a tagged value’s kind, held separately from the tag that describes how to interpret it.
In a calculator token, the payload holds either the numeric operand or the operator identified by its tag.
Pointer to const
A pointer to const gives access to an object that cannot be modified through that pointer, while the pointer itself may still change.
A printing function can read through const reading *p but cannot assign to p->id.
Pointer to pointer
A pointer to pointer holds the address of a pointer object, allowing a function to read or replace that pointer through the extra level.
A node **link can move from the head’s address to the address of a node’s next field.
Post-order traversal
A post-order traversal visits the left and right subtrees before their root, allowing cleanup to release children before their parent.
A tree-freeing function reads child links while the parent is live, then frees that parent last.
Postfix expression
A postfix expression writes each operator after its operands, allowing a stack to hold intermediate values until the operator combines them.
For "9 4 -", the first popped value is the right operand 4 and the result is 5.
Pre-order traversal
A pre-order traversal visits a subtree’s root before visiting its left and right subtrees, placing each parent before its descendants.
A pre-order output begins with the root key even when that key is neither the smallest nor the largest.
Prepend
To prepend is to insert an item before the current first item of a sequence, changing which item is reached from its head.
Prepending 1, then 2, then 3 makes a list whose traversal visits 3 before 2 and then 1.

Q

Queue
A queue is a collection that adds at the back and removes at the front, preserving arrival order among values still waiting.
A linked queue adds a new node after its tail and removes the node currently reached by its head.

R

Reallocation
Reallocation changes the size of an allocated block and can move it, requiring the owner to use the returned base after success.
A temporary result pointer preserves the old records when a nonzero-size array resize fails.
Rehashing
Rehashing recomputes stored keys’ placements for a new bucket count and moves or relinks entries so subsequent lookups use the right locations.
Doubling the bucket array requires reducing every key’s hash with the new count before placing it.
Root
The root is the node where a tree or subtree begins, reached by the owner’s root pointer or by a parent’s child link.
Replacing a deleted one-child root requires storing the surviving child as the tree’s new root.

S

Separate chaining
Separate chaining handles hash collisions by keeping a linked list of entries at each bucket and comparing keys within the chosen list.
New word entries can be prepended because their order inside a bucket does not affect exact lookup.
Shallow copy
A shallow copy duplicates a record’s member values, including pointer addresses, while leaving separately allocated pointed-to data shared.
After copying a person record, both name pointers still reach the same string unless a new copy is allocated.
Singly linked list
A singly linked list is a sequence of nodes connected by next pointers, with a head pointer at the start and a null link at the end.
Prepending a node gives it the old head as its successor and makes that new node the head.
Sorted insertion
Sorted insertion places a new value where it preserves the existing order, connecting the new node between its predecessor and successor.
An ascending-list loop stops before the first value not smaller than the new value.
Stack
A stack is a collection in which insertion and removal use the top, so the most recently inserted value is the next value removed.
A delimiter checker stores the most recent unmatched opener at the top of its stack.
String equality
String equality means two null-terminated strings contain matching character sequences, regardless of whether their storage addresses differ.
A word-table lookup uses strcmp(e->key, key) == 0 to recognize equal text in separate allocations.
Struct
A struct is a C type that groups named members, which can have different types, into one record with separate storage for each member.
A temperature record combines an integer sensor identifier and a temperature in one struct object.
Struct tag
A struct tag names a structure type after the struct keyword and can identify the type inside its own definition.
The tag in struct node lets the definition declare a struct node *next self-link.
Subtree
A subtree is a node together with all its descendants, forming a smaller tree on which the same search and update rules can operate.
A BST’s left-subtree restriction applies to distant descendants as well as the direct left child.
Switch statement
A switch statement selects a starting case from an integer expression and continues until control leaves the statement or function.
A switch on a tagged value chooses the output operation for the kind of payload currently stored.

T

Tagged union
A tagged union pairs shared payload storage with a separate tag that identifies which union member readers should access.
A value tagged K_CHAR is printed using the character member and a character format conversion.
Tail
The tail is the pointer naming the final node of a linked sequence, maintained so insertion at the back can avoid a full traversal.
A linked queue sets both head and tail to null when its only node is removed and freed.
Tombstone
A tombstone marks a deleted open-addressing slot without making it look never used, allowing lookup to continue toward later colliding entries.
A search passes a tombstone instead of stopping before a matching key stored farther along the probe path.
Traversal
A traversal visits nodes by following a structure’s links in a defined order, stopping at the appropriate end or empty-subtree case.
A separate list traversal pointer moves through next links while the owner retains the original head.
Tree size
A tree’s size is its number of nodes, found by adding one for its root to the sizes of both child subtrees, with zero for an empty tree.
A root with two leaf children has size 3 even though its height measured in edges is only 1.
Typedef
A typedef declaration introduces an alias for an existing type, letting code use a shorter name without creating a distinct type.
After typedef struct reading reading;, a declaration can use reading *p for a pointer to that struct.

U

Underflow
Underflow occurs when a program tries to remove an item from an empty collection, requiring a failure result before an invalid access.
An array-stack pop must check a zero item count before decrementing it and reading the array.
Union
A union is a C type whose members share storage, allowing that space to hold one stored alternative selected from different member types.
In a union with an integer member and a double member, writing the double replaces the prior integer payload instead of preserving two values.
Unlinking
Unlinking removes a node from a structure by reconnecting the references around it before any owned node storage is released.
Redirecting a predecessor’s next to the removed node’s successor keeps the remaining chain reachable.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started