Command Palette

Search for a command to run...

MTH 4300

Lecture 18

What Is a Linked List?

An array stores elements in contiguous memory — index access is O(1) but inserting or removing in the middle is O(n) because every subsequent element must shift. A singly linked list avoids that cost by storing each element in its own node. Each node holds a value and a pointer to the next node; the list itself holds only a pointer to the first node (the head) and the last node (the tail).

The trade-off runs the other way: prepending and removing from the front are O(1) — no shifting needed — but reaching element i is O(n) because you must walk from the head.

You already saw this pattern in lecture 16: the Queue implementation uses exactly this node-pointer structure. Here we build it as a first-class container.

LinkedList<T> — singly linked
template <typename T>
class LinkedList {
public:
    LinkedList() : head_(nullptr), tail_(nullptr), size_(0) {}

    ~LinkedList() {
        while (head_) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }

    void push_front(const T& value) {
        Node* node = new Node(value);
        node->next = head_;
        head_ = node;
        if (!tail_) tail_ = node;
        ++size_;
    }

    void push_back(const T& value) {
        Node* node = new Node(value);
        if (tail_) tail_->next = node;
        else        head_ = node;
        tail_ = node;
        ++size_;
    }

    void pop_front() {
        if (!head_) return;
        Node* old = head_;
        head_ = head_->next;
        if (!head_) tail_ = nullptr;
        delete old;
        --size_;
    }

    bool find(const T& value) const {
        Node* cur = head_;
        while (cur) {
            if (cur->value == value) return true;
            cur = cur->next;
        }
        return false;
    }

    void remove(const T& value) {
        if (!head_) return;
        if (head_->value == value) { pop_front(); return; }
        Node* prev = head_;
        Node* cur  = head_->next;
        while (cur) {
            if (cur->value == value) {
                prev->next = cur->next;
                if (!cur->next) tail_ = prev;
                delete cur;
                --size_;
                return;
            }
            prev = cur;
            cur  = cur->next;
        }
    }

    void print_all() const {
        Node* cur = head_;
        while (cur) {
            std::cout << cur->value;
            if (cur->next) std::cout << " -> ";
            cur = cur->next;
        }
        std::cout << "\n";
    }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    struct Node {
        T     value;
        Node* next;
        Node(const T& v) : value(v), next(nullptr) {}
    };

    Node* head_;
    Node* tail_;
    int   size_;
};

push_front and push_back

push_front allocates a new node, points its next at the current head, then updates head_ to the new node. If the list was empty, tail_ must also be set — the new node is both the first and last element.

push_front trace — prepend 1 to [2 -> 3]
// Before: head_ -> [2] -> [3] <- tail_
// Allocate node(1); node->next = head_ (which is [2])
// head_ = node(1)
// After: head_ -> [1] -> [2] -> [3] <- tail_

push_back appends at the tail. If the list is non-empty, tail_->next links the new node in; otherwise both head_ and tail_ are set. tail_ is then advanced to the new node.

push_back trace — append 4 to [1 -> 2 -> 3]
// Before: head_ -> [1] -> [2] -> [3] <- tail_
// Allocate node(4); tail_->next = node(4); tail_ = node(4)
// After: head_ -> [1] -> [2] -> [3] -> [4] <- tail_

pop_front

pop_front saves the current head, advances head_ to the next node, deletes the saved pointer, and decrements size_. When the last element is removed, head_ becomes nullptr and tail_ must be cleared too.

pop_front trace — remove from [10 -> 20 -> 30]
// old = head_ ([10])
// head_ = head_->next ([20])
// head_ is non-null, so tail_ stays unchanged
// delete old
// After: head_ -> [20] -> [30] <- tail_

When pop_front removes the only element, head_ becomes nullptr. Forgetting to also set tail_ = nullptr leaves a dangling pointer that corrupts the next push_back.

find and remove

find walks the list from head_ and returns true as soon as it finds a matching value. If it reaches the end without a match, it returns false. Traversal is O(n).

remove deletes the first node whose value matches. It handles two cases separately:

  • The head matches — delegate to pop_front.
  • A later node matches — keep a prev pointer one step behind cur, then splice cur out by setting prev->next = cur->next. If the removed node was the tail, update tail_ to prev.
remove trace — remove 20 from [10 -> 20 -> 30]
// head_->value is 10 ≠ 20; enter loop
// prev=[10], cur=[20]; cur->value == 20 — match
// prev->next = cur->next ([30])
// cur->next is non-null, so tail_ unchanged
// delete cur ([20])
// After: head_ -> [10] -> [30] <- tail_

The destructor walks head_ the same way find does, deleting each node in turn. Keeping a local next pointer before the delete is essential — the node is freed before you can read its next field.

Arrays vs. Linked Lists

