Jc-alt logo
jc
data structures and algorithms

LeetCode: Two Pointers II Linked List

LeetCode: Two Pointers II Linked List
115 min read
#data structures and algorithms
Table Of Contents

Linked List Intro:

Leetcode problems with elegant solutions using a linked list.

What is a Linked List

A linked list is a linear data structure where each element (node) points to the next. It does not offer direct index access like arrays, as all traversal is pointer based.

Structure of a Linked List

Every linked list falls into one of two structural forms:

  1. Linear (acyclic)
  • Nodes form a straight path that ends at None
  • No loops or repeated visits
    A → B → C → D → None
  1. Path + Cycle (Cyclic)
  • Initial path of nodes eventually linked back to an earlier node, forming a cycle.
  • Structure: Path (μ nodes) → Cycle (λ nodes repeating forever)
    A → B → C → D → E  
            ↑       ↓  
            H ← G ← F

Overall: Linked lists are either just a path or a path that leads to a cycle

Why Use a Linked List

Linked lists are ideal for:

  • Memory efficient manipulation (no resizing like arrays)
  • Representing dynamic data structures (stacks, queues, etc)
  • Insertions and deletions in constant O(1) time (if the node is known)

Linked List Application: Linear Traversal

We can traverse a linked list node by node using a single pointer. Commonly used for printing, searching, summing values, etc.

Ex: Count number of nodes in the list

    def countNodes(head):
        count = 0
        curr = head
        
        while curr: 
            count += 1
            curr = curr.next

        return count

Linked List Application: Dummy Head Trick

Using a dummy (sentinel) node simplifies edge cases in linked list operations, especially when manipulating the head node. Dummy node precedes the head, providing the uniform way to handle deletions, insertions, or merges without adding logic to handle the head.

Ex: Remove nth node from end of a list using dummy head

def removeNthFromEnd(head: Optional[ListNode], n: int) -> Optional[ListNode]:
    
    # dummy (sentinel) node used to point to the head
    dummy = ListNode(0, head)
    fast = slow = dummy

    # Move fast pointer n+1 steps ahead to keep gap
    for _ in range(n + 1):
        fast = fast.next

    # Move both pointers until fast reaches end
    while fast:
        fast = fast.next
        slow = slow.next

    # Remove the nth node (slow.next)
    slow.next = slow.next.next

    # Return head dummy is pointing to (may be different from original)
    return dummy.next 

Linked List Application: Tortoise and Hare

We can represent fast and slow pointers to traverse the list. Useful for splitting lists in half for sorting or palindrome checks and for checking for cycles.

Ex: Return middle node in list

    def findMiddle(head):
        slow = fast = head

        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        return slow

    # If F == None: Even length list
    # S points to start of right half
    # Left and right halves have equal length

    # If F != None: Odd length list
    # S points to middle node (belongs to left half) 
    # Left half is longer by 1 element

    # None                              S: None
    # S^F^                              F: None

    # 0     None                        S: 0
    # S^F^                              F: 0

    # 0   1   None                      S: 1
    #     S^  F^                        F: None

    # 0   1   2   None                  S: 1
    #     S^  F^                        F: 2

    # 0   1   2   3   None              S: 2
    #         S^      F^                F: None

    # 0   1   2   3   4   None          S: 2
    #         S^      F^                F: 4

    # 0   1   2   3   4   5   None      S: 3
    #             S^          F^        F: None

    # 0   1   2   3   4   5   6   None  S: 3
    #             S^          F^        F: 6

Linked List Application: In Place Modification

Iteratively manipulate linked list during traversal for O(1) time and space.

Ex: Reverse the entire list and return a new head

    def reverseList(head):
        prev = None
        curr = head

        while curr:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node

        return prev

206. Reverse Linked List ::4:: - Easy

Topics: Linked List, Recursion

Intro

Given the head of a singly linked list, reverse the list, and return the reversed list. A linked list can be reversed either iteratively or recursively. Could you implement both?

Example InputOutput
head = [1,2,3,4,5][5,4,3,2,1]
head = [1,2][2,1]
head = [][]

Constraints:

The number of nodes in the list is the range [0, 5000].

-5000 ≤ Node.val ≤ 5000

Abstraction

Given a linked list, reverse it iteratively and recursively.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive Linked List Reversal - Linked List/Simple Traversal

    def reverseList(self, currNode: Optional[ListNode]) -> Optional[ListNode]:
        
        # Recursive Linked Lists:
        # A linked list is made up of nodes which point to other nodes
        # They must have pointers and they may or may not have values:
        #
        #   () -> () -> () -> None
        #   (1) -> (3) -> (7) -> None

        # Reverse Linked List:
        #   - Update parents to point to their children
        #   - Original head points to None (becoming a tail)
        #   - Return the original tail representing the start of the list (becoming a head)
        #
        #           (1) -> (3) -> (7) -> None
        #   None <- (1) <- (3) <- (7)


        # Empty Case:
        #   - Nothing to reverse, just return 'None'
        if currNode == None:
            return currNode

        # Base Case: 
        #   - Reached 'None' of original list
        #   - CurrHead is at the original tail, which will become the new head, 
        #   - Begin passing original tail back up the recursive call
        # tc: O(1)
        if currNode.next == None:
            return currNode

        # Recursion: 
        # call reverse list on the rest of the list (head.next to end), 
        # and eventually the base case starts to pass back the tail (new head)
        # Recursive call stack is n times so O(n) space and time
        # tc: O(n)
        # sc: O(n)
        newHead = self.reverseList(currNode.next)


        # -----------------
        # Reverse order next and curr node
        
        # Point next node to current node 
        currNode.next.next = currNode

        # Point curr node to 'None' (temporarily act as a tail)
        currNode.next = None

        # overall: tc O(n)
        # overall: sc O(n)
        return newHead
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: Recursive Using Not Linked List Reversal - Linked List/Simple Traversal

    def reverseList(self, currNode: Optional[ListNode]) -> Optional[ListNode]:

        # Reverse Linked List:
        #   - Update parents to point to their children
        #   - Original head points to None (becoming a tail)
        #   - Return the original tail representing the start of the list (becoming a head)
        #   (1) -> (3) -> (7) -> None
        #   None <- (1) <- (3) <- (7)

        # See Solution 1 Notes:
        # Same as previous, just simplified with 'not'
        # In python 'None' is considered 'falsy', 
        # meaning it behaves like False in conditional statements such as this one
        
        # Empty Case:
        #   - Nothing to reverse, just return 'None'
        if not currNode:
            return currNode

        # Base Case: 
        #   - Reached 'None' of original list
        #   - CurrHead is at the original tail, which will become the new head, 
        #   - Begin passing original tail back up the recursive call
        # tc: O(1)
        if not currNode.next:
            return currNode

        # reverse the rest of the list (.next onwards)
        newHead = self.reverseList(currNode.next)
        
        next = currNode.next
        next.next = currNode

        currNode.next = None

        # overall: tc O(n)
        # overall: sc O(n)
        return newHead 
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 3: Iterative Linked List Reversal - Linked List/Simple Traversal

    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        
        # Reverse Linked List:
        #   - Update parents to point to their children
        #   - Original head points to None (becoming a tail)
        #   - Return the original tail representing the start of the list (becoming a head)
        #   (1) -> (3) -> (7) -> None
        #   None <- (1) <- (3) <- (7)

        # Creating a 'None' to act as the end of the list,
        # so the new tail can point at it
        prev = None

        # Curr will point to the previous element
        curr = head

        # Iterate until we have reached the end of the list 
        # (remember the tail points to None):
        #   head -> () -> () -> tail -> None
        # tc: iterate over n O(n)
        while curr != None:

            # Grab next node before disconnecting from current node
            nextNode = curr.next  

            # Reverse/disconnect current element, 
            # and point to previous node: 
            curr.next = prev      

            # Before reverse:
            #      <- prev    curr -> next -> ()

            # After reverse:
            #      <- prev <- curr    next -> ()

            # Iterate both nodes: prev and curr
            prev = curr
            curr = nextNode

            # Before iteration:
            #     <- prev <- curr    next -> ()

            # After iteration:
            #     <- ()   <- prev    curr -> ()
     

        # Loop exits when curr == None:
        
        # Before exiting:
        #       () -> tail -> None

        # After exiting:
        #       () <- prev -> curr

        # curr is at 'None'
        # prev is at the original tail / the new head
        newHead = prev

        # overall: tc O(n)
        # overall: sc O(1)
        return newHead  
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 4: Iterative Using While Linked List Reversal - Linked List/Simple Traversal

    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        
        # Reverse Linked List:
        #   - Update parents to point to their children
        #   - Original head points to None (becoming a tail)
        #   - Return the original tail representing the start of the list (becoming a head)
        #   (1) -> (3) -> (7) -> None
        #   None <- (1) <- (3) <- (7)

        prev = None
        curr = head

        # See Solution 3 Notes:
        # Same as previous, just simplified with 'not'
        # In python 'None' is considered 'falsy', 
        # meaning it behaves like False in conditional statements such as this one
        while curr:

            nextNode = curr.next  

            curr.next = prev      

            prev = curr            
            curr = nextNode       

        # overall: tc O(n)
        # overall: sc O(1)
        return prev  
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

21. Merge Two Sorted Lists ::2:: - Easy

Topics: Linked List, Recursion

Intro

You are given the heads of two sorted linked lists list1 and list2. Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists. Return the head of the merged linked list.

Example InputOutput
list1 = [1,2,4], list2 = [1,3,4][1,1,2,3,4,4]
list1 = [], list2 = [][]
list1 = [], list2 = [0][0]

Constraints:

The number of nodes in the list is the range [0, 50].

-100 ≤ Node.val ≤ 100

Both list1 and list2 are sorted in non-decreasing order.

Abstraction

Given two sorted linked lists, merge and return head of merged list.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive Linked List Merging - Linked List/Simple Traversal

    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:

        # Merge Linked Lists: 
        #   - Compare two current heads of lists, grab smaller one
        #   - Iterate the head we just grabbed
        #   - When one list runs out of items, link the rest of the non empty list

        
        # List1 and List2:
        #   (1) -> (3) -> (7) -> None
        #   (2) -> (9) -> (10) -> None

        # MergedList:
        #   (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None
      
        # Base case: 
        # One of the lists is empty, 
        # return remaining portion of the non empty list up the recursion call
        if not list1:
            return list2
        if not list2:
            return list1

        # Compare two current heads of lists, grab smaller one:
        if list1.val < list2.val:

            # Grab list1 node,
            # iterate the head we just grabbed by passing the next node to the recursive call,
            # and connect list1 node by pointing to the rest of the merged list
            list1.next = self.mergeTwoLists(list1.next, list2)
            
            # return merged list1 node
            return list1
        
        else:

            # Grab list2 node,
            # iterate the head we just grabbed by passing the next node to the recursive call,
            # and connect list2 node by pointing to the rest of the merged list
            list2.next = self.mergeTwoLists(list1, list2.next)

            # return merged list2 node
            return list2

        # overall: tc O(n + m)
        # overall: sc O(n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: Iterative DummyHead Linked List Merging [SC Opt] - Linked List/Simple Traversal

    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        
        # Merge Linked Lists: 
        #   - Iterate over both lists at the same time
        #   - Compare two current heads of lists, grab smaller one
        #   - When one list runs out of items, link the rest of the non empty list
        
        # List1 and List2:
        #   (1) -> (3) -> (7) -> None
        #   (2) -> (9) -> (10) -> None

        # MergedList:
        #   (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None

        # DummyHead:
        #   - Allows entering the iterative loop cleanly
        #     without handling first node assigning case
        #   - Always points at original head
        # sc: O(1)
        dummyHead = ListNode(-1)

        # Linked List Iterator:
        # sc: O(1)
        curr = dummyHead

        # Traverse both lists while both have nodes
        # tc: O(m + n)
        while list1 and list2:

            # grab smaller value between the two and iterate that list
            if list1.val < list2.val:

                # Grab list1 node
                # iterate list1 (new head becomes next node) 
                curr.next = list1
                list1 = list1.next

            else:

                # Grab list2 node
                # iterate list2 (new head becomes next node) 
                curr.next = list2
                list2 = list2.next    

            # iterate merged list
            curr = curr.next          

        # One list is empty,
        # connect remaining portion of non-empty list
        if list1:
            curr.next = list1
        else:
            curr.next = list2

        # Originally:
        #   dh      lst1 -> () -> ...
        #  curr     lst2 -> () -> ...

        # After:
        #   dh  ->  merged -> () -> ...

        # dummyHead:
        # dh is still pointing to original head of the merged list
        newHead = dummyHead.next

        # overall: tc O(m + n)
        # overall: sc O(1)
        return newHead
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

