Problem Statement in English

You’re given a list of linked lists. The elements of each linked list are sorted in ascending order.

Your task is to merge all the linked lists into one sorted (in ascending order) linked list and return the head to that list.


Approach

The naive approach would be to just iterate over linked list in the main list, which would have a time complexity of $O(n^2)$. It’s basically iterating over a matrix at that point. This is the bruteforce approach.

Try this and you’ll get a TLE (Time Limit Exceeded). How do I know? I tried it. Hehe.

We need something smarter. Enter Merge Sort. That’s it.

    graph TD
    A((1,2,3,4)) --> B((1,2))
    A --> C((3,4))

    B --> D((1))
    B --> E((2))

    C --> F((3))
    C --> G((4))

Now, why is this better? It’s classic divide and conquer.

The recursion relation for this is given by: $2T(\frac{n}{2}) + O(n)$ , which is of the form $aT(\frac{n}{b})+ f(x)$, where:

  • a is the number of problems each stage is divided into
  • b is the size of each sub problem
  • f(x) is the cost of combining the solutions to 2 such problems

On solving this you get a time complexity of $O(nlog(n))$. Which is a significant improvement from $O(n^2)$.


Solution in Python


class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        # edge case
        if len(lists) == 0:
            return None

        # while there are lists to merge, keep merging
        while len(lists) > 1:
            # this will hold merged lists
            tempList = []
            # we merge in steps of 2
            # for example, index 0 and 1 are merged,
            # then the next indices we merge are 2 and 3
            for i in range(0, len(lists), 2):
                # if there's an index after this one, we merge
                if i + 1 < len(lists):
                    # merge 2 lists
                    v = self.merge2Lists(lists[i], lists[i + 1])
                    # if the new head exists, add it else skip
                    if v is not None:
                        tempList.append(v)
                # else we can't merge this index with anything, so add it as is
                else:
                    tempList.append(lists[i])
            # our next iteration needs to merge these merged lists
            lists = tempList

        # if there's a final merged list, return that, otherwise just return an empty list
        return lists[0] if len(lists) is not 0 else []

    # helper function to merge 2 linked lists and return the new head
    def merge2Lists(self, l1: ListNode, l2: ListNode) -> ListNode | None:
        # edge cases
        # if both lists are empty, return empty
        if l1 is None and l2 is None:
            return None
        # is list 1 is empty, then list 2 should be returned
        elif l1 is None:
            return l2
        # is list 2 is empty, then list 1 should be returned
        elif l2 is None:
            return l1

        # pointers p1 and p2 for list 1 and list 2 respectively
        p1 = l1
        p2 = l2

        # this will serve as the head of the merged list
        p3 = None

        # this little bit decides which list's first element becomes the head of the merged list
        if p1.val < p2.val:
            p3 = p1
            p1 = p1.next
        else:
            p3 = p2
            p2 = p2.next

        head = p3

        # while both lists have elements in them, keep merging
        while p1 is not None and p2 is not None:
            if p1.val < p2.val:
                p3.next = p1
                p1 = p1.next
            else:
                p3.next = p2
                p2 = p2.next

            p3 = p3.next

        # now either only l1 or l2 has elements in it
        # all we need to do is append it to the merged list

        # if l1 has elements left, but l2 does not
        # just dump the remaining contents of l1 into the merged list
        while p1 is not None:
            p3.next = p1
            p1 = p1.next
            p3 = p3.next

        # if l2 has elements left, but l1 does not
        # just dump the remaining contents of l2 into the merged list
        while p2 is not None:
            p3.next = p2
            p2 = p2.next
            p3 = p3.next

        # return the head of the merged list
        return head

Complexity

  • Time: $O(nlog(n))$

  • Space: $O()$


Mistakes I Made

As I said, I tried to brute force it.


And we are done.