Jc-alt logo
jc
data structures and algorithms

LeetCode: Two Pointers I

LeetCode: Two Pointers I
94 min read
#data structures and algorithms
Table Of Contents

Two Pointers Intro

LeetCode problems with solutions using two pointers.

What are Two Pointers

Two Pointers is the strategy of using a left and right pointer to iterate over a data structure, usually an array, to solve a problem.

Two Pointers Application: One Pointer with Auxiliary State

We can use a single pointer to iterate linearly and have a second variable to keep track of some state or action.

Ex: Find the maximum consecutive ones in an array

    def max_consecutive_ones(nums: list[int]) -> int:

        # Aux state: track current streak
        count = 0

        # Aux state: track max streak
        max_count = 0

        # Left Pointer: iterate array
        for right in range(len(nums)):
            
            # Condition: while consecutive 1's is true
            if nums[right] == 1:
                # Aux state: add to streak
                count += 1
                max_count = max(max_count, count)
            else:
                # Aux state: reset streak
                count = 0

        return max_count

    # max_consecutive_ones([1,1,0,1,1,1]) -> 3

Two Pointers Application: Opposite Ends

We can have two pointers, left and right, starting at opposite ends of a list and move them inward while validating some sort of logic, stopping when their indexes hit left == right at the middle of the array

Ex: Determine if a string is a palindrome

    def is_palindrome(s: str) -> bool:

        # Left: start of array
        # Right: end of array
        left, right = 0, len(s) - 1

        # Break when left and right pointers match 
        # when the middle of the array is hit
        while left < right:

            # if palindrome invariant is broken
            if s[left] != s[right]:
                return False

            # shrink left and right pointers towards middle
            left += 1
            right -= 1

        # valid palindrome
        return True

    # is_palindrome("radar") -> True
    # is_palindrome("hello") -> False

Two Pointers Application: Sliding Window

We can have two pointers represent a imaginary window, [Left, Right], over a sequence that expands or shrinks while iterating or checking if a condition is satisfied.

Ex: Find the length of the longest substring without repeating characters.

    def longest_unique_substring(s: str) -> int:

        # Left: start of window
        left = 0

        # Window data: stores unique chars within window range
        char_set = set()

        # Window data: stores max window found up to now
        maxLength = 0

        # Right: end of window, expand window range as we iterate
        for right in range(len(s)):

            # Invariant: window holds list of unique chars
            # Broken: if condition is broken, shrink window from the 
            # left side until the unique char condition is true again
            while s[right] in char_set:

                # Window data: remove char on left boundary of window
                char_set.remove(s[left])
                
                # Left: start of window, shrink window range
                left += 1
            
            # Invariant: window holds list of unique chars

            # Window data: add char unique list, guaranteed to be unique
            char_set.add(s[right])
  
            # Window data: check global max
            maxLength = max(maxLength, right - left + 1)

        return maxLength

    # longest_unique_substring("abcabcbb") -> 3

Two Pointers Application: Fast & Slow Pointers

We can traverse linked lists using pointers. In this case two pointers moving at different speeds x1 and x2 can detect cycles or find midpoints in linked lists or arrays.

Ex: Detect a cycle in a linked list.


    # linked list node definition
    class ListNode:
        def __init__(self, value=0, next=None):
            self.value = value
            self.next = next

    def has_cycle(head: ListNode) -> bool:
        
        # Tortoise, hare pointers: same starting index
        slow, fast = head, head

        while fast and fast.next:

            # Tortoise pointer: x1 steps
            # Hare pointer: x2 steps
            slow = slow.next
            fast = fast.next.next

            # if pointers match, cycle exists
            # will be hit a n/2 iterations
            if slow == fast:
                return True

        # reached end of list, no cycles
        return False

    # LinkedList: 1 -> 2 -> 3 -> 4 -> 2
    # has_cycle(head) -> True

Two Pointers Application: Lomuto Partition

We can have two pointers in the same array moving inward/outward to rearrange elements based on a condition.

Ex: Lomuto partition scheme in quicksort

    def partition(nums, pivot):

        # Left: partition flip slot
        left = 0

        # Right: iterate array checking condition
        for right in range(len(nums)):

            # Condition: if curr element value is less than pivot val, 
            # flip element to left side of array in place
            if nums[right] < pivot:

                # Flip: swap element with flip slot
                nums[left], nums[right] = nums[right], nums[left]

                # Left: iterate flip slot by 1 step
                left += 1

        # Left: ends up pointing to first index where all elements 
        # are greater than the pivot value
        return (nums, left)

    # partition([9, 3, 5, 2, 8, 1, 6], 5) -> ([3, 2, 1, 5, 8, 9, 6], 5)

Two Pointers Application: Parallel Array Pointer Traversal

We can expand our previous application cards to use k pointers traversing separate k arrays in parallel to merge, compare, find intersections, or other patterns.

Ex: Merge two sorted arrays into one sorted array

    def merge_sorted_arrays(arr1, arr2):

        result = []

        # i / j: 2 pointers
        i, j = 0, 0

        # i / j: parallel iterate array, while elements remain in both lists 
        while i < len(arr1) and j < len(arr2):

            # Merge: append smaller element between arrays 
            if arr1[i] < arr2[j]:
                result.append(arr1[i])
                i += 1
            else:
                result.append(arr2[j])
                j += 1
        
        # Merge: one list has run out of elements, 
        # append list with remaining elements as its already sorted
        result.extend(arr1[i:])
        result.extend(arr2[j:])

        # merged sorted array
        return result

    # merge_sorted_arrays([1, 3, 5], [2, 4, 6]) -> [1, 2, 3, 4, 5, 6]

Two Pointers Application: Catchup Pointer

We can have two pointers traversing an array. The left pointer can be responsible for being frozen until the right pointer hits a delimiter, at which point some logic executes, then left jumps 'catches up' but jumping to right+1 to mark the start of the next section/iteration.

Ex: Split string by spaces

    def split_words(s: str, delim: str = ' ') -> list[str]:

        words = []

        # Left: frozen until right hits delim
        left = 0

        # Right: iterate list checking for delim
        right = 0

        # Right: iterate list
        while right < len(s):

            # Right condition: delimiter found
            if s[right] == delim:

                # Logic: check if non-empty word, 
                # then splice word and add to array
                if left != right:
                    words.append(s[left:right])

                # Left: Catch up, move to right+1, to 1 index
                # after the delimiter, to restart scanning for delim
                left = right + 1

            # Right: iterate pointer, either after delim
            # or to next index
            right += 1

        # Right: hit end of string, check if last word exists
        if left < len(s):
            words.append(s[left:])

        return words


    # split_words("catch up pointers example") -> ['catch', 'up', 'pointers', 'example']

Two Pointers Application: K Pointer Variants

We can extend the two pointers to k pointers. These pointers could follow any of the pointer applications, traverse the same list, different lists, freeze while moving others, etc.

Ex: Given array, return unique triplets [nums[i], nums[j], nums[k]] that sum to 0.

    def threeSum(nums):

        result = []
        nums.sort()

        # k: 3 pointers, i, left, right

        # i: iterate pointer as 'frozen' pointer
        for i in range(len(nums)):

            # i: Avoid duplicates
            if i > 0 and nums[i] == nums[i - 1]:
                continue

            # Inner two pointer approach:

            # Left: 1 index after frozen pointer
            # Right: right end of array
            left, right = i + 1, len(nums) - 1
            while left < right:

                # Condition: check if triplet sum == 0
                current_sum = nums[i] + nums[left] + nums[right]                
                if current_sum == 0:

                    # Match: add triplet
                    result.append([nums[i], nums[left], nums[right]])

                    # Left / Right: Iterate to avoid duplicates
                    left += 1
                    while left < right and nums[left] == nums[left - 1]:
                        left += 1
                    right -= 1
                    while left < right and nums[right] == nums[right + 1]:
                        right -= 1

                # Search: 
                # left end has lowest numbers, right end has highest numbers,
                # shift towards whichever gets triplet sum closer to 0

                # Left: shift towards higher numbers
                elif current_sum < 0:
                    left += 1 

                # Right: shift towards lower numbers 
                else:
                    right -= 1

        return result

    # threeSum([-1, 0, 1, 2, -1, -4]) -> [[-1, -1, 2], [-1, 0, 1]]

Two Pointers Application: Algorithm

We can have cases where problems that seems to require two pointers have an algorithm specifically made for that problem.

Ex: Manacher's Algorithm, longest palindromic substring

    def longestPalindrome(s: str) -> str:

        # Preprocess the string to handle even length palindromes
        t = "#".join(f"^{s}$")
        n = len(t)
        p = [0] * n
        center = right = 0

        
        for i in range(1, n - 1):

            # Mirror of `i` with respect to `center`
            mirror = 2 * center - i

            # If within bounds of the current right boundary, use mirror head start 
            if i < right:
                p[i] = min(right - i, p[mirror])

            # Expand around 'i' while palindrome condition true
            while t[i + p[i] + 1] == t[i - p[i] - 1]:
                p[i] += 1

            # Update the center and right boundary if the palindrome is expanded
            if i + p[i] > right:
                center = i
                right = i + p[i]

        # Find the maximum length palindrome
        maxLen, centerIndex = max((n, i) for i, n in enumerate(p))

        # Convert index back to original string
        start = (centerIndex - maxLen) // 2
        
        # Grab palindrome substring
        return s[start: start + maxLen]

344. Reverse String ::2:: - Easy

Topics: Two Pointers, String

Intro

Write a function that reverses a string.
The input string is given as an array of characters s. You must do this by modifying the input array in-place with O(1) extra memory.

InputOutput
s = ["h","e","l","l","o"]["o","l","l","e","h"]
s = ["H","a","n","n","a","h"]["h","a","n","n","a","H"]

Constraints:

1 ≤ s.length ≤ 10^5

s[i] is a printable ascii character

Abstraction

Using two pointers, L and R, we can traverse the string while swapping characters between pointers

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Regular strings
        "hello",
        "aaa",
        "abc",

        # Edge cases
        "",
        "a",

        # Palindrome patterns
        "racecar",
        "abba",
        "abca",
    ]

    for s in testCases:

        print(sol.isPalindrome(s))

Solution 1: [Two Pointer] Recursive In Place Reversal - Two Pointers/Opposite Ends

    def reverseString(self, s: List[str]) -> None:
        
        # Two Pointer Approach (In-Place)
        
        # Substring Representation:
        #   - Maintain window [left, right] representing characters to swap
        #   - Goal: Swap characters until window meets in the middle
        
        # Idea:
        #   - Initialize two pointers at the ends of the array
        #   - Swap s[left] and s[right]
        #   - Move pointers inward
        #   - Stop when left >= right

        # Yes, this is a dumb way to do recursion, just a test for syntax

        def helper(left, right):
            if left >= right:
                return
            
            # Swap characters at the current ends
            s[left], s[right] = s[right], s[left]
            
            # Recurse inward
            helper(left + 1, right - 1)
        
        helper(0, len(s) - 1)

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

Solution 2: [Two Pointer] Iterative In Place Reversal - Two Pointers/Opposite Ends

    def reverseString(self, s: List[str]) -> None:
        
        # Two Pointer Approach (In-Place)
        
        # Substring Representation:
        #   - Maintain window [left, right] representing characters to swap
        #   - Goal: Swap characters until window meets in the middle
        
        # Idea:
        #   - Initialize two pointers at the ends of the array
        #   - Swap s[left] and s[right]
        #   - Move pointers inward
        #   - Stop when left >= right

        left = 0
        right = len(s) - 1

        # tc: iterate over half the array O(n)
        while left < right:

            # Swap characters at left and right
            s[left], s[right] = s[right], s[left]

            # Shrink window from both ends
            left += 1
            right -= 1
         
        # overall: tc O(n)
        # overall: sc O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

