LeetCode: Two Pointers I

Table Of Contents
Two Pointers Intro
- What are Two Pointers
- Two Pointers Application: One Pointer with Auxiliary State
- Two Pointers Application: Opposite Ends
- Two Pointers Application: Sliding Window
- Two Pointers Application: Fast & Slow Pointers
- Two Pointers Application: Lomuto Partition
- Two Pointers Application: Parallel Array Pointer Traversal
- Two Pointers Application: Catchup Pointer
- Two Pointers Application: K Pointer Variants
- Two Pointers Application: Algorithm
271. String Encode and Decode ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Bug: Decoded Length + Delimiter (Grabbed str(len) instead int(len))
- Bug: Decoded Base 10 doing += instead of = (half asleep)
- Test Cases
- Solution 1: [Two Pointers] Splicing Delimiter With 2 Catchup pointers [TC Opt] - Two Pointers/Catchup
- Solution 2: [Two Pointers] Base 10 Auxiliary Length Delimiter With 1 pointer [TC Opt] - Two Pointers/One Pointer with Auxiliary State
189. Rotate Array ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force (all triplets comparison)
- Find the Bug:
- Test Cases
- Solution 1: [Two Pointer] Extra Array With Direct ReIndexing - Two Pointers/K Pointer Variants
- Solution 2: [Two Pointer] Modular Cycle Traversal Array With Direct ReIndexing - Two Pointers/K Pointer Variants
- Solution 3: [Two Pointer] Reversal Trick - Two Pointers/Algorithm
42. Trapping Rain Water ::3:: - Hard
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force
- Find the Bug: Complete misunderstanding of water heights (haha)
- Find the Bug: Two Pointers Early Break
- Find the Bug: Not checking if left wall exists after pop
- Find the Bug: Overwriting variables accidentally
- Find the Bug: Bad For Loop syntax
- Solution 1: [Monotonic L/R Pointers] 2 Inner/Outer Pointers Traversal Creating Bound Buckets By Monotonic Opposite Ends Pointer Shift Modification - Two Pointers/K Pointer Variants
- 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
- 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
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]) -> 3Two 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") -> FalseTwo 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") -> 3Two 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) -> TrueTwo 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 ::3:: - 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.
| Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# Regular strings
"hello",
"aaa",
"abc",
# Edge cases
"",
"a",
# Palindrome patterns
"racecar",
"abba",
"abca",
]
for s in test_cases:
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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
125. Valid Palindrome ::3:: - 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.
| Input | Output |
|---|---|
| "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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# Palindromes
"racecar",
"aaa",
"a",
"",
# Non-palindromes
"abc",
"leetcode",
"python",
# Palindrome after one deletion
"abca",
"baabx"
]
for s in test_cases:
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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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.
| Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# 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 test_cases:
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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1768. Merge Strings Alternately ::2:: - 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.
| Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
88. Merge Sorted Array ::2:: - 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?
| Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Bug:
Test Cases:
if __name__ == "__main__":
sol = Solution()
test_cases = [
# 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 test_cases:
# 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)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
26. Remove Duplicates from Sorted Array ::2:: - 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.
| height | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
[],
[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 test_cases:
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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
696. Count Binary Substrings ::2:: - 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.
| height | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# 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 test_cases:
print(s, "=>", sol.countBinarySubstrings(s))Solution 1: [Two Pointers] Grouping Consecutive Identical Characters - 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.
# - Whenever a character change is found, update result by min(prevGroupLength, currGroupLength)
# - Continue until the end of the string.
n = len(s)
# Empty Check:
if n <= 1:
return 0
# Initialize pointers
# start of current group
left = 0
# explore the string
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:
# Pairs depend on the shorter group:
# 0011 => "01" and "0011"
# So we need to check the min group every time a new group is found
res += min(prevGroupLen, currGroupLen)
# Make current group, the new previous group
prevGroupLen = currGroupLen
# start new group
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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
271. String Encode and Decode ::2:: - 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.
| Input | Output |
|---|---|
| ["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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Splicing Delimiter with Catchup 2 pointers | O(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 pointer | O(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 decodedBug: 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 decodedTest Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# 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 test_cases:
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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Encode loop | O(n * m) | O(n * m) | Iterate n strings of m length dominates, O(n * m) | Allocation for encoded string dominates, O(n * m) |
| Decode loop | O(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) |
| Overall | O(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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Encode loop | O(n * m) | O(n * m) | Iterate n strings of m length dominates, O(n * m) | Allocation for encoded string dominates, O(n * m) |
| Decode loop | O(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) |
| Overall | O(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?
| nums | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force (all triplets comparison)
Find the Bug:
Test Cases
if __name__ == "__main__":
sol = Solution()
test_cases = [
# 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 test_cases:
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 (Using Extra Array)
# Substring Representation:
# - Each element at index i moves to:
# (i + k) % n
# Issue:
# We are avoiding overwriting the original array
# before we rae done mapping all of the elements
# Solution:
# - Track where elements will be mapped to in 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
rotated = [0] * n
# tc: O(n)
for i in range(n):
# Calculate new index for curr num at index i
newIndex = (i + k) % n
# Push num to new index in extra array
rotated[newIndex] = nums[i]
# Overwrite original array with extra array
# tc: O(n)
for i in range(n):
nums[i] = rotated[i]
# overall: tc O(n)
# overall: sc O(n)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 creates cycles
# - Follow each cycle and rotate elements
# Circle
# - The array can be thought as a circle with nodes
# - We just traverse the cycle which takes us from node to node
# and we just place the nums in their new locations
# - Now the circle can actually 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
# number of elements moved
count = 0
# start of first cycle
start = 0
# tc: each element visited once O(n)
while count < n:
current = start
prevValue = nums[start]
# Follow cycle
while True:
# Cycle Formula
nextIndex = (current + k) % n
# Swap elements at Cycle point
nums[nextIndex], prevValue = prevValue, nums[nextIndex]
# Follow the cycle
current = nextIndex
count += 1
# If reached the end of the cycle
if start == current:
break
# Increase cycle count
start += 1
# overall: tc O(n)
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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: reverse subarray nums[left...right]
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 k elements
reverse(0, k - 1)
# Reverse remaining elements
reverse(k, n - 1)
# overall: tc O(n)
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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
| nums | Output |
|---|---|
| [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
| Solution | Time Complexity | Space Complexity | Time Remark | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Outer loop | O(n) | O(1) | Iteration over list of n length O(n) | No additional memory allocation for iteration O(1) |
| Inner loop | O(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 Water | O(1) | O(1) | Water height operation takes constant O(1) | No additional memory allocation O(1) |
| Overall | O(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])
# per test cases: width includes walls
# [1, 1] is 1 water, (rightIndex - leftIndex) = width
# thus, calculate curr water between outside pointers = (smallerHeight * width)
currWater = smallerHeight * (right-left)
# compare to global max
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
# Thus, we can continue to move our pointers,
# until we hit a bigger height than our current smaller height
# Or conversely, we only need to stop and check at a bigger height
# tc: iteration n list O(n)
if height[left] < height[right]:
# step past current left/right wall combination
left += 1
# Greedy Shift:
while left < right and height[left] < smallerHeight:
left += 1
else:
# step past current left/right wall combination
right -= 1
# Greedy Shift:
while left < right and height[right] < smallerHeight:
right -= 1
# overall: tc O(n)
# overall: sc O(1)
return maxWater| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Iteration over list of n length with two pointers O(n) | No additional memory allocation for iteration O(1) |
| Curr Water | O(1) | O(1) | Water height operation takes constant O(1) | No additional memory allocation O(1) |
| Overall | O(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.
| nums | Output |
|---|---|
| people = [1,2], limit = 3 | 1 |
| people = [3,2,2,1], limit = 3 | 3 |
| people = [3,5,3,4], limit = 5 | 4 |
Constraints:
1 ≤ people.length ≤ 5 * 10^4
1 ≤ people[i] ≤ limit ≤ 3 * 10^4
Abstract
Boats!
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Bug:
Solution 1: [Two Pointers] Opposite Ends After Sorting Greedy - Two Pointers/Opposite Ends
def numRescueBoats(self, people: List[int], limit: int) -> int:
# Greedy + Two Pointers (Opposite Direction)
# Substring Representation:
# - Sort people by weight
# - Maintain window [left, right]
# 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
boats = 0
# tc: O(n)
while left <= right:
# If lightest + heaviest fit together
if people[left] + people[right] <= limit:
left += 1 # pair them
# Heaviest person always boards
right -= 1
boats += 1
# overall: tc O(n log n)
# overall: sc O(1)
return boats| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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.
| height | Output |
|---|---|
| [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
| Solution | Time Complexity | Space Complexity | Time Remark | Space 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iterate | ||||
| Left/Right max | ||||
| Curr Water | ||||
| Overall |
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 waterFind 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 waterFind 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 waterFind 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 waterFind 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 waterSolution 1: [Monotonic L/R 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
# 1. Bucket from index 0-3 with heights of [4, 0, 2, 6] (left to right monotonic increasing bucket)
# 2. 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, the bucket will catch any water up to and including 4
# When we use 6 and 5 as the left and right bucket walls, the 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
# case 1: [4, 0, 2, 1, 3, 5], left < right for entire array (bucket from 5 -> 6)
# case 2: [4, 0, 2, 6, 3, 5], left < right broken at some point (bucket from 5 -> 9, 9 -> 6)
# case 1: case 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:
# L/R Outer Pointers: serve as left and right most wall for buckets
# L/R Inner Pointers: traverse inward from both ends to determine if monotonic quality (bucket definition) as broken
# Water Trapping: using implications to know if left or right most wall for buckets determines water height
# Defining Bucket Depth Via Height Implications:
# case 1 implies:
# 1. while height[left] < height[right]
# 2. and while height[left] < outerLeftMax
# 3. we also know that height[right] < outerRightMax (same implication as 2. but for the right)
# then (eventually), there is a bucket from [outerLeftMax ... left ... outerRightMax]
# 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 at minimum, this height is bounded by the taller height
# Check: Left wall is shorter than Right wall
# Implies: Left wall is covered by Right wall
if height[left] < height[right]:
# *
# *
# * *
# ? * *
# ------------------
# LM L R
# Check: Left wall is taller than Left Max
# Implies: new tallest left wall found
# Then: Update
if height[left] >= outerLeftMax:
# *
# *
# * *
# * * *
# ------------------
# LM L R
outerLeftMax = height[left]
# Check: Left wall is shorter than left max
# Implies: Left wall is covered by left max
# Invariant: Left wall is already covered by Right Wall
# Implies: Left max is also lower than Right Wall
# Implies: Left max is limiting factor for this bucket
# Then: Water depth is just limiting factor - height = outerLeftMax - height[left]
else:
# *
# * 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
else:
# *
# *
# * *
# * * ?
# ----------------
# L R RM
# Check: Right wall is taller than Right Max
# Implies: New tallest right wall found
# Then: update
if height[right] >= outerRightMax:
# *
# *
# * *
# * * *
# ----------------
# L R RM
outerRightMax = height[right]
# Check: Right wall is shorter than Right Max
# Implies: Right Wall is covered by Right Max
# Invariant: Right Wall is already covered by Left Wall
# Implies: Right Max is also lower than Left Wall
# Implies: Right Max is the limiting factor for this bucket
# Then: Water depth is just limiting factor - height = outerRightMax - height[right]
else:
# *
# * w *
# * * *
# * * *
# ----------------
# L R RM
water += outerRightMax - height[right]
# shift pointer
right -= 1
# overall: tc O(n)
# overall: sc O(1)
return water| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Two pointer iteration over list of n length O(n) | No additional memory allocation required for iteration O(1) |
| Comparing Heights | O(1) | O(1) | Height lookup and comparison in constant O(1) | No additional memory allocation for lookup or comparison O(1) |
| Water Calculation | O(1) | O(1) | Water calculation and sum in constant O(1) | No additional memory allocation for water calculation or sum O(1) |
| Overall | O(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
# Stack:
# * * * * *
# * * * * * * * w * * *
# * * * * * * * * w * * * * * *
# * * * * * * * * w * * * * * *
# * * * * * * * w * * * * * * * * * *
# * * * * + * ==> * * * * * => * * * * => * * * ==> * *
# ------------ new --- pop off ------------ --- --------- --- ------ --- ------
# older --> newer left candidates final result
# Monotonic stack to store indices
stack = []
water = 0
# iterate over list while pushing and popping each bar only once
# tc: iterate over n O(n)
for i in range(len(height)):
# Check: if stack is non empty, depth candidate (still need to check if left wall candidate exists)
# Check: if current height[i] breaks monotonic decreasing order, its viable as a right wall
# implies: we keep appending while monotonic decreasing stays valid
# 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]:
# curr while loop iteration:
# height[i]: right wall
# pop stack[-1]: depth candidate
# peak stack[-2]: left wall
# Check if left wall candidate exists:
# remove depth candidate from stack, check if stack is non-empty
# If non-empty, we will essentially dragging the right wall over the monotonic stack,
# comparing it to all depth candidates, and adding the corresponding water.
# Continue until a depth candidate is taller than the current right wall,
# then we just add the right wall to the stack maintaining monotonic order.
# Or continue until we run out of left wall/depth candidates,
# then the right wall becomes the new leftmost wall.
depthCandidateIndex = stack.pop()
# Check: if stack empty after pop, no left wall exists (had 1 but not 2 elements)
# implies: cannot trap water, end while loop, add item to stack
if not stack:
break
# Left wall exists:
# Check water between [Left Wall... Depth Candidate ...Dragged Right Wall] excluding walls
# width = (right - left - 1)
# After stack.pop():
# height[i]: right wall
# popped depthCandidate: depth
# peak stack[-1]: left wall
# while loop check implies: depthCandidate < height[i]
# monotonic stack check implies: depthCandidate < stack[-1]
# Thus the bucket is: [peak stack[-1]... depthCandidate ... height[i]]
# Distance:
rightWallIndex = i
leftWallIndex = stack[-1]
distance = rightWallIndex - leftWallIndex - 1
# Bucket Bounded Height:
rightHeight = height[rightWallIndex]
leftHeight = height[leftWallIndex]
bucketLowerHeight = min(rightHeight, leftHeight)
depthHeight = height[depthCandidateIndex]
# Water Caught:
# Smaller bucket wall height - depth = water getting caught
waterCaught = bucketLowerHeight - depthHeight
# add the trapped water for the current segment
# [5, 0, 0, 2]
# in this case, (0, 0, 2)
# left wall = 0, depth = 0, right wall = 2
# so no water captured
# but then due to pop, (5, 0, 2)
# left wall = 5, depth = 0, right wall = 2
# so water captured based on distance
# Originally: [5, 0, 0, 2]
# 0 1 2 3
# So distance is: 3 - 0 - 1 = 2
# So distance will always be 1 in
# unless we have a run of identical heights such as above
water += distance * waterCaught
# implies: monotonic decreasing is still valid
# thus: continue appending height to stack
stack.append(i)
# overall: tc O(n)
# overall: sc O(n)
return water| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Iterate over list of n length O(n) | No additional memory allocation for iteration O(1) |
| Stack Operations | O(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 comparisons | O(1) | O(1) | Each comparison in while loop in constant O(1) | No additional memory allocation for comparison O(1) |
| Water Calculation | O(1) | O(1) | Calculating distance and bounded height in constant O(1) | No additional memory for distant or bounded height operations O(1) |
| Overall | O(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 (bucket left wall)
# Right Maximum Array: Stores the maximum height encountered iterating from right to left (bucket right wall)
# Avoid recomputing maximum heights repeatedly, so instead build the bucket walls as you go.
# 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| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| LeftMax Calculation | O(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 Calculation | O(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 Calculation | O(n) | o(1) | Iterate over max height list of n length O(n) | No additional memory allocation for water calculation O(n) |
| Overall | O(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.
| height | Output |
|---|---|
| nums = [3,2,2,3], val = 3 | 2, nums = [2,2,,] |
| nums = [0,1,2,2,3,0,4,2], val = 2 | 5, nums = [0,1,4,0,3,,,_] |
Constraints:
0 ≤ nums.length ≤ 100
0 ≤ nums[i] ≤ 50
0 ≤ val ≤ 100
Abstraction
stuff!
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 = 0
right = len(nums) - 1
# Process Array
# left -> scans array
# right -> boundary of valid elements
while left <= right:
# found element to remove
if nums[left] == val:
# replace with last valid element
nums[left] = nums[right]
# shrink valid boundary
right -= 1
# DO NOT move left here
# new element must be checked again
else:
# keep element
left += 1
# right + 1 = number of remaining elements
# overall: tc O(n)
# overall: sc O(1)
return right + 1| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
28. Find the Index of the First Occurrence in a String ::3:: - 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.
| height | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 (String Matching)
# Idea:
# - Try every possible starting position in haystack.
# - Use two pointers to compare characters.
# If all characters match → return index.
# Otherwise continue scanning.
n = len(haystack)
m = len(needle)
# edge case
if m > n:
return -1
# Scan all possible starting positions
for i in range(n - m + 1):
# compare substring using two pointers
j = 0
while j < m and haystack[i + j] == needle[j]:
j += 1
# full match found
if j == m:
return i
# no match
# overall: tc O((n-m)*m)
# overall: sc O(1)
return -1| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
75. Sort Colors ::3:: - 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?
| Input | Output |
|---|---|
| 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
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
Brute Force
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space 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 (String Matching)
# Subarray Representation:
# - Maintain three regions:
# 1. [0, low-1] => all 0s (red)
# 2. [low, mid-1] => all 1s (white)
# 3. [mid, high] => unknown / unprocessed
# 4. [high+1, n-1] => all 2s (blue)
# 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)
# next position for 0s
low = 0
# current element being processed
mid = 0
# next position for 2
high = n - 1
# tc: iterate each element at most once O(n)
while mid <= high:
if nums[mid] == 0:
# Swap 0 to the front
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
# 1 is in correct region, just move forward
mid += 1
else: # nums[mid] == 2
# Swap 2 to the back
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
# overall: tc O(n)
# overall: sc O(1) (in-place, constant extra space)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|