// STUHUB · C++ DATA STRUCTURES

C++ Data Structure Drills: Fill in the Blank

Twenty short fill-in-the-blank C++ drills on nodes, head insert/delete, reversal, array stacks, circular queues, BSTs and adjacency lists, with answers and the tempting wrong answer explained.

Introduction

Reading pointer code and writing pointer code are different skills, and only the second one is worth marks. These twenty drills are deliberately tiny: a few lines with one or two holes, an answer, and one line on the wrong answer that looks right. Work down a section with the answers covered, then reveal. If you get a blank wrong, write the whole function out by hand once before moving on — recognising the fix is not the same as being able to produce it.

Every completed drill on this page was compiled with g++ -std=c++17 -Wall and run; where a drill quotes output, that is the real output.

Two struct definitions are used throughout, so you can stop re-reading them:

cpp
struct Cell { int value; Cell* next; Cell(int v) : value(v), next(nullptr) {} };
struct TNode { int key; TNode* left; TNode* right; TNode(int k) : key(k), left(nullptr), right(nullptr) {} };

The twenty drills are in the exercise list at the foot of the page, grouped in the same order as the sections below. Each section says what its group of drills is about and carries the shared setup that group assumes.

Dynamic allocation and delete

Drills 1–3. Every heap allocation has exactly one matching release, and the form of the release has to match the form of the allocation: new pairs with delete, new[] pairs with delete[]. Mismatching them is undefined behaviour, not merely a leak.

The other habit these three drills build is ordering. When you free a node that is part of a chain, read the next pointer out before the block goes back to the allocator — reading it afterwards usually prints garbage rather than crashing, which is exactly why the bug survives casual testing.

Head insertion and deletion

Drills 4–6. Head insertion is two assignments and the order of the two is the whole exercise: link the new cell forward at the old front first, then move the handle. Do it the other way round and the node points at itself while the rest of the list becomes unreachable.

Pushing 3, then 2, then 1 prints 1 2 3 — head insertion reverses the order you insert in, which is why it is the cheap way to build a list backwards.

In-place reversal

Drills 7–8. Reversal in place is a three-pointer loop — behind, the current node, and a saved ahead — and both drills turn on the same fact: once you have overwritten cur->next, the only way forward is the copy you saved before the overwrite. The function's return value is behind, the last node the loop touched, never the loop variable that has just run off the end.

Stack on a plain array

Drills 9–11. Throughout this section: const int CAP = 8; struct IntStack { int slot[CAP]; int top; }; and top starts at -1.

Push pre-increments and pop post-decrements; the asymmetry is the whole algorithm. The guards are where marks go missing, because top is an index and every bound stated in items has to be converted before it can be compared against it.

Pushing nine values into CAP = 8 prints 111111110: the ninth call is refused rather than overflowing.

Circular queue and the wrap arithmetic

Drills 12–14. Throughout: const int RCAP = 5; struct Ring { string slot[RCAP]; int front; int count; };. This version tracks a live count instead of a rear index, which makes full and empty unambiguous.

Every index that advances in a ring takes % capacity, without exception — that modulus is the thing that turns an array into a ring, and dropping it produces a bug that only appears after the first wrap.

Starting from front = 3 and enqueuing A, B, C really does lay the array out as [C][_][_][A][B] — C has wrapped into slot 0 while the queue is still, logically, A B C.

BST insert and recursive traversal

Drills 15–17. Recursive insert is written against a TNode*& — a reference to the parent's pointer slot — so assigning to the parameter is how the new subtree gets attached, and no return value is needed.

Inserting 40, 25, 60, 25, 70 gives root 40 with children 25 and 60, no left child under 25 — the second 25 matches neither < nor > and is silently dropped. Decide before the exam whether your convention discards duplicates or sends them right, and be consistent.

Adjacency-list construction

Drills 18–20. Two representations appear here: a vector<vector<int>> of out-lists, and an array of buckets built from raw linked nodes. The linked version is head insertion again, so each bucket lists its neighbours in reverse insertion order.

The counting questions are the ones that catch people: in-degree counts arrows arriving, so a directed arc credits its destination, and an undirected edge appears in two lists, so a total over the rows has to be halved.

How to use these

Work one section at a time rather than straight down the page: the value is in noticing which family of mistake is yours. Three patterns cover most of the losses above — saving a pointer before you overwrite it, remembering that an index is one less than a count, and putting % capacity on every index that advances in a ring.

When a blank defeats you, do not just read the answer. Close the page, write the whole function from the signature down, then compile it. Recall under a blank cursor is the thing being tested, and it is a different muscle from recognition.

Exercises with solutions

Work through each question before opening the solution below it.

Exercise 1

Drill 1 — one node on the heap

cpp
Cell* p = ______(1)______;      // a heap cell holding 42
cout << p->value << "\n";
______(2)______;                // hand the memory back

