Top-down merge sort
Problem¶
Implement recursive merge sort for a Python list or linked list.
Key trick¶
Recursively split the input into halves, then stably merge the sorted halves.
Trap¶
Choose from the left on equality for stability; linked lists also use \(O(\log n)\) recursion space.
Why is it interesting?¶
It guarantees stable \(O(n \log n)\) sorting and works especially well with linked lists.
Python solution¶
Top-down merge sort for a Python list¶
class sort_merge_sort_top_down:
def sort(self, nums: list[int]) -> list[int]:
# `arr` is the result; `buf` is reused by every merge.
arr = nums[:]
buf = [0] * len(arr)
self._sort_range(arr, buf, 0, len(arr))
return arr
def _sort_range(
self,
arr: list[int],
buf: list[int],
lo: int,
hi: int,
) -> None:
# Ranges are half-open: `[lo, hi)`.
# Empty and one-element ranges are already sorted.
if hi - lo <= 1:
return
mid = (lo + hi) // 2
# Recursively sort both halves before merging them.
self._sort_range(arr, buf, lo, mid)
self._sort_range(arr, buf, mid, hi)
self._merge(arr, buf, lo, mid, hi)
def _merge(
self,
arr: list[int],
buf: list[int],
lo: int,
mid: int,
hi: int,
) -> None:
# `i` reads the left half and `j` reads the right half.
i = lo
j = mid
k = lo
# Merge the smaller front value into the buffer.
while i < mid and j < hi:
# Choose the left value on equality to preserve stability.
if arr[i] <= arr[j]:
buf[k] = arr[i]
i += 1
else:
buf[k] = arr[j]
j += 1
k += 1
# At most one of these loops copies any values.
while i < mid:
buf[k] = arr[i]
i += 1
k += 1
while j < hi:
buf[k] = arr[j]
j += 1
k += 1
# Copy the merged range back into the main list.
arr[lo:hi] = buf[lo:hi]
Top-down merge sort for a singly linked list¶
class sort_merge_sort_linked_top_down:
def sort(
self,
head: ListNode | None,
) -> ListNode | None:
# Empty and one-node lists are already sorted.
if head is None or head.next is None:
return head
# Find the midpoint with the slow/fast-pointer pattern.
# Starting `fast` one node ahead leaves `slow` at the end
# of the left half.
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Split the list into two independent lists.
r = slow.next
slow.next = None
# Sort each half and merge their nodes.
l = self.sort(head)
r = self.sort(r)
return self._merge_linked(l, r)
def _merge_linked(
self,
l: ListNode | None,
r: ListNode | None,
) -> ListNode | None:
# The dummy node removes special handling for the result's head.
dummy = ListNode()
tail = dummy
while l and r:
# Take from the left on equality to preserve stability.
if l.val <= r.val:
tail.next = l
l = l.next
else:
tail.next = r
r = r.next
tail = tail.next
# The remaining list is already sorted, so attach it directly.
tail.next = l if l else r
return dummy.next