141. Linked List Cycle ::2:: - Easy

Topics: Hash Table, Linked List, Two Pointers

Intro

Given head, the head of a linked list, determine if the linked list has a cycle in it. There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail's next pointer is connected to. Note that pos is not passed as a parameter. Return true if there is a cycle in the linked list. Otherwise, return false. Follow up: Can you solve it using O(1) (i.e. constant) memory?

Example InputOutput
head = [1,2,3,4,5][5,4,3,2,1]
head = [1,2][2,1]
head = [][]

Constraints:

The number of nodes in the list is the range [0, 5000].

-5000 ≤ Node.val ≤ 5000

Abstraction

Given a linked list, determine if there exists a cycle.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Track Seen Notes In Set - Linked List/Simple Traversal

    def hasCycle(self, head: Optional[ListNode]) -> bool:
        
        # Linked List Cycles:
        #
        #   (1) -> (3) -> (7) -> (9)
        #                  ^      |     (cycle)
        #                  |      v
        #                 (11)<- (10)


        # Linked List Cycle Validation:
        # To check if a linked list, we simply iterate over the list
        # while keeping a set of nodes we have seen,
        # and if we try to add a node we have already seen, a cycle exists
        
        # Nodes stored by obj reference:
        # sc: O(n)
        seen = set()

        # Linked List Iteration Representation:
        # sc: O(1)
        curr = head

        # Iterate until we hit the 'None' at the end of the list
        # tc: iterate over list O(n)
        while curr:

            # Check: is current node in seen list
            # Implies: we have passed through as cycle
            if curr in seen:
                return True

            # Implies: have not seen current node
            # Then: add to seen list, continue iteration
            seen.add(curr)
            
            # Iterate
            curr = curr.next
        
        # Implies: have reached the end of list, no node was seen twice
        # Then: no cycles exist

        # overall: tc O(n)
        # overall: sc O(n)
        return False
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] Floyd Cycle Detection Tortoise and Hare [SC Opt] - Linked List/Simple Traversal

    def hasCycle(self, head: Optional[ListNode]) -> bool:
        
        # Floyd Cycle Detection Algorithm (Tortoise And Hare):

        # Iterate linked list using 2 pointers:
        #   - fast: a pointer going 2x speed (hare)
        #   - slow: a pointer going 1x speed (tortoise)

        # Cycle Validation:
        #   - Cycle exists:
        #       fast pointer hit the cycle and catches up with slow
        #   - No cycle Exists:
        #       fast pointer exits linked list and never catches up
        #       with slow

        # Linked List Cycle:
        #
        #   (1) -> (3) -> (7) -> (9)
        #                  ^      |     (cycle)
        #                  |      v
        #                 (11)<- (10)


        # Floyd Iterators:
        # sc: O(1)
        slow, fast = head, head
        
        # Iterate until either:
        #   - fast hits the 'None' at the end of the list
        #   - fast hits the slow pointer
        # tc: iterate over n O(n) 
        while fast and fast.next:

            # Iterate slow 1x and fast 2x
            slow = slow.next
            fast = fast.next.next
            
            # Check: if fast hit slow
            # Implies: fast passed through a cycle
            if slow == fast:
                return True

        # Implies: fast has reached end of list, 
        #          never caught up with slow, no cycle exists in list
        
        # overall: tc O(n)
        # overall: sc O(1)
        return False
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

287. Find the Duplicate Number ::3:: - Medium

Topics: Array, Two Pointers, Binary Search, Bit Manipulation

Intro

Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive. There is only one repeated number in nums, return
this repeated number. You must solve the problem without modifying the array nums and using only constant extra space.

Example InputOutput
nums = [1,3,4,2,2]2
nums = [3,1,3,4,2]3
nums = [3,3,3,3,3]3

Constraints:

1 ≤ n ≤ 105

nums.length == n + 1

1 ≤ nums[i] ≤ n

All the integers in nums appear only once except for precisely one integer which appears two or more times.

Abstraction

Given list, return which number is a duplicate.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug: While True instead of While slow != fast

    def findDuplicate(self, nums: List[int]) -> int:

        slow = nums[0]
        fast = nums[0]

        while True:
            slow = nums[slow]
            fast = nums[nums[fast]]
            if slow == fast:
                break

        slow = nums[0]

        # INCORRECT:
        # we are taking a step before the while loop check
        # (which in some edge cases will get messy)
        # Instead:
        # while True:
        #    if slow == fast:
        #        break
        #    slow = nums[slow]
        #    fast = nums[fast]
        while True:
            slow = nums[slow]
            fast = nums[fast]
            if slow == fast:
                break

        return slow

Solution 1: Max Bit Length Duplicate Flips - Linked List/Simple Traversal

    def findDuplicate(self, nums: List[int]) -> int:
        
        # Note:
        # 1. Idea: Compare bit counts between full nums list and range [1..n]
        # 2. Duplicate number will cause extra set bits in some positions
        # 3. Identify bits where counts mismatch, reconstruct duplicate from bits

        n = len(nums) - 1
        bit_reconstruction = 0

        # max_bits = how many binary bits needed to represent the largest number
        # determines how many bit positions we need to check
        # [1, 3, 4, 2, 2] -> 4 bit_length is 3 bits (100 binary) 
        # we will check bit positions 0, 1, 2
        max_bit = max(num.bit_length() for num in nums)

        # building duplicate number by iterating over each available bit position
        # bit = 0 -> mask = 1 (binary 001)
        # bit = 1 -> mask = 2 (binary 010)
        # bit = 2 -> mask = 3 (binary 100)
        for bit in range(max_bit):

            # current mask
            # 0 -> 1 << 0 = 0001
            # 1 -> 1 << 1 = 0010
            # 2 -> 1 << 2 = 0100
            # 3 -> 1 << 3 = 1000
            mask = 1 << bit

            # for each num in array, count if bit is set
            # &: bitwise AND
            # sets current bit to 1 if both mask and bit are set to 1
            # else sets current bit to 0,
            # determines if current num has the bit set
            curr_bit_set_count = sum((num & mask) > 0 for num in nums)

            # across expected range [1..n], sum if bit is expected to be set
            # &: bitwise AND
            # checks how many numbers within range [1..n]
            # are expected to have current bit set.
            expected_bit_set_count = sum((i & mask) > 0 for i in range(1, n + 1))

            # compare bit set to expected bit set,
            # if bit is set more times than expected
            # duplicate number must have that bit set:
            # add it to bit reconstruction
            if curr_bit_set_count > expected_bit_set_count:
                bit_reconstruction |= mask
        
        # overall: time complexity O(n log n)
        # overall: space complexity O(1)
        return bit_reconstruction
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Bit CountingO(n log n)O(1)log n bit positions, O(n) scan per bitOnly counters and bitmask
Max Bit CalcO(n)O(1)Iterate to determine max bitsNo additional memory allocation for iteration O(1)
OverallO(n log n)O(1)Iteration for log n each with n dominates, leading to O(n log n)No additional memory allocation, leading to constant O(1)

Solution 2: Pigeonhole Over Range 1 to N Then Count Array Binary Search - Linked List/Simple Traversal

    def findDuplicate(self, nums: List[int]) -> int:

        # Note:
        # Idea: Duplicate number must satisfy pigeonhole principle:
        #   With n+1 numbers range [1..n], at least one value is duplicated
        #  (n containers, n+1 pigeons -> at least one container has two pigeons)
        
        # 1. Binary search on value range [1..n]
        # 2. For mid value:
        #    mid = number of containers in lower half (including mid)
        #    count = numbers of pigeons (nums <= mid) in lower half
        # 3. Decision:
        #   If count <= mid -> equal or lesser pigeons to containers -> duplicate is in upper half
        #   Else count > mid -> more pigeons than containers -> duplicate is in lower half

        # smallest value 1
        # largest value n (but we have n+1) n+1 - 1 -> n (the largest value)
        left, right = 1, len(nums) - 1

        # [ 1 4 3 5 2 ], for some n,
        # there must be n-1 numbers below:
        # for 2, there is 1 number below
        # for 5, there are 4 numbers below
        # Binary optimization search: '<'
        while left < right:
            
            mid = (left + right) // 2

            # count how many numbers <= mid
            curr_count = sum(num <= mid for num in nums)

            # if count <= mid, there are no extra numbers in lower half:
            # duplicate must be in upper half
            if curr_count <= mid:
                left = mid + 1

            # if count > mid, there are too many numbers in lower half:
            # duplicate must be in lower half
            else:
                right = mid

        # Loop exit: left == right
        # left and right are pointing at duplicate number

        # overall: time complexity O(n log n)
        # overall: space complexity O(1)
        return left
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Binary SearchO(log n)O(1)Binary search in O(log n)No additional memory allocation for binary search
CountingO(n)O(1)Iterate over list of n lengthNo additional memory allocation for iteration
OverallO(n log n)O(1)Binary search O(log n) each with iteration of O(n) dominates, leading to O(n log n)No additional memory allocation O(1)

Solution 3: [Follow up] [Linked List] 3 Step Modified Floyd Cycle FindMeeting() To ResetSlow() To FindCycleStart() [SC Opt] - Linked List/Simple Traversal

    def findDuplicate(self, nums: List[int]) -> int:

        # ----------------------------------------------------------------------------------
        # Linked List Find Cycle Start Algorithm Proof:

        # 1. Variable Definitions:
        #   L = distance from head to start of cycle
        #   C = length of the cycle (number of nodes in the cycle)
        #   x = distance from the start of the cycle to the meeting point inside the cycle
        #   k = number of full cycles the fast pointer has completed by the time of the first meeting

        # 2. Setup:
        #   Iteration 1: Slow + Fast Until They Meet
        #   Slow Reset:  After Slow + Fast Meet, 
        #                immediately set Slow back to head,
        #                then run again until Slow and Fast meet again
        #   Iteration 2: After the reset, 
        #                Slow and Fast are guaranteed to meet at the cycle start
        
        # -----------------------------------------
        # Solving For L Variable Definitions: 
        
        #   Distance Traveled:
        #           L + x + (k * C) steps
        #   Where:
        #           L steps to reach the start of the cycle
        #           x steps inside the cycle to the meeting point
        #           k full extra loops around the cycle (each length C)

        # Solving For L:

        #   Total Distance Traveled To Meeting Point:

        #     Distance = Distance
        #     Fast Pointer Distance = Slow Pointer Distance
        #     L + x + (k * C)       = 2(L + x)
        
        # Solve for L:
        #     L + x + kC = 2L + 2x
        #             kX = L + x
        #              L = kC - x 

        # -----------------------------------------
        # Cycle Wrap around Proof
        # Traveling x steps inside cycle of length C =
        # to traveling C - x steps backwards, due to wrap around
        # x steps forward = C - x backwards
        # so:
        #   x = C - x

        # -----------------------------------------
        # Plugging In L Variable Definition and Wrap Around Proof: 
        
        #    L = kC - x
        #    L = kC + (C - x)

        #    L = kC + (C - x)   <-- this proves moving slow to right will cover

        # once we move slow to the head:
        # L -> distance from head to start of cycle

        # fast will stay at the meeting point:
        # kC + (C - x)

        # kC -> number of cycles (we can ignore this)
        # (C - x) -> steps remaining to get to the start (since we have traveled x steps already)

        # L = (C - x)
        # so both slow and fast will travel the same amount of steps 
        # to get to the start of the cycle


        # -----------------------------------------
        # Treating The Array Like A Linked List With A Cycle:

        # Cycle:
        # since the list only has 1 duplicate (which may appear multiple times), 
        # that means a cycle exists.

        # Array: value: [3, 1, 3, 4, 2]
        #        index:  0  1  2  3  4


        # Treat array as linked list: index -> nums[index]
        #    Problem set up with duplicate guarantees cycle in linked list representation
        
        # Array (Linked List) With Cycle (Horizontal):
        #
        #       value:   3        4        2        3        4        2
        #       index:  [0]  ->  [3]  ->  [4]  ->  [2]  ->  [3]  ->  [4] ...
        
        # Array (Linked List) With Cycle (Vertical + Horizontal):
        #
        # value:   3        4        2        3   
        # index:  [0]  ->  [3]  ->  [4]  ->  [2] 
        #                   ^                 ^
        #                   |                 |
        #                   -------------------
        #                         cycle

        # nums[0]=3  ->  jump to index 3
        # nums[3]=4  ->  jump to index 4
        # nums[4]=2  ->  jump to index 2
        # nums[2]=3  ->  jump to index 3   <= We've been here already,
        #                                     We've hit index 3 twice, cycle!
        
        # Per this diagram:
        #           The goal is then to find the start of the cycle,
        #           which starts at the duplicate number,
        #           in this case the duplicate value of 3 at index 0 and 2
        #           with the start of the cycle being at index 3


        # ----------------------------------------------------------------------------------
        # The 3 Step Modified Floyd Cycle Algorithm

        # -----------------------------------------
        # Iteration 1:

        # Ran Slow and Fast normally at x1 and x2 respectively, 
        # until they meet somewhere in the linked list

        # Slow Pointer:
        #   Distance Traveled:
        #           L + x steps
        #   Where:
        #           L steps to reach the start of the cycle
        #           x steps inside the cycle to the meeting pointer


        # -----------------------------------------
        # Iteration 1: First Meeting Point

        # Linked List Iterators:
        # sc: O(1)
        slow = nums[0]
        fast = nums[0]

        # Iterate Slow and Fast at x1 and x2 respectively so they meet at: 
        
        #        Slow                       Fast
        #     L + x + (k * C) steps      = 2(L + x) steps

        # steps into the cycle where:
        #   L steps to reach the start of the cycle
        #   x steps inside the cycle to the meeting point
        #   k full extra loops around the cycle (each length C)        
        while True:
            slow = nums[slow]
            fast = nums[nums[fast]]
            if slow == fast:
                break

        # -----------------------------------------
        # Reset Slow: Set Back To Head

        # Reset slow to head to prepare for iteration 2
        slow = nums[0]


        # -----------------------------------------
        # Iteration 2: Second Meeting Point

        # Fast and slow will meet at start of cycle, 
        # which starts at some instance of the duplicate number
        # (duplicate number may appear more than twice)
        while True:
            if slow == fast:
                break
            slow = nums[slow]
            fast = nums[fast]

        # overall: tc O(n)
        # overall: sc O(1)
        return slow
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Cycle DetectionO(n)O(1)Each pointer moves less than or equal to 2n O(n)No additional memory allocation for iteration O(1)
Finding EntryO(n)O(1)Both pointers move less than or equal to n steps O(n)No additional memory allocation for iteration O(1)
OverallO(n)O(1)Two iterations over list of n length dominate, leading to O(n)No additional memory allocation, leading to O(1)

