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.
- 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)
- Keep the left side of the list sorted.
- Take the first node on the right (“unsorted”), find where it belongs inside the left part, and insert it there.
- Repeat until there’s no right part left.
Think of holding a fan of playing cards in your left hand and inserting the next card into its place.
- 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 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: tiny, testable building blocks
- FindInsertionSpot(list, value, boundary): scans only within the sorted prefix (from head up to boundary), returning the node after which to insert (or nullptr for “insert at head”)
- ListInsertionSort: the algorithm glue that uses the helpers safely
- 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
Quick start (Makefile)
# Default build and run
make run
# With teaching trace enabled
make run TRACE=1
macOS/Linux direct compile (no Make/CMake)
Normal
clang++ -std=c++20 -O2 -Wall -Wextra -pedantic main.cpp -o ll_isort && ./ll_isort
# With visual TRACE
clang++ -std=c++20 -O2 -Wall -Wextra -pedantic -DTRACE main.cpp -o ll_isort && ./ll_isort
# If you prefer g++
g++ -std=c++20 -O2 -Wall -Wextra -pedantic main.cpp -o ll_isort && ./ll_isort
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -DTRACE main.cpp -o ll_isort && ./ll_isort
:: Command Prompt (.bat)
run.bat
run.bat trace :: enable teaching trace
# PowerShell
./run.ps1
./run.ps1 -Trace
cmake -S . -B build
cmake --build build
./build/linked_list_insertion_sort
g++ -std=c++17 -O2 -Wall -Wextra -pedantic main.cpp -o ll_isort
./ll_isort
Before sort: 39 -> 45 -> 11 -> 22
After sort: 11 -> 22 -> 39 -> 45
Trace hooks are already built into main.cpp. Enabling TRACE prints both a textual trace and a visual two‑line ASCII diagram that marks head
, prev
, curr
, next
, and spot
.
# CMake
cmake -S . -B build -DTRACE=ON && cmake --build build && ./build/linked_list_insertion_sort
# Makefile
make run TRACE=1
# Direct compile
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -DTRACE main.cpp -o ll_isort && ./ll_isort
With TRACE enabled, each step prints an ANSI-bordered box with the list and role markers (H,P,C,N,S). Colors auto-disable if not a TTY.
╔══════════════════════════════════════════════════════════════╗
║ State: BEFORE place (H=head, P=prev, C=curr, N=next, S=spot) ║
║ [39] -> [45] -> [11] -> [22] ║
║ H P C N ║
╚══════════════════════════════════════════════════════════════╝
╔═════════════════════════════════════╗
║ State: AFTER unlink (curr isolated) ║
║ [39] -> [45] -> [22] ║
║ H P N ║
╚═════════════════════════════════════╝
C (isolated): [11]
╔════════════════════════════════════╗
║ State: AFTER insert after S (spot) ║
║ [11] -> [22] -> [39] -> [45] ║
║ S C P ║
╚════════════════════════════════════╝
- 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
curr
will 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
trace_ui.hpp
and integrated intomain.cpp
under#ifdef TRACE
.
- The code favors readability and pointer hygiene over micro-optimizations.
- Each operation is isolated (prepend/insert/remove) so students can test them independently.
- The trace shows the invariants in action: prev marks the end of the sorted prefix; curr is the node being placed; next is saved before rewiring; spot marks where curr re-enters.
- Stability comes from scanning with
<
(not<=
), so equal keys keep their original relative order.
- Losing the rest of the list: Always set
Node* next = curr->next;
before you movecurr
. - Breaking links:
ListInsertAfter
setsnewNode->next = prev->next
beforeprev->next = newNode
. - Head updates: When inserting at the front, use
ListPrepend
(it updateshead
). - Scanning into unsorted area:
FindInsertionSpot
stops atboundary
to keep the search in the sorted prefix. - Advancing
prev
incorrectly: Only advanceprev
if no move occurred (i.e.,spot == prev
).
- Make it generic: turn
int
into a templatedT
with a comparator. - Add
PushFront
,PopFront
,PopAfter
to practice pointer edits. - Create randomized tests that compare to
std::stable_sort
on a vector as an oracle. - Show stability: add duplicates and verify the original order of equals is preserved.
.
├── CMakeLists.txt # Minimal CMake build file (TRACE toggle supported)
├── Makefile # make run | make run TRACE=1 | make clean
├── main.cpp # Full implementation + demo + trace hooks
├── pseudo.cpp # Pseudocode-style reference (not compiled)
├── trace_ui.hpp # ANSI-colored, bordered trace UI for the linked list
├── run.bat # Windows CMD helper (use: run.bat [trace])
├── run.ps1 # Windows PowerShell helper (use: ./run.ps1 [-Trace])
└── .gitignore # Ignores build artifacts and IDE files
struct Node { int data; Node* next; }
- A single list node.
data
holds the value,next
points to the next node ornullptr
.
- A single list node.
struct List { Node* head; }
- Holds a pointer to the first node.
head == nullptr
means empty list.
- Holds a pointer to the first node.
void ListPrepend(List* list, Node* newNode)
- Inserts
newNode
at the front. Updateslist->head
.
- Inserts
void ListInsertAfter(List* /*list*/, Node* prev, Node* newNode)
- Inserts
newNode
immediately 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->head
up to (but not including)boundary
and returns the node after whichvalue
should be inserted. Returnsnullptr
if it should go at the head.
- Scans from
void ListInsertionSort(List* list)
- Stable, in‑place insertion sort: grows a sorted prefix and inserts each
curr
into 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:
trace_ui.hpp
(enabled only whenTRACE
is 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.bat
orrun.ps1
with 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.