LeetCode #148 Medium

Sort List

Given the head of a linked list, sort it in ascending order in O(n log n) time and O(1) (or O(log n)) space.

Constraints
  • The number of nodes in the list is in the range [0, 5 * 10⁴].
  • -10⁵ <= Node.val <= 10⁵
linked-listsortingmerge-sort
Open on LeetCode ↗
02

Intuition

Sort list leetcode problem 148 sorts a linked list in O(n log n) time with constant extra space. Those two requirements together decide the algorithm. Quicksort is poor here — a linked list has no random access, so choosing a pivot and partitioning costs far more than on an array, and the worst case is O(n²). Merge sort is the natural fit: - Merge sort needs only sequential access and merges without extra storage, since linked nodes can be relinked rather than copied. On an array, merging requires a temporary buffer. On a list it does not, which is the one case where linked structures beat arrays outright. The algorithm splits the list at the middle, sorts each half recursively, and merges the sorted halves. Finding the middle uses the two-speed walk. The critical step afterwards is severing the halves — setting the first half's tail to null. Leaving them connected means the recursion never shrinks the input and the sort loops forever. The base case is a list of zero or one nodes, which is already sorted. Missing it produces infinite recursion on the smallest inputs. Merging two sorted lists is the standard routine, cleanest with a dummy head so every append is uniform, and the remainder attached in one assignment. Strictly, top-down merge sort uses O(log n) stack space rather than O(1). The bottom-up variant merges sublists of size 1, 2, 4, and so on iteratively, achieving genuine O(1) space — worth knowing when the constraint is taken literally. Sorting gives O(n log n) time with O(log n) stack space, or O(1) bottom-up.

How to spot this pattern

When asked to sort a linked list in O(n log n), merge sort is the go-to. The two core operations — find midpoint (slow/fast pointers) and merge two sorted lists — are standard linked list primitives. Quicksort is harder on linked lists because random-access pivoting is expensive. If the problem requires O(1) space, bottom-up merge sort avoids the O(log n) recursion stack.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask which pointers must be saved before you overwrite anything. Aim for O(n log n) time and O(log n) space.

1

Choose merge sort deliberately

A linked list has no random access, so quicksort's partitioning is costly and its worst case O(n²). Merge sort needs only sequential access.

2

Note the merging advantage

Merging linked nodes needs no temporary buffer — nodes are relinked rather than copied. This is the one case where lists beat arrays outright.

3

Handle the base case

A list of zero or one nodes is already sorted. Omitting this causes infinite recursion on the smallest inputs.

4

Split at the middle

Use the two-speed walk to locate the midpoint, then divide the list into two halves for independent sorting.

5

Sever the two halves

Set the first half's tail to null. Leaving them connected means the recursion never shrinks the input and the sort never terminates.

6

Merge with a dummy head

Compare heads, take the smaller, and attach the remainder in one assignment. The dummy makes every append uniform.

7

Cost of the sort

O(n log n) time with O(log n) stack space top-down. The bottom-up variant merges sublists iteratively for genuine O(1) space.

04

Solution & live demo

▶1class Solution:
▶2 def sortList(self, head):
▶3 if not head or not head.next:
▶4 return head
▶5 prev = None
▶6 slow = head
▶7 fast = head
▶8 while fast and fast.next:
▶9 prev = slow
▶10 slow = slow.next
▶11 fast = fast.next.next
▶12 prev.next = None
▶13 left = self.sortList(head)
▶14 right = self.sortList(slow)
▶15 return self.merge(left, right)
▶16 
▶17 def merge(self, l1, l2):
▶18 dummy = ListNode(0)
▶19 tail = dummy
▶20 while l1 and l2:
▶21 if l1.val <= l2.val:
▶22 tail.next = l1
▶23 l1 = l1.next
▶24 else:
▶25 tail.next = l2
▶26 l2 = l2.next
▶27 tail = tail.next
▶28 tail.next = l1 if l1 else l2
▶29 return dummy.next
05

Common pitfalls

Not cutting the list at the midpoint before recursing

✗ Wrong
mid = slow
return merge(sortList(head), sortList(mid))
✓ Right
prev.next = None
return merge(sortList(head), sortList(slow))

Without cutting, the left half still points to the right half. The left recursive call processes the entire list, causing infinite recursion.

Finding the wrong midpoint for a two-element list

✗ Wrong
while fast and fast.next:
    slow = slow.next
    fast = fast.next.next
✓ Right
while fast.next and fast.next.next:
    slow = slow.next
    fast = fast.next.next

For a two-element list A -> B, the first version leaves slow at B (the second node), making the left half the entire list and the right half empty — infinite recursion. The second version keeps slow at A, splitting into [A] and [B].

Not handling the base case for a single node

✗ Wrong
if not head:
    return head
✓ Right
if not head or not head.next:
    return head

A single node has no pair to split into. Without the not head.next check, the function tries to find a midpoint of a one-element list, and depending on the splitting logic, may recurse infinitely.

06

Edge cases

Empty list

Base case returns None immediately.

Single node

Base case returns the node as-is. No splitting or merging.

Already sorted list

Merge sort still splits and merges, but the merge step degenerates to appending one half after the other. Same O(n log n) time.

07

Complexity

Time
O(n log n)
Space
O(log n)
O(log n) for the recursion stack. The merge is done in-place by relinking nodes.