83. Remove Duplicates from Sorted List ::2:: - Easy

Topics: Linked List

Intro

Given the head of a sorted linked list, delete all duplicates such that each element appears only once. Return the linked list sorted as well.

Example InputOutput
head = [1,1,2][1,2]
head = [1,1,2,3,3][1,2,3]

Constraints:

The number of nodes in the list is in the range [0, 300].

-100 ≤ Node.val ≤ 100

The list is guaranteed to be sorted in ascending order.

Abstraction

Given a linked list, remove duplicates and return original.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Iterative In Place Traversal - Linked List/Simple Traversal

    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        
        # Note:
        # 1. List is already sorted, so duplicates are always adjacent
        # 2. Walk the list, whenever curr.val == curr.next.val, skip curr.next
        # 3. Otherwise advance curr
        # No dummy node needed since we never remove the head

        curr = head

        while curr and curr.next:
            if curr.val == curr.next.val:
                # skip duplicate
                curr.next = curr.next.next
            else:
                # no duplicate, advance
                curr = curr.next

        return head

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

82. Remove Duplicates from Sorted List II ::2:: - Medium

Topics: Linked List, Two Pointers

Intro

Given the head of a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list. Return the linked list sorted as well.

Example InputOutput
head = [1,2,3,3,4,4,5][1,2,5]
head = [1,1,1,2,3][2,3]

Constraints:

The number of nodes in the list is in the range [0, 300].

-100 ≤ Node.val ≤ 100

The list is guaranteed to be sorted in ascending order.

Abstraction

Given a linked list, any value that appears more than once is completely removed from the list, including the first occurrence.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Iterative In Place Traversal - Linked List/Simple Traversal

    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        
        # Note:
        # 1. Dummy node needed since head itself may be a duplicate
        # 2. prev pointer trails behind, only advances when curr group is distinct
        # 3. When we find a duplicate value, skip ALL nodes with that value
        # 4. Otherwise, advance prev and curr normally

        dummyHead = ListNode(0, head)
        prev = dummyHead
        curr = head

        while curr:
            # check if curr is start of a duplicate group
            if curr.next and curr.val == curr.next.val:
                # skip all nodes with this value
                dupVal = curr.val
                while curr and curr.val == dupVal:
                    curr = curr.next
                # connect prev to first node after duplicate group
                prev.next = curr
            else:
                # distinct node, safe to advance prev
                prev = curr
                curr = curr.next

        return dummyHead.next

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

143. Reorder List ::4:: - Medium

Topics: Linked List, Two Pointers, Stack, Recursion

Intro

You are given the head of a singly linked-list. The list can be represented as: There is a cycle in a linked list if there is some node in the list that can be L0 → L1 → … → Ln - 1 → Ln Reorder the list to be on the following form: L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … You may not modify the values in the list's nodes. Only nodes themselves may be changed.

Example InputOutput
head = [1,2,3,4][1,4,2,3]
head = [1,2,3,4,5][1,5,2,4,3]

Constraints:

The number of nodes in the list is the range [1, 5 * 104].

1 ≤ Node.val ≤ 1000

Abstraction