Array / VectorSingly Linked List
Index accessO(1)O(n)
Insert / remove frontO(n) — shift requiredO(1)
Insert / remove backO(1) amortizedO(1) with tail pointer
Insert / remove middleO(n)O(n) to find, O(1) to splice
Memory layoutContiguousScattered (pointer-linked)

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    LinkedList<int> lst;

    lst.push_back(10);
    lst.push_back(20);
    lst.push_front(5);

    lst.print_all();     // line A
    lst.pop_front();
    lst.print_all();     // line B
    std::cout << lst.size() << "\n"; // line C

    return 0;
}

Q2.

Without running the code, trace the list's node order after each operation and predict the output.

#include <iostream>

int main() {
    LinkedList<int> lst;

    lst.push_back(1);   // list after?
    lst.push_back(2);   // list after?
    lst.push_front(0);  // list after?
    lst.push_back(3);   // list after?
    lst.remove(2);      // list after?

    lst.print_all();    // line A

    std::cout << lst.find(0) << "\n"; // line B
    std::cout << lst.find(2) << "\n"; // line C

    return 0;
}

Q3.

Each snippet has a bug related to the linked list. Identify what's wrong.

// Snippet A — push_front forgets to link into an empty list
void push_front(const T& value) {
    Node* node = new Node(value);
    node->next = head_;
    head_ = node;
    ++size_;
    // tail_ never set when list was empty
}

// Snippet B — pop_front deletes head_ before reading next
void pop_front() {
    if (!head_) return;
    delete head_;
    head_ = head_->next; // use-after-free
    --size_;
}

// Snippet C — remove does not update tail_ when removing the last node
void remove(const T& value) {
    if (!head_) return;
    if (head_->value == value) { pop_front(); return; }
    Node* prev = head_;
    Node* cur  = head_->next;
    while (cur) {
        if (cur->value == value) {
            prev->next = cur->next;
            // tail_ not updated
            delete cur;
            --size_;
            return;
        }
        prev = cur;
        cur  = cur->next;
    }
}

// Snippet D — find returns false immediately instead of advancing
bool find(const T& value) const {
    Node* cur = head_;
    while (cur) {
        if (cur->value != value) return false; // exits on first non-match
        cur = cur->next;
    }
    return true;
}

Q4.

Implement push_back and pop_front for the linked list below.

#include <iostream>

template <typename T>
class LinkedList {
public:
    LinkedList() : head_(nullptr), tail_(nullptr), size_(0) {}

    ~LinkedList() {
        while (head_) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }

    void push_front(const T& value) {
        Node* node = new Node(value);
        node->next = head_;
        head_ = node;
        if (!tail_) tail_ = node;
        ++size_;
    }

    void push_back(const T& value) {
        // TODO: append a new node at tail_
    }

    void pop_front() {
        // TODO: remove head_; reset tail_ if list becomes empty
    }

    void print_all() const {
        Node* cur = head_;
        while (cur) {
            std::cout << cur->value;
            if (cur->next) std::cout << " -> ";
            cur = cur->next;
        }
        std::cout << "\n";
    }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    struct Node {
        T     value;
        Node* next;
        Node(const T& v) : value(v), next(nullptr) {}
    };

    Node* head_;
    Node* tail_;
    int   size_;
};

int main() {
    LinkedList<int> lst;
    lst.push_back(10);
    lst.push_back(20);
    lst.push_back(30);
    lst.print_all();

    lst.pop_front();
    lst.print_all();

    lst.pop_front();
    lst.pop_front();
    std::cout << lst.empty() << "\n";
    return 0;
}

Q5.

Implement find and remove for the linked list below.

#include <iostream>

template <typename T>
class LinkedList {
public:
    LinkedList() : head_(nullptr), tail_(nullptr), size_(0) {}

    ~LinkedList() {
        while (head_) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }

    void push_back(const T& value) {
        Node* node = new Node(value);
        if (tail_) tail_->next = node;
        else        head_ = node;
        tail_ = node;
        ++size_;
    }

    void pop_front() {
        if (!head_) return;
        Node* old = head_;
        head_ = head_->next;
        if (!head_) tail_ = nullptr;
        delete old;
        --size_;
    }

    bool find(const T& value) const {
        // TODO: return true if value exists in the list
    }

    void remove(const T& value) {
        // TODO: remove the first node whose value matches
    }

    void print_all() const {
        Node* cur = head_;
        while (cur) {
            std::cout << cur->value;
            if (cur->next) std::cout << " -> ";
            cur = cur->next;
        }
        std::cout << "\n";
    }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    struct Node {
        T     value;
        Node* next;
        Node(const T& v) : value(v), next(nullptr) {}
    };

    Node* head_;
    Node* tail_;
    int   size_;
};

int main() {
    LinkedList<int> lst;
    for (int v : {10, 20, 30, 40, 50}) lst.push_back(v);

    std::cout << lst.find(30) << "\n"; // line A
    std::cout << lst.find(99) << "\n"; // line B

    lst.remove(10); // remove head
    lst.remove(50); // remove tail
    lst.remove(30); // remove middle
    lst.print_all();  // line C
    return 0;
}

Practice Problems