148. Sort List
Problem Description
You are given the head of a linked list. Your task is to sort the linked list in ascending order based on the node values and return the head of the sorted list.
For example, if you have a linked list 4 -> 2 -> 1 -> 3
, after sorting it should become 1 -> 2 -> 3 -> 4
.
The solution uses a merge sort algorithm to efficiently sort the linked list. The approach works by:
-
Finding the middle point: Using the fast and slow pointer technique, where the slow pointer moves one step at a time while the fast pointer moves two steps. When the fast pointer reaches the end, the slow pointer will be at the middle of the list.
-
Splitting the list: The linked list is divided into two halves at the middle point. The first half starts from
head
and ends at the node beforeslow.next
, while the second half starts fromslow.next
. -
Recursive sorting: Both halves (
l1
andl2
) are recursively sorted using the same approach until we have lists with only one node (which are inherently sorted). -
Merging sorted lists: The two sorted halves are merged back together by comparing values. A dummy node is used to simplify the merging process. We iterate through both lists, always choosing the smaller value to append to our result list. After one list is exhausted, any remaining nodes from the other list are directly attached.
The base case for the recursion is when the list has zero or one node, which is already sorted and can be returned as-is.
The time complexity of this approach is O(n log n)
where n
is the number of nodes in the linked list, and the space complexity is O(log n)
due to the recursive call stack.
Intuition
When we need to sort a linked list, we face a unique challenge compared to sorting arrays. In arrays, we can easily access any element by index in O(1)
time, but in linked lists, we can only traverse sequentially. This makes many common sorting algorithms like quicksort less efficient for linked lists.
Merge sort becomes a natural choice for linked lists because:
-
No random access needed: Merge sort processes elements sequentially, which aligns perfectly with how we traverse linked lists.
-
Divide and conquer works well: We can split a linked list into two halves without needing extra space (unlike arrays where we'd need to copy elements). We just need to find the middle node and break the connection.
-
Merging is natural for linked lists: When merging two sorted linked lists, we simply adjust pointers rather than copying elements around. This is actually more efficient than merging arrays.
The key insight is using the fast and slow pointer technique to find the middle. Why does this work? If the fast pointer moves twice as fast as the slow pointer, when the fast pointer reaches the end, the slow pointer will have traveled exactly half the distance - landing us at the middle node.
The recursive nature of merge sort fits well here: keep breaking the problem into smaller pieces until we have lists of size 1 (which are trivially sorted), then build back up by merging. Each merge operation combines two already-sorted lists into one larger sorted list.
The beauty of this approach is that we're working with the natural structure of linked lists - following pointers and rearranging connections - rather than fighting against it by trying to access elements by position.
Solution Approach
The implementation follows the merge sort strategy outlined in the reference approach. Let's walk through the code step by step:
1. Base Case Check:
if head is None or head.next is None:
return head
If the list is empty or has only one node, it's already sorted, so we return it immediately.
2. Finding the Middle Using Two Pointers:
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
We initialize slow
at the head and fast
at the second node (head.next
). The fast pointer moves two steps while the slow pointer moves one step. When fast
reaches the end, slow
will be at the node just before the middle. This ensures we split the list correctly into two halves.
3. Splitting the List:
l1, l2 = head, slow.next
slow.next = None
l1
starts from the head and goes up to theslow
nodel2
starts fromslow.next
and goes to the end- We break the connection by setting
slow.next = None
, effectively creating two separate lists
4. Recursive Sorting:
l1, l2 = self.sortList(l1), self.sortList(l2)
We recursively sort both halves. This continues until we reach lists of size 1, which are inherently sorted.
5. Merging the Sorted Lists:
dummy = ListNode()
tail = dummy
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
tail.next = l1 or l2
return dummy.next
- A
dummy
node is created to simplify the merging process (we don't need to handle the special case of the first node) tail
keeps track of the last node in our merged list- We compare values from both lists and attach the smaller one to our result
- After one list is exhausted, we attach any remaining nodes with
tail.next = l1 or l2
- Finally, we return
dummy.next
, which is the head of our sorted list
The time complexity is O(n log n)
where n
is the number of nodes, as we divide the list log n
times and merge takes O(n)
time at each level. The space complexity is O(log n)
due to the recursive call stack.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through sorting the linked list 4 -> 2 -> 1 -> 3
step by step.
Initial State: 4 -> 2 -> 1 -> 3
First Split (Level 1):
- Initialize pointers:
slow = 4
,fast = 2
- Move pointers:
slow = 2
,fast = 1
(fast moves to 1) - Move again:
slow
stays at 2 sincefast.next = 3
butfast.next.next = None
- Split into:
l1 = 4 -> 2
(disconnect after 2) andl2 = 1 -> 3
Left Half Recursion (Level 2): 4 -> 2
- Initialize:
slow = 4
,fast = 2
- Since
fast.next = None
, loop doesn't execute - Split into:
l1 = 4
andl2 = 2
- Both are single nodes (base case), return as-is
- Merge: Compare 4 vs 2, pick 2 first, then 4
- Result:
2 -> 4
Right Half Recursion (Level 2): 1 -> 3
- Initialize:
slow = 1
,fast = 3
- Since
fast.next = None
, loop doesn't execute - Split into:
l1 = 1
andl2 = 3
- Both are single nodes (base case), return as-is
- Merge: Compare 1 vs 3, pick 1 first, then 3
- Result:
1 -> 3
Final Merge (Level 1):
Now we merge 2 -> 4
and 1 -> 3
:
- Create dummy node,
tail = dummy
- Compare: 2 vs 1 → pick 1,
tail.next = 1
, advance to next in second list - Compare: 2 vs 3 → pick 2,
tail.next = 2
, advance to next in first list - Compare: 4 vs 3 → pick 3,
tail.next = 3
, advance to next in second list - First list has 4 remaining, attach it:
tail.next = 4
- Return
dummy.next
which points to:1 -> 2 -> 3 -> 4
Final Result: 1 -> 2 -> 3 -> 4
The key insight is how the fast/slow pointer technique consistently finds the middle, allowing us to split the list evenly at each level, and how merging two sorted lists naturally produces a larger sorted list.
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7from typing import Optional
8
9class Solution:
10 def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
11 """
12 Sort a linked list using merge sort algorithm.
13 Time Complexity: O(n log n)
14 Space Complexity: O(log n) due to recursion stack
15 """
16 # Base case: empty list or single node
17 if head is None or head.next is None:
18 return head
19
20 # Find the middle of the list using two pointers
21 # slow moves one step, fast moves two steps
22 slow = head
23 fast = head.next # Start fast at head.next to ensure slow stops before middle
24
25 while fast and fast.next:
26 slow = slow.next
27 fast = fast.next.next
28
29 # Split the list into two halves
30 first_half = head
31 second_half = slow.next
32 slow.next = None # Break the connection between two halves
33
34 # Recursively sort both halves
35 sorted_first = self.sortList(first_half)
36 sorted_second = self.sortList(second_half)
37
38 # Merge the two sorted halves
39 dummy_head = ListNode() # Dummy node to simplify merge logic
40 current = dummy_head
41
42 # Merge nodes in sorted order
43 while sorted_first and sorted_second:
44 if sorted_first.val <= sorted_second.val:
45 current.next = sorted_first
46 sorted_first = sorted_first.next
47 else:
48 current.next = sorted_second
49 sorted_second = sorted_second.next
50 current = current.next
51
52 # Attach remaining nodes (if any)
53 current.next = sorted_first or sorted_second
54
55 return dummy_head.next
56
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12 /**
13 * Sorts a linked list using merge sort algorithm
14 * Time Complexity: O(n log n)
15 * Space Complexity: O(log n) due to recursive call stack
16 *
17 * @param head the head of the linked list to be sorted
18 * @return the head of the sorted linked list
19 */
20 public ListNode sortList(ListNode head) {
21 // Base case: empty list or single node
22 if (head == null || head.next == null) {
23 return head;
24 }
25
26 // Find the middle of the list using slow and fast pointers
27 // Fast pointer starts at head.next to ensure proper splitting for even-length lists
28 ListNode slowPointer = head;
29 ListNode fastPointer = head.next;
30
31 // Move slow pointer one step and fast pointer two steps
32 // When fast reaches the end, slow will be at the middle
33 while (fastPointer != null && fastPointer.next != null) {
34 slowPointer = slowPointer.next;
35 fastPointer = fastPointer.next.next;
36 }
37
38 // Split the list into two halves
39 ListNode firstHalf = head;
40 ListNode secondHalf = slowPointer.next;
41 slowPointer.next = null; // Break the connection between two halves
42
43 // Recursively sort both halves
44 firstHalf = sortList(firstHalf);
45 secondHalf = sortList(secondHalf);
46
47 // Merge the two sorted halves
48 ListNode dummyHead = new ListNode(); // Dummy node to simplify merging logic
49 ListNode currentTail = dummyHead;
50
51 // Merge nodes from both lists in sorted order
52 while (firstHalf != null && secondHalf != null) {
53 if (firstHalf.val <= secondHalf.val) {
54 currentTail.next = firstHalf;
55 firstHalf = firstHalf.next;
56 } else {
57 currentTail.next = secondHalf;
58 secondHalf = secondHalf.next;
59 }
60 currentTail = currentTail.next;
61 }
62
63 // Attach remaining nodes (if any) from either list
64 currentTail.next = (firstHalf != null) ? firstHalf : secondHalf;
65
66 // Return the head of the merged sorted list
67 return dummyHead.next;
68 }
69}
70
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13 /**
14 * Sorts a linked list using merge sort algorithm
15 * Time Complexity: O(n log n)
16 * Space Complexity: O(log n) for recursion stack
17 *
18 * @param head The head of the linked list to be sorted
19 * @return The head of the sorted linked list
20 */
21 ListNode* sortList(ListNode* head) {
22 // Base case: empty list or single node
23 if (!head || !head->next) {
24 return head;
25 }
26
27 // Find the middle of the list using two-pointer technique
28 // slow moves one step, fast moves two steps
29 ListNode* slowPtr = head;
30 ListNode* fastPtr = head->next; // Start fast at head->next to handle even-length lists correctly
31
32 while (fastPtr && fastPtr->next) {
33 slowPtr = slowPtr->next;
34 fastPtr = fastPtr->next->next;
35 }
36
37 // Split the list into two halves
38 ListNode* firstHalf = head;
39 ListNode* secondHalf = slowPtr->next;
40 slowPtr->next = nullptr; // Disconnect the two halves
41
42 // Recursively sort both halves
43 firstHalf = sortList(firstHalf);
44 secondHalf = sortList(secondHalf);
45
46 // Merge the two sorted halves
47 ListNode* dummyHead = new ListNode(0); // Dummy node to simplify merging
48 ListNode* currentTail = dummyHead;
49
50 // Merge nodes from both lists in sorted order
51 while (firstHalf && secondHalf) {
52 if (firstHalf->val <= secondHalf->val) {
53 currentTail->next = firstHalf;
54 firstHalf = firstHalf->next;
55 } else {
56 currentTail->next = secondHalf;
57 secondHalf = secondHalf->next;
58 }
59 currentTail = currentTail->next;
60 }
61
62 // Append remaining nodes from either list
63 currentTail->next = firstHalf ? firstHalf : secondHalf;
64
65 // Return the merged sorted list (skip dummy head)
66 ListNode* sortedHead = dummyHead->next;
67 delete dummyHead; // Clean up dummy node
68 return sortedHead;
69 }
70};
71
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 * val: number
5 * next: ListNode | null
6 * constructor(val?: number, next?: ListNode | null) {
7 * this.val = (val===undefined ? 0 : val)
8 * this.next = (next===undefined ? null : next)
9 * }
10 * }
11 */
12
13/**
14 * Sorts a linked list in ascending order using merge sort algorithm
15 * Time Complexity: O(n log n)
16 * Space Complexity: O(log n) due to recursive call stack
17 *
18 * @param head - The head node of the linked list to be sorted
19 * @returns The head of the sorted linked list
20 */
21function sortList(head: ListNode | null): ListNode | null {
22 // Base case: empty list or single node
23 if (head === null || head.next === null) {
24 return head;
25 }
26
27 // Find the middle of the list using two pointers (slow and fast)
28 // Fast pointer moves twice as fast as slow pointer
29 let slowPointer: ListNode = head;
30 let fastPointer: ListNode | null = head.next;
31
32 // Move pointers until fast reaches the end
33 while (fastPointer !== null && fastPointer.next !== null) {
34 slowPointer = slowPointer.next!;
35 fastPointer = fastPointer.next.next;
36 }
37
38 // Split the list into two halves
39 // First half: from head to slowPointer
40 // Second half: from slowPointer.next to end
41 let firstHalf: ListNode | null = head;
42 let secondHalf: ListNode | null = slowPointer.next;
43
44 // Disconnect the two halves
45 slowPointer.next = null;
46
47 // Recursively sort both halves
48 firstHalf = sortList(firstHalf);
49 secondHalf = sortList(secondHalf);
50
51 // Merge the two sorted halves
52 // Create a dummy node to simplify the merging process
53 const dummyHead: ListNode = new ListNode();
54 let currentTail: ListNode = dummyHead;
55
56 // Merge nodes by comparing values from both lists
57 while (firstHalf !== null && secondHalf !== null) {
58 if (firstHalf.val <= secondHalf.val) {
59 // Attach the smaller node from firstHalf
60 currentTail.next = firstHalf;
61 firstHalf = firstHalf.next;
62 } else {
63 // Attach the smaller node from secondHalf
64 currentTail.next = secondHalf;
65 secondHalf = secondHalf.next;
66 }
67 // Move the tail pointer forward
68 currentTail = currentTail.next;
69 }
70
71 // Attach any remaining nodes from either list
72 currentTail.next = firstHalf ?? secondHalf;
73
74 // Return the merged sorted list (skip dummy head)
75 return dummyHead.next;
76}
77
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm implements merge sort on a linked list. Breaking down the analysis:
- The list is recursively divided into halves
log n
times (similar to binary tree depth) - At each level of recursion, all
n
nodes are processed during the merge operation - The merge operation visits each node once to combine the sorted sublists
- Total operations:
n
nodes processed at each oflog n
levels =O(n × log n)
Space Complexity: O(log n)
The space complexity comes from the recursive call stack:
- The recursion depth is
log n
since we divide the list in half at each step - Each recursive call uses
O(1)
additional space (only a few pointer variables likeslow
,fast
,dummy
,tail
) - No additional data structures are created; the sorting is done in-place by rearranging the existing nodes
- Total space:
O(log n)
for the call stack
The algorithm achieves optimal time complexity for comparison-based sorting while maintaining efficient space usage through in-place merging of the linked list nodes.
Common Pitfalls
1. Incorrect Middle Point Calculation
The Pitfall:
A common mistake is initializing both slow
and fast
pointers at head
:
# INCORRECT
slow = head
fast = head # Wrong initialization
while fast and fast.next:
slow = slow.next
fast = fast.next.next
This causes the split to be uneven. For a list with 2 nodes, slow
would end up at the second node, making l1
contain both nodes and l2
empty, leading to infinite recursion.
The Solution:
Initialize fast
at head.next
:
# CORRECT
slow = head
fast = head.next # Correct initialization
while fast and fast.next:
slow = slow.next
fast = fast.next.next
This ensures slow
stops at the last node of the first half, allowing for a proper split.
2. Forgetting to Break the Connection
The Pitfall:
After identifying the middle point, forgetting to set slow.next = None
:
# INCORRECT
l1 = head
l2 = slow.next
# Missing: slow.next = None
Without breaking the connection, l1
would still point to the entire list, causing incorrect sorting or infinite loops.
The Solution: Always disconnect the two halves:
# CORRECT
l1 = head
l2 = slow.next
slow.next = None # Critical step to separate the lists
3. Using Value Comparison Instead of Node Manipulation
The Pitfall: Creating new nodes instead of reusing existing ones during the merge:
# INCORRECT - Creates new nodes
while l1 and l2:
if l1.val <= l2.val:
tail.next = ListNode(l1.val) # Creating new node
l1 = l1.next
else:
tail.next = ListNode(l2.val) # Creating new node
l2 = l2.next
tail = tail.next
This unnecessarily increases space complexity to O(n) and doesn't actually sort the original list.
The Solution: Reuse existing nodes by adjusting pointers:
# CORRECT - Reuses existing nodes
while l1 and l2:
if l1.val <= l2.val:
tail.next = l1 # Reuse existing node
l1 = l1.next
else:
tail.next = l2 # Reuse existing node
l2 = l2.next
tail = tail.next
4. Edge Case Handling in Base Case
The Pitfall:
Only checking for head is None
without checking head.next is None
:
# INCORRECT if head is None: return head # Missing check for single node
This misses the single-node case, which should also be treated as sorted.
The Solution: Check both empty and single-node cases:
# CORRECT
if head is None or head.next is None:
return head
These pitfalls often lead to infinite recursion, incorrect results, or runtime errors. The key is understanding that we're manipulating pointers to existing nodes rather than creating new structures.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Want a Structured Path to Master System Design Too? Don’t Miss This!