LeetCode: Two Pointers II Sliding Window

Table Of Contents
219. Contains Duplicate II ::3:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force: (iterative)
- Find the Bug:
- Solution 1: [Two Pointers] Sort List With Matching Index List LeetCode Runtime Space Analysis - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] MRI HashMap With Dynamic Left Based On Right and K - Sliding Window/Variable Size Window
- Solution 3: [Sliding Window] HashSet Tracking Nums Currently Within Window Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window
121. Best Time to Buy and Sell Stock ::4:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force: (iterative)
- Find the Bug:
- Solution 1: [Sliding Window] Tracking Left Min Buy Price And Right Max Profit Index - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Single Variable Sliding Window Efficient Max Profit [TC Opt] - Sliding Window/Variable Size Window
- Solution 3: [Dynamic Programming] Track Min Buy And Max Profit Up To Day i Table - Sliding Window/Variable Size Window
- Solution 4: [Dynamic Programming] Track Min Buy And Max Profit State Compression - Sliding Window/Variable Size Window
3. Longest Substring Without Repeating Characters ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] MRI HashMap With Jumping Left If Unique Flag Is Broken - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] HashSet Tracking Chars Currently Within Window Boundaries And Removing From Left While Unique Flag Is Broken - Sliding Window/Variable Size Window
424. Longest Repeating Character Replacement ::1:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force: (iterative)
- Find the Bug:
- Solution 1: [Sliding Window] HashMap Tracking CharCount In Window To Find MostOccur To Verify AllOtherChars In Window Are Replaceable Or Shrink From Left Until AllOtherChars Are Replaceable Less Than Or Equal To K - Sliding Window/Variable Size Window
567. Permutation in String ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Needle and Window HashMap For CharCount And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window
- Solution 2: [Sliding Window] Needle and Window Array With Lowercase Ascii Shifting Via 97 To Index 0 [SC Opt] - Sliding Window/Fixed Size Window
76. Minimum Window Substring ::2:: - Hard
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Needle and Window Hashmap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window
- Solution 2: [Sliding Window] Needle Hashmap And stillNeedThisMany And GlobalRemainingCountAcrossChars Indicate Valid Window Shrink While Valid - Sliding Window/Variable Size Window
239. Sliding Window Maximum ::4:: - Hard
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Two Pointers] Two Pointer And MaxHeap Priority Queue [TC Slow] - Sliding Window/Fixed Size Window
- Solution 2: [Dynamic Programming] Tracking Max Value By Dynamic Programming K Block Partitioning Encountered L To R and R to L Then Sliding Fixed Window And Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window
- Solution 3: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Values Unsafe For Duplicates [TC Opt] - Sliding Window/Variable Size Window
- Solution 4: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Indices Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window
930. Binary Subarrays With Sum ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Two Pointer] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal (Optimized For Binary Input) - Sliding Window/Variable Size Window
- Solution 2: [Prefix Sum] Prefix Sum With HashMap (Works For Any Input) - Sliding Window/Variable Size Window
- Solution 3: [Prefix Sum] Prefix Sum With Bounded Array (Optimized For Binary Input) - Sliding Window/Variable Size Window
1234. Replace the Substring for Balanced String :::: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Full Frequency HashMap Tracking All Chars Outside Window - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Excess Count HashMap Tracking Overages Outside Window - Sliding Window/Variable Size Window
904. Fruit Into Baskets ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Frequency Fruit Count With Shrink From Left While More Than 2 Fruit In Window - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Last Seen Index Tracking With Shrink From Left - Sliding Window/Variable Size Window
1004. Max Consecutive Ones III ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Two Pointer Sliding Window With Shrinking Max Tracking - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Two Pointer Sliding Window Without Shrinking - Sliding Window/Variable Size Window
1248. Count Number of Nice Subarrays ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Two Pointer Single Pass With Start Gap - Sliding Window/Variable Size Window
- Solution 3: [Sliding Window] Two Pointer Deque - Sliding Window/Variable Size Window
Sliding Window Intro
Leetcode problems with elegant solutions using the sliding window technique.
What is Sliding Window
Sliding Window is a technique used for iterating over a subset of data within a larger structure (like arrays or strings) by maintaining a moving window that can expand, contract, or shift across the input.
There are two main types of sliding windows:
- Fixed size (constant length) window
- Variable size (dynamic length) window
Sliding window often replaces nested loops by reusing computation from the previous window to achieve better efficiency.
Why Use Sliding Window
Sliding Window is ideal for problems that involve:
- Finding optimal subarray/substring
- Summation or counting within a range
- Tracking a condition inside a window
It typically reduces time complexity from O(n2) (naive nested loops) to O(n) since each element is processed at most twice (entering/exiting the window).
Sliding Window Application: Fixed Size Window
Fixed size windows help maintain a constant window of k elements while scanning through a sequence.
Ex: Maximum sum of any subarray of size k
def maxSumSubarray(nums, k):
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
# maxSumSubarray([1, 4, 2, 10, 2, 3, 1, 0, 20], 4) = 24Sliding Window Application: Variable Size Window
Variable size windows expand and shrink dynamically depending on whether some condition or constraint is met (e.g., substring uniqueness, sum ≤ target).
Ex: Longest substring without repeating characters
def lengthOfLongestSubstring(s: str) -> int:
char_index = {}
left = max_len = 0
for right in range(len(s)):
if s[right] in char_index and char_index[s[right]] >= left:
left = char_index[s[right]] + 1
char_index[s[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
# "abcabcbb" → lengthOfLongestSubstring = 3219. Contains Duplicate II ::3:: - Easy
Topics: Array, Hash Table, Sliding Window
Intro
Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) lte k.
| Example Input | Output |
|---|---|
| nums = [1,2,3,1], k = 3 | true |
| nums = [1,0,1,1], k = 1 | true |
| nums = [1,2,3,1,2,3], k = 2 | false |
Constraints:
1 ≤ nums.length ≤ 10^5
-10^9 ≤ nums[i] ≤ 10^9
0 ≤ k ≤ 10^9
Abstraction
duplicate! duplicate!
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: (iterative)
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Two Pointers] Sort List With Matching Index List LeetCode Runtime Space Analysis - Sliding Window/Variable Size Window
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
# Space Optimization:
# - LeetCode measures peak memory usage at runtime, not just asymptotic SC
# - HashMap/HashSet solutions are O(n), but carry large constant factors:
# hash table buckets, key-value pairs, pointers, hashing overhead, etc
# - A list of integers in contiguous in memory with no overhead,
# making it significantly lighter in practice despite same O(n) SC
# - No early exist via set to preserve this space advantage
# Sort By Value + Index Comparison
# Representation:
# - Sort indices by their values in nums
# - Adjacent indices in sorted order are the closest value matches
# Idea:
# - If any duplicate values exist, they will be adjacent after sorting
# - Sort indices based on original values
# - If duplicate is found, check if original indexes are within k
# - Avoids storing a window or hashmap entirely
n = len(nums)
# Empty Case:
# if not enough days to have a duplicate
if n <= 1 or k <= 0:
return False
# Create list of indices
# tc: O(n)
# sc: O(n)
index = list(range(n))
# Creates an array of indexes that maps to the new sorted array
# ex:
# nums = [3,1,2], idx=[0,1,2]
# nums = [3,1,2], idx=[1,2,0]
# tc: O(n log n)
index.sort(key=lambda i: nums[i])
# Check adjacent pairs in sorted order
# tc: O(n)
for i in range(1, n):
currIndex = index[i]
prevIndex = index[i-1]
# Check: if adjacent values are duplicates
if nums[currIndex] == nums[prevIndex]:
# Boundaries of original indices
left = min(currIndex, prevIndex)
right = max(currIndex, prevIndex)
# Distance between original indices
distance = right - left
# Check: if original indices are within k distance
if distance <= k:
return True
# No duplicates found within distance k
# overall: tc O(n log n)
# overall: sc O(n)
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] MRI HashMap With Dynamic Left Based On Right and K - Sliding Window/Variable Size Window
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
# Sliding Window + HashMap
# Window Representation:
# - Width of [left, right]
# - Window size of max k
# - HashSet stores most recent index appearance for nums
# Idea:
# - Slide window over num list and add index at right boundary
# - Check mri to determine if duplicate exists within window boundaries
# Track more recent index appearance for num
# sc: O(n)
mostRecentIndex = {}
# Sliding Window Variables
# sc: O(1)
# right boundary
n = len(nums)
right = 0
# Empty Case:
# if not enough days to have a duplicate
if n <= 1 or k <= 0:
return False
# tc: iterate over n O(n)
for right in range(n):
currNum = nums[right]
# Check: if num has already been seen
# tc: O(1)
if currNum in mostRecentIndex:
# k distance
# | ------------ |
# left right
# the furthest left index that is still within k distance
left = right - k
# previous occurrence of currNum
prevIndex = mostRecentIndex[currNum]
# Check: if previous num copy is within current window:
if left <= prevIndex <= right:
return True
# Implies: no duplicate exists
# Then: update mri for num
mostRecentIndex[currNum] = right
# No duplicates found within distance k
# tc: O(n)
# overall: sc O(min(n, k))
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Sliding Window] HashSet Tracking Nums Currently Within Window Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
# Sliding Window + HashSet
# Window Representation:
# - Width of [left, right]
# - Window size of max k
# - HashSet stores elements currently inside window
# Idea:
# - Slide window over num list and add num at right boundary
# - Check if num we added is already within window boundary
# - Keep max k elements within window boundaries
# - Remove elements from left boundary once window is large than size k
n = len(nums)
# Empty Case:
# if not enough days to buy and sell in time
if n <= 1 or k <= 0:
return False
# Sliding Window Data:
# sc: up to O(k)
window = set()
# Sliding Window Boundaries:
# sc: O(1)
left = 0
right = 0
# Early exit: if all elements are unique, no duplicates possible
# tc: O(n)
# sc: O(n)
if len(window) == n:
return False
# tc: iterate over n O(n)
for right in range(n):
# Check: if num already exists within window range
if nums[right] in window:
return True
# Add right index to sliding window and iterate:
window.add(nums[right])
# Remove left index from sliding window, if we've reached capacity, and iterate:
if len(window) > k:
window.remove(nums[left])
left += 1
# No duplicates found within distance k
# overall: tc O(n)
# overall: sc O(min(n, k))
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
121. Best Time to Buy and Sell Stock ::4:: - Easy
Topics: Array, Dynamic Programming, Sliding Window
Intro
You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.
| Example Input | Output |
|---|---|
| prices = [7,1,5,3,6,4] | 5 |
| prices = [7,6,4,3,1] | 0 |
Constraints:
1 ≤ prices.length ≤ 105
0 ≤ prices[i] ≤ 104
Abstraction
Given an array of stock values, find the highest profit possible.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: (iterative)
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Tracking Left Min Buy Price And Right Max Profit Index - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Sliding Window (Variable Size)
# Stock Representation:
# - Treat each day as a potential sell day
# - Maintain a window [left, right] where:
# left: best buy day so far (lowest price seen)
# right: current sell day candidate (highest prices seen)
# Idea:
# - Always buy before you sell
# - If current price is higher than buy price, compute profit
# - If current price is lower than buy price, we have found better buy price, reset window
n = len(prices)
# Sliding Window Representation:
# Data: max profit seen so far
# sc: O(1)
maxProfit = 0
# Left Boundary: lowest buy price seen so far
# tc: O(1)
left = 0
# Right Boundary: current price scanner
# tc: O(1)
right = 0
# tc: iterate over n O(n)
while right < n:
# Grab current window boundaries
# sc: O(1)
buyPrice = prices[left]
sellPrice = prices[right]
# Check: can we make a profit (is current price higher than our current buy)
# Then: calculate and compare profit
if buyPrice < sellPrice:
# Calculate profit
profit = sellPrice - buyPrice
# Compare profit
maxProfit = max(maxProfit, profit)
# Check: have we found a better buy price (is current price lower than our current buy)
# Then: update our lowest buy (our left boundary)
else:
left = right
# iterate current price scanner
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxProfit| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Iterate over array of n prices O(n) | No additional memory allocation for iteration O(1) |
| Profit check | O(1) | O(1) | Profit calculation in constant O(1) | No additional memory allocation for calculation O(1) |
| Overall | O(n) | O(1) | Iteration over array dominates O(n) | No additional memory allocation O(1) |
Solution 2: [Sliding Window] Single Variable Sliding Window Efficient Max Profit [TC Opt] - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Sliding Window (Variable Size)
# Stock Representation:
# - Treat each day as a potential sell day
# - Maintain a window [left, right] where:
# left: best buy day so far (lowest price seen)
# right: current sell day candidate (highest prices seen)
# Idea:
# - Track lowest price
# - If current price is lower than buy price, we have found better buy price, reset window
# - If current price is higher than buy price, check if higher profit found
# Sliding Window Representation:
# Data: max profit seen so far
maxProfit = 0
# Left Boundary: lowest buy price seen so far
minPrice = float('inf')
# tc: iterate over n O(n)
for price in prices:
# if today has a lower buy price
if price < minPrice:
minPrice = price
# if today has a better profit
elif price - minPrice > maxProfit:
maxProfit = price - minPrice
# overall: tc O(n)
# overall: sc O(1)
return maxProfit| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Iterate over array of n prices O(n) | No additional memory allocation for iteration O(1) |
| Profit check | O(1) | O(1) | Profit calculation in constant O(1) | No additional memory allocation for calculation O(1) |
| Overall | O(n) | O(1) | Iteration over array dominates O(n) | No additional memory allocation O(1) |
Solution 3: [Dynamic Programming] Track Min Buy And Max Profit Up To Day i Table - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Dynamic Programming (Full DP Table)
# Window Representation:
# - Maintain window within [i]
# - buyPrice[i][0]: best buy day so far:
# we represent this by min(-buyPrice) for easier later calculations
#
# - buyPrice[i][1]: best profit price up to day i
#
# Idea:
# - Always buy before you sell (<= index 1)
# - Check if new buy day found
# - Check if new best profit found
# Empty check: no profit to be made
if not prices:
return 0
n = len(prices)
# Initialize DP:
# sc: O(n)
dp = []
# Fill dp table:
# - dp[i][0]: best buy price up to day i
# - dp[i][1]: best profit price up to day i
# tc: O(n)
for i in range(n):
dp.append([0, 0])
# Initialize DP:
# i = day
# dp[i][0]: set best buy price to day 0
# dp[i][1]: set best profit made up to day 0
# (cannot buy and sell on same day, so profit on day 0 is 0)
# tc: O(1)
dp[0][0] = -prices[0]
dp[0][1] = 0
# start from second day
# tc: iterate across n days O(n)
for i in range(1, n):
# Check: grab todays buy price
# Use profit to explain our buy price:
# profit = hold + sellPrice
# = (-3) + 10
# = 7
todayPrice = -prices[i]
# Check: grab best buy price up to yesterday
bestBuyUpToYesterday = dp[i-1][0]
# Check: grab new buy price
dp[i][0] = max(bestBuyUpToYesterday, todayPrice)
# Check: todays profit
todayProfit = bestBuyUpToYesterday + prices[i]
# Check: grab new best profit, cannot sell today, so use yesterdays price
dp[i][1] = max(dp[i-1][1], todayProfit)
# overall: tc O(n)
# overall: sc O(n)
return dp[-1][1]| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 4: [Dynamic Programming] Track Min Buy And Max Profit State Compression - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Dynamic Programming (State Compression)
# Stock Representation:
# - Treat each day as potential sell day
# - Maintain a state tracking where:
# buyPrice: best buy price so far (lowest price seen)
# profit: most profit made so far (highest profit made using each day as a sell day)
# Idea:
# - Always buy before you sell
# - If current price is higher than buy price, compute profit
# - If current price is lower than buy price, we have found better buy price, reset window
n = len(prices)
# set up compressed dynamic programming table first iteration:
# [i][0] -> min buying price up to day i
# [i][1] -> max profit up to day i
# Initialize Compressed DP:
# - min price up to day 0
# - max profit up to day 0
bestBuyPrice = -prices[0]
maxProfit = 0
# start from second day
# tc: iterate over n days O(n)
for i in range(1, n):
# Put buy price as negative,
# to ensure we always subtract it from out buy price
# profit = hold + sellPrice
# = (-3) + 10
# = 7
todayPrice = -prices[i]
# Check: compare best buy price up to yesterday to today buy price
bestBuyPrice = max(bestBuyPrice, todayPrice)
todayProfit = bestBuyPrice + prices[i]
# profit = hold + sellPrice
# = (-3) + 10
# = 7
maxProfit = max(maxProfit, todayProfit)
# overall: tc O(n)
# overall: sc O(1)
return maxProfit| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
3. Longest Substring Without Repeating Characters ::2:: - Medium
Topics: Hash Table, String, Sliding Window
Intro
Given a string s, find the length of the longest without duplicate characters.
| Example Input | Output |
|---|---|
| s = "abcabcbb" | 3 |
| s = "bbbbb" | 1 |
| s = "pwwkew" | 3 |
Constraints:
0 ≤ s.length ≤ 5 * 104
s consists of English letters, digits, symbols and spaces.
Abstraction
Given a string, find longest substring without duplicates
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] MRI HashMap With Jumping Left If Unique Flag Is Broken - Sliding Window/Variable Size Window
def lengthOfLongestSubstring(self, s: str) -> int:
# Sliding Window (Variable Size)
# Window Representation:
# - Maintain window [left, right]
# - HashSet stores most recent index appearance for nums
# - Window will contain no duplicates
# Idea:
# - Expand right pointer to explore new characters
# - If new character results in a duplicate,
# move the left window boundary to 1 in front of the duplicate
n = len(s)-1
# Track more recent index appearance for char
mostRecentIndex = {}
# Sliding Window Variables
# sc: O(1)
# left boundary of current substring
left = 0
# right boundary of current substring
right = 0
# max length so far
maxLen = 0
# tc: iterate over n O(n)
while right <= n:
# character we just added to substring
newChar = s[right]
# If duplicate exists within current window
# tc: O(1)
if newChar in mostRecentIndex:
prevIndex = mostRecentIndex[newChar]
if left <= prevIndex <= right:
# Move left pointer to remove previous duplicate from window
left = prevIndex + 1
# Update most recent index for character
# tc: O(1)
mostRecentIndex[newChar] = right
# Update window length
# tc: O(1)
currLen = right - left + 1
# Check max length
maxLen = max(maxLen, currLen)
# Iterate right pointer to continue window shifting
# tc: O(1)
right += 1
# overall: tc O(n)
# overall: sc O(min(n, k))
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Hashmap | O(n) | O(n) | Iteration over string O(n) | Worst case will store n characters for completely unique string |
| Iteration | O(n) | O(1) | Iteration over string visited once by end pointer O(n) | No additional memory allocation for end pointer iteration O(1) |
| Hashmap operations | O(1) | O(1) | Lookup and insertion in constant O(1) | Constant length for 128-258 ascii characters O(1) |
| Window shifting | O(n) | O(1) | Iteration over string visited at most once by start pointer O(n) | No additional memory allocation for start pointer iteration O(1) |
| Overall | O(n) | O(1) | Iteration over string for start and end pointer dominates, leading to O(n) | Constant space for hashmap char indexes dominates, leading to O(n) |
Solution 2: [Sliding Window] HashSet Tracking Chars Currently Within Window Boundaries And Removing From Left While Unique Flag Is Broken - Sliding Window/Variable Size Window
def lengthOfLongestSubstring(self, s: str) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - HashSet stores unique characters currently inside window
# Idea:
# - Expand right to add new characters
# - If duplicate found, shrink from left until duplicate is removed
# - Track longest window seen with no duplicates
n = len(s)
# Sliding Window Data:
# sc: O(k)
window = set()
# Sliding Window Boundaries:
# sc: O(1)
left = 0
# Track longest valid window
maxLen = 0
# tc: iterate over n O(n)
for right in range(n):
# Shrink from left until duplicate is removed
# tc: O(1) amortized
while s[right] in window:
window.remove(s[left])
left += 1
# Add incoming character to window
# tc: O(1)
window.add(s[right])
# Update max length if current window is larger
# tc: O(1)
maxLen = max(maxLen, len(window))
# overall: tc O(n)
# overall: sc O(min(n, k))
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
424. Longest Repeating Character Replacement ::1:: - Medium
Topics: Hash Table, String, Sliding Window
Intro
You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times. Return the length of the longest substring containing the same letter you can get after performing the above operations.
| Example Input | Output |
|---|---|
| s = "ABAB", k = 2 | 4 |
| s = "AABABBA", k = 1 | 4 |
Constraints:
1 ≤ s.length ≤ 105
s consists of only uppercase English letters.
0 ≤ k ≤ s.length
Abstraction
Given a string, and count k, return the longest substring of matching characters you can get after replacing k or less characters.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: (iterative)
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] HashMap Tracking CharCount In Window To Find MostOccur To Verify AllOtherChars In Window Are Replaceable Or Shrink From Left Until AllOtherChars Are Replaceable Less Than Or Equal To K - Sliding Window/Variable Size Window
def characterReplacement(self, s: str, k: int) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - Hashmap: char -> count within window
# Idea:
# - Expand right pointer to explore new character
# - maxFreq will hold the mostOccur char in window
# - Window length of size right - left + 1
# Trick:
# - We need allOtherChars in window to be <= k, so that we know
# we can replace allOtherChars with the mostOccur
# - If allOtherChars is greater than k (allOtherChars > k),
# we simply shrink from left until we can replace allOtherChars,
# when allOtherChars is less than or equal to k, (allOtherChars <= k)
n = len(s)
# char count within window boundaries
count = defaultdict(int)
# Sliding Window Variables
# sc: O(1)
# left and right window boundaries
left = 0
right = 0
# Sliding Window Data
# sc: O(1)
# mostOccur: most occurring character in window
mostOccur = 0
# max length seen so far
maxLen = 0
# tc: iterate over n O(n)
for right in range(n):
# Character we just encountered
# tc: O(1)
currChar = s[right]
charCount[currChar] += 1
# Update mostOccur in window
# tc: O(1)
mostOccur = max(mostOccur, charCount[currChar])
# length of curr window
currWindowLen = right - left + 1
# how many other chars exists, (anything that is not mostOccur)
allOtherChars = currWindowLen - mostOccur
# We need allOtherChars to be less than k (num of chars we can replace),
# so that we can replace allOtherChars with the mostOccur.
# thus, shrink window from left by 1 until allOtherChars is valid
# tc: O(1) amortized
while k < allOtherChars:
# Note: we do NOT update mostOccur here
# While shrinking, the mostOccur may become stale
# - true max mostOccur may have decreased
# - mostOccur char may have shifted to a different character
# This is fine because:
# - for the current window from [left:right], we have already agreed that
# mostOccur is the highest char
# - so even if we shrink and remove
# A stale mostOccur will make allOtherChars smaller than actual
# - underestimating allOtherChars
# This is safe because:
# - No char can exceed the current mostOccur while shrinking,
# since we are only shrinking from the left and not modifying right
# - Regardless, we need to shrink because an invalid window
# won't count towards our maxLen
# Thus mostOccur:
# - not acting as a true most occurring character
# - acting as a flag for the original mostOccur for original window
# that existed before shrinking
# Remove left character from window
charCount[s[left]] -= 1
left += 1
# Recompute currWindowLen and allOtherChars after shrink
currWindowLen = right - left + 1
allOtherChars = currWindowLen - mostOccur
# Once allOtherChars is valid, check maxLen
maxLen = max(maxLen, currWindowLen)
# overall: tc O(n)
# overall: sc O(1)
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Iteration | O(n) | O(1) | Iterate right pointer over each character at least once for n length O(n) | No additional memory allocation for iteration O(1) |
| Frequency map operations | O(1) | O(1) | Insert in constant O(1) | Constant frequency map size for 26 uppercase O(1) |
| Window expansion and shrinking | O(n) | O(1) | Each character added and removed at most once from window O(n) | No additional memory allocation for virtual window O(1) |
| Overall | O(n) | O(1) | Iteration over string of n length dominates, leading to O(n) | Constant space frequency map dominates, leading to O(1) |
567. Permutation in String ::2:: - Medium
Topics: Hash Table, String, Sliding Window
Intro
Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise. In other words, return true if one of s1's permutations is the substring of s2.
| Example Input | Output |
|---|---|
| s1 = "ab", s2 = "eidbaooo" | true |
| s1 = "ab", s2 = "eidboaoo" | false |
Constraints:
1 ≤ s1.length, s2.length ≤ 104
s1 and s2 consist of lowercase English letters.
Abstraction
Given a string 1 and string 2, return true of string 1 is a permutation within string 2.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Needle and Window HashMap For CharCount And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window
def checkInclusion(s1: str, s2: str) -> bool:
# Sliding Window (Fixed Size Window)
# Substring Representation:
# - Maintain window [left, right]
# - Window size always equals len(s1)
# - Hashmap: char -> frequency
# Idea:
# - Build a frequency map for s1
# - Expand right pointer to build window in s2
# - If window size exceeds len(s1), shrink from left
# - If window frequency == s1 frequency, permutation exists
# Since window size is fixed, left moves whenever
# window grows larger than lens1)
n1 = len(s1)
n2 = len(s2)
# s2 < s1: permutation not possible
# tc: O(1)
if n2 < n1:
return False
# s1 Frequency
# tc: O(n)
# sc: O(n)
needleCharCount = defaultdict(int)
for c in s1:
needleCharCount[c] += 1
# Substring window frequency
# sc: O(1)
windowCharCount = defaultdict(int)
# left boundary of current substring
left = 0
# right boundary of current substring
right = 0
# tc: iterate over n O(n)
for right in range(n2):
# Add new character to substring
# tc: O(1)
rightChar = s2[right]
windowCharCount[rightChar] += 1
currWindowLen = right - left + 1
# Optimization:
# While window size is longer than needle size, shrink
# tc: O(1)
while n1 < currWindowLen:
# Remove character from substring
leftChar = s2[left]
# Update substring character count
windowCharCount[leftChar] -= 1
# Remove zero frequency keys
if windowCharCount[leftChar] == 0:
# need to remove empty char counts,
# so we can match window hashmap to needle hashmap
del windowCharCount[leftChar]
# Shrink substring from left
# tc: O(1)
left += 1
currWindowLen = right - left + 1
# Window is valid size,
# check if permutation found
if windowCharCount == needleCharCount:
return True
# overall: tc O(n)
# overall: sc O(n)
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| s1 Frequency | O(m) | O(1) | Iterate over s1 to generate frequency count O(m) | Constant frequency count up to 26 characters O(1) |
| Iteration | O(n) | O(1) | Iterate over s2 to generate sliding window frequency count O(n) | Constant frequency count up to 26 characters O(1) |
| Hashmap | O(1) | O(1) | Comparison in constant O(1) | No additional memory allocation for hashmap comparison O(1) |
| Overall | O(n + m) | O(1) | Iteration over s1 and s2 dominate, leading to O(n + m) | Constant frequency count for s1 and sliding window O(1) |
Solution 2: [Sliding Window] Needle and Window Array With Lowercase Ascii Shifting Via 97 To Index 0 [SC Opt] - Sliding Window/Fixed Size Window
def checkInclusion(self, s1: str, s2: str) -> bool:
# Sliding Window (Fixed Size Window)
# Window Representation:
# - Fixed window of size len(s1)
# - Two frequency arrays of size 26 (one per lowercase letter)
# - needleCharCount: frequency of each char in s1
# - fixedWindowCharCount: frequency of each char in current window of s2
# Idea:
# - Build needleCharCount fixedWindowCharCount from s1
# - Seed first window of s2 into fixedWindowCharCount
# - Slide window across s2: remove leftmost char, add new rightmost char
# - If fixedWindowCharCount == needleCharCount at any point, window is a permutation of s1
# Lowercase Ascii Index Shifting:
# lowercase chars start 97, so if we want 'a' to go from 97 to 0,
# we just subtract 97 from any and all chars.
# Now instead of 97 -> 122, we get 0 -> 26
# s2 too short to contain s1
# tc: O(1)
if len(s2) < len(s1):
return False
# Fixed Window Iteration:
# Create a fixed window size based on the needle size.
# Calculate the window on the leftmost indexes of the haystack.
# Iterate the fixed window over the haystack while simply
# adding the right char and removing the left char.
# Compare each iteration to expected needleCharCount
# Frequency arrays for each lowercase letter a-z
# sc: O(1) fixed size 26
needleCharCount = [0] * 26
fixedWindowCharCount = [0] * 26
# Fixed window right boundary
fixedWindowRight = len(s1)
# Seed needleCharCount and first window of s2
# tc: O(k) where k = len(s1)
for i in range(fixedWindowRight):
# populate the expected needle char count
needleCharCount[ord(s1[i]) - 97] += 1
# populate the first [0, right] chars in the haystack
fixedWindowCharCount[ord(s2[i]) - 97] += 1
# Check if first fixed window char count matches needle char count
# tc: O(1)
if fixedWindowCharCount == needleCharCount:
return True
# Slide window across remaining chars in s2
# tc: O(n)
for right in range(fixedWindowRight, len(s2)):
# Grab left boundary index for fixed window
left = right - fixedWindowRight
# Iterate fixed window, remove left char, add right char
fixedWindowCharCount[ord(s2[left]) - 97] -= 1
fixedWindowCharCount[ord(s2[right]) - 97] += 1
# Check if window char count matches needle char count
# tc: O(1)
if fixedWindowCharCount == needleCharCount:
return True
# No permutation found
# overall: tc O(n + k)
# overall: sc O(1)
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
76. Minimum Window Substring ::2:: - Hard
Topics: Hash Table, String, Sliding Window
Intro
Given two strings s and t of lengths m and n respectively, return the minimum window of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string "". The testcases will be generated such that the answer is unique.
| Example Input | Output |
|---|---|
| s = "ADOBECODEBANC", t = "ABC" | "BANC" |
| s = "a", t = "a" | "a" |
| s = "a", t = "aa" | "" |
Constraints:
m == s.length
n == t.length
1 ≤ m, n ≤ 105
s and t consist of uppercase and lowercase English letters.
Abstraction
Given two strings s1 and s2, of lengths m and n, return the minimum substring of s, such that every character including duplicates is in the window.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Needle and Window Hashmap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window
def checkInclusion(self, s1: str, s2: str) -> bool:
# Sliding Window (Fixed Size Window)
# Substring Representation:
# - Maintain window [left, right]
# - Two Hashmaps:
# needleFreq -> required counts
# windowFreq -> current window counts
# Idea:
# - Expand right pointer to explore new character
# - Track how many required character are satisfied via needleFreq
# - While required characters are satisfied, shrink from left to minimize window
# - Track smallest valid window found
# sLen < tLen: substring not possible
# tc: O(1)
if len(s) < len(t):
return ""
# Build needle character map
# sc: O(m)
needleCharCount = defaultdict(int)
for c in t:
needleCharCount[c] += 1
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
# Sliding Window Data (3)
# Frequency map for curr chars in window (1)
# sc: O(1)
windowCharCount = defaultdict(int)
# length and starting index of smallest window so far (2)
minLen = float('inf')
minStart = 0
# Flags for unique chars slots from needle hashmap length (3)
needCharSlots = len(needleCharCount)
haveCharSlots = 0
# tc: iterate over n O(n)
for right in range(len(s)):
# Expand window
# tc: O(1)
rightChar = s[right]
windowCharCount[rightChar] += 1
# If char is a slot char with requirements
if rightChar in needleCharCount:
# If char has met requirements
if windowCharCount[rightChar] == needleCharCount[rightChar]:
haveCharSlots += 1
# Shrink window while valid
# tc: amortized O(n)
while haveCharSlots == needCharSlots:
# Substring size
windowLen = right - left + 1
# Update minimum window
if windowLen < minLen:
minLen = windowLen
minStart = left
# Remove left character
leftChar = s[left]
windowCharCount[leftChar] -= 1
# If char is a slot char with requirements
if leftChar in needleCharCount:
# If char no longer meets requirements
if windowCharCount[leftChar] < needleCharCount[leftChar]:
haveCharSlots -= 1
# Shrink substring from left
# tc: O(1)
left += 1
# If no valid substring was found
if minLen == float("inf"):
return ""
# Splice min substring
res = s[minStart:(minStart + minLen)]
# overall: tc O(n + m)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Needle Hashmap And stillNeedThisMany And GlobalRemainingCountAcrossChars Indicate Valid Window Shrink While Valid - Sliding Window/Variable Size Window
def minWindow(self, s: str, t: str) -> str:
# Sliding Window (Variable Size Window)
# Substring Representation:
# - Maintain window [left, right]
# - Single hashmap: char -> remaining required count
# - targetCharsRemaining: tracks total chars still needed
# Idea:
# - Expand right pointer to explore new character
# - Decrease count of remaining required count
# - When targetCharsRemaining == 0, window is valid
# - Shrink from left to minimize window
# - Track smallest valid window
# sLen < tLen: substring not possible
# tc: O(1)
if len(s) < len(t):
return ""
# Build required frequency map
# sc: O(1)
needleCharCount = defaultdict(int)
# tc: O(m)
for ch in t:
needleCharCount[ch] += 1
# Total chars still needed
globalRemainingCountAcrossChars = len(t)
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
minWindow = (0, float("inf"))
# tc: iterate over n O(n)
for right in range(len(s)):
# Expand window
# tc: O(1)
rightChar = s[right]
# Note:
# to start, needleCharCount hashmap will only contain values of chars that
# we need for the needle.
# This means all other possible keys, are set to 0, and moving forward,
# can only be less than or equal to 0 (regardless of adding or removing).
# Thus, to track chars that are in the needle,
# we only count chars that at some point have a positive count,
# to contribute towards decreasing or increasing the global remaining
stillNeedThisMany = needleCharCount[rightChar]
if stillNeedThisMany > 0:
globalRemainingCountAcrossChars -= 1
needleCharCount[rightChar] -= 1
# Shrink window while its valid, so we can find the min valid window
# tc: amortized O(n)
while globalRemainingCountAcrossChars == 0:
currWindowLen = right - left + 1
currMinWindowLen = minWindow[1] - minWindow[0] + 1
# If smaller min window found, update min
if currWindowLen < currMinWindowLen:
minWindow = (left, right)
# Remove left character
leftChar = s[left]
needleCharCount[leftChar] += 1
# If after excluding this char we now need copies of this char,
# we can count it towards hurting and increasing the global counter
stillNeedThisMany = needleCharCount[leftChar]
if stillNeedThisMany > 0:
globalRemainingCountAcrossChars += 1
# Shrink substring from left
# tc: O(1)
left += 1
# If no valid window was found
if minWindow[1] == float("inf"):
return ""
# Grab min window
res = s[minWindow[0]:minWindow[1] + 1]
# overall: tc O(n + m)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
239. Sliding Window Maximum ::4:: - Hard
Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Monotonic Queue
Intro
You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.
| Example Input | Output |
|---|---|
| nums = [1,3,-1,-3,5,3,6,7], k = 3 | Output: [3,3,5,5,6,7] |
| nums = [1], k = 1 | [1] |
Constraints:
1 ≤ nums.length ≤ 105
-104 ≤ nums[i] ≤ 104
1 ≤ k ≤ nums.length
Abstraction
Given list of nums and sliding window size k, return array of largest sum for every window within the list.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Two Pointers] Two Pointer And MaxHeap Priority Queue [TC Slow] - Sliding Window/Fixed Size Window
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# Sliding Window (Fixed Size Window)
# Substring Representation:
# - Maintain window of size k
# - MaxHeap storing (-value, index)
# - Heap top always holds current window maximum
# Idea:
# - Push first k elements into heap
# - For each new element:
# 1. Push new element
# 2. Remove stale elements (outside window) (index <= (right - k))
# 3. Heap top is max of current window
# Empty check:
# tc: O(1)
if not nums or k == 0:
return []
n = len(nums)
# List of max values for each window
# sc: O(n)
result = []
# MaxHeap():
# - a Python minHeap with negative values, resulting in a maxHeap
# - O(log n) insertion and removal
# MaxHeap data representation: (-value, index)
# - Need the -value to turn MinHeap into MaxHeap
# - Need the index to check if top of heap is outside window
maxHeap = []
# MaxHeap top representation (maxHeap[0]):
# - Top: largest element index in current window
# - All other: no guarantee for any other element,
# we are only guaranteed that the top is the current max in the MaxHeap
#
# Sliding Window Variables
# sc: O(1)
right = 0
# Initialize first window:
# push first k elements to maxHeap
# tc: O(k)
while right < k:
# tc: O(log k)
heapq.heappush(maxHeap, (-nums[right], right))
# Expand substring
# tc: O(1)
right += 1
# MaxHeap:
# root of maxHeap now holds the max of the current window,
# so its holding the max for the first window,
# we append to the result list
peekMax = -maxHeap[0][0]
result.append(peekMax)
# tc: iterate over n O(n)
while right < n:
# Expand window
newElem = nums[right]
# Push new element: (-value, index)
heapq.heappush(maxHeap, (-newElem, right))
# Remove top of maxHeap if it is stale (outside window)
while maxHeap[0][1] <= (right - k):
heapq.heappop(maxHeap)
# MaxHeap:
# root of maxHeap now holds the max of the current window,
# we append to the result list
peekMax = -maxHeap[0][0]
result.append(peekMax)
# Expand substring
# tc: O(1)
right += 1
# overall: tc O(n log k)
# overall: sc O(n)
return result| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Dynamic Programming] Tracking Max Value By Dynamic Programming K Block Partitioning Encountered L To R and R to L Then Sliding Fixed Window And Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# Sliding Window (Fixed Size Window)
# Substring Representation:
# - Partition array into blocks of size k
# - leftMax[i] = max from block start to i
# - rightMax[i] = max from block end to i (reverse scan)
# Empty check:
if not nums or k == 0:
return []
# We will split the original nums array into k sized blocks:
# index: 0 1 2 3 4 5 6 7
# nums: 1 3 -1 -3 5 3 6 7
# blocks: [------] [------] [----]
# B0 B1 B2
# Blocks are based only on index position and k size
# They are not based on the direction of the scan
n = len(nums)
# L to R Max Values, to L Max Values
leftMax = [0] * n
rightMax = [0] * n
# Fill leftMax: find the running max on a per block basis L to R
for i in range(n):
# Start of new block: max is first element of block
if i % k == 0:
leftMax[i] = nums[i]
# In Middle of block: compare currNum to prevMax
else:
leftMax[i] = max(leftMax[i-1], nums[i])
# Fill rightMax: find the running max on a per block basis R to L
for i in range(n-1, -1, -1):
# Start of new block: max is first element of block
if (i+1) % k == 0 or i == n-1:
rightMax[i] = nums[i]
# In Middle of block: compare currNum to prevMax
else:
rightMax[i] = max(rightMax[i+1], nums[i])
# Compute sliding fixed window max
res = []
left = 0
right = k - 1
# -----------------------------
# Combining Precomputed with Fixed Window:
# we are iterating a fixed window of size k:
# Case 1: fixed window fully inside block
#
# [ Window ]
# [ BLOCK ]
#
# Case 2: fixed window spans 2 blocks
#
# [ Window ]
# [ BLOCK A ][ BLOCK B ]
# -----------------------------
# Grabbing Farthest Data Available To Use From Left and Right:
# Case 1: fixed window fully inside block
#
# [ Window ]
# [ BLOCK ]
# ^ ^
# R L
# Case 2: fixed window spans 2 blocks
#
# [ Window ]
# [ BLOCK A ][ BLOCK B ]
# ^ ^
# R L
# -----------------------------
# Window Max Coverage:
# R = index max from iterating Right to Left (<---)
# L = index max from iterating Left to Right (--->)
# Case 1: Entire Window Is Accounted For
#
# [ Window ]
# [ BLOCK ]
# ^ ^
# R --------- L
# (<---) (--->)
# Case 2: Entire Window Is Accounted For
#
# [ Window ]
# [ BLOCK A ][ BLOCK B ]
# ^ ^
# R --------- L
# (<---) (--->)
# tc: iterate over nums array O(n)
while right < n:
# Re: see above diagrams
res.append(max(rightMax[left], leftMax[right]))
left += 1
right +=1
# overall: time complexity O(n)
# overall: space complexity O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Values Unsafe For Duplicates [TC Opt] - Sliding Window/Variable Size Window
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# Sliding Window (Fixed Size Window) + Monotonic Deque
# Window Representation:
# - Maintain a monotonic decreasing deque
# - Deque stores actual values (not indices)
# - Front of deque is always the max of current window
# Idea:
# - Before adding new element, pop from back any values
# smaller than it (they can never be the max)
# - When window slides, pop from front if it equals
# the element being removed (nums[i])
# - Front of deque is always current window max
# Unsafe For Duplicates:
# - Stores values instead of indices
# - When removing outgoing element, we compare by value not position
# - If duplicate values exist, we may accidentally remove the wrong copy
# Empty check:
if not nums or k == 0:
return []
n = len(nums)
# result list for each window
# sc: O(n)
res = []
# Deque():
# - a double ended queue
# - O(1) insertion and removal from both sides
# Deque left and right ends representation:
# - Front (left end): largest element value in current window
# dq[0] always holds the current window max value
# - Back (right end): most recently added value
# dq[-1] holds the smallest element seen so far
# Monotonic decreasing order means:
# High -> Low
# left (high) right (low)
# [ val=8, val=6, val=3, val=1 ] Our Queue: Monotonic values always decreasing
# stores values in monotonic decreasing order
# sc: O(k)
dq = deque()
# Initialize first window:
# Grab first k elements and place into deque
# tc: O(k)
for i in nums[:k]:
# Monotonic Property:
# Remove elements smaller than new num from right side to keep Monotonic High -> Low
# tc: O(1) amortized
while dq and i > dq[-1]:
dq.pop()
# Append new value after ensuring Monotonic High -> Low
dq.append(i)
# Deque front now holds max value of first window
res = [dq[0]]
# Slide window across remaining elements
# tc: O(n - k)
for i, x in enumerate(nums[k:]):
# Sliding Window Single Iteration:
# since at most, we are only adding 1 new element,
# since we have a fixed window size,
# at most, only 1 old element can go stale (out of bounds of the window)
# Unsafe Duplicate Stale Check:
# Since we store values instead of indices,
# we identify the outgoing element by value comparison
# nums[i] is the element leaving the window
# Risk: if duplicates exist, may remove the wrong copy
# tc: O(1)
outgoingVal = nums[i]
if dq[0] == outgoingVal:
dq.popleft()
# Monotonic Property:
# Remove elements smaller than new num from right side to keep Monotonic High -> Low
# tc: O(1) amortized
while dq and x > dq[-1]:
dq.pop()
# Append new value after ensuring Monotonic High -> Low
dq.append(x)
# Deque front now holds max value of current window
res.append(dq[0])
# overall: tc O(n)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 4: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Indices Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# Sliding Window (Fixed Size Window)
# Substring Representation
# - Maintain window of size k [left, right]
# - Monotonic Decreasing Deque storing indices
# - Front of deque always holds max element index
# Idea:
# - Remove elements smaller than incoming element (from back)
# - Remove front if it exits window
# - Front of deque is always window max
# Safe For Duplicates:
# - Stores indices instead of values
# - When removing outgoing element, we compare by index not value
# - Each index is unique, so we always know exactly which element is leaving
# - Duplicate values are handled correctly since we never confuse two
# elements at different positions
# Empty check:
if not nums or k == 0:
return []
n = len(nums)
# result list for each window
# sc: O(n)
res = []
# Deque():
# - a double ended queue
# - O(1) insertion and removal from both sides
# Deque left and right ends representation:
# - Front (left end): largest element index in current window
# dq[0] always holds the index of the current window max
# - Back (right end): most recently added index
# dp[-1] holds the index of the smallest element seen so far
# Monotonic decreasing order means:
# High -> Low
# left (high) right (low)
# [ idx=2, idx=4, idx=5, idx=7 ] Our Queue: Monotonic Increasing Indexes
# [ val=8, val=6, val=3, val=1 ] We Can Access: Values always Monotonic Decreasing
# stores indices in monotonic decreasing order of their values
# sc: O(k)
dq = deque()
# Initialize first window:
# Grab first k elements and place into deque
# tc: O(k)
for right in range(k):
# Monotonic Property:
# Remove elements smaller than new num from right side to keep Monotonic High -> Low
# tc: O(1) amortized
while dq and nums[dq[-1]] < nums[right]:
dq.pop()
# Append new index after ensuring Monotonic High -> Low
dq.append(right)
# tc: iterate over n O(n)
for right in range(n):
# Monotonic Property:
# Remove elements smaller new num from the right side to keep Monotonic High -> Low
# tc: O(1) amortized
while dq and nums[dq[-1]] < nums[right]:
dq.pop()
# Append new num after ensuring Monotonic High -> Low
dq.append(right)
# Sliding Window Single Iteration:
# since at most, we are only adding 1 new element,
# since we have a fixed window size,
# at most, only 1 old element can go stale (out of bounds of the window)
# Monotonic Trick:
# Since we keeping both:
# - Monotonic High -> Low
# - Iterating by 1 Index at a time
# left (high) right (low)
# [ idx=2, idx=4, idx=5, idx=7 ] Our Queue: Monotonic Increasing Indexes
# [ val=8, val=6, val=3, val=1 ] We Can Access: Values always Monotonic Decreasing
# So we only have to check if the highest value,
# which by definition will have the lowest index (oldest value in the deque)
# is now out of bounds of the fixed window
left = right - k
HighestValueIndex = dq[0]
# Remove stale highest number from front
# tc: O(1)
if not left <= HighestValueIndex <= right:
dq.popleft()
# Deque front now holds max index of current window
res.append(nums[dq[0]])
# overall: tc O(n)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
930. Binary Subarrays With Sum ::2:: - Medium
Topics: Array, Hash Table, Sliding Window, Prefix Sum
Intro
Given a binary array nums and an integer goal, return the number of non-empty subarrays with a sum goal. A subarray is a contiguous part of the array.
| Example Input | Output |
|---|---|
| nums = [1,0,1,0,1], goal = 2 | 4 |
| nums = [0,0,0,0,0], goal = 0 | 15 |
Constraints:
1 ≤ nums.length ≤ 3 * 10^4
nums[i] is either 0 or 1
0 ≤ goal ≤ nums.length
Abstraction
wow!
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Two Pointer] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal (Optimized For Binary Input) - Sliding Window/Variable Size Window
def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
# Two Pointer (Variable Size Window)
# Substring Representation:
# - Maintain window [left, right]
# - curr_sum tracks sum of current window
# Idea:
# - Directly counting subarrays with sum == goal is hard
# with two pointers because we can't know when to stop shrinking
# - Instead, count subarrays with sum <= goal via atMost()
# - exactly(goal) = atMost(goal) - atMost(goal - 1)
# - Inside atMost: expand right, shrink left when sum > goal,
# right - left + 1 counts all valid subarrays ending at right
# goal = 2
# nums = [1, 0, 1, 0, 1]
# --------------------------------------
# atMost(2): all windows with sum <= 2
# nums = [1, 0, 1, 0, 1]
# LR currSum=1, res += 1 (1 subarray ending at R)
# L R currSum=1, res += 2 (2 subarrays ending at R)
# L R currSum=2, res += 3 (3 subarrays ending at R)
# L R currSum=2, res += 4 (4 subarrays ending at R)
# L R currSum=3 -> shrink -> currSum=2, res += 4 (4 subarrays ending at R)
# atMost(2) total = 1+2+3+4+4 = 14
#
# --------------------------------------
# atMost(1): all windows with sum <= 1
# nums = [1, 0, 1, 0, 1]
# LR currSum=1, res += 1 (1 subarray ending at R)
# L R currSum=1, res += 2 (2 subarrays ending at R)
# L R currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
# L R currSum=1, res += 3 (3 subarrays ending at R)
# L R currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
# atMost(1) total = 1+2+2+3+2 = 10
# --------------------------------------
# exactly(2) = atMost(2) - atMost(1) = 14 - 10 = 4
# [1, 0, 1]
# [1, 0, 1, 0]
# [0, 1, 0, 1]
# [1, 0, 1]
# Only valid because nums contains only 0 and 1:
# shrinking window always decreases sum predictably
# tc: O(1)
def atMost(goal) -> int:
# Guard against negative goal
if goal < 0:
return 0
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
currSum = 0
res = 0
# tc: iterate over n O(n)
for right in range(len(nums)):
# Expand window
# tc: O(1)
currSum += nums[right]
# Shrink window until sum <= goal
# tc: O(1) amortized
while currSum > goal:
currSum -= nums[left]
left += 1
# All subarrays ending at right with sum <= goal
# tc: O(1)
windowLen = right - left + 1
# Add to total res
res += windowLen
return res
# exactly(goal) = atMost(goal) - atMost(goal - 1)
# overall: tc O(n)
# overall: sc O(1)
return atMost(goal) - atMost(goal - 1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Prefix Sum] Prefix Sum With HashMap (Works For Any Input) - Sliding Window/Variable Size Window
def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
# Prefix Sum + HashMap
# Representation:
# - prefix_count[s] = number of times prefix sum s has occurred
# - curr_sum = running prefix sum up to current index
# Idea:
# - For each index, check how many previous prefix sums
# equal curr_sum - goal
# - If prefix_count[curr_sum - goal] exists, those subarrays
# all sum to goal
# - prefix_count[0] = 1 accounts for subarrays starting at index 0
# goal = 2
# nums = [1, 0, 1, 0, 1]
# nums = [1, 0, 1, 0, 1]
# vaild: [.......]
# L R
# currSum = 2
# exludingSum = 1
# [....]
# nums = [1, 0, 1, 0, 1]
# valid: [......]
# L R
# currSum = 3
# goal - currSum = exludingSum
# 3 - 2 = 1
n = len(nums)
# Empty check
# tc: O(1)
if n == 0:
return 0
# HashMap: prefixSum -> number of occurrences
# sc: O(n)
prefixSumCounts = defaultdict(int)
# Base case: empty prefix has sum 0, seen once
# tc: O(1)
prefixSumCounts[0] = 1
# Running prefix sum
currSum = 0
# Total valid subarrays found
res = 0
# right boundary of prefixSum [0, right]
right = 0
# tc: iterate over n O(n)
while right < n:
# Expand prefix sum
# tc: O(1)
currSum += nums[right]
# If: prefixSum is higher than goal,
# check if we can restrict left, so that we exclude enough numbers
# to get our prefixSum back down to goal
# tc: O(1)
if currSum >= goal:
# Check Target:
# if at any point when iterating left to right,
# the prefixSum added up to this target
excludingSum = currSum - goal
# Check Times:
# number of times we encountered the target,
# this tells us how many places we can shift left
# to exclude numbers to get our prefixSum back down to goal
excludingSumTimes = prefixSumCounts[excludingSum]
# Add Times:
# each location we can move left to,
# is another prefixSum window that can be restricted via left to achieve goal
res += excludingSumTimes
# Record current prefixSum to occurrence count
# tc: O(1)
prefixSumCounts[currSum] += 1
# Move right pointer
right += 1
# overall: tc O(n)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Prefix Sum] Prefix Sum With Bounded Array (Optimized For Binary Input) - Sliding Window/Variable Size Window
def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
# Prefix Sum + Array of (N+1)
# Representation:
# - prefix_count[s] = number of times prefix sum s has occurred
# - curr_sum = running prefix sum up to current index
# Idea:
# - For each index, check how many previous prefix sums
# equal curr_sum - goal
# - If prefix_count[curr_sum - goal] exists, those subarrays
# all sum to goal
# - prefix_count[0] = 1 accounts for subarrays starting at index 0
# goal = 2
# nums = [1, 0, 1, 0, 1]
# nums = [1, 0, 1, 0, 1]
# vaild: [.......]
# L R
# currSum = 2
# exludingSum = 1
# [....]
# nums = [1, 0, 1, 0, 1]
# valid: [......]
# L R
# currSum = 3
# goal - currSum = exludingSum
# 3 - 2 = 1
n = len(nums)
# Empty check
# tc: O(1)
if n == 0:
return 0
# Tracks for up to all possible sums (everything set to 1)
# how many times we've seen a certain sum
# sc: O(n)
prefixSumCount = [0] * (n + 1)
# Base case: we encounter the sum 0, for the first time
# tc: O(1)
prefixSumCount[0] = 1
# Sum from left up to right
currSum = 0
# Total valid subarrays found
res = 0
# tc: iterate over n O(n)
for num in nums:
# Expand prefix sum
# tc: O(1)
currSum += num
# If: currSum is higher than goal,
# check if we can restrict left, so that we exclude enough numbers
# to get our currSum back down to goal
# tc: O(1)
if currSum >= goal:
# Check Target:
# if at any point when iterating left to right,
# the currSum added up to this target
exludingSum = currSum - goal
# Check Times:
# check number of times we encountered the target,
# this tells us how many places we can shift left to exlucde numbers to get our currSum back down to goal
exludingSumTimes = prefixSumCount[exludingSum]
# Add Times:
# each location we can move left to,
# is another currSum window that can be restricted via left to achieve goal
res += exludingSumTimes
# Record current prefix sum to use in future
# tc: O(1)
prefixSumCount[currSum] += 1
# overall: tc O(n)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1234. Replace the Substring for Balanced String :::: - Medium
Topics: String, Sliding Window
Intro
You are given a string s of length n containing only four kinds of characters: 'Q', 'W', 'E', and 'R'. A string is said to be balanced if each of its characters appears n / 4 times where n is the length of the string. Return the minimum length of the substring that can be replaced with any other string of the same length to make s balanced. If s is already balanced, return 0.
| Example Input | Output |
|---|---|
| s = "QWER" | 0 |
| s = "QQWE" | 1 |
| s = "QQQW" | 2 |
Constraints:
n == s.length
4 ≤ n ≤ 10^5
n is a multiple of 4
s contains only 'Q', 'W', 'E', and 'R'.
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Full Frequency HashMap Tracking All Chars Outside Window - Sliding Window/Variable Size Window
def balancedString(self, s: str) -> int:
# Sliding Window (Variable Size Window)
# String Representation:
# - Maintain variable window [left, right]
# - Current window represents the substring we will replace
# - Frequencies OUTSIDE the window must all be <= target
# Idea:
# - Store frequencies of chars OUTSIDE current window
# - Expand right to bring chars INTO window (removing from outside freq)
# - Once all outside frequencies <= target, window is valid
# - Shrink left to minimize valid window
# - Track smallest valid window seen
# Balanced String:
# - 4 possible characters: Q W E R
# - Each must appear exactly len(s) // 4 times
# - Any character appearing more than target times needs replacement
#
# e.g. s = "QWER", target = 1
# outsideCount = {Q:1, W:1, E:1, R:1} -> already balanced
#
# e.g. s = "QQWE", target = 1
# outsideCount = {Q:2, W:1, E:1, R:0}
# Q exceeds target by 1, need to replace 1 char in window
n = len(s)
# Target frequency for each character:
# - The string s has n characters, and 4 possible characters (Q W E R)
# - A perfectly balanced string would have each character appear n // 4 times
# tc: O(1)
# sc: O(1)
target = n // 4
# Sliding Window Boundaries
# sc: O(1)
left = 0
right = 0
# Frequency HashMap:
# - Stores frequencies of chars OUTSIDE current window
# - Only 4 possible characters Q W E R
# sc: O(1)
outsideCount = defaultdict(int)
for c in s:
outsideCount[c] += 1
# Minimum valid window size
minLen = float('inf')
# Early Exit:
# - If no character exceeds target balance count
# - Then all characters == target,
# since you can't have any char count above target
# without another char count below target to compensate leading to unbalance
# tc: O(1)
if max(outsideCount.values()) <= target:
return 0
# Expand sliding window
# tc: O(n)
for right in range(n):
# New character has entered window,
# remove count from outside window tracker
rightChar = s[right]
outsideCount[rightChar] -= 1
# Shrink from left while valid outside conditions:
# - All outside frequencies <= target => string is balanced
# - Window pointers are still valid
# tc: O(1) amortized
while max(outsideCount.values()) <= target and left <= right:
# Check curr valid window
currWindowLen = right - left + 1
minLen = min(minLen, currWindowLen)
# Remove char from window,
# add it back to outside count frequencies
# tc: O(1)
leftChar = s[left]
outsideCount[leftChar] += 1
# Shrink left pointer
# tc: O(1)
left += 1
# MinLen:
# smallest window available, with a valid outside count
# The window needs to be just large enough to absorb all excess, minLen,
# which tells you the minimum number of characters you need to replace.
# You don't need to know exactly what to replace them with, just that
# a replacement of that size exists that can fix the balance
# overall: tc O(n)
# overall: sc O(1)
return minLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Excess Count HashMap Tracking Overages Outside Window - Sliding Window/Variable Size Window
def balancedString(self, s: str) -> int:
# Sliding Window (Variable Size Window)
# Substring Representation:
# - Maintain window [left, right] representing the substring we can replace
# - Goal: Find minimum length substring which, when replaced, balances the string
# - Count of each character outside window should be <= n // 4
n = len(s)
target = n // 4
# Count total occurrences of each character in s
totalCount = defaultdict(int)
for c in s:
totalCount[c] += 1
# Only keep excess counts,
# characters that already <= target are ignored
excess = defaultdict(int)
for c in "QWER":
# Current total occurrences in string
currCount = totalCount[c]
# Extra occurrences beyond allowed target
extraCount = currCount - target
# If character does not exceed target,
# no replacement is needed
excess[c] = max(0, extraCount)
# Check whether any character still has excess frequency
hasExcess = False
for count in excess.values():
# Found a character occurring
# more than the allowed target count
if count > 0:
hasExcess = True
break
# If no character has excess frequency,
# the string is already balanced
if not hasExcess:
return 0
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
# Initialize min length as length of full original string
minLen = n
# tc: iterate over n O(n)
while right < n:
# Expand window:
# decrease count for current character if it's in excess
if s[right] in excess:
excess[s[right]] -= 1
while True:
# A character is considered "balanced"
# once the window has absorbed enough of its excess count
#
# Why <= 0 instead of == 0 ?
# excess[c] tracks how many more of character c
# still need to be included inside the window
#
# Example:
# excess['Q'] = 2
#
# After including 2 Q's in window:
# excess['Q'] = 0
#
# After including 3 Q's in window:
# excess['Q'] = -1
#
# Negative values are still valid because the window
# contains MORE than enough of that character
qBalanced = (excess['Q'] <= 0)
wBalanced = (excess['W'] <= 0)
eBalanced = (excess['E'] <= 0)
rBalanced = (excess['R'] <= 0)
# If any character excess is still uncovered,
# stop shrinking
if not (qBalanced and wBalanced and eBalanced and rBalanced):
break
# Current window is valid
minLen = min(minLen, right - left + 1)
# Remove left char from window
# and restore its excess requirement
leftChar = s[left]
if leftChar in excess:
excess[leftChar] += 1
# Iterate left window
left += 1
# Expand right pointer
right += 1
# overall: tc O(n)
# overall: sc O(1)
return minLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1208. Get Equal Substrings Within Budget ::1:: - Medium
Topics: String, Sliding Window
Intro
You are given two strings s and t of the same length and an integer maxCost. You want to change s to t. Changing the ith character of s to ith character of t costs |s[i] - t[i]| (i.e., the absolute difference between the ASCII values of the characters). Return the maximum length of a substring of s that can be changed to be the same as the corresponding substring of t with a cost less than or equal to maxCost. If there is no substring from s that can be changed to its corresponding substring from t, return 0.
| Example Input | Output |
|---|---|
| s = "abcd", t = "bcdf", maxCost = 3 | 3 |
| s = "abcd", t = "cdef", maxCost = 3 | 1 |
| s = "abcd", t = "acde", maxCost = 0 | 1 |
Constraints:
1 ≤ s.length ≤ 10^5
t.length == s.length
0 ≤ maxCost ≤ 10^6
s and t consist of only lowercase English letters.
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
# Sliding Window + Two Pointers
# Substring Representation:
# - Maintain window [left, right] representing the current substring of s
# - The window is valid if totalCost <= maxCost
# - Find the max length valid window
# Idea:
# - For each character at index 'right', compute cost to change s[right] -> t[right]
# - Expand window by moving right pointer and accumulating totalCost
# - If totalCost exceeds maxCost, shrink window from left until valid
# - Track max window length during iteration
n = len(s)
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
# MaxLen
# sc: O(1)
totalCost = 0
maxLen = 0
# tc: iterate over n O(n)
while right < n:
# Add new character
rightCharS = s[right]
rightCharT = t[right]
# Get Cost:
# calculate cost to change S to T, we need to spend this money now
cost = abs(ord(rightCharS) - ord(rightCharT))
totalCost += cost
# Shrink window while totalCost is invalid
while maxCost < totalCost:
# Remove old character
leftCharS = s[right]
leftCharT = t[right]
# Get Cost:
# calculate cost to change S to T, we are saving this money now
totalCost -= abs(ord(s[left]) - ord(t[left]))
# Iterate window
left += 1
# TotalCost is now valid:
# valid window found, compare to max length
windowLen = (right - left) + 1
maxLen = max(maxLen, windowLen)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
992. Subarrays with K Different Integers ::1:: - Hard
Topics: Array, Hash Table, Sliding Window, Counting
Intro
Given an integer array nums and an integer k, return the number of good subarrays of nums. A good array is an array where the number of different integers in that array is exactly k. For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3. A subarray is a contiguous part of an array.
| Example Input | Output |
|---|---|
| nums = [1,2,1,2,3], k = 2 | 7 |
| nums = [1,2,1,3,4], k = 3 | 3 |
Constraints:
1 ≤ nums.length ≤ 2 * 10^4
1 ≤ nums[i], k ≤ nums.length
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window
def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window)
# Substring Representation:
# - Maintain window [left, right] representing current subarray
# - Window is valid if it contains <= k distinct numbers
# - For exactly k distinct: count = atMostK(k) - atMostK(k-1)
# Idea:
# - Use a sliding window to count number of subarrays
# with at most k distinct integers
# - For each right pointer expansion:
# 1. Add nums[right] to window
# 2. Shrink left while distinct count exceeds k
# 3. Add window length (right - left + 1) to total count
# - Exact k distinct subarrays = atMostK(k) - atMostK(k-1)
# Helper: count subarrays with at most k distinct integers
# overall: tc O(n)
# overall: sc O(n)
def atMostK(nums, currK):
# Sliding Window Data
# sc: O(n)
count = defaultdict(int)
# Tracking Unique Values In Window
distinct = 0
# Sliding Window Variables
# sc: O(1)
left = 0
right = 0
n = len(nums)
res = 0
# tc: iterate over n O(n)
while right < n:
# Expand window
rightNum = nums[right]
# Check if new num has been found
if count[rightNum] == 0:
distinct += 1
# Increase num count
count[rightNum] += 1
# Shrink window while distinct count is invalid
while currK < distinct:
# Remove old value
leftNum = nums[left]
count[leftNum] -= 1
# Check if we lost a unique value
if count[leftNum] == 0:
distinct -= 1
# Shrink window
# tc: O(1)
left += 1
# Valid window found:
# All subarrays within this valid subarray are valid as well,
# so the length of the larger subarray is the number of valid subarrays we have found
res += (right - left) + 1
# We don't double count because each subarray is counted exactly once at its right endpoint
# Expand right pointer
right += 1
# tc: O(n)
# sc: O(n)
return res
# atMost(k): counts all subarrays with <= k distinct elements
# atMost(k-1): counts all subarrays with <= k-1
# so doing atMost(l) - atMost(k-1) removes all the answers that were <= k-1
# which leaves only the answers that were == k
res = atMostK(nums, k) - atMostK(nums, k-1)
# overall: tc O(n)
# overall: sc O(n)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
904. Fruit Into Baskets ::2:: - Medium
Topics: Array, Hash Table, Sliding Window
Intro
You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces. You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow: You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.
| Example Input | Output |
|---|---|
| fruits = [1,2,1] | 3 |
| fruits = [0,1,2,2] | 3 |
| fruits = [1,2,3,2,2] | 4 |
Constraints:
1 ≤ fruits.length ≤ 10^5
0 ≤ fruits[i] < fruits.length
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Frequency Fruit Count With Shrink From Left While More Than 2 Fruit In Window - Sliding Window/Variable Size Window
def totalFruit(self, fruits: List[int]) -> int:
# Sliding Window + Hashmap
# Idea:
# - Use a sliding window to maintain at most 2 distinct fruit types.
# - Expand right; shrink left when more than 2 distinct types appear.
# - Track maximum window length.
# Rules:
# 1. Maintain a hashmap: fruit type -> count in current window.
# 2. Expand right pointer, increment count.
# 3. If distinct fruits > 2, shrink window from left until <= 2.
# 4. Update maximum length for each right.
# Fruit Count:
# at most 2 types of fruit in hashmap
# sc: O(1)
count = defaultdict(int)
# Window Representation:
# sc: O(1)
left = 0
right = 0
n = len(fruits)
maxLen = 0
# Iterate over array
# tc: O(n)
while right < n:
# Add right num
rightNum = fruits[right]
count[rightNum] += 1
# Shrink window while window is invalid
# - more than 2 distinct fruits in window
while len(count) > 2:
# Remove left num
leftNum = fruits[left]
count[leftNum] -= 1
# Check if ran out of a certain type of fruit, then remove
if count[fruits[left]] == 0:
del count[fruits[left]]
# Iterate window
left += 1
# Update maximum number of fruits collected
currWindow = right - left + 1
maxLen = max(maxLen, currWindow)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Last Seen Index Tracking With Shrink From Left - Sliding Window/Variable Size Window
def totalFruit(self, fruits: List[int]) -> int:
# Sliding Window + Last Seen Index Map
# Idea:
# - Maintain a window [l, r]
# - Track last seen index of each fruit type
# - If we ever have > 2 fruit types:
# remove the fruit with the smallest last seen index
# and move left pointer just past it
# Invariant:
# - window always contains at most 2 fruit types
last_seen = {}
l = 0
res = 0
for r in range(len(fruits)):
# Update last seen index of current fruit
last_seen[fruits[r]] = r
# If we have more than 2 fruit types,
# remove the one that was seen least recently
if len(last_seen) > 2:
# Find fruit with smallest last seen index
fruit_to_remove = min(last_seen, key=last_seen.get)
# Move left pointer past it
l = last_seen[fruit_to_remove] + 1
# Remove it from map
del last_seen[fruit_to_remove]
# Update result
res = max(res, r - l + 1)
#
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit ::1:: - Medium
Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Ordered Set, Monotonic Queue
Intro
Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.
| Example Input | Output |
|---|---|
| nums = [8,2,4,7], limit = 4 | 2 |
| nums = [10,1,2,4,7,2], limit = 5 | 4 |
| nums = [4,2,2,2,4,4,2,2], limit = 0 | 3 |
Constraints:
1 ≤ nums.length ≤ 10^5
1 ≤ nums[i] ≤ 10^9
0 ≤ limit ≤ 10^9
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window
def longestSubarray(self, nums: List[int], limit: int) -> int:
# Sliding Window + Monotonic Deques
# Idea:
# - Maintain two deques:
# max_d: decreasing deque for maximum
# min_d: increasing deque for minimum
# - Expand right pointer; shrink left if max - min > limit.
# - Track maximum window length.
# Rules:
# 1. Append nums[right] to both deques, maintaining monotonic quality
# 2. While current window violates limit (max - min > limit), shrink from left
# 3. Update maximum window length
# Sliding Window Data
# sc: O(n)
maxDeque = deque() # stores elements in decreasing order
minDeque = deque() # stores elements in increasing order
# Window Representation:
# sc: O(1)
left = 0
right = 0
maxLen = 0
n = len(nums)
# Iterate over window
while right < n:
# Add new num to window
rightNum = nums[right]
# Maintain Monotonic decreasing max deque
while maxDeque and rightNum > maxDeque[-1]:
maxDeque.pop()
# put num in corresponding location in maxDeque
maxDeque.append(rightNum)
# Maintain Monotonic increasing min deque
while minDeque and rightNum < minDeque[-1]:
minDeque.pop()
# put num in corresponding location in minDeque
minDeque.append(rightNum)
difference = maxDeque[0] - minDeque[0]
# Shrink window if difference exceeds limit
while limit < difference:
# if current max is the left most num, remove from maxDeque from window
if nums[left] == maxDeque[0]:
maxDeque.popleft()
# if current min is the left most num, remove from minDeque from window
if nums[left] == minDeque[0]:
minDeque.popleft()
# Iterate window
left += 1
# grab new difference
difference = maxDeque[0] - minDeque[0]
# Update maximum window length
maxLen = max(maxLen, right - left + 1)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(n)
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
658. Find K Closest Elements ::2:: - Medium
Topics: Array, Binary Search, Sliding Window, Prefix Sum
Intro
Given a sorted integer array arr, two integers k and x, return the k closest integers to x in the array. The result should also be sorted in ascending order. An integer a is closer to x than an integer b if:
- |a - x| lt |b - x|, or
- |a - x| == |b - x| and a lt b
| Example Input | Output |
|---|---|
| arr = [1,2,3,4,5], k = 4, x = 3 | [1,2,3,4] |
| arr = [1,1,2,3,4,5], k = 4, x = -1 | [1,1,2,3] |
Constraints:
1 ≤ k ≤ arr.length
1 ≤ arr.length ≤ 10^4
arr is sorted in ascending order
-10^4 ≤ arr[i], c ≤ 10^4
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Simple Window Version - Sliding Window/Variable Size Window
def findClosestElements(self, arr, k, x):
n = len(arr) - 1
# Sliding Window Boundaries
# sc: O(1)
left = 0
right = n
currWindowLen = right - left + 1
# Shrink window from n down to k elements
# At each step, remove the element farther from x
while k < currWindowLen:
# value difference from left and right nums to target x
leftNum = arr[left]
leftValue = abs(leftNum - x)
rightNum = arr[right]
rightValue = abs(rightNum - x)
# Between left and right, remove farther num from target
# If equal, default to remove right
# (keep left, problem prefers smaller)
if leftValue <= rightValue:
right -= 1
else:
left += 1
# Check new window length
currWindowLen = right - left + 1
# left is now at the best location
res = arr[left:(right + 1)]
# overall: tc O()
# overall: sc O()
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Binary Search To Create Sliding Window - Sliding Window/Variable Size Window
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
# Binary Search + Sliding Window Approach
# Idea:
# 1. The closest k elements must form a contiguous subarray of length k.
# 2. Use binary search to find the left boundary of that window.
# 3. Compare distances from x for potential windows, and shrink towards the closest.
n = len(arr)
# Binary search boundaries for left index of window
left = 0
right = n - k # window of size k, so left can go at most n-k
# Binary search over possible left boundaries
# tc: O(log(n-k))
while left < right:
# grab mid
mid = (left + right) // 2
# Compare distances of window edges to x
leftEdgeDist = x - arr[mid]
rightEdgeDist = arr[mid + k] - x
# Left edge is farther from x than the right edge
# Shift left boundary to get left edge closer
# tc: O(1)
if rightEdgeDist < leftEdgeDist:
left = mid + 1
# Right edge is farther or equal to from x than the left edge
# Shift left boundary to get right edge closer
# tc: O(1)
else:
right = mid
# Left now points to optimal left boundary because:
# - Every position to the left was eliminated (left edge was too far)
# - Every position to the right was eliminated (right edge was too far)
# - left == right means only one candidate remains that was never eliminated
# - This is the position where the window [left, left+k] is closest to x
# Splice window of size k
# tc: O(k)
res = arr[left:left + k]
# overall: tc O(log(n-k) + k)
# overall: sc O(k)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1358. Number of Substrings Containing All Three Characters ::2:: - Medium
Topics: Hash Table, String, Sliding Window
Intro
Given a string s consisting only of characters a, b and c. Return the number of substrings containing at least one occurrence of all these characters a, b and c.
| Example Input | Output |
|---|---|
| s = "abcabc" | 10 |
| s = "aaacb" | 3 |
| s = "abc" | 1 |
Constraints:
3 ≤ s.length ≤ 5 * 10^4
s only consists of a, b or c characters
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window
def numberOfSubstrings(self, s: str) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - HashMap stores frequency of each char in current window
# - Window is valid when it contains at least one of each: a, b, c
# Idea:
# - Expand right to add new characters
# - Once window is valid (has all 3 chars), shrink from left
# to find the smallest valid window ending at right
# - Every starting index from [0, left-1] also forms a valid substring
# ending at right, since adding more chars to the left keeps it valid
# - So add left to result at each right
# Counting Valid Substrings:
# s = "abcabc", right = 2 (window = "abc")
#
# s = [a, b, c, a, b, c]
# [.........]
# 0 2
# Window "abc" is valid, shrink left until invalid:
# left=0 -> shrink -> left=1, window="bc" invalid
# left=1, add left=1 to res
# This counts 1 valid substring ending at right=2: ["abc"]
#
# s = [a, b, c, a, b, c]
# [...............]
# 0 5
# right=5: left shrinks to 3, add left=3 to res
# This counts 3 valid substrings ending at right=5:
# ["abcabc", "bcabc", "cabc"]
n = len(s)
# HashMap: char -> frequency in current window
# sc: O(1) at most 3 chars
count = defaultdict(int)
# Sliding Window Boundaries
# sc: O(1)
left = 0
# Total valid substrings found
res = 0
# tc: iterate over n O(n)
for right in range(n):
# Add incoming character to window
# tc: O(1)
count[s[right]] += 1
# Shrink from left while window contains all 3 chars
# Every position we shrink past becomes a valid starting index
# tc: O(1) amortized
while all(count[c] > 0 for c in "abc"):
count[s[left]] -= 1
left += 1
# All starting indices [0, left-1] form valid substrings ending at right
# since adding more chars to the left of the window keeps it valid
# tc: O(1)
res += left
# overall: tc O(n)
# overall: sc O(1)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Last Seen Index] Track Last Seen Index For Each Char - Sliding Window/Variable Size Window
def numberOfSubstrings(self, s: str) -> int:
# Last Seen Index Tracking (Single Pass)
# Representation:
# - Track the last seen index of each char: a, b, c
# - last_a, last_b, last_c = most recent index of each char seen so far
# Idea:
# - For each index i, the earliest we can start a valid substring
# ending at i is min(last_a, last_b, last_c)
# - Any starting index from [0, min(last_a, last_b, last_c)]
# forms a valid substring ending at i
# - So add min(last_a, last_b, last_c) + 1 to count at each i
# Why min(last_a, last_b, last_c) + 1?
# s = "abcabc"
#
# i=2 (c): last_a=0, last_b=1, last_c=2
# min = 0, count += 0 + 1 = 1
# valid substrings ending at i=2: ["abc"]
#
# i=3 (a): last_a=3, last_b=1, last_c=2
# min = 1, count += 1 + 1 = 2
# valid substrings ending at i=3: ["abca", "bca"]
#
# i=5 (c): last_a=3, last_b=4, last_c=5
# min = 3, count += 3 + 1 = 4
# valid substrings ending at i=5: ["abcabc", "bcabc", "cabc", "abc"]
# Last seen index for each character
# initialized to -1 meaning not yet seen
# sc: O(1)
last_a = last_b = last_c = -1
# Total valid substrings found
count = 0
# tc: iterate over n O(n)
for i in range(len(s)):
# Update last seen index for current char
# tc: O(1)
if s[i] == 'a':
last_a = i
elif s[i] == 'b':
last_b = i
else:
last_c = i
# Earliest valid start = min of all last seen indices
# All starting indices [0, min+1] form valid substrings ending at i
# +1 because index is 0-based
# tc: O(1)
count += min(last_a, last_b, last_c) + 1
# overall: tc O(n)
# overall: sc O(1)
return count| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1004. Max Consecutive Ones III ::2:: - Medium
Topics: Array, Binary Search, Sliding Window, Prefix Sum
Intro
Given a binary array nums and an integer k, return the maximum number of consecutive 1's in the array if you can flip at most k 0's.
| Example Input | Output |
|---|---|
| nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 | 6 |
| nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], k = 3 | 10 |
Constraints:
1 ≤ nums.length ≤ 10^5
nums[i] is either 0 or 1
0 ≤ k ≤ nums.length
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window With Shrinking Max Tracking - Sliding Window/Variable Size Window
def longestOnes(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - zeros tracks number of 0s in current window
# - Window is valid when zeros <= k
# Idea:
# - Expand right to add new elements
# - If new element is 0, increment zeros
# - If zeros > k, shrink from left until valid again
# - Track longest valid window seen
# Window Example:
# nums = [1, 1, 0, 0, 1, 1, 0, 1], k = 2
#
# right=2: zeros=1, window=[1,1,0]
# [1, 1, 0, 0, 1, 1, 0, 1]
# L R
# valid, maxLen=3
#
# right=3: zeros=2, window=[1,1,0,0]
# [1, 1, 0, 0, 1, 1, 0, 1]
# L R
# valid, maxLen=4
#
# right=6: zeros=3 > k, shrink left
# [1, 1, 0, 0, 1, 1, 0, 1]
# L R
# zeros=2, maxLen=6
n = len(nums)
# Sliding Window Boundaries
# sc: O(1)
left = 0
# Number of zeros in current window
numZeros = 0
# Longest valid window seen
maxLen = 0
# tc: iterate over n O(n)
for right in range(n):
# Add num
# tc: O(1)
if nums[right] == 0:
numZeros += 1
# Shrink from left while window is invalid:
# - num of 0's must be <= k, else window is invalid
# - shift until number of 0's is below k
# tc: O(1) amortized
while k < numZeros:
if nums[left] == 0:
numZeros -= 1
left += 1
# Check curr window
# tc: O(1)
currWindow = right - left + 1
maxLen = max(maxLen, currWindow)
# overall: tc O(n)
# overall: sc O(1)
return maxLen| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Two Pointer Sliding Window Without Shrinking - Sliding Window/Variable Size Window
def longestOnes(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - zeroCount tracks number of 0s in current window
# - Window never shrinks — left only moves when zeroCount > k
# Idea:
# - Expand right to add new elements
# - If new element is 0, increment zeroCount
# - If zeroCount > k, slide window forward by 1 (move left by 1)
# without shrinking — window size stays the same or grows
# - Final answer is len(nums) - left (window size at end)
# Key Insight vs Solution 1:
# - S1 shrinks window back down when invalid, tracking maxLen explicitly
# - S2 never shrinks — once we find a window of size w,
# we only move forward when we find something bigger
# - left only advances when zeroCount > k, keeping window size stable
# - At the end, len(nums) - left gives the largest window we ever reached
# Window Example:
# nums = [1, 1, 0, 0, 1, 1, 0, 1], k = 2
#
# right=3 (0): zeroCount=2, window=[1,1,0,0], left=0
# [1, 1, 0, 0, 1, 1, 0, 1]
# L R
# valid, window size=4
#
# right=6 (0): zeroCount=3 > k
# [1, 1, 0, 0, 1, 1, 0, 1]
# L R
# slide left by 1, nums[left=2]=0 so zeroCount=2
# window size stays at 6, never shrinks
#
# final: len(nums) - left = 8 - 2 = 6
n = len(nums)
# Sliding Window Boundaries
# sc: O(1)
left = 0
# Number of zeros in current window
numZeros = 0
# tc: iterate over n O(n)
for right in range(n):
# Add incoming element to window
# tc: O(1)
if nums[right] == 0:
numZeros += 1
# Shrink from left while window is invalid:
# - num of 0's must be <= k, else window is invalid
# - shift until number of 0's is below k
# tc: O(1) amortized
if k < numZeros:
if nums[left] == 0:
numZeros -= 1
left += 1
# Window never shrank, so largest window size = len(nums) - left
res = n - left
# overall: tc O(n)
# overall: sc O(1)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
1248. Count Number of Nice Subarrays ::3:: - Medium
Topics: Array, Hash Table, Math, Sliding Window, Prefix Sum
Intro
Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it. Return the number of nice sub-arrays.
| Example Input | Output |
|---|---|
| nums = [1,1,2,1,1], k = 3 | 2 |
| nums = [2,4,6], k = 1 | 0 |
Constraints:
1 ≤ nums.length ≤ 50000
1 ≤ nums[i] ≤ 10^5
1 ≤ k ≤ nums.length
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [left, right]
# - oddCount tracks number of odd numbers in current window
# Idea:
# - Directly counting subarrays with exactly k odds is hard
# with two pointers because we can't know when to stop shrinking
# - Instead, count subarrays with at most k odds via atMost()
# - exactly(k) = atMost(k) - atMost(k - 1)
# Why atMost works:
# - atMost(k) counts subarrays with 0, 1, 2, ... k odds
# - atMost(k - 1) counts subarrays with 0, 1, 2, ... k-1 odds
# - subtracting leaves only subarrays with exactly k odds
# Window Example:
# nums = [1, 1, 2, 1, 1], k = 3
#
# atMost(3): all windows with oddCount <= 3
# [1, 1, 2, 1, 1]
# LR oddCount=1, res += 1
# L R oddCount=2, res += 2
# L R oddCount=2, res += 3
# L R oddCount=3, res += 4
# L R oddCount=4 > 3, shrink -> left=1, oddCount=3, res += 4
# atMost(3) = 14
#
# atMost(2): all windows with oddCount <= 2
# [1, 1, 2, 1, 1]
# LR oddCount=1, res += 1
# L R oddCount=2, res += 2
# L R oddCount=2, res += 3
# L R oddCount=3 > 2, shrink -> left=1, oddCount=2, res += 3
# L R oddCount=3 > 2, shrink -> left=2, oddCount=2, res += 3
# atMost(2) = 12
#
# exactly(3) = atMost(3) - atMost(2) = 14 - 12 = 2
# valid subarrays: [1,1,2,1] and [1,1,2,1,1]
def atMost(k: int) -> int:
# Sliding Window Boundaries
# sc: O(1)
left = 0
right = 0
n = len(nums)
# Sliding Window Data:
# Number of odds in current window
# sc: O(1)
oddNums = 0
# Total valid subarrays found
res = 0
# tc: iterate over n O(n)
while right < n:
# Add num, check if odd
# tc: O(1)
if nums[right] % 2 == 1:
oddNums += 1
# Shrink from left until oddCount <= k
# tc: O(1) amortized
while k < oddNums:
# Remove left, check if odd
if nums[left] % 2 == 1:
oddNums -= 1
# Iterate window
left += 1
# All subarrays within curr subarrays are also valid
# tc: O(1)
res += right - left + 1
# Iterate window
right += 1
# tc: O(n)
# sc: O(n)
return res
# exactly(k) = atMost(k) - atMost(k - 1)
# overall: tc O(n)
# overall: sc O(1)
return atMost(k) - atMost(k - 1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Sliding Window] Two Pointer Single Pass With Start Gap - Sliding Window/Variable Size Window
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Maintain window [lp, rp]
# - counter tracks number of odd numbers in current window
# - start_gap tracks number of valid left boundaries for current window
# Idea:
# - Expand right to add new elements
# - Once counter == k, shrink from left counting how many positions
# keep counter == k (these are all valid left boundaries)
# - start_gap persists across iterations — once we find valid left
# boundaries for a window of k odds, any future right expansion
# that doesn't add an odd number still has the same valid left boundaries
# - Add start_gap to total at each right
# Why start_gap persists:
# nums = [1, 1, 2, 1, 1], k = 3
#
# rp=3 (odd): counter=3 == k, start_gap=0
# [1, 1, 2, 1, 1]
# L R
# shrink: lp=0 (odd) -> counter=2, start_gap=1
# [1, 1, 2, 1, 1]
# L R
# counter != k, stop. total += 1
#
# rp=4 (odd): counter=3 == k, start_gap=0
# [1, 1, 2, 1, 1]
# L R
# shrink: lp=1 (odd) -> counter=2, start_gap=1
# [1, 1, 2, 1, 1]
# L R
# counter != k, stop. total += 1
#
# total = 2
# valid subarrays: [1,1,2,1] and [1,1,2,1,1]
# Single pass — no need to call atMost twice
# sc: O(1)
lp = 0
# Number of odd numbers in current window
counter = 0
# Number of valid left boundaries for current window
start_gap = 0
# Total valid subarrays found
total = 0
# tc: iterate over n O(n)
for rp in range(len(nums)):
# Add incoming element to window
# tc: O(1)
if nums[rp] % 2 != 0:
counter += 1
# Once counter == k, count valid left boundaries
# Reset start_gap since new odd resets the boundary count
# tc: O(1) amortized
if counter == k:
start_gap = 0
# Shrink from left while window stays valid (counter == k)
# Each shrink that keeps counter == k is another valid left boundary
while counter == k:
start_gap += 1
if nums[lp] % 2 != 0:
counter -= 1
lp += 1
# start_gap persists: even if right adds an even number,
# same left boundaries remain valid from previous valid window
# tc: O(1)
total += start_gap
# overall: tc O(n)
# overall: sc O(1)
return total| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Sliding Window] Two Pointer Deque - Sliding Window/Variable Size Window
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
total = 0
count_ones = 0
ones_indicies = deque()
threshold = -1
for j in range(len(nums)):
if nums[j] % 2 == 1:
ones_indicies.append(j)
if count_ones == k:
threshold = ones_indicies.popleft()
count_ones -= 1
count_ones += 1
if count_ones == k:
total += ones_indicies[0] - threshold
return total| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
862. Shortest Subarray with Sum at Least K ::1:: - Hard
Topics: Array, Binary Search, Queue, Sliding Window, Heap (Priority Queue), Prefix Sum, Monotonic Queue
Intro
Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1. A subarray is a contiguous part of an array.
| Example Input | Output |
|---|---|
| nums = [1], k = 1 | 1 |
| nums = [1,2], k = 4 | -1 |
| nums = [2,-1,2], k = 3 | 3 |
Constraints:
1 ≤ nums.length ≤ 10^5
-10^5 ≤ nums[i] ≤ 10^5
1 ≤ k ≤ 10^9
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Prefix Sum With Monotonic Deque - Sliding Window/Variable Size Window
def shortestSubarray(self, nums: List[int], k: int) -> int:
# Prefix Sum + Monotonic Increasing Deque
# Representation:
# - P[i] = prefix sum from nums[0] to nums[i-1]
# - Subarray sum [i, j] = P[j+1] - P[i]
# - Deque stores indices of monotonic increasing prefix sums
# Idea:
# - We want shortest subarray with sum >= k
# - Equivalent to finding shortest j - i where P[j] - P[i] >= k
# - Maintain monotonic increasing deque of prefix sum indices
# - For each j, pop from front when P[j] - P[dq[0]] >= k
# (valid subarray found, pop to find even shorter one)
# - Pop from back when P[j] <= P[dq[-1]]
# (current prefix is smaller, back element can never be a useful left boundary)
# Why not standard sliding window?
# - nums can contain negative numbers
# - Expanding window doesn't guarantee increasing sum
# - Standard two pointer breaks down with negatives
# - Prefix sum + monotonic deque handles negatives correctly
# Deque Representation:
# - Monotonic Increasing order means:
# Low -> High
#
# left (low) right (high)
# [ P[2], P[4], P[5], P[7] ] prefix sums always increasing
# [ idx=2, idx=4, idx=5, idx=7 ] indices always increasing
# Window Example:
# nums = [2, -1, 2], k = 3
# P = [0, 2, 1, 3]
#
# i=0: dq=[], append 0 -> dq=[0]
# i=1: P[1]=2, P[1]-P[0]=2 < 3, no pop front
# append 1 -> dq=[0,1]
# i=2: P[2]=1 <= P[1]=2, pop back
# append 2 -> dq=[0,2]
# i=3: P[3]=3, P[3]-P[0]=3 >= k, res=3-0=3, pop front
# P[3]-P[2]=2 < k, stop -> dq=[2]
# append 3 -> dq=[2,3]
# res = 3 (full array [2,-1,2] sums to 3)
n = len(nums)
# Build prefix sum array
# P[i] = sum of nums[0..i-1]
# sc: O(n)
P = [0] * (n + 1)
# tc: O(n)
for i in range(n):
P[i + 1] = P[i] + nums[i]
# Monotonic Increasing Deque storing prefix sum indices
# sc: O(n)
dq = deque()
# Shortest valid subarray length found
res = n + 1
# tc: iterate over n+1 prefix sums O(n)
for i in range(n + 1):
# Pop from front while valid subarray found
# P[i] - P[dq[0]] >= k means subarray [dq[0], i] sums to >= k
# Pop to find even shorter valid subarray
# tc: O(1) amortized
while dq and P[i] - P[dq[0]] >= k:
res = min(res, i - dq.popleft())
# Maintain monotonic increasing order from back
# If current prefix <= back of deque, back can never be a useful left boundary
# since current index is further right and has smaller/equal prefix sum
# tc: O(1) amortized
while dq and P[i] <= P[dq[-1]]:
dq.pop()
dq.append(i)
# overall: tc O(n)
# overall: sc O(n)
return res if res <= n else -1| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
713. Subarray Product Less Than K ::1:: - Medium
Topics: Array, Binary Search, Sliding Window, Prefix Sum
Intro
Given an array of integers nums and an integer k, return the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than k.
| Example Input | Output |
|---|---|
| nums = [10,5,2,6], k = 100 | 8 |
| nums = [1,2,3], k = 0 | 0 |
Constraints:
1 ≤ nums.length ≤ 3 * 10^4
1 ≤ nums[i] ≤ 1000
0 ≤ k ≤ 10^6
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Running Product Sliding Window - Sliding Window/Variable Size Window
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
# Early Exit:
if k <= 1:
return 0
# Sliding window boundaries
left = 0
# Running product of current window
product = 1
# Total valid subarrays
maximus = 0
# Expand right boundary
# tc: O(n)
for right in range(len(nums)):
# Include current element into window
product *= nums[right]
# Shrink window until valid
# product < k
#
# tc: O(1) amortized
while product >= k:
# Remove left element from product
product //= nums[left]
# Shrink window
left += 1
# Add all valid subarrays
# ending at right
maximus += right - left + 1
# overall: tc O(n)
# overall: sc O(1)
return maximus| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
2962. Count Subarrays Where Max Element Appears at Least K Times ::1:: - Medium
Topics: Array, Sliding Window
Intro
You are given an integer array nums and a positive integer k. Return the number of subarrays where the maximum element of nums appears at least k times in that subarray. A subarray is a contiguous sequence of elements within an array.
| Example Input | Output |
|---|---|
| nums = [1,3,2,3,3], k = 2 | 6 |
| nums = [1,4,2,1], k = 3 | 0 |
Constraints:
1 ≤ nums.length ≤ 10^5
1 ≤ nums[i] ≤ 10^6
1 ≤ k ≤ 10^5
Abstraction
wow! again
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Sliding Window] Sliding Window - Sliding Window/Variable Size Window
def countSubarrays(self, nums: List[int], k: int) -> int:
# Sliding Window / Variable Size Window
# Idea:
# - Find global maximum element
# - Maintain sliding window
# - Track frequency of maximum element inside window
# - Once max frequency >= k:
# every subarray starting from left
# and ending at/right of current right is valid
# Key Observation:
#
# If current window already contains k occurrences of max_num,
# then extending further right keeps condition valid
#
# all subarrays:
# [left ... right]
# [left ... right+1]
# ...
# [left ... n-1]
#
# Therefore all subarrays from right -> n-1 are valid
# Count added: n - right
n = len(nums)
# Global maximum element
# tc: O(n)
maxNum = max(nums)
# Left boundary
left = 0
# Frequency of maxNum inside window
maxCount = 0
# Total valid subarrays
res = 0
# Expand right boundary
# tc: O(n)
for right in range(n):
# Include nums[right]
if nums[right] == maxNum:
maxCount += 1
# Shrink while valid
#
# Every window starting at left
# and ending at/right of right is valid
#
# tc: O(1) amortized
while maxCount >= k:
res += n - right
# Remove nums[left]
if nums[left] == maxNum:
maxCount -= 1
left += 1
# overall: tc O(n)
# overall: sc O(1)
return res| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|