125. Valid Palindrome ::2:: - Easy

Topics: Two Pointers, String

Intro

A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers. Given a string s, return true if it is a palindrome, or false otherwise.

InputOutput
"A man, a plan, a canal: Panama"true
"race a car"false
" "true

Constraints:

string s consists only of printable ASCII characters.

Abstraction

Using two pointers, L and R, we can traverse the string while swapping characters between pointers

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Palindromes
        "racecar",
        "aaa",
        "a",
        "",

        # Non-palindromes
        "abc",
        "leetcode",
        "python",

        # Palindrome after one deletion
        "abca",
        "baabx"
    ]

    for s in testCases:

        print(s, "=>", sol.isPalindrome(s))

Solution 1: [Two Pointers] Clean Then Reverse Slicing [::-1] Comparison - Two Pointers/Algorithm

    def isPalindrome(self, s: str) -> bool:
        
        # Note:
        # Appending to a list and joining once is more efficient than repeatedly
        # appending to a string. Strings are immutable, so each concatenation
        # creates a new string and copies all existing characters.
        # tc: list append + join: O(m), repeated string concat: O(m^2)

        # Helper: to skip over non alphaNum chars
        def isAlphaNum(c):
            if (ord('a') <= ord(c) <= ord('z') or
                ord('A') <= ord(c) <= ord('Z') or
                ord('0') <= ord(c) <= ord('9')):
                return True
            return False

        # Helper: to turn uppercase into lowercase
        def upperClean(c):
            if (ord('A') <= ord(c) <= ord('Z')):
                return chr(ord(c)+32)
            return c

        # sc: cleaned version of string O(n)
        cleaned = []

        # tc: iterate string O(n)
        for c in s:

            # only grab alphaNum chars
            if isAlphaNum(c):
                cleaned.append(upperClean(c))
        
        # tc: join alphaNum list O(n)
        phrase = "".join(cleaned)

        # Note:
        # Slicing: [start:stop:step]
        # if start and stop are omitted, slice includes the entire sequence
        # if step is -1, indicates to traverse in reverse

        # tc: single iteration over the two strings
        # sc: creates new reversed string
        res = phrase == phrase[::-1]

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

Solution 2: [Two Pointers] Cleaning String In Place - Two Pointers/Opposite Ends

    def isPalindrome(self, s: str) -> bool:
        
        # Helper: to skip over non alphaNum chars
        def isAlphaNum(c):
            if (ord('a') <= ord(c) <= ord('z') or
                ord('A') <= ord(c) <= ord('Z') or
                ord('0') <= ord(c) <= ord('9')):
                return True
            return False

        # Helper: to turn uppercase into lowercase
        def UpperClean(c):
            if (ord('A') <= ord(c) <= ord('Z')):
                return chr(ord(c)+32)
            return c

        # outer ends pointers
        # sc: O(1)
        left = 0
        right = len(s)-1

        # tc: O(1)
        while left < right:

            # skip non-alphaNum, while within bounds
            # tc: O(n)
            while left < right and not isAlphaNum(s[left]):
                left += 1

            # tc: O(n)
            while left < right and not isAlphaNum(s[right]):
                right -= 1

            # grab pointer values
            # sc: O(1)
            leftChar = s[left]
            rightChar = s[right]

            # Convert to lowercase
            leftClean = UpperClean(leftChar)
            rightClean = UpperClean(rightChar)

            # Check: if chars match
            # tc: O(1)
            if leftClean != rightClean:
                return False

            # Shrink pointers towards center
            # tc: O(1)
            left += 1
            right -= 1

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

680. Valid Palindrome II ::2:: - Easy

Topics: Two Pointers, String, Greedy

Intro

Given a string s, return true if the s can be palindrome after deleting at most one character from it.

InputOutput
s = "aba"true
s = "abca"true
s = "abc"false

Constraints:

1 ≤ s.length ≤ 10^5

s consists of lowercase English letters

Abstraction

Using two pointers, L and R, we can traverse the string while swapping characters between pointers

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Palindromes
        "racecar",
        "aaa",
        "a",
        "",

        # One deletion needed
        "abca",
        "baabx",
        "deeee",
        "raceecar",

        # Already near-palindrome
        "abba",
        "abbba",

        # Non-palindromes
        "abc",
        "abcdef",
        "leetcode",

        # Boundary patterns
        "ab",
        "ac",
        "ba",
        "aaab",
    ]

    for s in testCases:
        print(s, "->", sol.validPalindrome(s))

Solution 1: [Two Pointers] Opposite Ends With Greedy Shrink [SC Opt] - Two Pointers/Opposite Ends

    def validPalindrome(self, s: str) -> bool:
        
        # Two Pointers (Opposite Ends) Greedy Decision at First Mismatch

        # Substring Representation:
        #   - Maintain window [left, right] over string s
        #   - Move inward while characters match
        #   - On first mismatch, we are allowed to delete at most ONE character
        
        # Greedy Insight:
        #   - If s[left] != s[right], only two deletions can fix the mismatch:
        #         1) Delete s[left]
        #         2) Delete s[right]
        #   - Deleting any other character will NOT fix this mismatch.
        #   - Therefore, trying these two options is sufficient and exhaustive.
        #   - This makes the solution greedy: we resolve the first conflict locally
        #     and never revisit earlier decisions.

        # Helper: check if strings are palindromes
        def isPalindrome(i: int, j: int) -> bool:

            # tc: worst case O(n)
            while i < j:

                # If char fails palindrome check
                if s[i] != s[j]:
                    return False

                # shrink towards center
                i += 1
                j -= 1

            return True

        # original string length
        n = len(s)


        def isPalindromeSafety(s2):

            # Opposite Ends Variables
            # sc: O(1)
            left = 0
            right = n - 1

            # tc: iterate over n O(n)
            while left < right:

                # Safety hit:
                # current mismatch chars fails palindrome check,
                # check if passes by skipping mismatch chars
                if s[left] != s[right]:

                    # Greedy Skip:
                    # Create two candidate substrings, 
                    # each by skipping one of the 2 mismatched characters
                    return isPalindrome(left + 1, right) or isPalindrome(left, right - 1)

                left += 1
                right -= 1

            return True

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

Solution 2: [Two Pointers] Interpreter Level Slicing Skipping Char At Fail [TC Opt] - Two Pointers/Opposite Ends

    def validPalindrome(self, s: str) -> bool:
        
        # Sliding Window (Two Pointers + Slicing)

        # Substring Representation:
        #   - Maintain window [left, right] over string s
        #   - On mismatch, try removing either:
        #         1) left character
        #         2) right character
        #   - Use slicing to verify palindrome

        n = len(s)

        # Sliding Window Variables
        # sc: O(n) due to slicing creating new substrings
        left = 0
        right = n - 1


        # Helper:
        # su using slicing
        def isPalindrome(sub: str) -> bool:

            # CPython Interpreter:
            #
            #   Slicing: [::-1] is implemented at the interpreter level in CPython
            #   Copy:    sub[::-1] creates a reversed string copy using fast C memory operations
            #   Equal:   sub == sub[::-1] compares strings at C level with fast memory comparison
            #
            # Interpreter level ends up being much faster than a python loop over indices
            #  which adds multiple steps for simple operations
            #
            # tc: O(k), sc: O(k) where k = length of substring
            return sub == sub[::-1]


        # tc: iterate over n O(n)
        while left < right:

            # Safety hit:
            # current mismatch chars fails palindrome check,
            # check if passes by skipping mismatch chars
            if s[left] != s[right]:

                # Greedy Skip:
                # Create two candidate substrings, 
                # each by skipping one of the 2 mismatched characters

                # Slicing:
                # [0:2] = [0, 2) => [0, 1]

                # Skip left char, include right char
                skipLeft = s[left + 1 : right + 1]

                # Include left char, skip right char
                skipRight = s[left : right]         

                # Check if either resulting substring is a palindrome
                return isPalindrome(skipLeft) or isPalindrome(skipRight)

            # Shrink towards center
            left += 1
            right -= 1


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

1768. Merge Strings Alternately ::1:: - Easy

Topics: Two Pointers, String

Intro

You are given two strings word1 and word2. Merge the strings by adding letters in alternating order, starting with word1. If a string is longer than the other, append the additional letters onto the end of the merged string. Return the merged string.

InputOutput
word1 = "abc", word2 = "pqr""apbqcr"
word1 = "ab", word2 = "pqrs""apbqrs"
word1 = "abcd", word2 = "pq""apbqcd"

Constraints:

1 ≤ word1.length, word2.length ≤ 100

word1 and word2 consist of lowercase English letters

Abstraction

Using two pointers, L and R, we can traverse the string while swapping characters between pointers

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Solution 1: [Two Pointers] Opposite Ends - Two Pointers/Opposite Ends

    def mergeAlternately(self, word1: str, word2: str) -> str:
        
        # Two Pointers (Same Direction Traversal)

        # Substring Representation:
        #   - Traverse both strings from left to right
        #   - Append characters alternately from word1 and word2
        #   - If one string finishes first, append the remaining characters
        #
        # Goal:
        #   - Build merged string by alternating characters
        #
        # Pattern:
        #   - Two pointers moving forward independently

        n1 = len(word1)
        n2 = len(word2)

        # Two Pointer Variables
        # sc: O(n1 + n2) for result storage
        i = 0
        j = 0

        # Result list
        # sc: O(n1 + n2)
        merged = []

        # tc: iterate over lists O(min(n1, n2))
        while i < n1 and j < n2:

            # Alternate Appending
            merged.append(word1[i])
            merged.append(word2[j])

            i += 1
            j += 1


        # Word1 has remaining letters
        # tc: O(n1)
        while i < n1:
            merged.append(word1[i])
            i += 1

        # Word2 has remaining letters
        # tc: O(n2)
        while j < n2:
            merged.append(word2[j])
            j += 1

        # Join iterates over list and creates a new string
        # tc: O(n1 + n2)
        # sc: O(n1 + n2)
        res = "".join(merged)

        # overall: tc O(n1 + n2)
        # overall: sc O(n1 + n2)
        return res
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

88. Merge Sorted Array ::1:: - Easy

Topics: Two Pointers, Sorting

Intro

You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively. Merge nums1 and nums2 into a single array sorted in non-decreasing order. The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n. Follow up: Can you come up with an algorithm that runs in O(m + n) time?

InputOutput
nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3[1,2,2,3,5,6]
nums1 = [1], m = 1, nums2 = [], n = 0[1]
nums1 = [0], m = 0, nums2 = [1], n = 1[1]

Constraints:

nums1.length == m + N

nums2.length == n

0 ≤ m, n ≤ 200

1 ≤ m + n ≤ 200

-10^9 ≤ nums1[i], nums2[i] ≤ 10^9

Abstraction

Using two pointers, L and R, we can traverse the string while swapping characters between pointers

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Test Cases:

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Edge cases
        ([[], 0, [], 0]),
        ([[1], 1, [], 0]),

        # nums2 merges into empty portion of nums1
        ([[0,0,0], 0, [1,2,3], 3]),

        # Already sorted merge
        ([[1,2,3,0,0,0], 3, [4,5,6], 3]),

        # Reverse ordering merge
        ([[4,5,6,0,0,0], 3, [1,2,3], 3]),

        # Interleaving values
        ([[1,3,5,0,0,0], 3, [2,4,6], 3]),

        # Duplicate values
        ([[1,2,2,3,0,0,0], 4, [2,2,4], 3]),

        # Larger spread values
        ([[1,4,7,9,0,0,0], 4, [2,5,8], 3]),

        # nums1 dominant values
        ([[10,20,30,40,0,0,0], 4, [1,2,3], 3]),

        # Mixed pattern
        ([[2,5,7,0,0,0], 3, [1,3,6], 3]),

        # Small length merges
        ([[5,0], 1, [1], 1]),
        ([[1,0], 1, [2], 1])
    ]

    # Run tests
    for nums1, m, nums2, n in testCases:

        # Copy list to avoid mutation issues between tests
        arr = nums1.copy()
        sol.merge(arr, m, nums2, n)
        print(arr)
        print('------')