Solution

(1) new Cell(42) (2) delete p

Writing Cell p(42); and passing &p compiles and even prints 42, but that cell dies at the end of the enclosing block, so any list still pointing at it holds a dangling pointer.

Exercise 2

Drill 2 — free a whole list

cpp
void destroy(Cell* head) {
    while (head != nullptr) {
        Cell* doomed = head;
        head = ______(1)______;
        ______(2)______;
    }
}

Solution

(1) doomed->next (2) delete doomed

The tempting order is delete head; head = head->next; — that reads next out of a block you have already released, which is undefined behaviour and often prints garbage rather than crashing, so it survives casual testing.

Exercise 3

Drill 3 — array on the heap

cpp
struct Buffer { int* data; int n; };

Buffer makeBuffer(int n) { Buffer b; b.n = n; b.data = ______(1)______; return b; }
void   freeBuffer(Buffer& b) { ______(2)______; b.data = nullptr; b.n = 0; }

Solution

(1) new int[n]() (2) delete[] b.data

Plain delete b.data is the trap: it releases one int from a block of n, and the mismatch between new[] and delete is undefined behaviour, not merely a leak. The () on new int[n]() zero-initialises; without it the elements hold whatever was in that memory.

Exercise 4

Drill 4 — push at the front

cpp
void pushFront(Cell*& head, int v) {
    Cell* fresh = new Cell(v);
    ______(1)______;    // new cell points at the old front
    ______(2)______;    // the new cell is now the front
}

Solution

(1) fresh->next = head (2) head = fresh

Doing these two in the opposite order sets head = fresh first, after which fresh->next = head makes the node point at itself and every other node in the list is unreachable. Link forward, then move the handle.

Exercise 5

Drill 5 — pop from the front

cpp
bool popFront(Cell*& head, int& out) {
    if (______(1)______) return false;
    Cell* old = head;
    out = old->value;
    head = ______(2)______;
    delete old;
    return true;
}

Solution

(1) head == nullptr (2) old->next

head = head->next; delete head; is the wrong-but-tempting pair: it deletes the second node and leaks the first. You need the saved old precisely because the handle has already moved on.

Exercise 6

Drill 6 — head insert when a tail pointer exists

cpp
void pushFront(Cell*& head, Cell*& tail, int v) {
    Cell* fresh = new Cell(v);
    fresh->next = head;
    head = fresh;
    if (______(1)______) tail = fresh;
}

Solution

(1) tail == nullptr

Testing head == nullptr here always fails, because head was just assigned two lines above — the test has to look at the pointer you have not touched yet. Miss this and the first insertion into an empty list leaves tail null forever, and the next insertAtTail dereferences it.

Exercise 7

Drill 7 — the three-pointer loop

cpp
Cell* reverse(Cell* head) {
    Cell* behind = nullptr;
    Cell* cur = head;
    while (cur != nullptr) {
        Cell* ahead = cur->next;
        ______(1)______;         // flip this link
        behind = cur;
        cur = ______(2)______;
    }
    return behind;
}

Solution

(1) cur->next = behind (2) cur = ahead

Writing cur = cur->next for blank 2 is the natural reflex and it is fatal: line (1) has already overwritten cur->next, so you would walk backwards into the part you just reversed and loop forever. That is exactly what ahead was saved for.

Exercise 8

Drill 8 — reversal without a separate cursor

cpp
Cell* reverse(Cell* head) {
    Cell* behind = nullptr;
    while (head != nullptr) {
        Cell* ahead = ______(1)______;
        head->next = behind;
        behind = head;
        head = ahead;
    }
    return ______(2)______;
}

Solution

(1) head->next (2) behind

return head is the classic loss of marks: the loop only ends when head is null, so you would hand back an empty list. The new front is the last node the loop touched, which is sitting in behind.

Exercise 9

Drill 9 — push with an overflow guard

cpp
bool push(IntStack& s, int v) {
    if (______(1)______) return false;    // no room
    ______(2)______;                      // move, then write
    return true;
}

Solution

(1) s.top == CAP - 1 (2) s.slot[++s.top] = v

s.top == CAP is the tempting guard and it is off by one: top is the index of the last item, so on a full stack it is 7, never 8, and the check never fires — you write slot[8] and corrupt whatever sits after the array. Note also that push moves first and writes second; pop is the mirror.

Exercise 10

Drill 10 — pop with an underflow guard

cpp
bool pop(IntStack& s, int& out) {
    if (______(1)______) return false;
    out = ______(2)______;
    return true;
}

Solution

(1) s.top == -1 (2) s.slot[s.top--]

s.slot[--s.top] is the symmetric-looking wrong answer: it decrements before reading, so it skips the actual top element and returns the one underneath. Push pre-increments, pop post-decrements — the asymmetry is the whole algorithm.

