A tiny, well-commented C++ program that implements stable insertion sort on a singly linked list. It’s written for learning: clear names, ASCII diagrams, and a step-by-step walkthrough.
Kapture.2025-12-07.at.04.03.24.mp4
- How a singly linked list is wired: head, next, nodes
- Insertion sort as “growing a sorted prefix” (like sorting playing cards)
- Pointer hygiene: saving the next node before moving, unlinking, reinserting
- Why this version is stable and O(n²) (and when that’s OK)
The algorithm grows a sorted region from left to right:
- Start with the first node as the "sorted" portion (left side)
- Take the first node from the "unsorted" portion (right side)
- Find where it belongs in the sorted portion and insert it there
- Repeat until no unsorted nodes remain
Like sorting a hand of cards: pick up one card at a time and slide it into the right spot among the cards you're already holding.
- prev always points to the last node of the sorted prefix.
- curr is the first node of the unsorted part (the one we’re placing).
- We must save next = curr->next before moving curr.
- If curr already belongs at the end of the sorted part, just advance prev. Otherwise, unlink curr and re-insert it earlier (possibly at the head).
These rules are exactly what the implementation in src/main.cpp does.
FindInsertionSpot scans while curr->data < value (strictly <, not <=). So if two equal values appear, the earlier one stays earlier. That’s stability.
[39] -> [45] -> [11] -> [22]
^ ^
prev curr
We maintain: sorted = head..prev, unsorted = curr..end
- prev = 39, curr = 45, next = 11
- Find spot for 45 in [39] → spot is 39 (the last < 45)
- spot == prev → already in correct place; grow sorted region:
sorted: [39] -> [45]
unsorted: [11] -> [22]
^ ^
curr next (saved)
Advance: prev = 45, curr = 11.
- prev = 45, curr = 11, next = 22
- Find spot for 11 in [39 -> 45] → spot is nullptr (new smallest)
- spot != prev → move needed:
- ListRemoveAfter(list, prev) unlinks 11 from after 45
- ListPrepend(list, curr) inserts 11 at the head
[11] -> [39] -> [45] -> [22]
^ ^
head prev (unchanged)
Advance: curr = 22 (the next we saved earlier).
- prev = 45, curr = 22, next = nullptr
- Find spot for 22 in [11 -> 39 -> 45] → spot is the node 11 (since 11 < 22 and 39 !< 22)
- spot != prev → move needed:
- Unlink 22 from after 45
- Insert after 11
[11] -> [22] -> [39] -> [45]
Advance curr = next = nullptr → done.
Result is sorted and stable.
- Node, List: minimal data structures (head pointer + next links)
- ListPrepend, ListInsertAfter, ListRemoveAfter: core list operations
- FindInsertionSpot: scans the sorted portion to find where a value belongs
- ListInsertionSort: the main algorithm that ties everything together
- Time: O(n²) comparisons/moves in the worst case (like array insertion sort)
- Space: O(1) extra space (we move nodes in place)
- Good when you want stable behavior and cheap insertion without shifting elements
make run # normal mode
make run TRACE=1 # visual trace mode
make clean # remove binary# Normal mode
cmake -S . -B build && cmake --build build
./build/linked_list_insertion_sort
# Visual trace mode
cmake -S . -B build -DTRACE=ON && cmake --build build
./build/linked_list_insertion_sort# clang++ (default on macOS)
clang++ -std=c++20 -O2 -Wall -Wextra -pedantic -Iinclude src/main.cpp -o ll_isort
clang++ -std=c++20 -O2 -Wall -Wextra -pedantic -Iinclude -DTRACE src/main.cpp -o ll_isort # trace
# g++
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -Iinclude src/main.cpp -o ll_isort
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -Iinclude -DTRACE src/main.cpp -o ll_isort # trace
./ll_isort# macOS/Linux
./scripts/run.sh # normal mode
./scripts/run.sh trace # visual trace mode
# Windows (Command Prompt)
scripts\run.bat
scripts\run.bat trace
# Windows (PowerShell)
.\scripts\run.ps1
.\scripts\run.ps1 -TraceNormal mode:
=== Linked List Insertion Sort ===
Input: 39 -> 45 -> 11 -> 22
Output: 11 -> 22 -> 39 -> 45
Algorithm: O(n^2) time, O(1) space, stable
With TRACE enabled, each step prints an ANSI-bordered box with the list and role markers. Nodes with multiple roles show them separated by slashes (e.g., H/P/S), each letter in its own color. Colors auto-disable if not a TTY.
┌────────────────────────────────────┐
│ BEFORE place │
├────────────────────────────────────┤
│ [39] -> [45] -> [11] -> [22] │
│ H/P/S C N │
├────────────────────────────────────┤
│ H=head P=prev C=curr N=next S=spot │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ AFTER unlink │
├────────────────────────────────────┤
│ [39] -> [45] -> [22] │
│ H P N │
│ C (isolated): [11] │
├────────────────────────────────────┤
│ H=head P=prev C=curr N=next S=spot │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ AFTER insert (at head) │
├────────────────────────────────────┤
│ [11] -> [39] -> [45] -> [22] │
│ H/C P N │
├────────────────────────────────────┤
│ H=head P=prev C=curr N=next S=spot │
└────────────────────────────────────┘
Sorted: 11 -> 22 -> 39 -> 45
- H = head node
- P = prev (end of sorted prefix)
- C = curr (node being placed)
- N = next (saved pointer for the next loop)
- S = spot (node after which
currwill be inserted; S at head means insert at head)
- Colors are 256-color safe and disabled when stdout is not a TTY (e.g., piped to file).
- The bordered state is generated by
include/trace_ui.hppand integrated intosrc/main.cppunder#ifdef TRACE.
- The code prioritizes clarity over performance tricks
- Each list operation (prepend, insert, remove) is separate and can be tested on its own
- The trace shows what each pointer is doing at every step
- Stability: we use
<(not<=) when scanning, so equal values stay in their original order
- Losing the rest of the list: Always set
Node* next = curr->next;before you movecurr. - Breaking links:
ListInsertAftersetsnewNode->next = prev->nextbeforeprev->next = newNode. - Head updates: When inserting at the front, use
ListPrepend(it updateshead). - Scanning into unsorted area:
FindInsertionSpotstops atboundaryto keep the search in the sorted prefix. - Advancing
previncorrectly: Only advanceprevif no move occurred (i.e.,spot == prev).
- Support any data type (not just
int) by making the code use templates - Add more list operations like
PushFront,PopFront,PopAfter - Write tests with random data and compare results to
std::stable_sort - Test stability: use duplicate values and check that equal items keep their original order
.
├── CMakeLists.txt # CMake build file (TRACE toggle supported)
├── Makefile # make run | make run TRACE=1 | make clean
├── README.md # This file
├── .gitignore # Ignores build artifacts and IDE files
├── src/
│ └── main.cpp # Full implementation + demo + trace hooks
├── include/
│ └── trace_ui.hpp # ANSI-colored, bordered trace UI for the linked list
├── docs/
│ └── pseudo.cpp # Pseudocode-style reference (not compiled)
└── scripts/
├── run.sh # macOS/Linux helper (use: ./scripts/run.sh [trace])
├── run.bat # Windows CMD helper (use: scripts\run.bat [trace])
└── run.ps1 # Windows PowerShell helper (use: .\scripts\run.ps1 [-Trace])
struct Node { int data; Node* next; }- A single list node.
dataholds the value,nextpoints to the next node ornullptr.
- A single list node.
struct List { Node* head; }- Holds a pointer to the first node.
head == nullptrmeans empty list.
- Holds a pointer to the first node.
void ListPrepend(List* list, Node* newNode)- Inserts
newNodeat the front. Updateslist->head.
- Inserts
void ListInsertAfter(List* /*list*/, Node* prev, Node* newNode)- Inserts
newNodeimmediately afterprev. Requiresprev != nullptr.
- Inserts
Node* ListRemoveAfter(List* list, Node* prev)- Removes and returns the node after
prev. Ifprev == nullptr, removes the head. Safely isolates the removed node’snext.
- Removes and returns the node after
Node* FindInsertionSpot(const List* list, int value, Node* boundary)- Scans from
list->headup to (but not including)boundaryand returns the node after whichvalueshould be inserted. Returnsnullptrif it should go at the head.
- Scans from
void ListInsertionSort(List* list)- Stable, in-place insertion sort: grows a sorted prefix and inserts each
currinto the correct spot.
- Stable, in-place insertion sort: grows a sorted prefix and inserts each
void PushBack(List* list, int data)- Test helper: append a new node to the end.
void PrintList(const List* list)- Prints values like
1 -> 2 -> 3.
- Prints values like
- Header:
include/trace_ui.hpp(enabled only whenTRACEis defined)traceui::print_state(title, head, roles)prints a bordered, colored snapshot.- Roles struct:
traceui::PtrRoles<Node>{H,P,C,N,S}marks head, prev, curr, next, spot. - Palette constants (256-color):
C_HEAD, C_PREV, C_CURR, C_NEXT, C_SPOT, C_TEXT, C_BORDER. - Colors auto-disable if stdout is not a TTY.
- Compiler not found on Windows
- Use
run.batorrun.ps1with g++/clang++ installed (MSYS2/MinGW or LLVM). Or use CMake + Visual Studio generator.
- Use
- Box characters look wrong
- Ensure your terminal uses UTF-8 and a monospace font. Colors may be disabled if piping to a file.
- No colors shown
- Colors only enable when writing to an interactive terminal (TTY). They are intentionally off when redirected.