Solution 1: [Two Pointers] Reverse Direction Fill Elegant While Loop - Two Pointers/Opposite Ends

    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        
        # Two Pointers (Reverse Direction Traversal)

        # Substring Representation:
        #   - nums1 has length m + n
        #   - First m elements are valid
        #   - Last n elements are placeholders (0s)
        
        # Idea:
        #   - Iterate from right to left across both lists appending the larger number
        #   - We avoid overwriting values in nums1 by going right to left


        # Two Pointer Variables
        # sc: O(1) (in-place modification)

        # Last valid element in nums1
        p1 = m - 1

        # Last element in nums2
        p2 = n - 1

        # Position to write next largest element in num1
        num1Write = m + n - 1

        # While loop is determined by nums2 (p2) since once that finishes,
        # the remaining elements in nums1 (p1) are already in correct order
        # since the array was originally sorted
        # tc: iterate through both arrays once O(m + n)
        # sc: in place merge O(1)
        while p2 >= 0:

            # If nums1 has elements
            # If nums1 has larger num
            # Move nums1[p1] to new position
            if p1 >= 0 and nums1[p1] > nums2[p2]:

                # Place larger value from nums1
                nums1[num1Write] = nums1[p1]

                # Move nums1 pointer left
                p1 -= 1

            # Nums1 has run out of elements
            # or
            # Nums2 has the larger value
            else:

                # Place larger value from nums2
                nums1[num1Write] = nums2[p2]

                # Move nums2 pointer left
                p2 -= 1

            # Always move write pointer left
            num1Write -= 1

        # Nums2 has run out of elements
        # Remaining Num1 elements are already in correct order

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

26. Remove Duplicates from Sorted Array ::1:: - Easy

Topics: Array, Two Pointers

Intro

Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place such that each unique element appears only once. The relative order of the elements should be kept the same. Consider the number of unique elements in nums to be k. After removing duplicates, return the number of unique elements k. The first k elements of nums should contain the unique numbers in sorted order. The remaining elements beyond index k - 1 can be ignored. Custom Judge: The judge will test your solution with the following code: int[] nums = [...]; // Input array int[] expectedNums = [...]; // The expected answer with correct length int k = removeDuplicates(nums); // Calls your implementation assert k == expectedNums.length; for (int i = 0; i < k; i++) assert nums[i] == expectedNums[i]; If all assertions pass, then your solution will be accepted.

heightOutput
nums = [1,1,2]2, nums = [1,2,_]
nums = [0,0,1,1,1,2,2,3,3,4]5, nums = [0,1,2,3,4,,,,,_]

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

-100 ≤ nums[i] ≤ 100

nums is sorted in non decreasing order.

Abstraction

They don't really want you to remove the duplicates. They want you to sort the uniques at the front, then return the length of the sorted part. Then, behind the scenes, they slice the array at the length you give them and the result of that is what they check.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force

    
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [
        [],
        [1],
        [2,2,2,2],
        [1,2,3,4],
        [1,1,2],
        [0,0,1,1,1,2,2,3],
        [1,2,2,3,3,4],
        [5,5,5,5,6,7],
        [1,2,3,4,4,4,4],
        [1,1,2,3,3,3,4,5,5]
    ]

    for nums in testCases:
        k = sol.removeDuplicates(nums)

        # Print result count + modified array
        print("k =", k)
        print("array =", nums)
        print("---")

Solution 1: [Two Pointers] Optimal Two Pointers - Two Pointers/Lomuto Partition

    def removeDuplicates(self, nums: List[int]) -> int:
        
        # Two Pointer Pattern
        
        # Idea:
        #   - Array is sorted, so duplicates will appear consecutively
        #   - Two Pointers
        #       left: write position for next unique value
        #       fast: scanning and grabs non duplicates
        #   - left results in pointing to the start of the duplicates
    
        # Edge case: single element
        if not nums:
            return 0

        n = len(nums)

        # Write index for next unique value
        left = 1

        # Check for unique value
        right = 1

        # tc: iterate over n O(n)
        while right < n:

            # Since array is sorted,
            # we can check if previous index is a duplicate
            if nums[right] != nums[right - 1]:

                # unique number found, put in write index
                nums[left] = nums[right]

                # iterate write index
                left += 1

            # iterate unique finder
            right += 1

        # left = index of first duplicate value
        # left = number of unique elements

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

696. Count Binary Substrings ::1:: - Easy

Topics: Two Pointers, String

Intro

Given a binary string s, return the number of non-empty substrings that have the same number of 0's and 1's, and all the 0's and all the 1's in these substrings are grouped consecutively. Substrings that occur multiple times are counted the number of times they occur.

heightOutput
s = "00110011"6
s = "10101"4

Constraints:

1 ≤ s.length ≤ 10^5

s[i] is either '0' or '1'

Abstraction

They don't really want you to remove the duplicates. They want you to sort the uniques at the front, then return the length of the sorted part. Then, behind the scenes, they slice the array at the length you give them and the result of that is what they check.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force

    
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Edge cases
        "",
        "a",
        "0",
        "1",

        # Minimal valid cases
        "01",
        "10",

        # Simple repeating groups
        "0011",      # 2
        "000111",    # 3
        "00001111",  # 4

        # Uneven groups
        "00011",     # 2
        "001",       # 1
        "1100",      # 2

        # Alternating pattern
        "010101",    # 5
        "101010",    # 5

        # Long same-character blocks
        "000000",    # 0
        "111111",    # 0

        # Mixed patterns
        "00110011",  # 6
        "1011000",   # 3
        "00110",     # 3

        # Realistic patterns
        "0001110011", # 6
        "100111001",  # 4

        # Random patterns
        "110001110",  # 5
        "010011",     # 3
    ]

    for s in testCases:
        print(s, "=>", sol.countBinarySubstrings(s))

Solution 1: [Two Pointers] Count Groups Inner To Outer So 000111 Makes 3 Individual Groups - Two Pointers/K Pointer Variants

    def countBinarySubstrings(self, s: str) -> int:
        
        # Consecutive Group Lengths Approach (Two Pointers)

        # Idea:
        #   - Track consecutive characters as groups using two pointers.
        #   - prevGroupLength stores length of previous group
        #   - currGroupLength stores current group we scan as we match characters
        #
        #   - The number of groups found, depends on the shorter group:
        #
        #    0000110 =>  ["01", "0011"]
        #      00110 =>  ["01", "0011"]
        #   
        #   We add the min to the total as the length is the number of groups
        #    0001110 => ["01", "0011", "000111"]  
        #   makes 3 individual groups

        n = len(s)

        # Empty Check:
        if n <= 1:
            return 0

        # Initialize pointers
        
        # Scan for groups
        right = 1 

        # Start prev group at 0
        prevGroupLen = 0
        
        # Start curr group at 1, first element in array
        currGroupLen = 1
        
        # 
        res = 0

        # tc: iterate over n O(n)
        while right < n:

            # Extend current group
            if s[right] == s[right-1]:
                currGroupLen += 1

            # Form new group:
            else:
                
                # The number of groups found, depends on the shorter group:
                #  000011 =>  "01", "0011"
                #    0011 =>  "01", "0011"
                # We add the min to the total as the length is the number of groups
                # we have found:
                #   000111 => "01", "0011", "000111"
                res += min(prevGroupLen, currGroupLen)
                
                # Make current group, the new previous group
                prevGroupLen = currGroupLen

                # Start new group length
                currGroupLen = 1


            # Iterate group finder
            right += 1

        # Process the last group,
        # as no groups follow the last group to mark it
        res += min(prevGroupLen, currGroupLen)

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

271. String Encode and Decode ::1:: - Medium

Topics: Two Pointers, Design

Intro

Design an algorithm to encode a list of strings to a single string and a decode algorithm to decode the single string back to the original list of strings, strs[i] contains only UTF-8 characters.

InputOutput
["leet", "code", "love", "you"]["leet", "code", "love", "you"]
["we", "say", ":", "yes"]["we", "say", ":", "yes"]

Constraints:

string s contains only UTF-8 characters

Abstraction

Create an encode and decode function to encode a list to a string, and a string back to the original list.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
Splicing Delimiter with Catchup 2 pointersO(n * m)O(n * m)Linear iteration for encode and decode across all strings and chars dominates, O(n * m)Allocation for encoded and decoded strings dominates, O(n * m)
Base 10 Auxiliary Length Delimiter with 1 pointerO(n * m)O(n * m)Linear iteration for encode and decode across all strings and chars dominates, O(n * m)Allocation for encoded and decoded strings dominates, O(n * m)

Bug: Decoded Length + Delimiter (Grabbed str(len) instead int(len))

    def decode(self, encoded: str) -> List[str]:

        # space complexity: list of n strings O(n) each of n length O(n), leading to O(n^2)
        decoded = []
        left = 0

        # time complexity: iterate over representation of n strings O(n) each of n length O(n), leading to O(n^2)
        while left < len(encoded):

            # grab length prefix behind "#" delimiter
            right = left
            while encoded[right] != "#":
                right += 1

            # splicing the length of the string
            # BUG: 
            # length is a string, cannot use it to index or add
            # need to use int()
            length = encoded[left:right]

            # skip delimiter, point to start of string
            right += 1

            # splicing the string of 'length' characters
            # time complexity: splice over string n length O(n) for n iterations O(n), leading to O(n^2)
            decoded.append(encoded[right:right + length])

            # skip string, point to start of next len
            left = right + length

        # overall: time complexity O(n^2)
        # overall: space complexity O(n^2)
        return decoded

Bug: Decoded Base 10 doing += instead of = (half asleep)

    def decode(self, encoded: str) -> List[str]:
        
        # base 10 strategy 
        # or 2 pointer splice strategy

        decoded = []
        left = 0

        while left < len(encoded):

            # grab length before delim
            currLen = 0
            while encoded[left] != "#":

                # BUG:
                # we need to update currLen, not add to it
                currLen += (10*currLen) + int(encoded[left])
                left += 1
            
            # step past delim
            left += 1
            substring = []

            for _ in range(currLen):
                substring.append(encoded[left])
                left += 1

            decoded.append(''.join(substring))

        return decoded

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Edge cases with custom delim
        [],
        [""],
        ["a"],
        ["#"],
        ["##"],

        # Mixed length strings
        ["hello"],
        ["leet", "code"],
        ["abc", "defg", "h"],
        ["racecar", "level", "radar"],
    ]

    # Test encoding + decoding round trip
    for strs in testCases:

        print("Original:", strs)

        encoded = sol.encode(strs)
        decoded = sol.decode(encoded)

        print("Encoded:", encoded)
        print("Decoded:", decoded)
        print("***")