Given a linked list, order it in values tending towards middle.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow.next Reversal Manual Split To Right == None Merge Prepared Tail - Linked List/Simple Traversal

    def reorderList(self, head: Optional[ListNode]) -> None:

        # -----------------------------------------
        # Linked List Reorder Pattern:

        # Before
        #   (1) -> (2) -> (3) -> (4) -> (5) -> None

        # After
        #   (1) -> (5) -> (2) -> (4) -> (3) -> None

        # This combination is a zig zag of the following:
        # (1 connects to 5, then 2, then 4, etc)
        #
        # list1 =>   (1) -> (2) -> (3) -> None
        # list2 =>      (5) -> (4) -> None     (+)

        # Where:
        # list1 = first half of the list
        # list2 = reversed second half of the list

        # -----------------------------------------
        # 1. First Half Of List 
        #   - keep pointing to original head
        #   - stop iteration at half
        #   - point middle to None to split list in half

        # -----------------------------------------
        # 2. Reversed Second Half Of List
        #   - stop iteration at half
        #   - keep pointing at middle element
        #   - reverse list starting at middle element

        # -----------------------------------------
        # 3. Join the two elements
        #   - iterate over forward and reversed list
        #   - add in correct order 
 

        # -----------------------------------------
        # 1. First Half Of List

        # Floyd Cycle Algorithm Variation:
        #   - use the slow fast pointers to find the middle of the list

        # Odd Even Case:
        #   - depending on whether the list has an even or odd number of nodes,
        #     slow pointer will stop at either:
        #
        #                  s            f
        #      () -> () -> () -> () -> () -> None
        #                 mid
        #
        #                  s     f
        #      () -> () -> () -> () -> None
        #           mid1  mid2

        # slow travels half distance vs fast pointer
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # Slow result:
        # Odd List:  
        #   - slow = mid
        # Even List: 
        #   - slow = mid2

        # -----------------------------------------
        # Prepare for 2. reverse list:
        #   1.1: set curr to beginning of right half
        #   1.2: disconnect left and right half of lists

        # Odd List: 
        #   - curr = slow.next, (mid.next)
        #     left will be 1 element longer (keeps mid element)
        #
        #    - - - - L - - -     - - R - - 
        #                  s   s.next  f
        #      () -> () -> () -> () -> () -> None
        #                 mid

        # Even List:
        #   - curr = slow.next, (mid2.next)
        #     left half will be 2 elements longer (keeps mid1 and mid2)
        #
        #    - - - - L - - -    - R -
        #                      s.next 
        #                  s      f
        #      () -> () -> () -> () -> None
        #           mid1  mid2

        # Prepare for iterative reversal with prev/curr
        prev = None
        curr = slow.next

        # Disconnect 1st half of list:
        # Set slow to be the tail of the left list (belonging solely to left list)
        #   - slow -> None
        slow.next = None 

        # Odd Even Case:
        #                  s      s.next  f
        #      () -> () -> ()       () -> () -> None
        #                 mid
        #                  | 
        #                  V
        #                 None
        # 
        #                         s.next 
        #                  s         f
        #      () -> () -> ()       () -> None
        #           mid1  mid2
        #                  |
        #                  v
        #                 None


        # -----------------------------------------
        # 2. Reverse Second Half Of List

        # Keep reversing until we reach the end of the right half
        # tc: O(n/2) ~ O(n) 
        while curr:

            # grab next and reverse flow
            next_node = curr.next
            curr.next = prev
            
            # iterate 
            prev = curr
            curr = next_node

        # After iteration:
        # prev = tail
        # prev.next = None


        # -----------------------------------------
        # 3. Connect First Half And Reversed Second Half:

        # iterate over left half and right half
        #   - head is still at the start of the original list (start of the left half)
        #   - prev is at the end of the original list (start of the right half) 
        left = head
        right = prev

        # Explicit Termination Needed:
        # right half is always shorter than the left half 
        # (by 1 or 2 nodes for even and odd original lists, see above)
        # These extra 1 or 2 nodes serve as a prepared tail for the left half: 

        #    - - - - L - - -      - - R - - 
        #                  s             f
        #   (1) -> (2) -> (3) -> (4) -> (5)        (1) -> (2) -> (3) -> None       (1) -> (5) -> (2) -> (4) -> (3) -> None
        #                 mid                 ==>     (5) -> (4) -> None      ==> 
        #                                                        ^
        #                                                      right terminates,
        #                                                      notice how "(3) -> None" is already prepared
        #    - - - - L - - -    - R -                           
        #                  s      f          
        #   (1) -> (2) -> (3) -> (4)               (1) -> (2) -> (3) -> None       (1) -> (4) -> (2) -> (3) -> None
        #           mid1  mid2                ==>      (4) -> None            ==> 
        #                                                  ^
        #                                                 right terminates,
        #                                                 notice how "(2) -> (3) -> None" is already prepared

        # The extra 1 or 2 prepared tail, 
        # allows us to know that when right terminates,
        # the rest of the remaining list is already in the correct order
        while right:

            # grab next before disconnect
            next_left, next_right = left.next, right.next

            # merge lists with the alternating pattern (see above)
            left.next = right
            right.next = next_left

            # iterate both lists 
            left, right = next_left, next_right
        

        # overall: tc O(n)
        # overall: sc O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow Reversal Manual Split To Right == Slow Merge Prepared Tail - Linked List/Simple Traversal

    def reorderList(self, head: Optional[ListNode]) -> None:

        # -----------------------------------------
        # Linked List Reorder Pattern:

        # Before
        #   (1) -> (2) -> (3) -> (4) -> (5) -> None

        # After
        #   (1) -> (5) -> (2) -> (4) -> (3) -> None

        # This combination is a zig zag of the following:
        # (1 connects to 5, then 2, then 4, etc)
        #
        # list1 =>   (1) -> (2) -> (3) -> None
        # list2 =>      (5) -> (4) -> None     (+)

        # Where:
        # list1 = first half of the list
        # list2 = reversed second half of the list


        # -----------------------------------------
        # 1. First Half Of List 
        #   - keep pointing to original head
        #   - stop iteration at half
        #   - point middle to None to split list in half

        # -----------------------------------------
        # 2. Reversed Second Half Of List
        #   - stop iteration at half
        #   - keep pointing at middle element
        #   - reverse list starting at middle element

        # -----------------------------------------
        # 3. Join the two elements
        #   - iterate over forward and reversed list
        #   - add in correct order 

    
        # -----------------------------------------
        # 1. First Half Of List

        # Floyd Cycle Algorithm Variation:
        #   - use the slow fast pointers to find the middle of the list

        # Odd Even Case:
        #   - depending on whether the list has an even or odd number of nodes,
        #     slow pointer will stop at either:
        #
        #                  s            f
        #      () -> () -> () -> () -> () -> None
        #                 mid
        #
        #                  s     f
        #      () -> () -> () -> () -> None
        #           mid1  mid2
 
        # slow travels half distance vs fast pointer
        slow = fast = head
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
 
        # Slow result:
        # Odd List:  
        #   - slow = mid
        # Even List: 
        #   - slow = mid2

        # -----------------------------------------
        # Prepare for 2. reverse list:
        #   1.1: set curr to beginning of right half
        #   1.2: disconnect left and right half of lists

        # Odd List: 
        #   - curr = slow, (mid)
        #     left will be 1 element longer (keeps mid element)
        #
        #    - - - - L - - -     - - R - - 
        #                  s           f
        #      () -> () -> () -> () -> () -> None
        #                 mid

        # Even List:
        #   - curr = slow, (mid2)
        #     left and right half will be equal
        #
        #     - - L - -    - - R - -                        
        #                  s      f
        #      () -> () -> () -> () -> None
        #           mid1  mid2


        # Prepare for iterative reversal with prev/curr
        prev = None
        curr = slow

        # Implicit Disconnect:
        # Both left and right contain slow (either odd mid or mid2) in their list
        # during the first iteration of reversing the list, we set "slow -> None"
        # now, both lists share the same reference to  "slow -> None"

        # Odd Even Case:
        #   - depending on whether the list has an even or odd number of nodes,
        #     slow pointer will stop at either:
        #
        #                  s              f
        #      () -> () -> ()       () -> () -> None
        #                 mid
        #                  |
        #                  V
        #                 None
        #
        # 
        #                  s         f
        #      () -> () -> ()        () -> None
        #           mid1  mid2
        #                  |
        #                  V
        #                 None


        # Implicit Right -> Left Connect:
        # In the first iteration of the reversal we set "slow -> None" and then "slow.next -> slow":
        #
        #                  s             f
        #      () -> () -> ()   <- () -> () -> None
        #                 mid
        #                  |
        #                  V
        #                 None
        #
        # 
        #                  s        f
        #      () -> () -> ()   <-  () -> None
        #           mid1  mid2
        #                  |
        #                  V
        #                 None

        # -----------------------------------------
        # 2. Reverse Second Half Of List

        # Keep reversing until we reach the end of the right half
        # tc: O(n/2) ~ O(n) 
        while curr:

            # grab next and reverse flow
            next_node = curr.next
            curr.next = prev

            # iterate 
            prev = curr
            curr = next_node

        # After iteration:
        # prev = tail
        # prev.next = None


        # -----------------------------------------
        # 3. Connect First Half And Reversed Second Half:

        # iterate over left half and right half
        #   - head is still at the start of the original list (start of the left half)
        #   - prev is at the end of the original list (start of the right half)
        left = head
        right = prev

        # Implicit Termination:
        # right is either equal or shorter than the left half
        # (by 1 node for odd and equal length for even original lists, see above)
        # This extra 1 node or evenness serve as a prepared tail for the left half

        #    - - - - L - - -     - - R - - 
        #                  s             f
        #   (1) -> (2) -> (3) -> (4) -> (5)       (1) -> (2) -> (3) -> None       (1) -> (5) -> (2) -> (4) -> (3) -> None
        #                 mid                ==>     (5) -> (4) -> (mid/3)      ==> 
        #                                                        ^
        #                                                      right terminates,
        #                                                      notice how "(4) -> (3) -> None" is already prepared
        #
        #    - - L - -     - - R - -                           
        #                  s      f          
        #   (1) -> (2) -> (3) -> (4)               (1) -> (2) -> (3) -> None       (1) -> (4) -> (2) -> (3) -> None
        #           mid1  mid2                ==>      (4) -> (5) -> (mid/3)            ==> 
        #                                                          ^
        #                                                        right terminates,
        #                                                        notice how "(4) -> (3) -> None" is already prepared

        # the extra 1 or even tail,
        # allows us to know that when right == slow,
        # the rest of the remaining list is already in the correct order
        # tc: O(n/2) ~ O(n) 
        while right != slow:

            # grab next before disconnect
            next_left, next_right = left.next, right.next

            # merge lists with the alternating pattern (see above)
            left.next = right
            right.next = next_left

            # iterate both lists 
            left, right = next_left, next_right

        # brain teaser:
        # why does this work:
        right.next = None
        # but right.next.next = None 
        # does not work

        # overall: tc O(n)
        # overall: sc O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 3: [Stack] Floyd Cycle With Stack - Linked List/Simple Traversal

    def reorderList(self, head: Optional[ListNode]) -> None:

        # Note:
        # 1. Use Tortoise Hare to find the middle
        # 2. Iterate over right half in order and store elements in stack
        # 3. Pop all elements (in reverse order) and place in between left elements:
        # left -> right -> left.next
        # Result: Fully reordered

        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # Slow result:
        # Odd List: slow -> odd mid
        # Even List: slow -> mid2
        
        # store right half list in order
        # space complexity: store half list of n length O(n)
        stack = []

        # Set Curr:

        # Odd List:
        # curr = slow.next, (mid.next)
        # left will be 1 element longer (keeps mid element), right does not

        # Even List:
        # curr = slow.next, (mid2.next)
        # left will be 2 elements longer (keeps mid1 and mid2), right does not

        curr = slow.next 

        # Explicit Disconnect:
        # Set slow (belonging solely to left list)
        # to slow -> None      
        slow.next = None  

        # time complexity: iterate over list of n length O(n)
        while curr:
            stack.append(curr)
            curr = curr.next

        # forward iteration over list 
        left = head

        # Odd List Len: left will be 1 element longer (keeps mid element)
        # Even List Len: left is 2 elements longer (keeps 1st and 2nd mid elements)

        # Explicit Termination:
        # right half stack is always than the left half (by 1 or 2 nodes)
        # when we terminate right,
        # right.next = next_left guarantees that 
        # the rest of the left list was connected,
        # and since we already terminated the left list 'Explicit Disconnect'
        # our merged list will terminate
        # time complexity: iterate over stack O(n)
        while stack:

            # iterate over right half
            right = stack.pop()

            # set: left -> right -> left.next 
            right.next = left.next
            left.next = right

            # iterate left list
            left = right.next

        # overall: time complexity O(n)
        # overall: space complexity O(n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 4: [Stack] Brain Teaser slow longer or equal Right list Stack - Linked List/Simple Traversal

    def reorderList(self, head: Optional[ListNode]) -> None:

        # Note:
        # 1. Use Tortoise Hare to find the middle
        # 2. Iterate over right half in order and store elements in stack
        # 3. Pop all elements (in reverse order) and place in between left elements:
        # left -> right -> left.next
        # Result: Fully reordered

        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # Slow result:
        # Odd List: slow -> odd mid
        # Even List: slow -> mid2

        # store right half list in order
        # space complexity: store list of n length O(n)
        stack = []

        # Set Curr:

        # Odd List:
        # curr = slow, (mid)
        # left and right will be even (both keep mid element)

        # Even List:
        # curr = slow, (mid2)
        # left will be 1 element longer (keeps mid1 and mid2), (right keeps mid2)
    
        curr = slow 

        # No Implicit Disconnect:
        # (deal with that later)
        
        # time complexity: iterate over list of n length O(n)
        while curr:
            stack.append(curr)
            curr = curr.next

        # iterate over left list 
        left = head

        # time complexity: iterate over stack O(n)
        while stack:

            # grab right half node element
            right = stack.pop()

            # place right half node in between left half curr and curr.next: 
            # curr -> tail -> curr.next 
            right.next = left.next
            left.next = right

            # iterate left half of list
            left = right.next

        # brain teaser:
        # why does this work:
        # Odd/Even Case:
        # left is equal or longer by 1 node
        # Odd: 
        # left and right are equal in length, both share 'slow'
        # so at the last iteration, left -> right -> left.next
        # but we never disconnected, so left slow -> right slow
        # and since they share the same object in memory
        # this does not matter since it is pointing to itself
        # Even:
        # left is longer by 1, both share 'slow'
        # last iteration, right slow -> left slow
        # but we never disconnected, so left slow -> right slow
        # and since they share the same object in memory
        # this does not matter since it is pointing to itself
        right.next.next.next.next.next = None

        # overall: time complexity O(n)
        # overall: space complexity O(n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

86. Partition List ::1:: - Medium

Topics: Linked List, Two Pointers

Intro

Given the head of a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x. You should preserve the original relative order of the nodes in each of the two partitions.

Example InputOutput
head = [1,4,3,2,5,2], x = 3[1,2,2,4,3,5]
head = [2,1], x = 2[1,2]

Constraints:

The number of nodes in the list is in the range [0, 200].

-100 ≤ Node.val ≤ 100

-200 ≤ x ≤ 200

Abstraction

Given a linked list and a partition value, partition the list based on that value.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: todo - Linked List/Simple Traversal

    def reorderList(self, head: Optional[ListNode]) -> None:

        holder
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

147. Insertion Sort List ::1:: - Medium

Topics: Linked List, Sorting

Intro

Given the head of a singly linked list, sort the list using insertion sort, and return the sorted list's head. The steps of the insertion sort algorithm: First: Insertion sort iterates, consuming one input element each repetition and growing a sorted output list. Second: At each iteration, insertion sort removes one element from the input data, finds the location it belongs within the sorted list and inserts it there. Third: It repeats until no input elements remain.

Example InputOutput
head = [4,2,1,3][1,2,3,4]
head = [-1,5,3,4,0][-1,0,3,4,5]

Constraints:

The number of nodes in the list is in the range [1, 5000].

-5000 ≤ Node.val ≤ 5000

Abstraction

Given a linked list and a partition value, partition the list based on that value.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Iterative In Place Insertion Sort - Linked List/Simple Traversal

    def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # Note:
        # 1. Dummy node needed since sorted portion grows from scratch
        # 2. curr walks the unsorted input list one node at a time
        # 3. For each curr, walk the sorted list from dummy to find insertion point
        # 4. Insert curr between prev and prev.next in sorted list

        dummyHead = ListNode(0)
        curr = head

        while curr:
            # save next unsorted node before we overwrite curr.next
            nextUnsorted = curr.next

            # find insertion point in sorted list:
            # walk until prev.next is >= curr, or end of sorted list
            prev = dummyHead
            while prev.next and prev.next.val < curr.val:
                prev = prev.next

            # insert curr between prev and prev.next
            curr.next = prev.next
            prev.next = curr

            # advance to next unsorted node
            curr = nextUnsorted

        return dummyHead.next

        # tc: O(n^2)  — for each node, walk sorted portion to find insertion point
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] Iterative In Place Insertion Sort [SC Opt] - Linked List/Simple Traversal

    def insertionSortList(self, head: ListNode | None) -> ListNode | None:
        dummy = ListNode(0)
        prev = dummy  # the last and thus largest of the sorted list

        while head:  # the current inserting node
            next = head.next  # Cache the next inserting node.
            if prev.val >= head.val:
                prev = dummy  # Move `prev` to the front.
            while prev.next and prev.next.val < head.val:
                prev = prev.next
            head.next = prev.next
            prev.next = head
            head = next  # Update the current inserting node.

        return dummy.next
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

148. Sort List ::1:: - Medium

Topics: Linked List, Two Pointers

Intro

Given the head of a linked list, return the list after sorting it in ascending order. Follow up: Can you sort the linked list in O(n logn) time and O(1) memory (i.e. constant space)?

Example InputOutput
head = [4,2,1,3][1,2,3,4]
head = [-1,5,3,4,0][-1,0,3,4,5]
head = [][]

Constraints:

The number of nodes in the list is in the range [0, 5 * 10^4]

-10^5 ≤ Node.val ≤ 10^5

Abstraction