Exercise 11

Drill 11 — peek and size

cpp
int peek(const IntStack& s) { return ______(1)______; }
int size(const IntStack& s) { return ______(2)______; }

Solution

(1) s.slot[s.top] (2) s.top + 1

Returning s.top from size is tempting because it is the only number in sight, but an index is one less than a count: a stack holding one item has top == 0. Note also that peek never modifies top — if your peek decrements, it is a pop.

Exercise 12

Drill 12 — enqueue

cpp
bool enqueue(Ring& r, const string& v) {
    if (______(1)______) return false;    // full
    int rear = ______(2)______;           // first free slot
    r.slot[rear] = v;
    r.count++;
    return true;
}

Solution

(1) r.count == RCAP (2) (r.front + r.count) % RCAP

Dropping the % RCAP is the whole bug the structure exists to prevent: with front = 3 and two items already queued, front + count is 5, one past the end of a five-slot array. The modulus is what turns the array into a ring.

Exercise 13

Drill 13 — dequeue

cpp
bool dequeue(Ring& r, string& out) {
    if (r.count == 0) return false;
    out = r.slot[r.front];
    r.front = ______(1)______;
    ______(2)______;
    return true;
}

Solution

(1) (r.front + 1) % RCAP (2) r.count--

Plain r.front + 1 works for the first few dequeues and then walks off the end the moment front reaches the last slot — the failure appears only after the queue has wrapped once, which is why it survives a quick test. Forgetting blank 2 is worse: the queue reports itself permanently full.

Exercise 14

Drill 14 — how many items are in a ring?

Given a buffer where front is the index of the first item and rear is the index one past the last:

cpp
int occupancy(int front, int rear, int cap) { return ______(1)______; }

Solution

(1) (rear - front + cap) % cap

rear - front is right only while the data has not wrapped; with front = 4, rear = 1, cap = 6 it gives -3 instead of 3. Adding cap before taking the modulus keeps the numerator non-negative, which matters because % in C++ returns a negative result for a negative left operand.

Exercise 15

Drill 15 — recursive insert through a pointer reference

cpp
void insert(TNode*& node, int k) {
    if (node == nullptr) { ______(1)______; return; }
    if (k < node->key)      insert(______(2)______, k);
    else if (k > node->key) insert(node->right, k);
}

Solution

(1) node = new TNode(k) (2) node->left

Writing TNode* fresh = new TNode(k); in blank 1 compiles and leaks: the parameter is a reference to the parent's pointer, and assigning to node is how the new subtree gets attached. A local variable attaches to nothing. Because node is TNode*&, the recursive call passes the child slot itself, so no return value is needed.

Exercise 16

Drill 16 — postorder

cpp
void postorder(const TNode* n, vector<int>& out) {
    if (n == nullptr) return;
    postorder(n->left, out);
    ______(1)______;
    ______(2)______;
}

Solution

(1) postorder(n->right, out) (2) out.push_back(n->key)

Swap those two lines and you have written inorder, which for a BST prints a sorted sequence and therefore looks convincingly correct. Postorder visits both children before the node, which is why it is the traversal you use to delete a tree.

For the tree built from 40, 25, 60, 10, 30, 70, postorder gives 10 30 25 70 60 40.

Exercise 17

Drill 17 — height in edges

Convention: an empty tree has height -1, a single node has height 0.

cpp
int height(const TNode* n) {
    if (n == nullptr) return ______(1)______;
    return 1 + ______(2)______;
}

Solution

(1) -1 (2) max(height(n->left), height(n->right))

Returning 0 for the null case is the tempting answer and it silently switches you to counting nodes on the longest path rather than edges, so every answer comes out one too big. Using + instead of max in blank 2 is the other trap — that counts the whole subtree, not the deepest path.

Exercise 18

Drill 18 — a directed arc, with in-degrees

cpp
void addArc(vector<vector<int>>& out, vector<int>& indeg, int from, int to) {
    ______(1)______;    // record the arc
    ______(2)______;    // one more edge arriving at 'to'
}

Solution

(1) out[from].push_back(to) (2) indeg[to]++

indeg[from]++ is the easy slip: in-degree counts arrows arriving, so the vertex to credit is the destination. A self-loop addArc(g, d, 3, 3) correctly bumps both the out-list and the in-degree of 3, which is why a self-loop contributes two to the total degree.

Exercise 19

Drill 19 — buckets made of linked nodes

cpp
struct Arc { int to; Arc* next; };

void addArc(Arc* buckets[], int from, int to) {
    Arc* a = new Arc;
    a->to = to;
    ______(1)______;    // splice into the front of the bucket
    ______(2)______;    // the bucket now starts here
}

Solution

(1) a->next = buckets[from] (2) buckets[from] = a