Solution 1: [Two Pointers] Splicing Delimiter With 2 Catchup pointers [TC Opt] - Two Pointers/Catchup

    def encode(self, strs: List[str]) -> str:

        # Note:
        # Appending to a list and joining once is more efficient than repeatedly
        # appending to a string. Strings are immutable, so each concatenation
        # creates a new string and copies all existing characters.
        # tc: list append + join: O(m), repeated string concat: O(m^2)

        # sc: n strings with m chars O(n * m)
        encoded = []

        # tc: iterate list O(n)
        for s in strs:
            
            # Note:
            # custom delimiter to mark start of string "{length}#" -> "5#""

            # tc: delimiter length proportional to log10(m) ~= O(1)
            encoded.append(str(len(s)) )
            encoded.append("#")
            encoded.append(s)
    
        # overall: tc O(n * m)
        # overall: sc O(n * m)
        return ''.join(encoded)


    def decode(self, encoded: str) -> List[str]:

        # sc: n strings with m chars O(n * m)
        decoded = []
        left = 0

        # tc: iterate over encoded O(n * m)
        while left < len(encoded):

            # set right to start of length prefix
            right = left

            # tc: log 10 (m) ~= O(1)
            # shift right until pointing to delimiter
            while encoded[right] != "#":
                right += 1

            # after:
            # [ 2 # h i ... ]
            #   ^ ^
            #   l r 

            # slice out string length
            length = int(encoded[left:right])

            # skip delimiter, point to start of string
            right += 1

            # after:
            # [ 2 # h i ... ]
            #   ^   ^
            #   l   r 

            # tc: slice out substring of length m 
            decoded.append(encoded[right:right + length])
           
            # set left to start of next custom delimiter
            left = right + length
            
            # after:
            # [ 2 # h i 3 # b y e ...]
            # [ 0 1 2 3 4 5 6 7 8 ...]
            #       ^   ^
            #       r   l

        # overall: tc O(n * m)
        # overall: sc O(n * m)
        return decoded
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Encode loopO(n * m)O(n * m)Iterate n strings of m length dominates, O(n * m)Allocation for encoded string dominates, O(n * m)
Decode loopO(n * m)O(n * m)Iterate over encoded string of n strings of m length dominates, O(n * m)Allocation for decoded list of n strings of m length O(n * m)
OverallO(n * m)O(n * m)Linear iteration for encode and decode across all strings and chars dominates, O(n * m)Allocation for encoded and decoded strings dominates, O(n * m)

Solution 2: [Two Pointers] Base 10 Auxiliary Length Delimiter With 1 pointer [TC Opt] - Two Pointers/One Pointer with Auxiliary State

    def encode(self, strs: List[str]) -> str:

        # Note:
        # Appending to a list and joining once is more efficient than repeatedly
        # appending to a string. Strings are immutable, so each concatenation
        # creates a new string and copies all existing characters.
        # tc: list append + join: O(m), repeated string concat: O(m^2)

        # sc: for n strings O(n) store n characters O(n), leading to O(n^2)
        encoded = []

        # tc: iterate over list of strings n length O(n)
        for s in strs:
            
            # Note:
            # custom delimiter to mark start of string "{length}#" -> "5#""

            # tc: delimiter length proportional to log10(m) ~= O(1)
            encoded.append(str(len(s)) )
            encoded.append("#")
            encoded.append(s)
        
        # overall: tc O(n^2)
        # overall: sc O(n^2)
        return ''.join(encoded)


    def decode(self, encoded: str) -> List[str]:

        # sc: list of n strings O(n) each of n length O(n), leading to O(n^2)
        decoded = []
        left = 0

        # tc: iterate over representation of n strings O(n) each of n length O(n), leading to O(n^2)
        while left < len(encoded):

            # grab length prefix behind "#" delimiter
            currLen = 0
            while encoded[left] != "#":

                # grabbing value while calculating base 10 of prev
                currLen = currLen * 10 + int(encoded[left]) 
                left += 1

            # skip delimiter, point to start of string
            left += 1  

            # after:
            # [ 2 # h i ... ]
            #       ^
            #       l  

            # tc: iterate string O(n) for n delimiters O(n), O(n^2)
            substring = []
            for _ in range(currLen):

                # grab string of currLen
                substring.append(encoded[left])
                left += 1
            
            # left is set to start of next length

            # after:
            # [ 2 # h i 3 # b y e ...]
            # [ 0 1 2 3 4 5 6 7 8 ...]
            #           ^
            #           l

            # add string to decoded list of strings
            decoded.append(''.join(substring))


        # overall: tc O(n^2)
        # overall: sc O(n^2)
        return decoded
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Encode loopO(n * m)O(n * m)Iterate n strings of m length dominates, O(n * m)Allocation for encoded string dominates, O(n * m)
Decode loopO(n * m)O(n * m)Iterate over encoded string of n strings of m length dominates, O(n * m)Allocation for decoded list of n strings of m length O(n * m)
OverallO(n * m)O(n * m)Linear iteration for encode and decode across all strings and chars dominates, O(n * m)Allocation for encoded and decoded strings dominates, O(n * m)

189. Rotate Array ::3:: - Medium

Topics: Array, Math, Two Pointers, Graph Theory

Intro

Given an integer array nums, rotate the array to the right by k steps, where k is non-negative. Follow up: Try to come up with as many solutions as you can. There are at least three different ways to solve this problem. Could you do it in-place with O(1) extra space?

numsOutput
nums = [1,2,3,4,5,6,7], k = 3[5,6,7,1,2,3,4]
nums = [-1,-100,3,99], k = 2[3,99,-1,-100]

Constraints:

1 ≤ nums.length ≤ 10^5

-2^31 ≤ nums[i] ≤ 2^31 - 1

0 ≤ k ≤ 10^5

Abstraction

Rotate!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force (all triplets comparison)

Find the Bug:

Test Cases

if __name__ == "__main__":

    sol = Solution()

    testCases = [

        # Edge cases
        ([1], 1),
        ([1], 5),

        # Small arrays
        ([1,2], 1),
        ([1,2], 2),
        ([1,2], 3),

        # Classic rotation patterns
        ([1,2,3,4,5,6,7], 3),
        ([1,2,3,4,5,6,7], 1),
        ([1,2,3,4,5,6,7], 7),

        # k greater than length
        ([1,2,3,4,5], 8),
        ([1,2,3,4,5], 12),

        # Reverse pattern
        ([7,6,5,4,3,2,1], 2),

        # Already rotated
        ([3,4,5,6,7,1,2], 2),

        # Duplicate values
        ([1,1,1,1,2,2,2], 3),

        # Mixed values
        ([10,20,30,40,50], 2),

        # Realistic dataset
        ([9,8,7,6,5,4,3,2,1], 4)
    ]

    for (nums, k) in testCases:

        arr = nums.copy()
        sol.rotate(arr, k)
        print(f"k = {k}: {arr}")

Solution 1: [Two Pointer] Extra Array With Direct ReIndexing - Two Pointers/K Pointer Variants

    def rotate(self, nums: List[int], k: int) -> None:
        
        # Array Re Indexing On Extra Array

        # Substring Representation:
        #   - Each element moves to (i + k) % n
        #   - We avoid overwriting the original array
        #     before mapping is complete by moving nums to extra array

        n = len(nums)

        # If k is larger than n,
        # just mod it so its relative to the actual array size
        k = k % n

        # sc: O(n) for extra array
        rotatedCopy = [0] * n

        # Calculate all the new indexes and move nums
        # tc: iterate over n O(n)
        for indexCandidate in range(n):

            # Shift index i to its new index
            newIndex = (indexCandidate + k) % n

            # Put num in corresponding new index
            rotatedCopy[newIndex] = nums[indexCandidate]

        # Overwrite original array with shifted extra array
        # tc: iterate over n O(n)
        for i in range(n):
            nums[i] = rotatedCopy[i]

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

Solution 2: [Two Pointer] Modular Cycle Traversal Array With Direct ReIndexing - Two Pointers/K Pointer Variants

    def rotate(self, nums: List[int], k: int) -> None:
        
        # Cyclic Replacements (Modular Cycle Traversal)

        # Idea:
        #   - Each element moves to (i + k) % n
        #   - This movement creates a cycle that we can follow as 
        #     we rotate elements 

        # Cycle:
        #   - Think of the cycle as a circle with nodes
        #   - We can step through the cycle, going from node to node
        #     as we place nums in their new nodes
        
        #   - The circle can have multiple cycles:
        #      0 => 2 => 0
        #      1 => 3 => 1
        #     or have a single cycle:
        #      0 => 2 => 4 => 1 => 3 => 0
        #     which is why we need the inner outer while loop to make sure:
        #        - complete the current cycle
        #        - complete all cycles
        
        n = len(nums)

        # If k is larger than n,
        # just mod it so its relative to the actual array size
        k = k % n

        # Total num of elements we have moved to new indexes
        numsMoved = 0  

        # Start of the first cycle
        currCycleEntryPoint = 0

        # tc: shift n elements O(n)
        while numsMoved < n:

            # First shifted index will be the index at the cycle entry point
            indexToShift = currCycleEntryPoint

            # First carried value will be the value at the cycle entry point
            carriedValue = nums[currCycleEntryPoint]

            # While we have not reached the beginning of the current cycle
            while True:

                # Cycle Formula:

                # shift the index 
                shiftedIndex = indexToShift + k
                
                # make sure it does go out of bounds
                inBoundsIndex = shiftedIndex % n

                # Placed num we are carrying into its new rotated position
                # The displaced number becomes our new carry
                nums[inBoundsIndex], carriedValue = carriedValue, nums[inBoundsIndex]

                # Set new index, as next index we are calculating the new position for
                indexToShift = inBoundsIndex

                # Number of numbers we have replaced
                numsMoved += 1

                # If we returned to the current cycle entry point, cycle is complete
                if currCycleEntryPoint == indexToShift:
                    break

            # Move to the next potential cycle entry point
            currCycleEntryPoint += 1

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

Solution 3: [Two Pointer] Reversal Trick - Two Pointers/Algorithm

    def rotate(self, nums: List[int], k: int) -> None:

        # Two Pointers (Reversal Trick Technique)

        # Key Insight:
        #   Reverse entire array
        #         [7,6,5,4,3,2,1]
        #   Reverse first k elements
        #         [5,6,7,4,3,2,1]
        #   Reverse remaining n-k elements
        #         [5,6,7,1,2,3,4]

        # This achieves rotation in-place.

        n = len(nums)

        # If k is larger than n,
        # just mod it so its relative to the actual array size
        k = k % n
        
        # Helper: reverses an array
        def reverse(left, right) -> None:

            # tc: iterate over n O(n)
            while left < right:

                # swap elements at index
                nums[left], nums[right] = nums[right], nums[left]

                # shrink towards middle
                left += 1
                right -= 1

        # Reverse entire array
        reverse(0, n - 1)

        # Reverse first [0, k] elements
        reverse(0, k - 1)

        # Reverse second [k, n] elements
        reverse(k, n - 1)

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

11. Container With Most Water ::1:: - Medium

Topics: Array, Two Pointers, Greedy

Intro

You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]). Find two lines that together with the x-axis form a container, such that the container contains the most water. Return the maximum amount of water a container can store

numsOutput
[1,8,6,2,5,4,8,3,7]49
[1,1]1

Constraints:

trapped water involves the space between and including two walls (bars). width = (right - left)

Abstract

We need to calculate the container with the most water.

The integer value represents the height of a side of a container, and the distance between two sides is calculated using the index of the array

We can iterate over the array calculating the sides of the container that will give us the most water.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
Brute Force
Two Pointer Early Break

Brute Force

    def maxArea(self, height: List[int]) -> int:

        n = len(height)
        maxWater = 0
        
        # time complexity: iterate over array of n length O(n)
        for i in range(n - 1):

            # time complexity: iterate over array of n length for each outer iteration O(n^2)
            for j in range(i + 1, n):

                # calculate current water
                currWater = min(height[i], height[j]) * (j - i)
                maxWater = max(maxWater, currWater)
        
        # overall: time complexity O(n^2)
        # overall: space complexity O(1)
        return maxWater
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Outer loopO(n)O(1)Iteration over list of n length O(n)No additional memory allocation for iteration O(1)
Inner loopO(n2)O(1)Iteration over list of n length per iteration in outer loop O(n2)No additional memory allocation for iteration O(1)
Curr WaterO(1)O(1)Water height operation takes constant O(1)No additional memory allocation O(1)
OverallO(n2)O(1)Inner loop dominates leading to O(n2)No additional memory allocation leading to O(1)

