Skip to content

A LeetCode Daily Series — Day 77

Today’s problem is called "Sort List"

Updated: at 12:49 AM

Welcome to another deep dive into algorithmic problem-solving! Today, we are tackling the problem of sorting a linked list in ascending order. We will explore multiple approaches to solve this problem efficiently, including a brute force method, and two optimized solutions using different techniques.

If you’re just joining us, it might be helpful to catch up on the previous entries. Want to see more problem-solving techniques? Follow me here on my blog!

Cover Image

Understanding the Problem

Given the head of a linked list, the task is to return the list sorted in ascending order. Here are some examples to illustrate the problem:

Example 1:

Example 1

Input: head = [4,2,1,3]
Output: [1,2,3,4]

Example 2:

Example 2

Input: head = [-1,5,3,4,0]
Output: [-1,0,3,4,5]

Example 3:

Input: head = []
Output: []

Brute Force Approach

The brute force approach involves extracting the values from the linked list into a Python list, sorting the list using a built-in sort method, and then reconstructing the sorted linked list.

def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
    dummy = ListNode(0)
    dummy.next = head
    curr = dummy.next

    lst = []
    while curr:
        lst.append(curr.val)
        curr = curr.next

    lst.sort()

    curr = dummy.next
    for v in lst:
        curr.val = v
        curr = curr.next

    return dummy.next

Explanation

  1. We create a dummy node to facilitate operations on the linked list.
  2. We traverse the linked list and append all node values to a Python list.
  3. We sort the list using the sort() method.
  4. We traverse the linked list again, updating each node’s value with the sorted values from the list.

Time Complexity: O(n x logn) due to the sorting operation.

Space Complexity: O(n) for storing the values in a list.

Brute Force Approach

Using Efficient Approach (Merge Sort)

Merge Sort is a divide-and-conquer algorithm that works well with linked lists due to their sequential nature. We split the list into halves, sort each half recursively, and then merge the sorted halves.

def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
    if not head or not head.next:
        return head
    left = head
    right = self.getMid(head)
    temp = right.next
    right.next = None
    right = temp

    left = self.sortList(left)
    right = self.sortList(right)

    return self.merge(left, right)

def getMid(self, head: ListNode):
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

def merge(self, left: ListNode, right: ListNode):
    tail = dummy = ListNode()

    while left and right:
        if left.val < right.val:
            tail.next = left
            left = left.next
        else:
            tail.next = right
            right = right.next
        tail = tail.next

    if left:
        tail.next = left
    if right:
        tail.next = right

    return dummy.next

Explanation

  1. Base Case: If the list is empty or has only one node, it is already sorted, so we return it as is.
  2. Splitting: Use the getMid function to find the middle of the list. This function uses a slow and fast pointer technique to find the midpoint.
  3. Dividing: Split the list into two halves at the midpoint. The left half starts from the head, and the right half starts from the node after the midpoint.
  4. Recursion: Recursively sort each half by calling sortList on both halves.
  5. Merging: Merge the two sorted halves using the merge function merge. This function compares the nodes of the two halves and merges them into a single sorted list.

Time Complexity: O(n x logn) for splitting and merging.

Space Complexity: O(logn) due to the recursion stack.

Merge Sort Result

Using Efficient Approach 2 (Bottom-Up Merge Sort)

Bottom-Up Merge Sort iteratively sorts the list by progressively larger sublists, starting with sublists of size one. This avoids recursion and can be more space-efficient.

def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
    if not head or not head.next:
        return head
    # get size
    size = 0
    cur = head
    while cur is not None:
        cur = cur.next
        size += 1
    # split
    step = 1
    dummy = ListNode(None, head)
    while step < size:
        cur, pre = dummy.next, dummy
        while cur is not None:
            # split left half
            l = r = cur
            i = 1
            while i < step and r is not None:
                r = r.next
                i += 1
            if r is not None:
                tmp = r.next
                r.next = None
                r = tmp

            # split right half
            cur = r
            i = 1
            while i < step and cur is not None:
                cur = cur.next
                i += 1
            if cur is not None:
                tmp = cur.next
                cur.next = None
                cur = tmp

            # merge
            while l is not None and r is not None:
                if l.val <= r.val:
                    pre.next, l = l, l.next
                else:
                    pre.next, r = r, r.next
                pre = pre.next
            pre.next = l if l is not None else r
            while (nxt := pre.next) is not None: pre = nxt
        step <<= 1
    return dummy.next

Explanation

  1. Calculate Size: First, we determine the size of the linked list by traversing it from the head to the end.
  2. Iterative Splitting: We use a bottom-up approach to iteratively split the list into sublists of increasing sizes, starting with size one.
    • For each step size, we split the list into pairs of sublists of the current step size.
    • We maintain pointers for the current sublist (cur), the previous node (pre), and the left and right halves (l and r) of each pair.
    • We split the list into two halves and move cur and r pointers accordingly.
  3. Merging Sublists: After splitting, we merge the two sorted halves back together.
    • We compare the nodes from the left and right halves (l and r) and link the smaller node to the merged list.
    • We continue merging until one of the sublists is exhausted, and then link the remaining nodes from the non-exhausted sublist.

Time Complexity: O(n x logn) due to iterative splitting and merging.

Space Complexity: O(1) as it avoids recursion and uses a constant amount of extra space, excluding the input and output list space.

Bottom Up Merge Sort Result

Conclusion

Sorting a linked list efficiently can be achieved through various methods, each with its trade-offs. The brute force approach is simple but not the most efficient. Merge Sort and Bottom-Up Merge Sort provide more optimal solutions, leveraging the structure of the linked list for better performance.

Understanding these methods enhances your problem-solving skills and prepares you for tackling similar challenges in coding interviews and competitions.

Found this helpful? Follow me for more leetcode adventures! Questions? React out via email.

Next Articles in the Series