Leaving a->next unset is the tempting shortcut when the bucket looks empty — but Arc has no constructor, so a->next holds garbage rather than null, and traversal runs off into nothing. This is head insertion again, so each bucket lists its neighbours in reverse insertion order: adding 0→1 then 0→2 prints 0 -> 2 1.

Exercise 20

Drill 20 — reading the structure back

cpp
// undirected graph: every edge stored in both endpoints' lists
int countEdges(const vector<vector<int>>& adj) {
    int sum = 0;
    for (const auto& row : adj) sum += static_cast<int>(row.size());
    return ______(1)______;
}

bool hasArc(const vector<vector<int>>& adj, int u, int v) {
    for (int w : adj[u]) if (______(2)______) return true;
    return false;
}

Solution

(1) sum / 2 (2) w == v

Returning sum counts every undirected edge twice, once from each end — the path 0–1–2–3 has three edges but six list entries. For blank 2, comparing w == u instead of w == v is the transposition to watch for; it silently answers "does u have a self-loop".

Which C++ standard does this page assume?

Standard C++17. C++ began as C with Classes in 1979 and first shipped commercially in 1985; what a compiler flag actually selects today is one of the ISO revisions below, and the committee has published a new one every 3 years since 2011. Build these listings with -std=c++17 and they compile as written.

C++ ISO revisions, their publication identifiers, and what each one changes for the code on this page
RevisionPublished asWhat it changes for the code on this page
C++98ISO/IEC 14882:1998The first ISO C++, and the dialect most data-structures courses still teach from: templates, the STL containers, and raw pointers doing the work.
C++03ISO/IEC 14882:2003A defect-fix revision. Nothing on these pages depends on it, and nothing on these pages is broken by it.
C++11ISO/IEC 14882:2011Where nullptr, auto, range-based for, move semantics and the unordered containers arrive. Every listing here writes nullptr rather than NULL because of it.
C++14ISO/IEC 14882:2014A small revision: generic lambdas and std::make_unique. Used only where it makes ownership clearer.
C++17ISO/IEC 14882:2017What every listing on StuHub targets and was compiled against. If you build these files, build them with -std=c++17.
C++20ISO/IEC 14882:2020Concepts, ranges and std::midpoint. Flagged in the prose where it offers a shorter correct form, never assumed by the code.
C++23ISO/IEC 14882:2024Not used here. Named so you can tell whether a snippet you found elsewhere will compile on a lab machine that predates it.

Common questions

What does this page cover?

Twenty short fill-in-the-blank C++ drills on nodes, head insert/delete, reversal, array stacks, circular queues, BSTs and adjacency lists, with answers and the tempting wrong answer explained.

How long does this page take to work through?

About 13 minutes of reading at 200 words per minute, plus 20 questions with worked solutions at the foot of the page. Reading it end to end is the slow way; the intended use is to find the section you are stuck on, then do the questions for that section with the solutions covered.

Which C++ standard do these examples target?

Standard C++17 — ISO/IEC 14882:2017. Every listing was compiled with -std=c++17 and -Wall -Wextra before publication, and the linked-structure examples were also run under AddressSanitizer. Where C++20 offers a shorter correct form, such as std::midpoint, the prose says so instead of quietly using it.

Is StuHub free, and do I need an account?

It is free and there is nothing to sign in to. No login, account or payment is required to read any of the 20 topics — StuHub is published by DUOCODE TECHNOLOGY alongside APRide, and the ride board's accounts have nothing to do with it.

Can I paste this code into my assignment?

Treat it as a reference, not as an answer key. StuHub is educational material only, it is not coursework and it is not affiliated with or endorsed by any institution, so your own submission rules decide what you may reuse. Every listing was compiled and run before publication, and you should still compile and test anything you take.

Why is everything written in C++ rather than pseudocode?

Because most of the mistakes worth catching are C++ mistakes, not algorithm mistakes: a lost pointer, a destructor that never runs, an index that underflows because it was unsigned. Pseudocode hides exactly the layer where a data-structures assignment is actually failed.

Where should I check what the standard library really guarantees?

cppreference for the day-to-day answer, and the WG21 working drafts when the exact wording matters — both are linked below. Compiler documentation settles the rest: a warning you cannot explain is usually the compiler being right.

Does StuHub replace my lecture notes?

No. It is written to sit beside them: your course decides what is examinable, in what notation, and with which library restrictions. Where this page and your module handbook disagree about scope, the handbook wins.

Is StuHub connected to Asia Pacific University?

No. It is an independent reference published by DUOCODE TECHNOLOGY, not affiliated with or endorsed by Asia Pacific University or any other institution. It was written for APU students because that is who asked for it, and it is open to anyone.

Where can I check this against the language itself?

Nothing on this page outranks the standard or the library reference. When this page and one of these disagree, they are right and we want to know.

Published 2026-08-09 · updated 2026-08-27 · DUOCODE TECHNOLOGY