Solution 1: [Greedy] Opposite Ends Pointer With Greedy Shift by BinarySearch Modification [TC Opt] - Two Pointers/Opposite Ends

    def maxArea(self, height: List[int]) -> int:
        
        # boundaries
        left, right = 0, len(height)-1
        maxWater = 0

        # tc: iteration n O(n)
        while left < right:

            # grab smaller height between outside pointers
            smallerHeight = min(height[left], height[right])

            # Width includes walls 
            # According to test case: [1, 1] is 1 water 
            # Thus, width = index 1 - index 0 = 1
            # Or, width = rightIndex - leftIndex
            width = (right - left)

            # Water is limiting height * width
            currWater = smallerHeight * width
            
            # Compare to global max water
            maxWater = max(maxWater, currWater)

            # Greedy Shift:
            # As we move pointers inwards, width is guaranteed to shrink
            # Thus, the only way to beat our currWater is with a taller height
            # So we can continue to move our pointers until we hit a bigger height,
            # as we do not care about smaller or equal heights
            # In other words, we only need to stop and check the max water
            # at a taller heights

            # tc: iteration n list O(n)
            if height[left] < height[right]:
                
                # Iterate past current left/right wall combination
                left += 1
                
                # Greedy Shift:
                # We only need to stop at taller heights 
                while left < right and height[left] < smallerHeight:
                    left += 1 

            else:

                # Iterate past current left/right wall combination
                right -= 1

                # Greedy Shift:
                # We only need to stop at taller heights 
                while left < right and height[right] < smallerHeight:
                    right -= 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxWater
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Iteration over list of n length with two pointers O(n)No additional memory allocation for iteration O(1)
Curr WaterO(1)O(1)Water height operation takes constant O(1)No additional memory allocation O(1)
OverallO(n)O(1)Iteration over list of n length dominates leading to O(n)No additional memory allocation O(1)

881. Boats to Save People ::1:: - Medium

Topics: Array, Two Pointers, Greedy, Sorting

Intro

You are given an array people where people[i] is the weight of the ith person, and an infinite number of boats where each boat can carry a maximum weight of limit. Each boat carries at most two people at the same time, provided the sum of the weight of those people is at most limit. Return the minimum number of boats to carry every given person.

numsOutput
people = [1,2], limit = 31
people = [3,2,2,1], limit = 33
people = [3,5,3,4], limit = 54

Constraints:

1 ≤ people.length ≤ 5 * 10^4

1 ≤ people[i] ≤ limit ≤ 3 * 10^4

Abstract

Boats!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Bug:

Solution 1: [Two Pointers] Greedy Pairing Lightest Heaviest Opposite Ends After Sorting - Two Pointers/Opposite Ends

    def numRescueBoats(self, people: List[int], limit: int) -> int:
        
        # Greedy + Two Pointers (Opposite Direction)

        # Substring Representation:
        #   - Sort people by weight
        #   - left:  lightest remaining person
        #   - right: heaviest remaining person
        #
        # Greedy Insight:
        #   - If the heaviest person cannot pair with the lightest,
        #     they cannot pair with anyone.
        #   - So the heaviest must go alone.
        #   - Otherwise, pair them together.
        #
        # Goal:
        #   - Minimize number of boats


        # tc: O(n log n) due to sorting
        people.sort()

        # Two Pointer Variables
        # sc: O(1) (ignoring sort space)
        left = 0
        right = len(people) - 1

        # total boats we need
        boats = 0

        # tc: O(n)
        while left <= right:

            # Check the combined weight
            combinedWeight = people[left] + people[right]

            # Greedy:
            # Check: if the lightest person fits with the heaviest person
            # Implies: then the lightest can pair with the heaviest
            if people[left] + people[right] <= limit:

                # Board the lightest person
                left += 1

            # The heaviest person will always board,
            # it just depends whether the lightest person will board with them
            right -= 1

            # Increase number of boats
            boats += 1


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

42. Trapping Rain Water ::3:: - Hard

Topics: Array, Two Pointers, Dynamic Programming, Stack, Monotonic Stack

Intro

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

heightOutput
[0,1,0,2,1,0,1,3,2,1,2,1]6
[4,2,0,3,2,5]9

Constraints:

trapped water involves the space between two walls (bars). width = (right - left - 1)

Abstraction

To calculate trapped water:

[1, 0, 1] -> 1 unit of water

[1, 0, 2] -> 1 unit of water

[1, 0, 0] -> 0 units of water

Definition of trapped water is: [ min(left, right) - currHeight ]

Now we need a way to traverse the array that allows us to take advantage of this pattern.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
Brute Force
Two Pointer Early Break

Brute Force

    def trap(self, height: List[int]) -> int:
        n = len(height)
        water = 0

        # time complexity: iterate over list of n length o(n)
        for i in range(n):

            # Use two pointers to calculate leftMax and rightMax
            leftMax, left = 0, i
            rightMax, right = 0, i

            # time complexity: iterate over left hand side of list n length per outer iteration O(n^2)
            while left >= 0:
                leftMax = max(leftMax, height[left])
                left -= 1

            # time complexity: iterate over right hand side of list n length per outer iteration O(n^2)
            while right < n:
                rightMax = max(rightMax, height[right])
                right += 1

            # curr water trapped for curr bar i
            water += min(leftMax, rightMax) - height[i]

        # overall: time complexity O(n^2)
        # overall: space complexity O(1)
        return water
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
Iterate
Left/Right max
Curr Water
Overall

Bugs

Find the Bug: Complete misunderstanding of water heights (haha)

    def trap(self, height: List[int]) -> int:
        
        # dynamic programming
        n = len(height)

        leftMax = [0] * n
        rightMax = [0] * n
        water = 0

        # INCORRECT:
        # what are you deranged?!?!?
        # why do you need a separate max!
        # its just the previous max vs the current height
        left = 0
        for i in range(1, n):
            left = max(left, height[i-1])
            leftMax[i] = left

        # INCORRECT:
        # again! only a deranged person would create a separate 
        # max variable to compare against a max array
        right = 0
        for i in range(n-2, -1, -1):
            right = max(right, height[i+1])
            rightMax[i] = right

        for i in range(n):
            water += min(rightMax[i], leftMax[i]) - height[i]

        return water

Find the Bug: Two Pointers Early Break

    def trap(self, height: List[int]) -> int:
        
        L, R = 0, len(height) - 1
        water = 0

        # setting curr left/right max to ends of list
        leftMax, rightMax = height[L], height[R]

        # time complexity: iterate over list of n length with two pointers O(n)
        while L < R:
            
            # grabbing weak link
            if height[L] < height[R]:
                # INCORRECT: skips water trapped at previous position before the pointer moved
                L += 1

                # INCORRECT: updating leftMax *before* calculating trapped water causes error
                leftMax = max(leftMax, height[L])

                # INCORRECT: water calculation uses updated leftMax prematurely
                water += min(leftMax, rightMax) - height[L]

            # grabbing weak link
            else: 
                # INCORRECT: skips water trapped at previous position before the pointer moved
                R -= 1

                # INCORRECT: updating rightMax *before* calculating trapper water causes error
                rightMax = max(rightMax, height[R])

                # INCORRECT: water calculation uses updated rightMax prematurely
                water += min(leftMax, rightMax) - height[R]
        
        # overall: time complexity O(n)
        # overall: space complexity O(1)
        return water

Find the Bug: Not checking if left wall exists after pop

    def trap(self, height: List[int]) -> int:

        stack = []  
        water = 0

        for i in range(len(height)):

            while stack and height[i] > height[stack[-1]]:

                depthCandidateIndex = stack.pop() 

                # INCORRECT:
                # we are assuming a left wall exists,
                # when the stack might just be 1 element which we just popped
                # so it could be empty
                # missing:
                # if not stack:
                #   break 

                rightWallIndex = i
                leftWallIndex = stack[-1]
                distance = rightWallIndex - leftWallIndex - 1

                rightHeight = height[rightWallIndex]
                depthHeight = height[depthCandidateIndex]
                leftHeight = height[leftWallIndex]

                boundedHeight = min(rightHeight, leftHeight) - depthHeight
                water += distance * boundedHeight

            stack.append(i)

        return water

Find the Bug: Overwriting variables accidentally

    def trap(self, height: List[int]) -> int:
        stack = []  
        water = 0

        for i in range(len(height)):

            while stack and height[i] > height[stack[-1]]:

                depthCandidate = stack.pop() 

                if not stack:
                    break

                leftIndex = stack[-1]
                rightIndex = i
                distance = rightIndex - leftIndex - 1

                leftHeight = height[leftIndex]
                rightHeight = height[rightIndex]
                depthHeight = height[depthCandidate]

                # INCORRECT:
                # TypeError: 'int' object is not subscriptable
                # because are overwriting 'height'
                # instead use boundedHeight 
                height = min(leftHeight, rightHeight) - depthHeight

                water += distance * height
            
            stack.append(i)

        return water

Find the Bug: Bad For Loop syntax

    def trap(self, height: List[int]) -> int:
        
        n = len(height)
        water = 0
        
        leftMax = [0] * n
        rightMax = [0] * n

        leftMax[0] = height[0]
        for i in range(1, n):
            leftMax[i] = max(leftMax[i - 1], height[i])
        
        rightMax[n - 1] = height[n - 1]
        # INCORRECT:
        # missing ',' between -1
        # for loop says to go till -2
        # for loop says: for i in range(n - 2, -2)
        for i in range(n - 2, -1 -1):
            rightMax[i] = max(rightMax[i + 1], height[i])

        for i in range(n):
            water += min(rightMax[i], leftMax[i]) - height[i]

        return water