Given a linked list and a partition value, partition the list based on that value.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Recursive Top Down Merge Sort - Linked List/Simple Traversal

    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # Note:
        # 1. Find midpoint with slow/fast pointers, split list in two
        # 2. Recurse on each half
        # 3. Merge the two sorted halves
        # Base case: 0 or 1 nodes, already sorted

        # base case
        if not head or not head.next:
            return head

        # find midpoint and split
        slow, fast = head, head.next
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # split: slow is end of left half
        mid = slow.next
        slow.next = None

        # recurse on each half
        left = self.sortList(head)
        right = self.sortList(mid)

        # merge sorted halves
        dummy = ListNode(0)
        curr = dummy
        while left and right:
            if left.val <= right.val:
                curr.next = left
                left = left.next
            else:
                curr.next = right
                right = right.next
            curr = curr.next
        curr.next = left or right

        return dummy.next

        # tc: O(n log n) — log n splits, O(n) merge per level
        # sc: O(log n)   — recursion stack depth
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] Iterative Bottom-Up Merge Sort [SC Opt] - Linked List/Simple Traversal

    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # Note:
        # 1. Count list length
        # 2. Instead of recursing top-down, merge bottom-up
        # 3. Start with sublist size=1, merge pairs, double size each round
        # 4. log n rounds, O(n) work per round -> O(n log n), O(1) space

        if not head or not head.next:
            return head

        # count length
        length = 0
        node = head
        while node:
            length += 1
            node = node.next

        dummy = ListNode(0, head)

        size = 1
        while size < length:
            curr = dummy.next
            tail = dummy       # tail of last merged group

            while curr:
                # split off left half of size `size`
                left = curr
                right = split(left, size)
                curr  = split(right, size)

                # merge left and right, attach to tail
                merged, tail = merge(left, right)
                tail.next = curr   # connect to remainder

            size *= 2

        return dummy.next

        # tc: O(n log n)
        # sc: O(1)


    def split(head, size):
        # walk `size` steps, sever link, return new head
        for _ in range(size - 1):
            if not head:
                return None
            head = head.next
        if not head:
            return None
        rest = head.next
        head.next = None
        return rest


    def merge(l1, l2):
        # merge two sorted lists, return (head, tail)
        dummy = ListNode(0)
        curr = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                curr.next = l1
                l1 = l1.next
            else:
                curr.next = l2
                l2 = l2.next
            curr = curr.next
        curr.next = l1 or l2
        while curr.next:
            curr = curr.next
        return dummy.next, curr

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

328. Odd Even Linked List ::1:: - Medium

Topics: Linked List

Intro

Given the head of a singly linked list, group all the nodes with odd indices together followed by the nodes with even indices, and return the reordered list. The first node is considered odd, and the second node is even, and so on. Note that the relative order inside both the even and odd groups should remain as it was in the input. You must solve the problem in O(1) extra space complexity and O(n) time complexity.

Example InputOutput
head = [1,2,3,4,5][1,3,5,2,4]
head = [2,1,3,5,6,4,7][2,3,6,7,1,5,4]

Constraints:

The number of nodes in the linked list is in the range [0, 10^4]

-10^6 ≤ Node.val ≤ 10^6

Abstraction

Given a linked list, group all the odd node followed by the even nodes.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Iterative In Place Odd Even Partition - Linked List/Simple Traversal

    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # Note:
        # 1. Maintain two sublists: odd-indexed and even-indexed
        # 2. odd  pointer weaves through nodes 1, 3, 5, ...
        # 3. even pointer weaves through nodes 2, 4, 6, ...
        # 4. Save even head, append even list to end of odd list

        if not head:
            return head

        odd  = head
        even = head.next
        evenHead = even

        while even and even.next:
            # advance odd to next odd node
            odd.next  = even.next
            odd       = odd.next
            # advance even to next even node
            even.next = odd.next
            even      = even.next

        # attach even list to end of odd list
        odd.next = evenHead

        return head

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

19. Remove Nth Node From End of List ::2:: - Medium

Topics: Linked List, Two Pointers

Intro

Given the head of a linked list, remove the nth node from the end of the list and return its head. Follow up: Could you do this in one pass?

Example InputOutput
head = [1,2,3,4,5], n = 2[1,2,3,5]
head = [1], n = 1[]
head = [1,2], n = 1[1]

Constraints:

The number of nodes in the list is sz.

1 ≤ sz ≤ 30

0 ≤ Node.val ≤ 100

1 ≤ n ≤ sz

Abstraction

Given a linked list, remove nth node and return head of list.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Two Pass GetLength() then StopPrev() To The SkipIndex() - Linked List/Simple Traversal

    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        
        # Note:
        # 1. Traverse list once to calculate total length
        # 2. Use a dummy node to handle edge cases (like removing head)
        # 3. Traverse again, stop at node prior to removal index
        # 4. point prior.next = prior.next.next, disconnecting removal node
        # Result: removed node at distance n from end of list

        # First Pass: get total length of list
        # tc: O(n)
        listLen = 0
        curr = head
        while curr:
            listLen += 1
            curr = curr.next

        # DummyHead to keep track of original head
        # sc: O(1)
        dummyHead = ListNode(-1, head)

        # Linked List Iterator
        # sc: O(1)
        curr = dummyHead

        # Second Pass: calculate distance to node behind the target node
        removeDist = listLen - n
        i = 0
        while i != removeDist:
            i += 1
            curr = curr.next
        
        # Arrived at n-1th node:
        # simply skip the nth node
        curr.next = curr.next.next

        # Originally:
        #   dh  ->  lst1 -> () -> ...
        #  curr     

        # After
        #   dh  ->   ()  -> () -> ...

        # overall: tc O(n)
        # overall: sc O(1)
        return dummyHead.next
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] One Pass Modified Floyd Algorithm With Fast Starting At kth Index - Linked List/Simple Traversal

    def removeNthFromEnd(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        
        # Note:
        # 1. Use two pointers spaced nth apart (fast and slow)
        # 2. Move both pointers until fast hits end
        # 3. Slow pointer will be right before the node to remove
        # 4. Remove node by skipping it
       
        # DummyHead to keep track of original head
        # sc: O(1)
        dummyHead = ListNode(-1, head)
        
        # Linked List Iterators
        # sc: O(1)
        slow = fast = dummyHead
        
        # Modified Floyd Algorithm With Fast Stopping:
        # instead of doing fast x2 iteration, 
        # do a x1 iteration and make it stop at k+1 instead of 'None'
        # tc: O(n)
        i = 0
        while i != k+1:
            i += 1
            fast = fast.next

        # Fast is now at the nth index
        # [ slow ... fast ... None]
        #   0    ...  k   ...  n

        # Modified Floyd Algorithm With Fast Starting At k:        
        # There is a distance of k between the slow and fast pointers
        # [slow .... n + 1 .... fast]
        
        # Iterate slow and fast until fast reaches end of list:
        # tc: O(n)
        while fast:
            fast = fast.next
            slow = slow.next

        # Before Iteration:
        # slow = head
        # fast = skip.next

        # After Iteration:
        # slow = skip.prev
        # fast = 'None'
        
        # Slow is pointing to Node we are planning to skip:
        # skip it by changing .next to its parent (.next.next)
        slow.next = slow.next.next

        # Originally:
        #   dh  ->  lst1 -> () -> ...
        #  curr     

        # After
        #   dh  ->   ()  -> () -> ...

        # overall: tc O(n)
        # overall: sc O(1)
        return dummyHead.next 
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

61. Rotate List ::2:: - Medium

Topics: Linked List, Two Pointers

Intro

Given the head of a linked list, rotate the list to the right by k places.

Example InputOutput
head = [1,2,3,4,5], k = 2[4,5,1,2,3]
head = [0,1,2], k = 4[2,0,1]

Constraints:

The number of nodes in the list is in the range [0, 500].

1 ≤ sz ≤ 30

-100 ≤ Node.val ≤ 100

0 ≤ k ≤ 2 * 10^9

Abstraction

Given a linked list, rotate the list by k steps

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: todo - Linked List/Simple Traversal

    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        
        todo holder
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

138. Copy List with Random Pointer ::3:: - Medium

Topics: Hash Table, Linked List

Intro

A linked list of length n is given such that each node contains an additional random pointer, which could point to any node in the list, or null. Construct a deep copy of the list. The deep copy should consist of exactly n brand new nodes, where each new node has its value set to
the value of its corresponding original node. Both the next and random pointer of the new nodes should point to new nodes in the copied list such that the pointers in the original list and copied list represent the same list state. None of the pointers in the new list should point to nodes in the original list. For example, if there are two nodes X and Y in the original list, where X.random --> Y, then for the corresponding two nodes x and y in the copied list, x.random --> y. Return the head of the copied linked list. The linked list is represented in the input/output as a list of n nodes. Each node is represented as a pair of [val, random_index] where: val: an integer representing Node.val random_index: the index of the node (range from 0 to n-1) that the random pointer points to, or null if it does not point to any node. Your code will only be given the head of the original linked list.

Example InputOutput
head = [[7,null],[13,0],[11,4],[10,2],[1,0]][[7,null],[13,0],[11,4],[10,2],[1,0]]
head = [[1,1],[2,1]][[1,1],[2,1]]
head = [[3,null],[3,0],[3,null]][[3,null],[3,0],[3,null]]

Constraints:

0 ≤ n ≤ 1000

-104 ≤ Node.val ≤ 104

Node.random is null or is pointing to some node in the linked list.

Abstraction

Given a linked list where each node has an additional random node pointers, create a deep copy.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Two Pass Hashmap Create Nodes and Set Next and Random Deep References - Linked List/Simple Traversal

    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        
        # .get():
        # returns 'None' if nothing exists in the hashMap for that key, 
        # so its perfect for our use case of Node vs Tail cases

        # Note:
        # 1. Traverse once: create all nodes (no wiring of next/random yet)
        # 2. Use a dictionary to map old nodes -> new nodes
        # 3. Traverse Twice: wire up both next and random using the map
        # 4. Return deep copy of head

        # Empty Check:
        if not head:
            return None

        # Orig node -> deep copy of node
        origToDeepCopy = {}

        # Iteration 1: 
        # Copy all nodes values (no next/random pointers yet)
        # tc: O(n)
        curr = head
        while curr:
            
            # create a deep copy,
            # insert key:value into hashmap
            # iterate original list
            copy = Node(curr.val)
            origToDeepCopy[curr] = copy
            curr = curr.next

        # Iteration 2:
        # All nodes created in hashmap, 
        # now iterate over and assign next and random pointers from original list
        # tc: O(n)
        curr = head
        while curr:

            # grab deepCopy of node
            deepCopy = origToDeepCopy[curr]
            
            # Node vs Tail Grabbing Info:
            # grab original node, and get .next and .random nodes,
            # if we are at the tail .next and .random could == 'None',
            # (No 'None' node exists in the HashMap)
            # so .get() returns 'None' if no key:value pair exists,
            # which covers our .next and .random == 'None cases for Tails
            deepCopy.next = origToDeepCopy.get(curr.next)
            deepCopy.random = origToDeepCopy.get(curr.random)

            # Iterate over original list
            curr = curr.next

        # Return deepCopy of the head node,
        # which will be connected to the rest of the deepCopy list

        # overall: tc O(n)
        # overall: sc O(n)
        return origToDeepCopy[head]

Solution 2: [Linked List] One Pass Memoization HashMap Lazy Construction - Linked List/Simple Traversal

    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        
        # Note:
        # 1. Maintain a memo dictionary to store mapping on the fly
        # 2. Iterate once through original list while building the copy
        # 3. Create each copy node only whe first encountered (lazy construction)
        # 4. Maintain a pointer to build out the next chain

        # Empty check:
        if not head:
            return None
        
        # DummyHead to keep track of original head
        # sc: O(1)
        dummyHead = Node(-1)

        # prev allows deepCopy to connect once we create them lazily
        # for the entire deepCopy list as we go
        prev = dummy
        curr = head

        # orig -> deepCopy
        memo = {}
        
        # iterate over list of n length
        # tc: O(n)
        while curr:
            
            # If we have not created a deepCopy of the curr node yet, create
            if curr not in memo:
                memo[curr] = Node(curr.val)

            # Grab deepCopy
            currDeepCopy = memo[curr]
            
            # If curr node has a random connection
            if curr.random:

                # If we have not created a deepCopy of the curr.random node yet, create
                if curr.random not in memo:
                    memo[curr.random] = Node(curr.random.val)
                
                # Set deepCopy random to curr random
                currDeepCopy.random = memo[curr.random]
            
            # Connect previous iteration to curr node
            prev.next = currDeepCopy

            # Iterate over original list
            prev = prev.next
            curr = curr.next
        
        # deepCopyHead = dummyHead.next
        #    or 
        deepCopyHead = memo[head]

        # overall: tc O(n)
        # overall: sc O(n)
        return deepCopyHead

Solution 3: [Linked List] Three Pass In Place Interleaving Technique [SC Opt] - Linked List/Simple Traversal

    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        
        # -----------------------------------------
        # Linked List Reorder Pattern:

        # Before
        #   A -> B -> C -> D -> E -> None

        # After
        #   A -> A' -> B -> B' -> C -> C' -> ... -> None

        # This combination is a zip zag of the following:
        # (A connects to A', then B, then B', etc)
        #
        # list1 =>   A -> B -> C -> None
        # list2 =>     A' -> B' -> C' -> None (+)

        # Where:
        # list1 = original list
        # list2 = deepCopy of nodes from original list

        # -----------------------------------------
        # 1. Merged List from Original List And Created DeepCopy Nodes
        #   - Start at original head
        #   - Iterate over original list
        #   - Create deepCopy of curr node as we iterate
        #   - Connect curr node to its corresponding deepCopy
        #   - Iterate to the next original node (skipping the deepCopy we just made)
        #     to continue making deepCopies for the rest of the original list

        # -----------------------------------------
        # 2. Split Merged List into Original And DeepCopy
        #   - Start at original head
        #   - Create deepCopy of curr node as we iterate
        #   - Connect curr node to its deepCopy
        #   - Iterate to the next original node (skipping the deepCopy we just made)


        # -----------------------------------------
        # Linked List Reorder Pattern Split:

        # Before
        #   A -> A' -> B -> B' -> C -> C' -> ... -> None

        # After
        #   A -> B -> C -> D -> E -> None
        #   A' -> B' -> C' -> D' -> E' -> None

        # Where:
        # mergedList = orig nodes with deepCopy nodes
        # origList = orig list split off from mergedList
        # deepCopyList = new deepCopyList split off from mergedList
        
        # Which:
        # leads to sc O(1) since we used the original input to create our deepCopy List
        
        # Empty Check
        if not head:
            return None

        # -----------------------------------------
        # 1. Merged List from Original List And Created DeepCopy Nodes

        # Linked List Iterator:
        # sc: O(1)
        curr = head

        # Iteration 1: deepCopy Nodes
        # tc: O(n)
        while curr:

            # Interleave deepCopy nodes
            # A -> A' -> B -> B' -> C -> C'
            deepCopy = Node(curr.val)
            deepCopy.next = curr.next

            # link A -> A' and iterate to next orig
            curr.next = deepCopy
            curr = deepCopy.next

        # Linked List Iterator:
        # sc: O(1)
        curr = head

        # Iteration 2: create the random connections
        # tc: O(n)
        while curr:

            # grab intertwined deepCopy
            deepCopy = curr.next

            # If curr node has a random connection
            if curr.random:

                # Grab the deepCopy of the random node from the curr node reference: 
                # curr.random points to a original node, not the deepCopy,
                # so we grab the deepCopy of the random node with curr.random.next, 
                # since original nodes and their deepCopy are directly next to each other:   
                #     ... -> Random -> Random' -> ...
                deepCopy.random = curr.random.next

            # Iterate original list
            curr = deepCopy.next


        # -----------------------------------------
        # 2. Split Merged List into Original And DeepCopy

        # Original and DeepCopy List Iterators
        # sc: O(1)
        
        # head is still == A
        curr = head

        # head is .next comes from A -> A', so head.next is now == A'
        deepCopyDummyHead = head.next

        # Iterate over list of 2n length (node + deepCopyNode)
        # tc: O(n)
        while curr:

            # grab curr nodes corresponding deepCopy (A -> A', grabbing A')
            deepCopy = curr.next
            
            # grab deepCopies next original node (A' -> B, grabbing B) 
            curr.next = deepCopy.next

            # Iterate to B
            curr = curr.next

            # Before
            # A -> A' -> B -> ...

            # After
            # A' -> B -> B' -> ...
            # A -> B -> C -> ...

            
            # If:   B != 'None'
            # Then: B must have (B -> B')
            if curr:

                # set A' -> B' (grabbing B' from B -> B')
                deepCopy.next = curr.next

            # Before 
            # A' -> B -> B' ...

            # After
            # A' -> B' -> ...


        # return deepCopy head

        # overall: tc O(n)
        # overall: sc O(1)
        return deepCopyDummyHead
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

2. Add Two Numbers ::2:: - Medium

Topics: Linked List, Math, Recursion

Intro

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list. You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example InputOutput
l1 = [2,4,3], l2 = [5,6,4][7,0,8]
l1 = [0], l2 = [0][0]
l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9][8,9,9,9,0,0,0,1]

