C++ In Practice - Linked Lists Internals

Linked List Manipulation (C++11 Deep Dive)

Linked Lists are unique because they force you to manage raw pointers. While std::list (doubly linked) and std::forward_list (singly linked) exist, didactic problems usually define a custom struct ListNode.

The Didactic Data Structure (Raw Pointers)

This is the standard definition you will encounter:

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

System Level Note: In modern C++ production code, you would rarely use raw pointers like this due to memory leak risks. You would use std::unique_ptr<Node> for strict ownership. However, for algorithms (and LeetCode), raw pointers are the standard.


1. Reverse Linked List (Iterative)

Pattern: Pointer manipulation.

Complexity: Time O(N) | Space O(1).

Deep Dive: We need three pointers: prev, curr, and next. The order of operations is critical to avoid losing the reference to the rest of the list.

ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr;
    ListNode* curr = head;
    
    while (curr != nullptr) {
        ListNode* nextTemp = curr->next; // 1. Save next
        curr->next = prev;               // 2. Reverse link
        prev = curr;                     // 3. Advance prev
        curr = nextTemp;                 // 4. Advance curr
    }
    
    return prev; // Prev is the new head
}

2. Merge Two Sorted Lists (The Dummy Node)

Pattern: Merge Sort Step.

Complexity: Time O(N + M) | Space O(1).

C++11 Optimization: Stack Allocation Instead of new ListNode(0) for the dummy node (which requires manual delete to prevent leaks), allocate the dummy node on the stack. Use the address operator & to get its pointer.

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode dummy(0); // Stack allocation (RAII)
    ListNode* tail = &dummy;

    while (l1 && l2) {
        if (l1->val < l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }

    // Attach remaining part
    if (l1) tail->next = l1;
    else if (l2) tail->next = l2;

    return dummy.next;
}

3. Merge K Sorted Lists (Priority Queue)

Pattern: Min-Heap.

Complexity: Time O(N \log K) | Space O(K).

C++11 Details: We need a custom comparator for the priority_queue because ListNode* comparison compares memory addresses, not values.

#include <vector>
#include <queue>

struct CompareNode {
    // Min-Heap needs 'greater' logic
    bool operator()(ListNode* a, ListNode* b) {
        return a->val > b->val;
    }
};

ListNode* mergeKLists(std::vector<ListNode*>& lists) {
    std::priority_queue<ListNode*, std::vector<ListNode*>, CompareNode> pq;

    // 1. Init Heap
    for (ListNode* l : lists) {
        if (l) pq.push(l);
    }

    ListNode dummy(0);
    ListNode* tail = &dummy;

    // 2. Process Heap
    while (!pq.empty()) {
        ListNode* smallest = pq.top();
        pq.pop();

        tail->next = smallest;
        tail = tail->next;

        if (smallest->next) {
            pq.push(smallest->next);
        }
    }

    return dummy.next;
}

4. Cycle Detection (Floyd’s Cycle Finding)

Pattern: Fast & Slow Pointers.

Complexity: Time O(N) | Space O(1).

bool hasCycle(ListNode *head) {
    if (!head) return false;
    ListNode *slow = head;
    ListNode *fast = head;

    while (fast && fast->next) {
        slow = slow->next;       // 1 step
        fast = fast->next->next; // 2 steps

        if (slow == fast) return true;
    }
    return false;
}

5. LRU Cache (The STL Powerhouse)

Problem: O(1) Get and Put.

Architecture: Hash Map + Doubly Linked List.

Complexity: Time O(1) | Space O(N).

C++11 Implementation Strategy: We use std::list (Doubly Linked List) and std::unordered_map. The map stores {key, iterator_to_list_node}. The list stores {key, value} pairs (we need the key in the list to delete from the map during eviction).

The Secret Weapon: std::list::splice splice moves an element from one position to another within a list (or between lists) in O(1) time by simply adjusting pointers. It does not copy or reallocate memory.

#include <list>
#include <unordered_map>
#include <utility> // for std::pair

class LRUCache {
private:
    int capacity;
    // List stores pairs of <Key, Value>
    // Front = Most Recently Used, Back = Least Recently Used
    std::list<std::pair<int, int>> lruList; 
    
    // Map: Key -> Iterator to the node in lruList
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> cache;

public:
    LRUCache(int capacity) : capacity(capacity) {}

    int get(int key) {
        auto it = cache.find(key);
        if (it == cache.end()) return -1;

        // Move this node to the front of the list (Mark as MRU)
        // splice(position_to_move_to, source_list, iterator_to_move)
        lruList.splice(lruList.begin(), lruList, it->second);

        return it->second->second;
    }

    void put(int key, int value) {
        auto it = cache.find(key);
        
        if (it != cache.end()) {
            // Key exists: Update value and move to front
            it->second->second = value;
            lruList.splice(lruList.begin(), lruList, it->second);
        } else {
            // Key does not exist: Insert
            if (cache.size() == capacity) {
                // Evict LRU (Back of list)
                int keyToDelete = lruList.back().first;
                lruList.pop_back();   // Remove from list
                cache.erase(keyToDelete); // Remove from map
            }
            
            // Insert new pair at front
            lruList.push_front({key, value});
            cache[key] = lruList.begin();
        }
    }
};

ASCII Visualization of STL LRU:

[ Map ]
 Key 1 -> Iterator --+
 Key 2 -> Iterator --|--+
                     |  |
                     v  v
[ std::list (Doubly Linked) ]
[ MRU ] <-> [ {1, Val} ] <-> [ {2, Val} ] <-> [ LRU ]

6. Remove Nth Node From End

Pattern: Two Pointers (Delay).

Logic: Move fast pointer n steps ahead. Then move fast and slow together. When fast hits end, slow is at N from end.

ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode dummy(0);
    dummy.next = head;
    ListNode* slow = &dummy;
    ListNode* fast = &dummy;

    // 1. Move fast ahead by n + 1 steps
    // (+1 ensures slow stops at the node BEFORE the one to delete)
    for (int i = 0; i <= n; ++i) {
        fast = fast->next;
    }

    // 2. Move both
    while (fast != nullptr) {
        slow = slow->next;
        fast = fast->next;
    }

    // 3. Skip the node
    ListNode* toDelete = slow->next;
    slow->next = slow->next->next;
    
    // In C++, we must manually free memory if we own it.
    // In LeetCode, it's optional but good practice to mention.
    // delete toDelete; 

    return dummy.next;
}

Summary of Linked List Patterns

Pattern C++ Tool/Technique Complexity
Reverse 3-Pointer Swap O(N)
Merge Stack-based Dummy Node O(N)
Merge K priority_queue with comparator O(N \log K)
Cycle Floyd's (Fast/Slow) O(N)
LRU Cache std::list (Splice) + unordered_map O(1)
Nth from End Offset Pointers O(N)