Solution 1: [Monotonic] [Two Pointers] 2 Inner/Outer Pointers Traversal Creating Bound Buckets By Monotonic Opposite Ends Pointer Shift Modification - Two Pointers/K Pointer Variants

    def trap(self, height: List[int]) -> int:

        # Bound Buckets:
        # [4, 0, 2, 6, 3, 5]: Monotonic quality defines buckets 
        #    Bucket from index 0-3 with heights of [4, 0, 2, 6]  (left to right monotonic increasing bucket)
        #    Bucket from index 3-5 with heights of [6, 3, 5]     (left to right monotonic decreasing bucket)
        #
        #    When we use 4 and 6 as the left and right bucket walls, bucket will catch any water up to and including 4
        #    When we use 6 and 5 as the left and right bucket walls, bucket will catch any water up to and including 5

        #   Bucket 1: w     Bucket 2: m
        #       --------- ++++++
        #                *     
        #                *  m  *
        #       *  w  w  *  m  *
        #       *  w  w  *  *  *
        #       *  w  *  *  *  *
        #       *  w  *  *  *  *
        #      ------------------
        #       0  1  2  3  4  5

        # Types Of Graphs
        # full width:  [4, 0, 2, 1, 3, 5], left < right for entire array (bucket from 5 -> 6)
        # split width: [4, 0, 2, 6, 3, 5], left < right broken at some point (bucket from 5 -> 9, 9 -> 6)

        #  Diagram 1:                  Diagram 2:
        #       ----------------            --------- ++++++
        #                                            *     
        #                      *                     *  m  *
        #       *  w  w  w  w  *            *  w  w  *  m  *
        #       *  w  w  w  *  *            *  w  w  *  *  *
        #       *  w  *  w  *  *            *  w  *  *  *  *
        #       *  w  *  *  *  *            *  w  *  *  *  *
        #      ------------------          ------------------
        #       0  1  2  3  4  5            0  1  2  3  4  5
        #       ^              ^            ^              ^
        #       L              R            L              R

        # Creating Buckets: 
        # Outer Pointers: serve as left and right most wall for buckets
        # Inner Pointers: traverse inward from both ends to verify if monotonic quality (our bucket definition) is kept or broken
        
        # Water Trapping: 
        # We use implications between outer and inner pointers
        # to find the limited wall for the current water height

        # Bucket Depth via Height Implications:
        # See diagrams below

        # outer pointers
        outerLeftMax, outerRightMax = 0, 0
        
        # inner pointers
        left, right = 0, len(height) - 1
        
        water = 0

        # tc: iterate over n O(n)
        while left < right:
            
            # We grab the lower height, so we know that 
            # this lower height is bounded by the taller height on one of its sides,
            # we then are just left with finding the bound on the opposite side if one exists

            # Check: Left wall is shorter than Right wall
            # Implies: Left wall is covered by Right wall
            # Implies: We have a right wall bound
            # Implies: We have to check if a left wall bound exists
            if height[left] < height[right]:    
                
                # Implies:
                #
                #                   *   
                #                   *   
                #             *     *   
                #       ?     *     *   
                #      ------------------
                #      LM     L     R   

                # Check: Left wall is taller than current Left Max
                # Implies: New tallest left wall found
                # Implies: There is no left wall bound as left is taller than everything to the left,
                #          we cannot catch water
                # Then: Update left max
                if height[left] >= outerLeftMax:

                    # Implies:
                    #
                    #                   *   
                    #                   *   
                    #             *     *   
                    #       *     *     *   
                    #      ------------------
                    #      LM     L     R   
                    outerLeftMax = height[left]

                # Check: Left wall is shorter than left max
                # Implies: Left wall is covered by left max
                # Implies: There is a left wall bound, something to the left is taller,
                #          we can catch water
                # Invariant: There already exists a right bound, 
                #            and now we know height[left] < height[outerLeftMax] < height[right]
                # Implies: Water we catch is bounded by outerLeftMax
                # Then: Water caught is height difference between outerLeftMax and left
                else:
                    
                    # Implies:
                    #
                    #                   *   
                    #       *     w     *   
                    #       *     *     *   
                    #       *     *     *   
                    #      ----------------
                    #      LM     L     R   
                    water += outerLeftMax - height[left]

                # shift pointer
                left += 1

            # Check: Right wall is shorter than Left wall
            # Implies: Right wall is covered by Left Wall
            # Implies: We have a left wall found
            # Implies: We have to check if a right wall bound exists
            else:

                # Implies:
                #
                #       *              
                #       *                
                #       *     *         
                #       *     *     ?   
                #      ----------------
                #       L     R     RM   

                # Check: Right wall is taller than Right Max
                # Implies: New tallest right wall found
                # Implies: There is no right wall bound as right is taller than everything to the right,
                #          we cannot catch water
                # Then: update right max
                if height[right] >= outerRightMax:

                    # Implies:
                    #
                    #       *              
                    #       *                
                    #       *     *         
                    #       *     *     *   
                    #      ----------------
                    #       L     R     RM   
                    outerRightMax = height[right]

                # Check: Right wall is shorter than right Max
                # Implies: Right Wall is covered by right Max
                # Implies: There is a right wall bound, something to the right is taller,
                #          we can catch water
                # Invariant: There already exists a left bound,
                #            and now we know height[right] < height[outerRightMax] < height[left]
                #
                # Implies: Water we catch is bounded by outerRightMax
                # Then: Water caught is height difference between outerRightMax and left
                else:

                    # Implies:
                    #
                    #       *              
                    #       *     w     *     
                    #       *     *     *    
                    #       *     *     *   
                    #      ----------------
                    #       L     R     RM   
                    water += outerRightMax - height[right]

                # shift pointer
                right -= 1

        # overall: tc O(n)
        # overall: sc O(1)
        return water
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Two pointer iteration over list of n length O(n)No additional memory allocation required for iteration O(1)
Comparing HeightsO(1)O(1)Height lookup and comparison in constant O(1)No additional memory allocation for lookup or comparison O(1)
Water CalculationO(1)O(1)Water calculation and sum in constant O(1)No additional memory allocation for water calculation or sum O(1)
OverallO(n)O(1)Two pointer iteration over list of n length dominates, leading to O(n)No additional memory allocation required, leading to O(1)

Solution 2: [Monotonic] [Stack] Dragging Right Wall Height Over The Array And Catching Water With Depth Candidates And Left Wall By Building Monotonic Stack - Two Pointers/Algorithm

    def trap(self, height: List[int]) -> int:

        # Monotonic Stack: 
        # A stack that maintains monotonic decreasing heights
        # When monotonic decreasing rule breaks, curr height will serve as right wall,
        # and if stack is non empty, top of stack will serve as depth, 
        # and second top of stack - 1 will serve as left wall
        # we then pop off the top of the stack until the monotonic rule comes back into play,
        # or the right wall becomes the new leftmost wall

        # Monotonic Stack Of Heights:
        #
        #      *                                  *                    *                 *              *
        #      *                   *              *            *       *         *       *  w   *       *  *
        #      *  *                *              *  *         *       *  *  w   *       *  *   *       *  *
        #      *  *                *              *  *         *       *  *  w   *       *  *   *       *  *
        #      *  *  *             *              *  *  *  w   *       *  *  *   *       *  *   *       *  *
        #      *  *  *  *     +    *       ==>    *  *  *  *   *   =>  *  *  *   *   =>  *  *   *  ==>  *  *
        #     ------------   new  ---  pop off   ------------ ---     --------- ---     ------ ---     ------
        #    older --> newer        left candidates                                                  final result
        #                                              1 water            2 water        1 water
        #                                                caught            caught         caught

        # Monotonic stack to store indices
        stack = []  
        water = 0

        # We will iterate over list and pushing and popping each bar only once
        # tc: iterate over n O(n)
        for i in range(len(height)):

            # Goal:
            # To find a left bound, right bound, and depth candidate to catch water at the current index
            # We will find these candidates at feeder locations:
            # Right Bound Candidate: current index during iteration
            # Left Bound Candidate:  on the stack
            # Depth Candidate:       on the stack

            # Check: If stack is non empty, a depth candidate exists,
            # Check: If current height[i] breaks monotonic decreasing order (is taller than top of stack), 
            #        its viable as a new right wall bound
            # Implies: While right wall bound breaks monotonic decreasing, it serves as a right bound
            #           for whatever is on the top of the stack
            # Implies: We need to check if a left wall candidate exists (stack needs to have at least 2 elements)

            # implies: stack is kept in monotonic decreasing order
            # implies: when monotonic decreasing breaks, we have found right wall
            # implies: we have a depth candidate
            while stack and height[stack[-1]] < height[i]:
                
                depthCandidateIndex = stack.pop() 

                # Implies:
                #
                #                   *  
                #                   *   
                #             *     *   
                #       ?     *     *   
                #      ------------------
                #      LB   depth   RB   
                #           pop()

                # height[i]: right wall
                # pop stack[-1]: depth candidate
                # peak stack[-2]: ? 

                # Check:
                # If stack has at least 1 other elem,
                # it will serve as the left wall bound

                # While Loop:
                # We remove the depth candidate from stack, 
                # We check if stack is non-empty, then a left bound exists
                # We continue this, popping depth candidates, and using left bounds
                # as long as the right wall bound is taller than the top of the stack.
                # Imagine dragging the right wall over the monotonic stack,
                # and while its taller than all depth candidates, we add the corresponding water.

                # Exit Loop:
                # Once the right wall is shorter than the top of the stack, 
                # it can no longer serve as a right bound, 
                # so we simply add it to the stack so it itself can serve as a future depth candidate


                # Check: if stack empty after pop, (we popped the only element that was on the stack)
                # Implies: No left wall bound exists, we cannot trap water, add right wall bound to stack
                if not stack:
                    break

                # Left wall exists:
                # We have a left and right bound, so we can catch water
                # Water is bound between the shorter of the left and right wall bounds

                 # Implies:
                #
                #       ?           ?   
                #       *           *   
                #       *     *     *   
                #       *     *     *   
                #      ------------------
                #      LB   depth   RB   
                #           pop()

                # Summary:
                # height[i]: right wall
                # popped depthCandidate: depth
                # peak stack[-1]: left wall
                # width = (right wall bound index - left wall bound index - 1)
                
                # Width:
                rightWallIndex = i
                leftWallIndex = stack[-1]
                width = rightWallIndex - leftWallIndex - 1

                # Water Caught:
                rightWallBoundHeight = height[rightWallIndex]
                leftWallBoundHeight = height[leftWallIndex]
                bucketWallBoundHeight = min(rightBoundHeight, leftBoundHeight)

                depthHeight = height[depthCandidateIndex]

                # Water Caught:
                # Smaller bucket wall bound height - depth height = water getting caught

                # Implies:
                #
                #                   *   
                #       *     w     *         1 water
                #       *     *     *          caught
                #       *     *     *   
                #      ------------------
                #      LB   depth   RB   
                #           pop()

                waterCaught = bucketWallBoundHeight - depthHeight

                # Width Edge Case:
                # [5, 0, 0, 2]
                # in this case, (0, 0, 2)
                # left wall = 0, depth = 0, right wall = 2
                
                # So no water captured fir (0, 0, 2)
                # So water is ignored until a left wall bound is eventually found (5, 0, 2) 
                # or its determined that no left wall bound exists

                # but then due to pop, (5, 0, 2)
                # left wall = 5, depth = 0, right wall = 2
                # so water captured based on distance

                # Index Edge Case:
                # We take the above into account by saving the indexes on the stack, instead of the heights
                
                # Originally: [5, 0, 0, 2]
                #              0  1  2  3
                
                # So we can do width = right - left - 1 = (3 - 0) - 1 = 2
                
                water += width * waterCaught

            # Implies: right wall allows monotonic decreasing is be valid
            # Implies: right wall is shorter than top of stack
            # Then: append right wall to stack to serve as future depth candidate
            stack.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return water
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Iterate over list of n length O(n)No additional memory allocation for iteration O(1)
Stack OperationsO(n)O(n)Each push() and pop() operation in constant O(1) but for n elements leading to O(1) * n, leading to O(n)Stack holding n elements O(n)
Heights comparisonsO(1)O(1)Each comparison in while loop in constant O(1)No additional memory allocation for comparison O(1)
Water CalculationO(1)O(1)Calculating distance and bounded height in constant O(1)No additional memory for distant or bounded height operations O(1)
OverallO(n)O(n)Iterating over list of n length dominates, leading to O(n)Stack growing for n elements dominates, leading to O(n)

Solution 3: [Dynamic Programming] Creating Bucket Left Right Boundaries By Dynamic Programming Tracking Max Height Bucket Bounds Encountered L To R and R to L - Two Pointers/Algorithm

    def trap(self, height: List[int]) -> int:
        
        # Dynamic Programming Concept:
        # Left Maximum Array: 
        #       Stores the maximum height encountered iterating from left to right,
        #       these will serve as the left wall bounds
        # Right Maximum Array: 
        #       Stores the maximum height encountered iterating from right to left,
        #       these will serve as the right wall bounds
        #
        # To avoid recomputing maximum heights repeatedly, 
        # we instead build the bucket walls as we iterate

        # Empty Check:
        n = len(height)
        if n == 0:
            return 0

        # Iteration Arrays:
        # Store max heights from perspective of iterating left to right and right to left for all indexes
        # leftMax[i]:  Maximum height from  0 -> i:    (iterating left to right)
        # rightMax[i]: Maximum height from  i <- n-1:  (iterating right to left)
        # sc: relative to input O(n)
        leftMax = [0] * n
        rightMax = [0] * n
        water = 0

        # First Max Left To Right:
        leftMax[0] = height[0]

        # Iterating Left To Right:
        # Compare previous max to current height
        # tc: iterate over n O(n)
        for i in range(1, n):
            previousMax = leftMax[i-1]
            leftMax[i] = max(previousMax, height[i])

        # First Max Right To Left:
        rightMax[n-1] = height[n-1]

        # Iterating Right To Left:
        # Compare previous max to current height
        # tc: iterate over n O(n)
        for i in range(n-2, -1, -1):
            previousMax = rightMax[i+1]
            rightMax[i] = max(previousMax, height[i])

        # Depth Calculation:
        # tc: iterate over n O(n)
        for i in range(n):

            # Bucket Wall Height:
            # The bucket is bounded by lower of 2 maxes (they represent the Left and Right bucket side heights)
            # Just subtract the lower height against the height of the bottom of the bucket
            water += min(leftMax[i], rightMax[i]) - height[i]

        # overall: tc O(n)
        # overall: sc O(n)
        return water
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
LeftMax CalculationO(n)O(1)Iterate over height list of n length O(n)Stores max height for current index for list of n length O(n)
RightMax CalculationO(n)O(n)Iterate over height list of n length O(n)Store max height for current index for list of n length O(n)
Water CalculationO(n)o(1)Iterate over max height list of n length O(n)No additional memory allocation for water calculation O(n)
OverallO(n)O(n)Iterating over height list dominates, leading to O(n)Memory allocation for leftMax and rightMax arrays dominates, leading to O(n)

