Given the head of a singly linked list, sort the linked list in ascending order.
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Quicksort loves arrays because it jumps to any index instantly. A linked list has no random access — reaching index i means walking i steps. Merge sort only ever walks forward and splices nodes, so it fits linked lists perfectly and stays O(n log n) in the worst case.
Merge sort in three moves: split the list into two halves (find the middle with fast/slow pointers), recursively sort each half, then merge the two sorted halves by repeatedly splicing on the smaller front node. No indexing required — just pointer walking.
Let’s start by finding a sorting algorithm that allows us to sort a linked list.
Choosing a sorting algorithm
We're tasked with sorting a linked list, not an array. This distinction is crucial because algorithms like quicksort rely on random access through indexing, which linked lists don't support. Merge sort is a great time option, where denotes the length of the linked list, because it does not require random access and works well with linked lists, as we’ll see in this explanation.
Merge sort
The merge sort algorithm uses a divide and conquer strategy. At a high level, it can be broken down into three steps:
We can see what the entire process looks like in the diagram below:
This is what this process looks like as pseudocode:
def merge_sort(head):
# Split the linked list into two halves.
second_head = split_list(head)
# Recursively sort both halves.
first_half_sorted = merge_sort(head)
second_half_sorted = merge_sort(second_head)
# Merge the sorted sublists.
return merge(first_half_sorted, second_half_sorted)
Let’s discuss in more detail how to split a linked list, and how to merge two sorted linked lists.
Splitting the linked list in half
To split a linked list in half, we need access to its middle node because the node next to the middle node can represent the head of the second linked list:
We can retrieve the middle node using the fast and slow pointer technique, as described in the Linked List Midpoint problem:
Then, we just need to disconnect the two halves by setting slow.next to null:
Note that when the linked list is of even length, there are two middle nodes. We want the slow pointer to stop at the first middle node so we can get the head of the second half more easily. As mentioned in Linked List Midpoint, we can achieve this by stopping the fast pointer when fast.next.next is null:
Merging two sorted linked lists
First, let's consider merging two linked lists, each containing a single node, meaning they’re both inherently sorted. We merge them by placing the smaller node first, followed by the other node:
With that established, how would we merge two longer linked lists? To do this, we can set two pointers, one at the start of each linked list, then perform the following steps:
Compare the nodes at each pointer and add the node with the smaller value to the merged linked list.
Advance the pointer at that node with the smaller value to the next node in its linked list.
Repeat the above two steps until we can no longer advance either pointer.
Before discussing the final step, observe how these steps are applied to the following two sorted linked lists. We can use a dummy node to point to the head of the merged linked list:
If one of the linked lists has been entirely added to the merged list, we can just add the rest of the other linked list to the merged list:
from typing import List
def sort_linked_list(head: ListNode) -> ListNode:
# If the linked list is empty or has only one element, it's already sorted.
if not head or not head.next:
return head
# Split the linked list into halves using the fast and slow pointer technique.
second_head = split_list(head)
# Recursively sort both halves.
first_half_sorted = sort_linked_list(head)
second_half_sorted = sort_linked_list(second_head)
# Merge the sorted sublists.
return merge(first_half_sorted, second_half_sorted)
def split_list(head: ListNode) -> ListNode:
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
second_head = slow.next
slow.next = None
return second_head
def merge(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
# This pointer will be used to append nodes to the tail of the merged linked list.
tail = dummy
# Continually append the node with the smaller value from each linked list to the
# merged linked list until one of the linked lists has no more nodes to merge.
while l1 and l2:
if l1.val < l2.val:
tail.next = l1
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
# One of the two linked lists could still have nodes remaining. Attach those nodes
# to the end of the merged linked list.
tail.next = l1 or l2
return dummy.next// ListNode has fields: int val, and a next pointer.
ListNode* splitList(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
// Stop 'slow' at the first middle node so the split is clean.
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* secondHead = slow->next;
slow->next = nullptr; // sever the two halves
return secondHead;
}
ListNode* mergeTwo(ListNode* l1, ListNode* l2) {
ListNode dummy(0);
ListNode* tail = &dummy;
// Splice the smaller front node until one list runs out.
while (l1 != nullptr && l2 != nullptr) {
if (l1->val <= l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
// Attach whatever remains (already sorted).
tail->next = (l1 != nullptr) ? l1 : l2;
return dummy.next;
}
ListNode* sortLinkedList(ListNode* head) {
// A list with 0 or 1 node is already sorted.
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* secondHead = splitList(head); // cut into two halves
ListNode* firstSorted = sortLinkedList(head); // sort the first half
ListNode* secondSorted = sortLinkedList(secondHead);// sort the second half
return mergeTwo(firstSorted, secondSorted); // merge the sorted halves
}// ListNode has fields: int val, and a next reference.
ListNode sortLinkedList(ListNode head) {
// A list with 0 or 1 node is already sorted.
if (head == null || head.next == null) {
return head;
}
ListNode secondHead = splitList(head); // cut into two halves
ListNode firstSorted = sortLinkedList(head); // sort the first half
ListNode secondSorted = sortLinkedList(secondHead); // sort the second half
return mergeTwo(firstSorted, secondSorted); // merge the sorted halves
}
ListNode splitList(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// Stop 'slow' at the first middle node so the split is clean.
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode secondHead = slow.next;
slow.next = null; // sever the two halves
return secondHead;
}
ListNode mergeTwo(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
// Splice the smaller front node until one list runs out.
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
// Attach whatever remains (already sorted).
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;
}Time complexity: The time complexity of sort_linked_list is because it uses merge sort. Here’s the breakdown:
The linked list is recursively split until each sublist contains only one node. This splitting process happens about times because each split reduces the size of the linked list by half.
At each level, we merge the split linked lists. Merging all elements at one level takes about operations.
Since there are levels of splitting and merging, and there are operations at each level, the time complexity is .
Space complexity: The space complexity is due to the recursive call stack, which can grow up to in height.
Merge sort is a stable sorting algorithm. This is important in scenarios where the original order of equal elements must be preserved. For example, if we are sorting a list of nodes that share the same value, but have an additional attribute storing the time they were created, then it’s important for the relative order of these nodes to stay the same. Stability can be important for database systems; for instance, where stable sorting is required to maintain data integrity and consistency across multiple operations.
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
4 → 2 and 1 → 3.
4 → 2 becomes 2 → 4; 1 → 3 stays 1 → 3.
1 → 2 → 3 → 4.
| l1 front | l2 front | splice → result |
|---|---|---|
| 2 | 1 | 1 → [1] |
| 2 | 3 | 2 → [1,2] |
| 4 | 3 | 3 → [1,2,3] |
| 4 | — | l2 empty → attach 4 → [1,2,3,4] |
Sorted list 1 → 2 → 3 → 4. ✓
With 0 or 1 node the base case not head or not head.next fires immediately — there is nothing to split or merge, so the list is returned unchanged.
| head | head.next | action |
|---|---|---|
| 5 | null | base case → return head |
Returns 5 unchanged; an empty list returns null. ✗
tail.next = l1 if l1 else l2 attaches the non-empty remainder in one line.
Use a stack-allocated ListNode dummy(0) and return dummy.next; compare with nullptr.
Same structure with null checks; a dummy node keeps the merge loop simple.