Constraints:

The number of nodes in each linked list is in the range [1, 100].

0 ≤ Node.val ≤ 9

It is guaranteed that the list represents a number that does not have leading zeros.

Abstraction

Given a two linked lists representing numbers, return the sum.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive - Linked List/Simple Traversal

    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        
        # Note:
        # 1. Recursion to simulate digit addition with carry
        # 2. Add Corresponding digits with carry passed to recursive call
        # 3. Current node created with .next -> recursively created node
        # 4. Base case: both lists empty and carry is 0
        # Result: new sum list created recursively
        
        # time complexity: iterate over two lists of m and n length O(max(m, n))
        # space complexity: recursive call over two lists of m and n length O(max(m, n))
        def recursiveSum(l1, l2, carry):
            
            # Base Case:
            # Stop once we have nothing to add
            #   - Both lists are empty
            #   - We have no carry
            if not l1 and not l2 and carry == 0:
                return None

            # Check if value exists and grab
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0

            # Check curr sum
            sum = val1 + val2 + carry
            carry = sum//10

            # Iterate lists if exist
            l1 = l1.next if l1 else None
            l2 = l2.next if l2 else None

            # Create new node and put curr sum into it
            newNode = ListNode(sum % 10)

            # Attach node to next node
            newNode.next = recursiveSum(l1, l2, carry)

            # Pass curr node back up recursive calls
            # tc: O(n)
            # sc: O(n)
            return newNode

        # calculate new sum list over two lists

        # overall: tc O(max(m, n))
        # overall: sc O(max(m, n))
        return recursiveSum(l1, l2, 0)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: Iterative - Linked List/Simple Traversal

    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        
        # Note:
        # 1. Dummy node to create a sum list
        # 2. Iterate over both lists until complete
        # 3. Added values node by node, and pass the carry
        # Result: sum of two lists

        # DummyHead to keep tack of original head
        dummy = ListNode(-1)

        # Linked List Iterators:
        # sc: O(1)
        prev = dummy
        carry = 0

        # Traverse while we still have something to add
        #   - either list is non empty
        #   - we have a carry value
        while l1 or l2 or carry:
            
            # Check if value exists and grab
            v1 = l1.val if l1 else 0
            v2 = l2.val if l2 else 0

            # add values and carry
            sum = v1 + v2 + carry
            carry = sum // 10

            # create new Node for new value  
            newNode = ListNode(sum % 10)
            prev.next = newNode
            prev = prev.next

            # iterate list
            l1 = l1.next if l1 else None
            l2 = l2.next if l2 else None


        # return new head of sum list

        # overall: tc O(max(m, n))
        # overall: sc O(n)
        return dummyHead.next
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

146. LRU Cache ::2:: - Medium

Topics: Hash Table, Linked List, Design, Doubly Linked List

Intro

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. Implement the LRUCache class: LRUCache(int capacity) Initialize the LRU cache with positive size capacity. int get(int key) Return the value of the key if the key exists, otherwise return -1. void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key. The functions get and put must each run in O(1) average time complexity.

Example InputOutput
nums = [1,3,4,2,2]2
nums = [3,1,3,4,2]3
nums = [3,3,3,3,3]3

Constraints:

1 ≤ capacity ≤ 3000

0 ≤ key ≤ 104

0 ≤ value ≤ 105

At most 2 * 105 calls will be made to get and put.

Abstraction

Design an efficient LRU cache using linked lists.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug: Did not add to cache when adding to linked list put()

class Node:
    def __init__(self, key : int, val : int):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity : int):
        self.capacity = capacity
        self.cache = {}
        self.head = Node(0, 0)
        self.tail = Node(0, 0)
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove_node(self, node:Node):
        prev, next = node.prev, node.next
        prev.next = next
        next.prev = prev

    def _push_MRU(self, node:Node):
        twoMRU = self.head.next
        node.next = twoMRU
        node.prev = self.head
        self.head.next = node
        twoMRU.prev = node

    def get(self, key:int):
        if key not in self.cache:
            return -1

        node = self.cache[key]
        self._remove_node(node)
        self._push_MRU(node)
        return node.val

    def put(self, key:int, val:int):
        if key in self.cache:
            node = self.cache[key]
            self._remove_node(node)
            node.val = val
        else:

            # INCORRECT:
            # forgot to add new node to cache
            # Instead:
            # node = self.cache[key] = node
            node = Node(key, val)

        self._push_MRU(node)

        if len(self.cache) > self.capacity:
            lruNode = self.tail.prev
            self._remove_node(lruNode)
            self.cache.pop(lruNode.key)

Solution 1: [Doubly Linked List] Doubly Linked List with Hashmap - Linked List/Simple Traversal

class Node:
    def __init__(self, key: int, val: int):

        # key -> value node
        self.key = key
        self.val = val
        # doubly linked prev and next
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):

        # list capacity
        self.capacity = capacity

        # key -> Node
        self.cache = {} 

        # MRU head
        self.head = Node(0, 0)
        # LRU tail
        self.tail = Node(0, 0)

        # set dummy head and tail
        self.head.next = self.tail
        self.tail.prev = self.head

    # time complexity: O(1)
    # space complexity: O(1)
    def _remove_node(self, node: Node):

        # Remove node from doubly linked list by linked neighbors
        prev, next = node.prev, node.next
        prev.next = next
        next.prev = prev

    # time complexity: O(1)
    # space complexity: O(1)
    def _push_MRU(self, node: Node):

        # Insert node between dummy head and MRU 
        node.next = self.head.next
        node.prev = self.head

        # Update head and 2nd MRU
        self.head.next = node
        node.next.prev = node

    # time complexity: O(1)
    # space complexity: O(1)
    def get(self, key: int) -> int:       

        # if node does not exist, return -1
        if key not in self.cache:
            return -1

        # grab node
        node = self.cache[key]

        # Move node and update MRU
        self._remove_node(node)
        self._push_MRU(node)

        # return val
        return node.val

    # time complexity: O(1)
    # space complexity: O(1)
    def put(self, key: int, value: int) -> None:

        # If key exists, remove to avoid duplicates,
        # need to grab node to update prev and next nodes
        if key in self.cache:
            node = self.cache[key]
            self._remove_node(node)

            # update existing node
            node.val = value

        # If key does not exist, create new node set to new value
        else:
            node = Node(key, value)
            self.cache[key] = node

        # push updates node to top of MRU
        self._push_MRU(node)

        # Remove LRU if capacity broken
        if len(self.cache) > self.capacity:
            lruNode = self.tail.prev
            self._remove_node(lruNode)
            self.cache.pop(lruNode.key)

    # overall: time complexity O(1)
    # overall: space complexity O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
removeO(1)O(1)Linking prev and next of removed node in constant O(1)No additional memory allocation for relinking nodes O(1)
insert_at_frontO(1)O(1)Linking new head in between dummy head and current head in constant O(1)No additional memory allocation for relinking nodes O(1)
getO(1)O(1)Hashmap lookup O(1) with DLL remove O(1) and DLL insert_at_front O(1) all in constant O(1)No additional memory allocation for either sub step O(1)
putO(1)O(1)Hashmap lookup O(1 ) with DLL remove O(1) and DLL insert_at_front O(1) all in constant O(1)No additional memory allocation for either sub step O(1)
overallO(1)O(capacity)All operations in constant O(1)Hashmap and DLL will store up to capacity O(capacity)

Solution 2: [Doubly Linked List] Python Implementation OrderedDict a Doubly Linked List with Hashmap - Linked List/Simple Traversal