27. Remove Element ::1:: - Easy

Topics: Array, Two Pointers

Intro

Given an integer array nums and an integer val, remove all occurrences of val in nums in-place. The order of the elements may be changed. Then return the number of elements in nums which are not equal to val. Consider the number of elements in nums which are not equal to val be k, to get accepted, you need to do the following things: Change the array nums such that the first k elements of nums contain the elements which are not equal to val. The remaining elements of nums are not important as well as the size of nums. Return k. Custom Judge: The judge will test your solution with the following code: int[] nums = [...]; // Input array int val = ...; // Value to remove int[] expectedNums = [...]; // The expected answer with correct length. // It is sorted with no values equaling val. int k = removeElement(nums, val); // Calls your implementation assert k == expectedNums.length; sort(nums, 0, k); // Sort the first k elements of nums for (int i = 0; i < actualLength; i++) assert nums[i] == expectedNums[i]; If all assertions pass, then your solution will be accepted.

heightOutput
nums = [3,2,2,3], val = 32, nums = [2,2,,]
nums = [0,1,2,2,3,0,4,2], val = 25, nums = [0,1,4,0,3,,,_]

Constraints:

0 ≤ nums.length ≤ 100

0 ≤ nums[i] ≤ 50

0 ≤ val ≤ 100

Abstraction

stuff!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force

    
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointers] Optimal Two Pointers - Two Pointers/K Pointer Variants

    def removeElement(self, nums: List[int], val: int) -> int:
        
        # Two Pointer Pattern (Unordered Removal)
        
        # Idea:
        #   - Order of elements does NOT matter.
        #   - If we find val:
        #       swap with last valid element.
        #   - Shrink array boundary.
        
        # This avoids unnecessary shifts.

        # Left: scans for the target so we can send it to the end of the list,
        #       all elements to the left of left are valid nums verified do not equal target
        left = 0
        
        # Right: Write pointer to send all target elements we want to remove,
        #        all elements to the right of right are targets we have moved
        right = len(nums) - 1

        # Process Array:
        # left:   scans array
        # right:  boundary of valid elements

        # tc: iterate over n O(n)
        while left <= right:

            # Found target: move to end of list
            if nums[left] == val:

                # Switch target with write pointer
                nums[left] = nums[right]

                # Shrink valid boundary:
                # invalidates target we just moved
                right -= 1

            # Current element is safe, iterate left target scanner
            else:
                # keep element
                left += 1

        # Right:
        # is pointing at valid non target, since its 0 indexed we need to add 1

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

28. Find the Index of the First Occurrence in a String ::2:: - Easy

Topics: Two Pointers, String, String Matching

Intro

Given two strings needle and haystack, return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

heightOutput
haystack = "sadbutsad", needle = "sad"0
haystack = "leetcode", needle = "leeto"-1

Constraints:

1 ≤ haystack.length, needle.length ≤ 10^4

haystack and needle consist of only lowercase English characters.

Abstraction

stuff!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force

    
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointers] Optimal Two Pointers - Two Pointers/K Pointer Variants

    def strStr(self, haystack: str, needle: str) -> int:
        
        # Two Pointer Pattern (Substring Search)
        
        # Idea:
        #   - Each index in haystack is a potential starting point
        #   - From that starting index, attempt to match needle character-by-character.
        #   - If all characters match → return that starting index.
        #   - Otherwise shift starting index by one and repeat.
        
        n = len(haystack)
        m = len(needle)

        # Edge Case:
        # If needle is longer than haystack,
        # it is impossible for a match to exist.
        if m > n:
            return -1

        # Left Pointer:
        # Represents candidate starting index in haystack
        left = 0

        # We only need to check positions where
        # a full needle-length substring can exist
        while left <= n - m:

            # Right Pointer:
            # Traverses needle and compares against haystack
            right = 0

            # Compare characters while:
            #   1) We haven't matched entire needle
            #   2) Characters continue matching
            while right < m and haystack[left + right] == needle[right]:
                right += 1

            # If right reached m,
            # all characters matched successfully
            if right == m:
                return left

            # Otherwise, shift starting index forward
            left += 1

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

Solution 2: [Two Pointers] KPM Longest Prefix Suffix Array Needle And Haystack Pointers - Two Pointers/Algorithm

    def strStr(self, haystack: str, needle: str) -> int:
        
        # KMP Pattern Matching Algorithm (Knuth Morris Pratt)
        
        # KMP Pattern Matching:
        # A optimized string matching algorithm that avoids redundant character comparisons,
        # by tracking repeating patterns within the needle string.

        # LPS 
        #   = [0 0 1 2 0 1 2 3 4]
        #   = [a b a b c a b a b]   <= needle "ababcabab"
        #      0 1 2 3 4 5 6 7 8

        #    - - - - x 
        # [b a b a b a b a b c a  b  a  b  c  a  b  a  b]    <= haystack "bababababcababcabab"
        #  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
        #    - - - - x
        #    a b a b c   <= needle "ababcabab"

        # The trick is clear when C mismatches for the first time at this index:
        # Usually, we would need to restart the full search iteratively, 
        # since the check at index 1 failed, we check starting at 2, then 3, etc.
        
        # However, we have valuable information in the LPS table
        # We know that since we just mismatched C, we need to check if the previous letter
        # has a prefix match within our needle string.

        # LPS 
        #   = [0 0 1 2 0 
        #   = [a b a b c ...       <= needle "ababcabab" in LPS
        #      0 1 2 3 4 

        # What this tells us, is that 'b' has already been prefixed match,
        # so we can continue our check at index 2
        # and lets verify it:

        #    - - - - x 
        # [b a b a b a  ...        <= haystack "bababababcababcabab"
        #  0 1 2 3 4 5 
        #    - - - - x
        #    A B A B C

        # Its true, we can shift our matching needle and it will still be true:

        #        - - - 
        # [b a b a b a  ...        <= haystack "bababababcababcabab"
        #  0 1 2 3 4 5 
        #        - - -
        #        A B A B C

        # Idea:
        # When a mismatch occurs, 
        # we do NOT need to re-check characters that we already know match
        # We check the letter before the currently failed match,
        # and see if we can check skip using a previous prefix that matches

        # LPS Table (Longest Prefix Suffix):
        # The LPS table is the memory component of KMP
        # This allows KMP to achieve linear time complexity
        
        # Idea:
        #   - Instead of restarting matching after a mismatch,
        #   - Use prefix pattern structure to see if we can skip redundant comparisons.
        
        # This allows pattern pointer jumps within the needle string instead of full reset
        # to the beginning of the needle string everything a failure occurs


        # Edge Case:
        # If needle is empty → match at beginning
        if not needle:
            return 0

        n = len(haystack)
        m = len(needle)

        # Build LPS (Prefix Table)
        # store prefix jump skips for each char in needle string
        # sc: O(m)
        lps = [0] * m

        # length = length of previous longest prefix suffix ????
        length = 0

        # Start computing LPS from index 1
        # (LPS[0] is always 0 since single character has no proper prefix/suffix)
        i = 1

        # tc: iterate over needle string O(m)
        while i < m:

            # If characters match:
            # Extend current prefix length
            if needle[i] == needle[length]:

                length += 1
                lps[i] = length
                i += 1

            else:

                # If mismatch occurs:
                # Fall back to previous best prefix candidate
                if length != 0:
                    length = lps[length - 1]

                else:
                    # No valid prefix exists
                    lps[i] = 0
                    i += 1



        # KMP Search Phase

        # Two Pointer Variables:
        # i: scans the haystack for prefix matches
        # j: scans the needle for failure points and jumps to prefix skips
        i = 0
        j = 0

        # tc: iterate over haystack text O(n)
        while i < n:

            # If characters match: advance both needle and haystack pointers
            if haystack[i] == needle[j]:
                i += 1
                j += 1

            # If full pattern matched: return starting index
            if j == m:
                return i - j

            # If mismatch occurs:
            elif i < n and haystack[i] != needle[j]:

                # If pattern pointer can fall back using LPS
                if j != 0:
                    j = lps[j - 1]

                # Otherwise move text pointer forward
                # j is already pointed to beginning of needle string
                else:
                    i += 1


        # No match found

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

5. Longest Palindromic Substring ::3:: - Medium

Topics: Two Pointers, String, Dynamic Programming

Intro

Given a string s, return the longest palindromic substring in s.

InputOutput
"cbbd""bb"
"babad""bab" or "aba"

Constraints:

1 ≤ s.length ≤ 1000

s consists of only digits and English letters.

Abstraction

Find the longest palindrome in a string.

Bugs

Find the Bug: Did not create Expanded String

    def longestPalindrome(self, s: str) -> str:
        
        # INCORRECT:
        # did not create expandedStr
        # did not solve odd vs even palindrome problem
        # missing:
        # expandedStr = "#".join(f"^{s}$")
        
        center = 0
        right = 0
        n = len(s)
        p = [0] * n

        for i in range(1, n-1):
            
            # 1 
            mirror = (2*center) - i

            # 2 
            if i < right:
                p[i] = min(right-i, p[mirror])

            # 3
            while s[i + p[i] + 1] == s[i - p[i] - 1]:
                p[i] += 1

            # 4
            if i + p[i] > right:
                center = i
                right = i + p[i]
        
        (maxRadius, center) = max((maxRadius, i) for (i, maxRadius) in enumerate(p))
        start = (center-maxRadius) // 2 

        return s[start:start+maxRadius]

Find the Bug: Defined List instead of Slicing

    def longestPalindrome(self, s: str) -> str:
        
        def expandAroundCenter(left, right):
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1
                right += 1

            # INCORRECT:
            # defined list instead of slicing
            # should be: s[left+1:right]
            return s[left+1, right]
        
        n = len(s)
        maxPalin = ""

        for i in range(n):
            oddPalin = expandAroundCenter(i, i)
            evenPalin = expandAroundCenter(i, i+1)

            if len(oddPalin) > len(maxPalin):
                maxPalin = oddPalin
            if len(evenPalin) > len(maxPalin):
                maxPalin = evenPalin
                
        return maxPalin

Find the Bug: Bad iteration leading to out of bounds on string expansion

    def longestPalindrome(self, s: str) -> str:
        
        expandedStr = "#".join(f"^{s}$")
        n = len(expandedStr)
        p = [0] * n
        center = 0
        right = 0

        # INCORRECT:
        # iteration will also expand from the sentinals '^' and '$'
        for i in range(n):

            # grab mirror
            mirror = (2*center)-i

            # validate mirror
            if i < right:
                p[i] = min(p[mirror], (right-i))

            # expand
            # INCORRECT:
            # due to iteration, expansion is not guaranteed and prevent
            # out of bounds grab, since we are missing the eventual
            # '^' != '$'
            while expandedStr[i - p[i] - 1] == expandedStr[i + p[i] + 1]:
                p[i] += 1

            # find new right most
            if p[i] + i > right:
                center = i
                right = p[i] + i

        # translate back to height
        maxRadi, center = max((maxRadi, center) for (center, maxRadi) in enumerate(p))

        startIndex = (center-maxRadi)//2


        return s[startIndex:startIndex+maxRadi]

