Sort a singly linked list using insertion sort by splicing each node into a growing sorted list.
Problem Restatement
We are given the head of a singly linked list.
Sort the list using insertion sort and return the new head.
Insertion sort builds a sorted output list one node at a time. At each step, it removes one node from the input, finds where it belongs in the sorted part, and inserts it there. LeetCode gives examples such as [4,2,1,3] -> [1,2,3,4] and [-1,5,3,4,0] -> [-1,0,3,4,5].
Examples
Example 1:
head = [4, 2, 1, 3]Output:
[1, 2, 3, 4]Example 2:
head = [-1, 5, 3, 4, 0]Output:
[-1, 0, 3, 4, 5]Input and Output
| Item | Meaning |
|---|---|
| Input | head: Optional[ListNode] |
| Output | Head of the sorted linked list |
| Sort order | Ascending |
| Algorithm | Insertion sort |
| Node count | From 1 to 5000 |
| Node values | From -5000 to 5000 |
Function shape:
class Solution:
def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
...First Thought: Convert to Array
A simple way is:
- Read all values into an array.
- Sort the array.
- Write the sorted values back into the list.
But this does not really implement insertion sort on the linked list. It also modifies values instead of rearranging nodes.
The point of this problem is pointer manipulation.
We should sort by moving nodes.
Key Insight
Maintain a separate sorted linked list.
Process the original list one node at a time.
For each node:
- Save its next pointer.
- Find its correct position in the sorted list.
- Insert it there.
- Continue with the saved next node.
A dummy node makes insertion at the front easy.
dummy -> sorted nodesThe final sorted head is:
dummy.nextInsertion Example
Input:
4 -> 2 -> 1 -> 3Start with an empty sorted list:
dummyInsert 4:
dummy -> 4Insert 2 before 4:
dummy -> 2 -> 4Insert 1 before 2:
dummy -> 1 -> 2 -> 4Insert 3 between 2 and 4:
dummy -> 1 -> 2 -> 3 -> 4Return dummy.next.
Algorithm
Create a dummy node for the sorted list.
Set curr = head.
While curr exists:
- Save the next unsorted node:
next_node = curr.next- Find the insertion position in the sorted list:
prev = dummy
while prev.next and prev.next.val < curr.val:
prev = prev.next- Insert
currafterprev:
curr.next = prev.next
prev.next = curr- Move to the saved next node:
curr = next_nodeReturn:
dummy.nextCorrectness
The algorithm maintains this invariant:
Before each iteration, the list starting at dummy.next is sorted and contains exactly the nodes already processed from the original list.
Initially, no nodes have been processed, so the sorted list is empty and the invariant holds.
During each iteration, the algorithm takes one unprocessed node curr. It scans the sorted list until it finds the first node with value greater than or equal to curr.val. Inserting curr before that node keeps the sorted order. All previously processed nodes remain in the list, and curr is added exactly once.
Therefore, after the iteration, the sorted list contains exactly all processed nodes and remains sorted.
When the loop finishes, every original node has been processed. By the invariant, dummy.next points to a sorted list containing all original nodes.
Thus, the algorithm returns the sorted linked list.
Complexity
Let n be the number of nodes.
| Metric | Value | Why |
|---|---|---|
| Time | O(n^2) | Each insertion may scan the sorted list |
| Space | O(1) | Only pointer variables are used |
Insertion sort is quadratic in the worst case, which matches the intended algorithm for this problem.
Implementation
from typing import Optional
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def insertionSortList(
self,
head: Optional[ListNode],
) -> Optional[ListNode]:
dummy = ListNode(0)
curr = head
while curr:
next_node = curr.next
prev = dummy
while prev.next and prev.next.val < curr.val:
prev = prev.next
curr.next = prev.next
prev.next = curr
curr = next_node
return dummy.nextCode Explanation
The dummy node anchors the sorted list:
dummy = ListNode(0)The current pointer walks through the original list:
curr = headBefore moving curr, we save the next original node:
next_node = curr.nextThis is important because insertion changes curr.next.
To find the insertion point, start from the dummy node:
prev = dummyMove forward while the next sorted node is smaller:
while prev.next and prev.next.val < curr.val:
prev = prev.nextInsert curr after prev:
curr.next = prev.next
prev.next = currThen continue with the saved node:
curr = next_nodeFinally:
return dummy.nextreturns the real head of the sorted list.
Small Optimization
If the input is nearly sorted, we can remember the previous insertion position.
When the next value is larger than the last inserted value, the search can continue from there instead of restarting from the dummy node.
A concise optimized version:
class Solution:
def insertionSortList(
self,
head: Optional[ListNode],
) -> Optional[ListNode]:
dummy = ListNode(0)
prev = dummy
curr = head
while curr:
next_node = curr.next
if prev.val >= curr.val:
prev = dummy
while prev.next and prev.next.val < curr.val:
prev = prev.next
curr.next = prev.next
prev.next = curr
curr = next_node
return dummy.nextThe simpler version is easier to reason about. The optimized version still has worst-case O(n^2) time.
Testing
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build_list(values):
dummy = ListNode(0)
curr = dummy
for value in values:
curr.next = ListNode(value)
curr = curr.next
return dummy.next
def to_list(head):
result = []
curr = head
while curr:
result.append(curr.val)
curr = curr.next
return result
def run_tests():
s = Solution()
head = build_list([4, 2, 1, 3])
assert to_list(s.insertionSortList(head)) == [1, 2, 3, 4]
head = build_list([-1, 5, 3, 4, 0])
assert to_list(s.insertionSortList(head)) == [-1, 0, 3, 4, 5]
head = build_list([1])
assert to_list(s.insertionSortList(head)) == [1]
head = build_list([1, 2, 3, 4])
assert to_list(s.insertionSortList(head)) == [1, 2, 3, 4]
head = build_list([4, 3, 2, 1])
assert to_list(s.insertionSortList(head)) == [1, 2, 3, 4]
head = build_list([2, 2, 1, 1])
assert to_list(s.insertionSortList(head)) == [1, 1, 2, 2]
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
[4, 2, 1, 3] | Standard example |
[-1, 5, 3, 4, 0] | Negative and positive values |
[1] | Single node |
| Already sorted | No major reordering |
| Reverse sorted | Worst-case insertion pattern |
| Duplicates | Equal values remain sorted |