class LRUCache:
    def __init__(self, capacity: int):
        
        # OrderedDict in Python is implemented internally
        # as a doubly linked list + hashmap (same as our solution):
        self.cache = OrderedDict()
        self.capacity = capacity

    # time complexity: O(1) 
    # space complexity: O(1)
    def get(self, key: int) -> int:

        # Check if key exists, else return -1
        if key not in self.cache:
            return -1

        # Update MRU
        self.cache.move_to_end(key)

        # return value
        return self.cache[key]

    # time complexity: O(1)
    # space complexity: O(1)
    def put(self, key: int, value: int) -> None:

        # If key exists, remove to avoid duplicates
        if key in self.cache:
            self.cache.pop(key)
        
        # Insert new value, automatically updates MRU,
        # automatically updates doubly linked list
        self.cache[key] = value
        
        # capacity exceeded, remove LRU
        if len(self.cache) > self.capacity:
            
            # last=True -> pops MRU
            # last=False -> pops LRU
            self.cache.popitem(last=False)

    # overall: time complexity O(1)
    # overall: space complexity O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
getO(1)O(1)DLL move to end and lookup in constant O(1)No additional memory allocation O(1)
putO(1)O(1)DLL move to end and lookup in constant O(1)No additional memory allocation O(1)
overallO(1)O(capacity)All operations in constant O(1)Hashmap and DLL store up to capacity O(capacity)

23. Merge k Sorted Lists ::2:: - Hard

Topics: Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort

Intro

You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.

Example InputOutput
lists = [[1,4,5],[1,3,4],[2,6]][1,1,2,3,4,4,5,6]
lists = [][]
lists = [[]][]

Constraints:

k == lists.length

0 ≤ k ≤ 104

0 ≤ lists[i].length ≤ 500

-104 ≤ lists[i][j] ≤ 104

lists[i] is sorted in ascending order.

The sum of lists[i].length will not exceed 104

Abstraction

Given a list of sorted linked lists, combine into one sorted linked list.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] [MinHeap] Single MinHeap To Do Something Priority Queue - Linked List/Simple Traversal

    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        
        # Note:
        # 1. minHeap stores current head for each sorted list
        # 2. minHeap sorted by value, list_index breaking ties: 
        #    (value, list_index, node)
        # 3. smallest value across heads added to list
        # 4. push to minHeap to replaced added node
        # Result: merge list in O(n log k)

        # MinHeap():
        #   - O(log n) insertion and removal)

        # MinHeap data representation: (value, index, node)
        #   - Need the value to validate min
        #   - Need index to check if ___
        #   - Need the node to modify connections for new linked list
        minHeap = []

        # For each non empty list,
        # push the head into the heap
        for i, head in enumerate(lists):
            if head:
                heappush(minHeap, (head.val, i, head))  

        # MinHeap() After:
        # Contains the first node of each of the lists,
        # with the min one, at the top of the minHeap

        # DummyHead to keep track of new head
        dummyHead = ListNode(-1)
        prev = dummyHead

        # While the minHeap still has nodes from lists
        #   - pop the smallest from the heap and add to the new list
        #   - grab a new node from the list we just lost a node from
        # tc: O(n log k)
        while minHeap:

            # Current min node
            (minVal, i, node) = heappop(minHeap)

            # Point new list to curr node, and iterate
            prev.next = node
            prev = prev.next

            # If curr min node, has more nodes in their list
            if node.next:
                # Grab next node from list and add to minHeap
                heappush(minHeap, (node.next.val, i, node.next))

        # minHeap Empty:
        # all lists are empty, final list has been merged

        mergedListHead = dummyHead.next

        # overall: tc O(n log k)
        # overall: sc O(k)
        return mergedListHead
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] [Divide And Conquer] Iterative Divide And Conquer Merging 2 Lists Per Round So Log() [SC Opt] - Linked List/Simple Traversal

    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        
        # Note:
        # 1. Merge lists in pairs with standard merge two sorted lists
        # 2. Each round halves the total number of lists -> O(log k) rounds
        # 3. Each round processes all nodes (across all merges) -> O(n) work per round
        # Result: Merging k lists in place
        
        # Empty check
        if not lists: 
            return None

        # Merge two sorted linked lists
        # time complexity: iterate over lists of n length O(n)
        def mergeTwo(l1, l2):

            # dummy node trick
            dummy = ListNode(-1)
            prev = dummy

            # merge while both lists non empty
            while l1 and l2:
                # grab smaller head
                if l1.val < l2.val:
                    prev.next = l1
                    l1 = l1.next
                else:
                    prev.next = l2
                    l2 = l2.next
                # iterate
                prev = prev.next

            # attach remaining list 
            if not l1:
                prev.next = l2 
            else:
                prev.next = l1

            # return merged new merged list
            return dummy.next

        # iteratively merge lists in pairs until one list remains
        # time complexity: log(len(list)) total merges
        while len(lists) > 1:
            merged = []
            for i in range(0, len(lists), 2):
                l1 = lists[i]
                l2 = lists[i + 1] if i + 1 < len(lists) else None
                merged.append(mergeTwo(l1, l2))
            lists = merged

        # overall: time complexity O(n log k)
        # overall: space complexity O(1)
        return lists[0]
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

24. Swap Nodes in Pairs ::2:: - Medium

Topics: Linked List, Recursion

Intro

Given a linked list, swap every two adjacent nodes and return its head. You must solve the problem without modifying the values in the list's nodes (i.e., only nodes themselves may be changed.)

Example InputOutput
head = [1,2,3,4][2,1,4,3]
head = [][]
head = [1][1]
head = [1,2,3][2,1,3]

Constraints:

The number of nodes in the list is in the range [0, 100].

0 ≤ Node.val ≤ 100

Abstraction

Given a linked list, swap every pair of elements.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Iterative In Place Pair Swap - Linked List/Simple Traversal

    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        dummy.next = head
        
        prev = dummy
        
        while prev.next and prev.next.next:
            
            first = prev.next
            second = prev.next.next
            
            # swap
            first.next = second.next
            second.next = first
            prev.next = second
            
            prev = first
        
        return dummy.next
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] Iterative In Place Pair Swap - Linked List/Simple Traversal

    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # Note:
        # 1. Dummy node needed since head itself gets swapped
        # 2. prev trails behind, stitches each swapped pair back to the list
        # 3. For each pair: save pointers, swap, reconnect, advance

        dummy = ListNode(0, head)
        prev  = dummy

        while prev.next and prev.next.next:
            # grab the pair
            first  = prev.next
            second = prev.next.next

            # swap
            first.next  = second.next
            second.next = first
            prev.next   = second

            # advance prev to end of swapped pair
            prev = first

        return dummy.next

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

25. Reverse Nodes in k-Group ::2:: - Hard

Topics: Linked List, Recursion

Intro

Given the head of a linked list, reverse the nodes of the list k at a time, and return the modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left- out nodes, in the end, should remain as it is. You may not alter the values in the list's nodes, only nodes themselves may be changed. Follow-up: Can you solve the problem in O(1) extra memory space?

Example InputOutput
head = [1,2,3,4,5], k = 2[2,1,4,3,5]
head = [1,2,3,4,5], k = 3[3,2,1,4,5]

Constraints:

The number of nodes in the list is n. 1 ≤ k ≤ n ≤ 5000

0 ≤ Node.val ≤ 1000

-104 ≤ lists[i][j] ≤ 104

lists[i] is sorted in ascending order.

The sum of lists[i].length will not exceed 104

Abstraction

Given a linked lists, and group size k, reverse as many groups of k length as possible.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] Recursive - Linked List/Simple Traversal

    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        
        # ------------------------------------------------
        # Reverse Nodes In k-Group Pattern: State 
        
        #   k = 3, input: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> None
        #
        #   Before:
        #   [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None

        #   After:
        #   [2] -> [1] -> [4] -> [3] -> [6] -> [5] -> None

        # Note:
        # 1. Check if there are at least k nodes ahead (otherwise return head)
        # 2. Reverse the first k nodes
        # 3. Recurse and process sub list
        # 4. Connect reversed head with resulting sub list
        # Result: original lists with reversed groups of k length

        # Early Exit:
        # Get length of list,
        # if list has less than k nodes, return head        
        kLen = 0
        node = head
        while node and kLen < k:
            kLen += 1
            node = node.next
        if kLen < k:
            return head  


        # Standard Iterative Linked List Reversal:

        # Linked List Iterators:
        # sc: O(1)
        prev = None
        curr = head

        # Reverse the first k nodes
        # tc: O(k)
        for _ in range(k):
            
            # Grab next node before disconnecting
            # from current node
            next = curr.next

            # reverse flow of current node
            curr.next = prev
            
            # Iterate Linked List
            prev = curr
            curr = next
        
        # Head is still pointing to original first node,
        # but this original first node is now the new tail,
        # which needs to be connected to the head of the 
        # next section, hence: we need to update head.next
        head.next = self.reverseKGroup(curr, k)
        
        # Prev is pointing to the original tail of the k section,
        # but this original tail becomes the new head,
        # hence, we need to return it up the recursive call
        sectionNewHead = prev

        # overall: tc O(n)
        # overall: sc O(n)
        return sectionNewHead
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] Iterative [SC Opt] - Linked List/Simple Traversal

    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        
        # ------------------------------------------------
        # Reverse Nodes In k-Group Pattern: State 
        
        #   k = 3, input: 1->2->3->4->5->6->None
        #
        #   Before:
        #   [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None

        #   After:
        #   [2] -> [1] -> [4] -> [3] -> [6] -> [5] -> None

        # Note:
        # 1. Dummy node handles head swaps cleanly
        # 2. For each sub list:
        #    Find the kth node
        #    Reverse the group in place
        #    Connect previous reverse group's tail to head of new reversed group
        # 3. Continue until remaining nodes < k, then return

        # --------------------------------
        # Initial State: k = 3, input: 1->2->3->4->5->6->None
        #
        #   [dummy] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
        #      |
        #   somePrevTail

        # dummy node trick for head of overall list
        dummyHead = ListNode(0, head)
        somePrevTail = dummyHead

        # Iterate linked list, while we still have at least k nodes
        # tc: O(n)
        while True:

            # ------------------------------------------------
            # Check if k nodes remain

            # Last node (tail) of the prev k section
            kthNode = somePrevTail

            # Early Exit:
            # If remaining list has less than k nodes, return head
            # check if k more elements exist
            for _ in range(k):

                # Check next node
                kthNode = kthNode.next
                if not kthNode:
                    # return head
                    return dummyHead.next 

            # Reverse the next k section of nodes: 
            nextHead = kthNode.next

            # Linked List Iterators:
            # sc: O(1)
            prev = nextHead
            curr = somePrevTail.next

            # --------------------------------
            # After walking k steps, 
            # saving next section head, 
            # initializing curr/prev:
            #
            #                  curr                 prev
            #                   |                    |
            #   [dummyHead] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
            #        |                        |      |
            #    somePrevTail               kthNode  nextHead


            # ------------------------------------------------
            # Standard Iterative Linked List Reversal:

            # Reverse k nodes:
            for _ in range(k):
                
                # Grab next node before disconnecting from current node
                next = curr.next

                # Reverse curr node, point to previous node
                curr.next = prev

                # Iterate linked list
                prev = curr
                curr = next

            # After reversal:
            #   
            #      - - - - - - - - - - - - - - 
            #      |           prev           |     curr
            #      |            |             v      |
            #   [dummyHead]    [3] -> [2] -> [1] -> [4] -> [5] -> [6] -> None
            #      |                          ^
            #   groupPrevTail               (groupPrevTail.next,
            #   (unchanged)                  original head, now tail
            #                               of reversed group)
            #
            #                  - - - - - - - - -      - - - - - - - -
            #                   prev k section         curr k section
            #
            #
            # Connecting prev/curr sections:
            #   - groupPrevTail:       tail of the *previous* k section (dummy here on iter 1)
            #   - groupPrevTail.next:  original head / new tail of the group we just reversed
            #   - prev:                original tail / new head of the group we just reversed
            #   - curr:                head of the *next* k section (not yet reversed)

            # To link the "prev" and curr" sections: we need to:
            #   - connect the new tail of the previous k section => to the new head of the curr k section 

            # Grab head of the *next* k section (not yet reversed),
            # grab [4] in the above diagram
            currTail = somePrevTail.next
            
            # Grab dummyHead to point to new head,
            # [3] in this case
            somePrevTail.next = prev
            # update last node of previous group, to last node of curr sub list
            somePrevTail = currTail

            # After adjusting links:
            #   
            #              prev                 curr
            #               |                    |
            #   [dummy] -> [3] -> [2] -> [1] -> [4] -> [5] -> [6] -> None
            #      |                      ^
            #   groupPrevTail           (groupPrevTail.next,
            #   (unchanged)              original head, now tail
            #                            of reversed group)
            #
            #              - - - - - - - - -      - - - - - - - -
            #               prev k section         curr k section

        # overall: tc O(n)
        # overall: sc O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