Find the Bug: Bad enumerate method:

    def longestPalindrome(self, s: str) -> str:
        
        expandedStr = "#".join(f"^{s}$")
        n = len(expandedStr)
        p = [0] * n
        center = 0
        right = 0

        for i in range(1, n-1):

            # grab mirror
            mirror = (2*center)-i

            # validate mirror
            if i < right:
                p[i] = min(p[mirror], (right-i))

            # expand
            while expandedStr[i - p[i] - 1] == expandedStr[i + p[i] + 1]:
                p[i] += 1

            # find new right most
            if p[i] + i > right:
                center = i
                right = p[i] + i

        # translate back to height
        # INCORRECT:
        # should be 
        # enumerate(p)
        # instead of p.enumerate()
        maxRadi, center = max((maxRadi, center) for (center, maxRadi) in p.enumerate())

        startIndex = (center-maxRadi)//2


        return s[startIndex:startIndex+maxRadi]

Solution 1: Manacher's Algorithm (iterate, mirror radius optimization, and expand) - Two Pointers/Algorithm

    def longestPalindrome(self, s: str) -> str:

        # Note: 
        # Preprocessing with #, ^, and $:
        # '#': ensures uniform expansion, for both odd and even length palindromes
        # '^' and '$': sentinel characters don't match valid input characters, serve as true start and end markers
        # '#': ensures all palindromes start and end with '#'
        # '#': occur on odd indexes

        # Mapping:
        # we can map odd '#' indexes to their even character:
        # mapping '#' at index 1 to char pair 'a' at index 2, to original 'a' at index 0
        # [ ^ # a # b # a # $ ] -> [ a b a ]    via : originalStart = (expandedCenter - radius) / 2
        #   0 1 2 3 4 5 6 7 8        0 1 2      thus: originalStart = (4 - 3) / 2 = 0
        
        # Boundary expansion: 
        # For any index i, center of palindrome at i can either be: 
        # - character from the original string
        # - placeholder '#'
        # Center definition allows even length palindromes such as "abba", see below,
        # to have a single middle point, allowing the same expanding logic 
        # for even and odd strings for palindrome validation

        # Ex:
        # ^ # a # b # b # a # $    || new string len 11,
        # 0 1 2 3 4 5 6 7 8 9 10   ||
        #           ^              || index 5 center for even length "abba"

        # index 1 palindrome: "#"
        # index 2 palindrome: "#a#"
        # index 5 palindrome: "#a#b#b#a#"
        # etc...

        expandedStr = "#".join(f"^{s}$")
        n = len(expandedStr)

        # Right Most Palindrome and Mirror Trick: 
        # Iteration tracks the right boundary for the current farthest right palindromic substring, 
        # which allows us to take advantage of the mirror radius trick.
        # It speeds up palindrome expansion by starting the current palindrome radius
        # at the radius value of its mirror

        # p[i]: radius of palindrome centered at some index i
        p = [0] * n

        # mirror radius validation: tracking right boundary
        # right: right boundary of the current right most palindrome
        right = 0

        # mirror radius validation: tracking center of right most in order to calculate mirror index
        # center: center index of the current right most palindrome
        center = 0 

        # iteration range ignores sentinel indexes 0 and (n-1): ^ and $
        # i: center of current palindrome
        # time complexity: iterate over list of n length O(n)
        for i in range(1, n - 1):

            # mirror:
            # i is current index being processed
            # i is to the right of center and has a mirror to the left of center
            # ex: center = 6, i = 7 => mirror = (2 * 6) - 7 = 5
            mirror = (2 * center) - i 

            # mirror radius validation:
            # if i lies within bounds of the right most palindrome,
            # the right most palindrome symmetry guarantees that the palindrome radius
            # for the mirror of i, is applicable to i as well,
            # while within the bounds of the right most palindrome
            if i < right:

                # mirror radius is either:
                # - less than the distance between i and right bound,
                #   in which case all of the radius is valid
                # - exceeds bounds and is farther than right bound,
                #   in which case only the radius up until the right bound is valid
                
                # i radius is thus, bounded by minimum between: 
                # - mirror radius
                # - distance from i to the right bound
                p[i] = min(right - i, p[mirror])

            # assumption: if valid mirror, we pre-set p[i] to p[mirror]
            # now expand: expand radius p[i] until palindromic status is broken
            while expandedStr[i + p[i] + 1] == expandedStr[i - p[i] - 1]:
                p[i] += 1

            # p[i]: radius for palindrome at i
            # i: center for palindrome at i
            # check: if we have a new right most boundary, update center and right 
            if i + p[i] > right:
                right = i + p[i]
                center = i

        # expandedStr iteration complete:
        # p[] stores radius of palindrome centered at each index

        # scan p[] grabbing max palindrome radius alongside its center
        maxRadius, centerIndex = max((palindromeRadius, i) for (i, palindromeRadius) in enumerate(p))

        # Note:
        # index and radius are relative to expandedStr, not the original string
        # thus, we need to translate to original string indexes

        # Notice, how in the expanded string, 
        #  - all original characters are on even index
        #  - all original characters have matching # on the left odd index

        # abba =>  ^ # a # b # b # a # $   | a=2, b=4, b=6, a=8
        # 0123 =>  0 1 2 3 4 5 6 7 8 9 10  | #=1, #=3, #=5, #=7

        # aba =>   ^ # a # b # a # $       | a=2, b=4, a=6
        # 012 =>   0 1 2 3 4 5 6 7 8       | #=1, #=3, #=5

        # any palindrome will always end with a '#'.
        # so if we divide the starting odd position by 2, it will always map
        # to an original character.
        # so an easy translation formula is:

        start = (centerIndex - maxRadius) // 2
        
        # splice longest substring

        # overall: time complexity O(n)
        # overall: space complexity O(n)
        return s[start: start + maxRadius]
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
PreprocessingO(n)O(n)Building expanded stringMemory allocation for processed string O(n)
IteratingO(n)O(1)Iterate over processed string of n length O(n)No additional memory allocation for iteration O(1)
Expanding radiiO(n)O(n)Radius expansion over processed string of n length O(n)Radius array to store radii for each index for string of n length O(n)
Updating mirror boundsO(1)O(1)Updating center and right for right most palindrome in constant O(1)No additional memory allocation for center and right variables O(1)
OverallO(n)O(n)Iterating over expanded string dominates, leading to O(n) time complexity.Expanding string dominates, leading to O(n) space complexity.

Solution 2: Expand Around Center checking for Odd and Even palindromes (constant space) - Two Pointers/Algorithm

    def longestPalindrome(self, s: str) -> str:

        # expand from a given left and right while 
        # maintaining palindrome property
        def expandAroundCenter(left, right):
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1
                right += 1

            # curr iteration is not valid:
            # ignore left: incrementing index
            # ignore right: noninclusive right slicing
            return s[left+1:right] 
         
        n = len(s)
        maxPalindrome = ""

        # time complexity: iterate over list of n length O(n)
        for i in range(n):
            # odd expansion, centered at i
            oddPalindrome = expandAroundCenter(i, i)      
            # even expansion, centered at i and i + 1
            evenPalindrome = expandAroundCenter(i, i+1)

            # update longest
            if len(oddPalindrome) > len(maxPalindrome):
                maxPalindrome = oddPalindrome
            if len(evenPalindrome) > len(maxPalindrome):
                maxPalindrome = evenPalindrome

        # overall: time complexity O(n^2)
        # overall: space complexity O(1)
        return maxPalindrome
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IteratingO(n)O(1)Iterating over list of n length O(n)No additional memory allocation for iteration O(1)
Odd expansionO(n^2)O(1)For each center, expands outward for odd length palindromes n length O(n), for each outer iteration O(n), leading to O(n^2)No additional memory allocation needed during expansion O(1)
Even expansionO(n^2)O(1)For each center, expands outward for even length palindromes n length O(n), for each outer iteration O(n), leading to O(n^2)No additional memory allocation needed during expansion O(1)
Updating longestO(1)O(1)Comparing odd and even length palindrome to current max in constant O(1)No additional memory allocation needed for comparison to current max O(1)
OverallO(n^2)O(1)Even and odd expansion per outer iteration dominate, leading to O(n2)No additional memory allocation required for in place expansion or iteration, leading to constant O(1)

Solution 3: Dynamic Programming - 1D Dynamic Programming/Linear Property Tracking

        def longestPalindrome(self, s: str) -> str:
        
        n = len(s)
        dp = [[False]*n for _ in range(n)]
        start, max_len = 0, 1

        for i in range(n):
            dp[i][i] = True

        for length in range(2, n+1):  # substring length
            for i in range(n - length + 1):
                j = i + length - 1
                if s[i] == s[j]:
                    if length == 2 or dp[i+1][j-1]:
                        dp[i][j] = True
                        if length > max_len:
                            start, max_len = i, length

        return s[start:start+max_len]
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

75. Sort Colors ::1:: - Medium

Topics: Array, Two Pointers, String, Dutch National Flag

Intro

Given an array nums with n objects colored red, white, or blue, sort them in-place so that objects of the same color are adjacent, with the colors in the order red, white, and blue. We will use the integers 0, 1, and 2 to represent the color red, white, and blue, respectively. You must solve this problem without using the library's sort function. Follow up: Could you come up with a one-pass algorithm using only constant extra space?

InputOutput
nums = [2,0,2,1,1,0][0,0,1,1,2,2]
nums = [2,0,1][0,1,2]

Constraints:

n == nums.length

1 ≤ n ≤ 300

nums[i] is either 0, 1, or 2

Abstraction

stuff!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark

Brute Force

    
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointers] Dutch National Flag In Place Partition Problem - Two Pointers/K Pointer Variants

    def sortColors(self, nums: List[int]) -> None:
        
        # Two Pointer Pattern (Dutch National Flag / 3 Way Partition)
        
        # Subarray Representation:
        #   [0, low-1]    => all 0s (red)
        #   [low, mid-1]  => all 1s (white)
        #   [mid, high]   => unknown / unprocessed
        #   [high+1, n-1] => all 2s (blue)

        # Idea:
        #   - Use 3 pointers to define 4 sections: low, mid, high
        #   - 

        # Diagram:
        #
        # [ 0 => low-1, low => mid-1, mid => high, high+1 => n-1 ]
        #    0's           1's           2's             new

        # Idea:
        #   - Use 3 pointers: low, mid, high
        #   - Iterate mid pointer:
        #       nums[mid] == 0  => swap with nums[low], increment low and mid
        #       nums[mid] == 1  => leave in place, increment mid
        #       nums[mid] == 2  => swap with nums[high], decrement high (mid stays)
        #   - This ensures a single pass partitioning of 0/1/2

        n = len(nums)
        
        # Write pointer for 0s
        left = 0

        # Curr element        
        mid = 0

        # Write pointer for 2s
        right = n - 1

        # tc: iterate over n O(n)
        while mid <= right:

            # 0 Case:
            if nums[mid] == 0:

                # Swap 0s write left pointer with mid
                nums[left], nums[mid] = nums[mid], nums[left]

                # Iterate both
                left += 1
                mid += 1

            # 1 Case:
            elif nums[mid] == 1:

                # 1 is in correct region, just move forward
                # and expand current region
                mid += 1

            # 2 Case:
            else:

                # Swap 2s write right pointer with mid
                nums[mid], nums[right] = nums[right], nums[mid]

                # Iterate right
                right -= 1

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