109. Convert Sorted List to Binary Search Tree ::2:: - Medium

Topics: Linked List, Divide and Conquer, Tree, Binary Search Tree, Binary Tree

Intro

Given the head of a singly linked list where elements are sorted in ascending order, convert it to a height-balanced binary search tree. A height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.

Example InputOutput
head = [-10,-3,0,5,9][0,-3,9,-10,null,5]
head = [][]

Constraints:

The number of nodes in the list is in the range [0, 2 * 10^4].

-10^5 ≤ Node.val ≤ 10^5

Abstraction

Given a linked list, convert it to a height balanced binary search tree

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Linked List] [Tree] Recursive Top Down Divide and Conquer - Linked List/Simple Traversal

    def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
        # Note:
        # 1. Find midpoint with slow/fast pointers -> root
        # 2. Left half  -> recurse -> left subtree
        # 3. Right half -> recurse -> right subtree
        # 4. Midpoint becomes root of current subtree
        # sc: O(log n) recursion stack, not O(1)

        # base cases
        if not head:
            return None
        if not head.next:
            return TreeNode(head.val)

        # find mid with slow/fast, sever left half
        prev = None
        slow, fast = head, head
        while fast and fast.next:
            prev = slow
            slow = slow.next
            fast = fast.next.next

        # sever left half from mid
        if prev:
            prev.next = None

        # slow is mid -> root
        root       = TreeNode(slow.val)
        root.left  = self.sortedListToBST(head if prev else None)
        root.right = self.sortedListToBST(slow.next)

        return root

        # tc: O(n log n) — find mid is O(n), done log n times
        # sc: O(log n)   — recursion stack
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Linked List] [Tree] Recursive Inorder Simulation [SC Opt] - Linked List/Simple Traversal

    def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
        # Note:
        # 1. Count list length
        # 2. Simulate inorder traversal — left, root, right
        # 3. self.curr advances through list naturally as inorder visits nodes
        # 4. No need to find midpoint or sever list -> O(n) time
        # Key insight: inorder traversal of a BST visits nodes in sorted order,
        #              so we can build the BST by advancing the list pointer
        #              in lockstep with the inorder traversal

        # count length
        length = 0
        node = head
        while node:
            length += 1
            node = node.next

        self.curr = head

        def convert(left, right):
            if left > right:
                return None

            mid = (left + right) // 2

            # build left subtree first (inorder: left -> root -> right)
            leftChild  = convert(left, mid - 1)

            # self.curr is now at the midpoint -> root
            root       = TreeNode(self.curr.val)
            self.curr  = self.curr.next
            root.left  = leftChild

            # build right subtree
            root.right = convert(mid + 1, right)

            return root

        return convert(0, length - 1)

        # tc: O(n)      — each node visited exactly once
        # sc: O(log n)  — recursion stack only, no list severing
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

114. Flatten Binary Tree to Linked List ::3:: - Medium

Topics: Linked List, Stack, Tree, Depth First Search, Binary Tree

Intro

Given the root of a binary tree, flatten the tree into a "linked list": The "linked list" should use the same TreeNode class where the right child pointer points to the next node in the list and the left child pointer is always null. The "linked list" should be in the same order as a pre-order traversal of the binary tree. Follow up: Can you flatten the tree in-place (with O(1) extra space)?

Example InputOutput
root = [1,2,5,3,4,null,6][1,null,2,null,3,null,4,null,5,null,6]
root = [][]
root = [0][0]

Constraints:

The number of nodes in the tree is in the range [0, 2000].

-100 ≤ Node.val ≤ 100

Abstraction

Given a binary tree, return a linked list with the same order as the pre order traversal of the binary tree

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

    def flatten(self, root: Optional[TreeNode]) -> None:
        # Note:
        # 1. Collect all nodes in preorder (root, left, right)
        # 2. Relink all nodes: left=None, right=next node in list
        # Straightforward but O(n) space for the list

        nodes = []

        def preorder(node):
            if not node:
                return
            nodes.append(node)
            preorder(node.left)
            preorder(node.right)

        preorder(root)

        for i in range(len(nodes) - 1):
            nodes[i].left  = None
            nodes[i].right = nodes[i + 1]
        if nodes:
            nodes[-1].left  = None
            nodes[-1].right = None

        # tc: O(n)
        # sc: O(n) — preorder list + recursion stack
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Tree] Iterative Pre Order Stack - Linked List/Simple Traversal

    def flatten(self, root: Optional[TreeNode]) -> None:
        # Note:
        # 1. Simulate preorder iteratively with a stack
        # 2. For each node, relink right to stack top (next preorder node)
        # 3. No need to collect all nodes first — relink as we go
        # Still O(n) space for stack, but no separate list needed

        if not root:
            return

        stack = [root]

        while stack:
            node = stack.pop()

            # push right first so left is processed first
            if node.right:
                stack.append(node.right)
            if node.left:
                stack.append(node.left)

            # relink right to next preorder node (top of stack)
            if stack:
                node.right = stack[-1]
            node.left = None

        # tc: O(n)
        # sc: O(n) — stack
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 3: [Tree] Morris Traversal In Place [SC Opt] - Linked List/Simple Traversal

    def flatten(self, root: Optional[TreeNode]) -> None:
        # Note:
        # 1. For each node with a left child:
        #    find the rightmost node of the left subtree (inorder predecessor)
        #    attach current right subtree to that rightmost node
        #    move left subtree to right, clear left
        # 2. Repeat until no left children remain
        # Key insight: we are threading each node's right subtree
        #              onto the bottom-right of its left subtree,
        #              then pulling the left subtree up to the right
        # O(1) space, no recursion, no stack

        curr = root

        while curr:
            if curr.left:
                # find rightmost node of left subtree
                rightmost = curr.left
                while rightmost.right:
                    rightmost = rightmost.right

                # thread curr's right subtree onto rightmost
                rightmost.right = curr.right

                # pull left subtree up to right
                curr.right = curr.left
                curr.left  = None

            curr = curr.right

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

116. Populating Next Right Pointers in Each Node ::3:: - Medium

Topics: Linked List, Tree, Depth First Search, Breadth First Search, Binary Tree

Intro

You are given a perfect binary tree where all leaves are on the same level, and every parent has two children. The binary tree has the following definition: Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL. Initially, all next pointers are set to NULL. Follow-up: You may only use constant extra space. The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.

Example InputOutput
root = [1,2,3,4,5,6,7][1,#,2,3,#,4,5,6,7,#]
root = [][]

Constraints:

The number of nodes in the list is in the range [0, 2^12 - 1].

-1000 ≤ Node.val ≤ 1000

Abstraction

Given a linked list, swap every pair of elements.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Tree] BFS Level Order - Linked List/Simple Traversal

    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        # Note:
        # 1. BFS level by level
        # 2. For each node in level, point next to the next node in queue
        # 3. Last node in each level points to None
        # Straightforward but O(n) space for queue

        if not root:
            return root

        queue = deque([root])

        while queue:
            size = len(queue)

            for i in range(size):
                node = queue.popleft()

                # point to next node in same level
                if i < size - 1:
                    node.next = queue[0]

                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)

        return root

        # tc: O(n)
        # sc: O(n) — queue holds up to n/2 nodes (last level)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Tree] Recursive DFS - Linked List/Simple Traversal

    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        # Note:
        # 1. For each node, connect its two children (easy, same parent)
        # 2. Also connect right child to left child of node.next (cross connection)
        # 3. Recurse left then right
        # Key insight: by the time we process a node,
        #              node.next is already populated by parent

        if not root or not root.left:
            return root

        # connect children of same parent
        root.left.next = root.right

        # connect across parents using already-set root.next
        if root.next:
            root.right.next = root.next.left

        self.connect(root.left)
        self.connect(root.right)

        return root

        # tc: O(n)
        # sc: O(log n) — recursion stack, perfect binary tree height
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 3: [Tree] Iterative Level Traversal [SC Opt] - Linked List/Simple Traversal

    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        # Note:
        # 1. Use already-populated next pointers to traverse each level
        # 2. leftmost tracks the first node of each level (always left child)
        # 3. curr traverses current level via next pointers
        # 4. For each curr, connect its children using same/cross logic
        # Key insight: once a level's next pointers are set,
        #              we can traverse it like a linked list
        #              to set the next level's pointers

        if not root:
            return root

        leftmost = root

        while leftmost.left:
            curr = leftmost

            while curr:
                # same parent connection
                curr.left.next = curr.right

                # cross parent connection
                if curr.next:
                    curr.right.next = curr.next.left

                curr = curr.next

            # drop down to next level
            leftmost = leftmost.left

        return root

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

117. Populating Next Right Pointers in Each Node II ::3:: - Medium

Topics: Linked List, Tree, Depth First Search, Breadth First Search, Binary Tree

Intro

Given a binary tree Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL. Initially, all next pointers are set to NULL. Follow-up: You may only use constant extra space. The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.

Example InputOutput
root = [1,2,3,4,5,null,7][1,#,2,3,#,4,5,7,#]
root = [][]

Constraints:

The number of nodes in the tree is in the range [0, 6000].

-100 ≤ Node.val ≤ 100

Abstraction

Same as 116 but binary tree is not guaranteed to be perfect

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Tree] BFS Level Order - Linked List/Simple Traversal

    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        # Note:
        # 1. Identical to problem 116 Solution 1
        # 2. BFS naturally handles missing children since we
        #    only enqueue children that exist
        # 3. Works for both perfect and imperfect trees

        if not root:
            return root

        queue = deque([root])

        while queue:
            size = len(queue)

            for i in range(size):
                node = queue.popleft()

                if i < size - 1:
                    node.next = queue[0]

                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)

        return root

        # tc: O(n)
        # sc: O(n)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Tree] Iterative Next Level Linked List - Linked List/Simple Traversal

    def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
        # Note:
        # 1. Can't use 116 Sol 3's leftmost.left trick — children may be missing
        # 2. Instead, use a dummy node to build next level's linked list
        # 3. dummy.next always points to first child found in next level
        # 4. tail tracks where to attach next child found
        # Key insight: treat the next level as a linked list being built
        #              as we traverse the current level via next pointers

        if not root:
            return root

        curr = root

        while curr:
            # dummy head for next level's linked list
            dummy = ListNode(0)
            tail  = dummy

            # traverse current level via next pointers
            while curr:
                # try to attach left child to next level list
                if curr.left:
                    tail.next = curr.left
                    tail      = tail.next
                # try to attach right child to next level list
                if curr.right:
                    tail.next = curr.right
                    tail      = tail.next

                curr = curr.next

            # drop down: next level's list starts at dummy.next
            curr = dummy.next

        return root

        # tc: O(n)
        # sc: O(1) — dummy/tail are reused each level
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 3: [Tree] Iterative Next Level Linked List V2 [SC Opt] - Linked List/Simple Traversal

    def connect(self, root: 'Node') -> 'Node':
        # Note:
        # 1. Same idea as Solution 2 but without dummy node
        # 2. leftmost tracks first node of next level
        # 3. pre trails behind, links children together as we find them
        # 4. if pre exists, pre.next = current child
        #    if leftmost not yet set, current child is the new leftmost

        leftmost = root

        while leftmost:
            cur      = leftmost
            leftmost = None   # reset for next level
            pre      = None   # reset chain for next level

            while cur:
                # try left child
                if cur.left:
                    if not leftmost:
                        leftmost = cur.left   # first node of next level
                    if pre:
                        pre.next = cur.left   # chain to previous child
                    pre = cur.left

                # try right child
                if cur.right:
                    if not leftmost:
                        leftmost = cur.right  # first node of next level
                    if pre:
                        pre.next = cur.right  # chain to previous child
                    pre = cur.right

                cur = cur.next

        return root

        # tc: O(n)
        # sc: O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks