LeetCode: Two Pointers II Sliding Window

121. Best Time to Buy and Sell Stock ::3:: - Easy
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] N Size Window Tracking Best Buy And Today Sell With Left Right - Sliding Window/Variable Size Window
- Solution 2: [Dynamic Programming] Track Best Buy And Sell Up To ith Day Complete Table - Sliding Window/Variable Size Window
- Solution 3: [Dynamic Programming] Track Best Buy And Sell Up To Today State Compression Variables - Sliding Window/Variable Size Window
219. Contains Duplicate II ::2:: - Easy
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Window HashSet K With Left Right Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window
239. Sliding Window Maximum ::3:: - Hard
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] [MinHeap] Fixed Size K Window With MaxHeap - Sliding Window/Fixed Size Window
- Solution 2: [Dynamic Programming] K Block Partitions Shifted L To R and R to L Tracking Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window
- Solution 3: [Sliding Window] [Monotonic] [Deque] Fixed Size K Window With Indices Monotonic Decreasing Deque Max On Left MRI Min On Right Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window
567. Permutation in String ::2:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] Needle In Haystack Window Count HashMap For Char And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window
- Solution 2: [Sliding Window] Needle In Haystack Window Count Array With 97 Lowercase Ascii Shifting [SC Opt] - Sliding Window/Fixed Size Window
76. Minimum Window Substring ::2:: - Hard
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] Needle and Window Count 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
1248. Count Number of Nice Subarrays ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() [SC Opt] - Sliding Window/Variable Size Window
- Solution 2: [Sliding Window] Single Pass With Start Gap [TC Opt] - Sliding Window/Variable Size Window
- Solution 3: [Sliding Window] Deque + Threshold (Reference: index-arithmetic variant) - Sliding Window/Variable Size Window
930. Binary Subarrays With Sum ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Prefix Sum] Prefix Sum With HashMap [Any Input] - Sliding Window/Variable Size Window
- Solution 2: [Prefix Sum] Prefix Sum With Bounded Array [Binary Input Opt] - Sliding Window/Variable Size Window
- Solution 3: [Sliding Window] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal [Binary Input Opt] - 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 = 3121. Best Time to Buy and Sell Stock ::3:: - 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.
Pseudocode
Sol 1: N Size Window (Left Right Curr Buy Today Sell Prices)
1. (left, right = 0, 0)
1. (maxProfit = 0)
2. while right < n:
a. currBuyPrice = prices[left]
b. todaySellPrice = prices[right]
c. if currBuyPrice < todaySellPrice:
maxProfit = max(maxProfit, (todaySellPrice - currBuyPrice))
d. else:
left = right
e. right += 1
3. return maxProfit
Sol 3: Best Buy Sell Price Up To Today State Compression Variables
1. (currBuyPrice = -prices[0])
2. (maxProfit = 0)
2. For i in range(1, n):
a. currBuyPrice = max(currBuyPrice, -prices[i])
b. maxProfit = max(maxProfit, currBuyPrice + prices[i])
3. return maxProfitSolution 1: [Sliding Window] N Size Window Tracking Best Buy And Today Sell With Left Right - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Sliding Window (Variable Size)
# Stock Representation:
# - Window will grow from 0 to n as we iterate
# - Left and right represent:
# left: best buy day so far (lowest price seen)
# right: current sell day candidate (highest prices seen)
# - Treat each day as a potential 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)
# Sliding Window Representation:
# There is no window representation,
# the window simply grows to include the entire array
# with our left and right tracking the best buy and sell prices within the window
# Max profit seen so far given our best buy and best sell
maxProfit = 0
# Left: lowest price so far
# Right: todays price
left = 0
right = 0
# tc: O(n)
while right < n:
# Grab current best prices
currBuyPrice = prices[left]
todaySellPrice = prices[right]
# Check:
# can we make a profit
if currBuyPrice < todaySellPrice:
# Calculate profit and compare
profit = todaySellPrice - currBuyPrice
maxProfit = max(maxProfit, profit)
# Check:
# is this a better buy
else:
# update best buy day
left = right
# Iterate to next price
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxProfitSolution 2: [Dynamic Programming] Track Best Buy And Sell Up To ith Day Complete Table - Sliding Window/Variable Size Window
def maxProfit(self, prices: List[int]) -> int:
# Dynamic Programming (Full DP Table)
# Window Representation:
# - Maintain window within [i]
# - currBuyPrice[i][0]: best buy day so far:
# we represent this by min(-currBuyPrice) for easier later calculations
#
# - currBuyPrice[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 + todaySellPrice
# = (-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]Solution 3: [Dynamic Programming] Track Best Buy And Sell Up To Today State Compression Variables - 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:
# currBuyPrice: 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
currBuyPrice = -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 + todaySellPrice
# = (-3) + 10
# = 7
todayPrice = -prices[i]
# Check: compare best buy price up to yesterday to today buy price
currBuyPrice = max(currBuyPrice, todayPrice)
todayProfit = currBuyPrice + prices[i]
# profit = hold + todaySellPrice
# = (-3) + 10
# = 7
maxProfit = max(maxProfit, todayProfit)
# overall: tc O(n)
# overall: sc O(1)
return maxProfit219. Contains Duplicate II ::2:: - 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
Given a list of numbers, and a window range of k, determine if there is a duplicate anywhere in the array within of k steps of each other.
Pseudocode
Sol 1: MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers
1. if n <= 1 or k <= 0:
a. return false
2. (mri = {})
3. For right in range(n):
a. if nums[right] in mri:
left = right - k
prevIndex = mri[nums[right]]
if left <= prevIndex <= right:
return true
b. mri[nums[right]] = right
4. return false
Sol 2: Window HashSet K Tracking Nums Within Window
1. if n <= 1 or k <= 0:
return false
2. (window = {})
3. (left = 0)
3. while right < n:
a. if nums[right] in window:
return true
b. window.add(nums[right])
c. if k < len(window):
window.remove(nums[left])
left += 1
d. right += 1
4. return falseSolution 1: [Sliding Window] MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers - Sliding Window/Variable Size Window
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
# Sliding Window + HashMap (Fixed Size Window):
# Window Representation:
# - HashMap for each number representing (val -> most recent index)
# Idea:
# - Slide window over num list and add index at right boundary
# - Check mri if we have a duplicate of the current element,
# and if a duplicate exists within the k window [left, right]
# Track more recent index appearance for num
# sc: O(n)
mri = {}
# Sliding Window Variables
n = len(nums)
right = 0
# Empty Case:
# if not enough days to have a duplicate
if n <= 1 or k <= 0:
return False
# tc: O(n)
for right in range(n):
currNum = nums[right]
# Check: if num has already been seen
# tc: O(1)
if currNum in mri:
# k distance
# | ------------ |
# left right
# Index Distance No More Than k Apart (not element count):
# the furthest left index that is still within k distance
left = right - k
# Check:
# if previous num copy is within current window, duplicate exists
if left <= mri[currNum] <= right:
return True
# Implies: no duplicate exists
# Then: update mri for num
mri[currNum] = right
# No duplicates found within distance k
# overall: tc O(n)
# overall: sc O(min(n, k))
return FalseSolution 2: [Sliding Window] Window HashSet K With Left Right Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
# Sliding Window HashSet Right Left Representation (Fixed Size Window):
# Window Representation:
# - Set() of width k spanning [left, right]
# - Numbers enter and exit window via left and right
# Idea:
# - Slide fixed size window over list
# - Add element at right, remove element from left
# - Check if num we added is already within window boundary
n = len(nums)
# Empty Case:
# Single list or window of length k cannot have duplicates
if n <= 1 or k <= 0:
return False
# Sliding Window Data:
# sc: O(k)
window = set()
# Sliding Window Boundaries:
left = 0
right = 0
# Early exit:
# all elements are unique, no duplicates
if len(set(nums)) == n:
return False
# tc: O(n)
while right < n:
# Check:
# if duplicate num exist in window
if nums[right] in window:
return True
# Window Extend:
# right index enters window:
window.add(nums[right])
right += 1
# Window Shrink:
# if we've fixed width of window,
# left index exits window:
if k < len(window):
window.remove(nums[left])
left += 1
# No duplicates found within k window
# overall: tc O(n)
# overall: sc O(min(n, k))
return False239. Sliding Window Maximum ::3:: - 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 numbers, and sliding window size k, return array of max element for each window.
Pseudocode
Sol 3: Fixed Size K Window With Indices Monotonic Decreasing Dequeue
1. (n = len(nums))
2. (right = 0)
2. (res = [])
3. (dq = deque())
4. while right < n:
a. while dq and nums[dq[-1]] < nums[right]:
dq.pop()
dq.append(right)
left = right - k + 1
if not left <= dq[0] <= right:
dq.popleft()
if k - 1 <= right:
res.append(nums[dq[0]])
right += 1
5. return resSolution 1: [Sliding Window] [MinHeap] Fixed Size K Window With MaxHeap - Sliding Window/Fixed Size Window
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# Sliding Window (Fixed Size Window)
# Window Representation:
# - Maintain window of size k [left, right]
# - MinHeap will store (-value, index), which turns it into a MaxHeap
# - MaxHeap top element will always hold the window maximum
# Idea:
# - Push first k elements into heap
# - Iterate fixed window over array:
# 1. Add right as new element
# 2. Remove left as old element (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: O(n)
while right < n:
# Expand window
newElem = nums[right]
# Push new element: (-value, index)
heapq.heappush(maxHeap, (-newElem, right))
# Index Distance No More Than k Apart (not element count):
# The distance between left and right indices in the current window is right - left
left = right - k
# Keep removing top of maxHeap if it is stale and outside window
# tc: O(1)
while not left <= maxHeap[0][1] <= right:
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 resultSolution 2: [Dynamic Programming] K Block Partitions Shifted L To R and R to L Tracking 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: tc O(n)
# overall: sc O(n)
return resSolution 3: [Sliding Window] [Monotonic] [Deque] Fixed Size K Window With Indices Monotonic Decreasing Deque Max On Left MRI Min On Right 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 within window
# - Front of deque always holds max element index
# - Back of deque always holds the min element index
# - Back of deque always holds the most recently added index
# Idea:
# - Remove elements smaller than incoming element from the back of the deque
# - Remove max front element if it exits window
# - Front of deque will 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
# Deque():
# - a double ended queue
# - O(1) insertion and removal from both sides
# Deque left and right ends representation:
# - Front Left dq[0]: largest element index in current window
# - Back Right dq[-1]: most recently added index, min element in window
# 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
# Empty check:
if not nums or k == 0:
return []
n = len(nums)
right = 0
# Max element list for each window
# sc: O(n)
res = []
# stores indices in monotonic decreasing order of their values
# sc: O(k)
dq = deque()
# tc: O(n)
while right < n:
# Monotonic Property:
# dq[0]: current max element in deque (front of deque)
# dq[-1]: current min element in deque (back of deque) <-- pop this one
# remove any elements smaller than the current new element,
# so that the new element ends up at the back of the deque,
# and we keep the Monotonic property dq[0] High -> Low dq[-1]
# tc: O(1) amortized
while dq and nums[dq[-1]] < nums[right]:
dq.pop()
# Add INDEX to be the new back of deque: dq[-1] => index
dq.append(right)
# Sliding Window Single Iteration:
# since at most, we are only adding 1 new element
# and since we have a fixed window size,
# only 1 element can go stale (end up out of bounds of the window),
# however, we will only check the top of the deque for stale indexes
# 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
# Element Count of Max k (not index distance):
# left = right - (k-1): the leftmost index needed so the window
# spans exactly k elements (fencepost: k elements = k-1 steps back)
# left is INSIDE the window (first valid index),
# calculate NEW left distance after adding currNum
left = right - k + 1
# Keep removing top of deque if it is stale and outside window
# dq[0]: current max element in deque (front of deque) <-- pop this one
# dq[-1]: current min element in deque (back of deque)
if not left <= dq[0] <= right:
dq.popleft()
# Left K Fixed Window:
# We generated left from the fixed window size k,
# so we cannot use it in a right - left + 1
# as this would just always give us k,
# instead check if right has passed the kth index
if 0 <= left:
# Deque front now holds max valid index within the current window,
# now we only want to record once we have reached our fixed window full length,
# to ensure we have grabbed all the elements we need
res.append(nums[dq[0]])
# Iterate window:
right += 1
# overall: tc O(n)
# overall: sc O(n)
return res3. 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
Pseudocode
Sol 1: MRI HashMap With Jumping Left
1. (mri = {})
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right <= n:
a. newChar = s[right]
b. if newChar in mri:
prevIndex = mri[newChar]
if left <= prevIndex <= right:
left = prevIndex + 1
c. mri[newChar] = right
d. currLen = right - left + 1
d. maxLen = max(maxLen, currLen)
e. right += 1
6. return maxLen
Solution 2: Window HashSet Tracking Chars With Removal
1. (window = {})
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right < n:
a. newChar = s[right]
b. while newChar in window:
window.remove(s[left])
left += 1
c. window.add(newChar)
d. maxLen = max(maxLen, len(window))
e. right += 1
6. return maxLenSolution 1: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window
def lengthOfLongestSubstring(self, s: str) -> int:
# Sliding Window (Variable Size)
# Window Representation:
# - HashSet contains most recent index of all chars encountered
# - Window boundaries kept via [left, right]
# - Valid window contains no duplicate characters
# Idea:
# - Expand variable size window over list by extending right
# - If new character results in a duplicate,
# move the left window boundary to 1 in front of the duplicate
# - For each valid window compare size to max window
n = len(s)-1
# Track more recent index appearance for char
mri = {}
# Sliding Window Variables
# left and right boundary of substring
left = 0
right = 0
# max length so far
maxLen = 0
# tc: O(n)
while right <= n:
# Character we just added to substring
newChar = s[right]
# If we have encountered character already
if newChar in mri:
# Grab index of previous encounter
prevIndex = mri[newChar]
# Jump from left if window is invalid:
# - jump left to 1 index after duplicate
if left <= prevIndex <= right:
left = prevIndex + 1
# Update most recent index for character
mri[newChar] = right
# Valid window, compare to max
currLen = right - left + 1
maxLen = max(maxLen, currLen)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(min(n, k))
return maxLenSolution 2: [Sliding Window] HashSet Tracking Window Boundaries And Shrink While Invalid - Sliding Window/Variable Size Window
def lengthOfLongestSubstring(self, s: str) -> int:
# Sliding Window HashSet Right Left Representation (Variable Size Window)
# Window Representation:
# - Set() contains chars within window [left, right]
# - Numbers enter and exit window via left and right
# - Valid window contains no duplicate characters
# Idea:
# - Expand variable size window over list by extending right
# - If duplicate found, shrink window from left until duplicate is removed
# - For each valid window compare size to max window
n = len(s)
# Sliding Window Data:
# sc: O(n)
window = set()
# Sliding Window Boundaries:
left = 0
right = 0
# Track longest valid window
maxLen = 0
# tc: O(n)
while right < n:
# Grab char we want to add
newChar = s[right]
# Shrink from left while window is invalid:
# - if duplicate exists in window set
# tc: O(n)
while newChar in window:
window.remove(s[left])
left += 1
# Add char to window
window.add(newChar)
# Compare valid window length to max
maxLen = max(maxLen, len(window))
# Expand valid window
right += 1
# overall: tc O(n)
# overall: sc O(min(n, k))
return maxLen1208. 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
Find the largest window for s that stays under the budget for changing all necessary characters from s to match t
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Cost Tracking Window Cost And Shrink While Invalid - Sliding Window/Variable Size Window
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
# Sliding Window + Two Pointers
# Substring Representation:
# - Cost tracks cost to change every character in s to match t
# - Valid window has a windowChangeCost <= maxCost
# Idea:
# - Expand variable size window over list by extending right
# - Add new char cost to change s[right] -> t[right] to current total window cost
# - If cost exceeds budget, shrink window from left until cost is valid again
# - For each valid window compare size to max window
n = len(s)
# Sliding Window Variables
left = 0
right = 0
maxLen = 0
# Cost to change entire current window from s -> t
windowChangeCost = 0
# tc: O(n)
while right < n:
# Add new char cost to window cost:
# - cost of single char from s -> t
rightCharS = s[right]
rightCharT = t[right]
cost = abs(ord(rightCharS) - ord(rightCharT))
windowChangeCost += cost
# Shrink from left while window is invalid:
# - curr cost to change entire window is higher than budget
while maxCost < windowChangeCost:
# Remove old character and save cost from window cost:
# - cost of single char from s -> t
leftCharS = s[right]
leftCharT = t[right]
saving = abs(ord(s[left]) - ord(t[left]))
windowChangeCost -= saving
# Shrink window
left += 1
# Valid window, compare to max
windowLen = (right - left) + 1
maxLen = max(maxLen, windowLen)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxLen1004. 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
Find the largest window for nums that stays under the budget for changing at most k 0's from 0 to 1.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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:
# - Zero tracks total flips needed for substring to be consecutive 1's
# - Valid window only has consecutive 1's
# - Valid window has zeroCount <= k
# Idea:
# - Expand variable size window over list by extending right
# - Add new int to zero count
# - If zeroCount exceeds budget, shrink window from left until num of zeros is valid again
# - For each valid window compare size to max window
n = len(nums)-1
# Sliding Window Boundaries
left = 0
right = 0
# Number of zeros in current window
zeroCount = 0
# Longest valid window seen
maxLen = 0
# tc: O(n)
while right <= n:
# Update zero count:
if nums[right] == 0:
zeroCount += 1
# Shrink from left while window is invalid:
# - num of 0's must be <= k
# tc: O(1) amortized
while k < zeroCount:
# Update count
if nums[left] == 0:
zeroCount -= 1
left += 1
# Validate window, compare size to max
currWindow = right - left + 1
maxLen = max(maxLen, currWindow)
# overall: tc O(n)
# overall: sc O(1)
return maxLen904. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Fruit Count HashMap 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
# Window Representation:
# - HashMap tracks count of fruit within window
# - Valid window contains a max of 2 unique fruit
# Rules:
# - Expand variable size window over list by extending right
# - Add new fruit to count
# - If HashMap has more than 2 unique fruit, shrink window from left until unique fruit is valid again
# - For each valid window compare size to max window
# Fruit Count:
# Holds at most 2 unique fruits at any moment
# sc: O(1)
count = defaultdict(int)
# Window Representation:
left = 0
right = 0
n = len(fruits)
# Longest string up to now
maxLen = 0
# tc: O(n)
while right < n:
# Add right num
rightNum = fruits[right]
count[rightNum] += 1
# Shrink from left while window is invalid:
# - more than 2 unique fruits in window
while len(count) > 2:
# Decrease left fruit count
leftNum = fruits[left]
count[leftNum] -= 1
# Remove fruit type if no more fruit remain:
if count[fruits[left]] == 0:
del count[fruits[left]]
# Shrink window
left += 1
# Valid window, compare to max length
currWindow = right - left + 1
maxLen = max(maxLen, currWindow)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxLenSolution 2: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window
def totalFruit(self, fruits: List[int]) -> int:
# Sliding Window + Most Recent 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
mri = {}
n = len(fruits)-1
left = 0
right = 0
maxLen = 0
# tc: O(n)
while right <= n:
# Update most recent index of current fruit
mri[fruits[right]] = right
# Jump from left if window is invalid:
# - more than 2 unique fruit types, currently 3 types
if len(mri) > 2:
# Remove the fruit farthest to the left
# - jump left to 1 index after farthest fruit
farthestLeftFruit = min(mri, key=mri.get)
left = mri[farthestLeftFruit] + 1
# Remove fruit from map
del mri[farthestLeftFruit]
# Valid window, compare to max
currLen = right - left + 1
maxLen = max(maxLen, currLen)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(n)
return maxLen1438. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] [Monotonic] [Deque] Two Pointer Sliding Window - Sliding Window/Variable Size Window
def longestSubarray(self, nums: List[int], limit: int) -> int:
# Sliding Window + Monotonic Deques
# Window Representation:
# - Max and min deque track max and min for current window
# - Window boundaries kept via [left, right]
# - Valid window has absolute diff less <= limit
# Idea:
# - Expand variable size window over list by extending right
# -
# - Maintain two deques for the cu:
# 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
# Current max length
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()
maxDeque.append(rightNum)
# Maintain Monotonic increasing min deque
while minDeque and rightNum < minDeque[-1]:
minDeque.pop()
minDeque.append(rightNum)
# No need to use abs()
# since minDeque[0] <= maxDeque[0]
# so result will always be positive
difference = maxDeque[0] - minDeque[0]
# Shrink from left while window is invalid:
# - 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()
# Shrink 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 maxLen424. 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.
Pseudocode
Sol 1: HashMap Tracking CharCount To Find MostOccur
1. (charCount = {})
2. (left, right = 0, 0)
3. (mostOccur = 0)
4. (maxLen = 0)
2. while right < n:
a. charCount[s[right]] += 1
b. mostOccur = max(mostOccur, charCount[s[right]])
c. currWindowLen = right - left + 1
d. allOtherChars = currWindowLen - mostOccur
e. while k < allOtherChars:
charCount[s[left]] -= 1
left += 1
currWindowLen = right - left + 1
allOtherChars = currWindowLen - mostOccur
f. maxLen = max(maxLen, currWindowLen)
g. right += 1
3. return maxLenSolution 1: [Sliding Window] Count HashMap Tracking Char 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:
# - HashMap tracks char count within window
# - Valid window contains a single consecutive character
# - Valid window changes at most k characters from c -> seqChar
# Idea:
# - Expand variable size window over list by extending right
# - Add new char to count
# - mostOccur is total count of the most occurring character in the window
# - allOtherChars is the total count of all other characters in the window
# - If allOtherChars is more than the char change budget k, shrink window from left until 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: O(n)
while right < 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
# Shrink from left while window is invalid:
# 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:
# Do NOT update mostOccur here:
# - while shrinking, the mostOccur may become stale and decreased
# - mostOccur may have shifted to a different character
# This is fine because:
# - for window [left:right], we have agreed
# that mostOccur is highest char up to right
# - A stale mostOccur will under estimate allOtherChars
# This is safe because:
# - No char can exceed the current mostOccur while shrinking,
# we are only shrinking the window so counts can only go down
# Thus during shrink, 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)
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return maxLen713. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Running Product Sliding Window - Sliding Window/Variable Size Window
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
# Sliding Window + Running Product
# Window Representation:
# - Product tracks product of current window
# - Valid window has a product <= k
# Rules:
# - Expand variable size window over list by extending right
# - Multiple new num into running product
# - If product is greater than k, shrink window from left until product is valid again
# - For each valid window compare size to max window
# Early Exit:
# No array can have a product less than 1,
# since int values are guaranteed to be 1 <= int
if k <= 1:
return 0
# Sliding window boundaries
left = 0
# Running product of current window
runningProduct = 1
# Total valid subarrays
validArrayCount = 0
# Expand right boundary
# tc: O(n)
for right in range(len(nums)):
# Include current element into window
runningProduct *= nums[right]
# Shrink from left while window is invalid:
# - runningProduct < k
# tc: O(1) amortized
while runningProduct >= k:
# Remove left element from runningProduct
runningProduct //= nums[left]
# Shrink window
left += 1
# Add all valid subarray ending at right,
# all other subarrays are counted at different iterations
arraysEndingAtRight = right - left + 1
validArrayCount += arraysEndingAtRight
# overall: tc O(n)
# overall: sc O(1)
return validArrayCount567. 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, r return true if string 1 is a permutation within string 2.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Needle In Haystack Window Count HashMap For Char 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 and right boundary of current window
left = 0
right = 0
# tc: O(n)
for right in range(n2):
# Add char count to window
rightChar = s2[right]
windowCharCount[rightChar] += 1
# Window length
currWindowLen = right - left + 1
# Shrink from left while window is invalid:
# - window length is greater than needle length
# - since we extend window by 1 char, we only need to decrease by 1 char
if n1 < currWindowLen:
# Decrease char count from window
leftChar = s2[left]
windowCharCount[leftChar] -= 1
# Remove char if frequency reached zero,
# to match needle hashmap
if windowCharCount[leftChar] == 0:
del windowCharCount[leftChar]
# Shrink window
left += 1
# If window hashmap matches the needle hashmap,
# valid permutation found
if windowCharCount == needleCharCount:
return True
# overall: tc O(n)
# overall: sc O(n)
return FalseSolution 2: [Sliding Window] Needle In Haystack Window Count Array With 97 Lowercase Ascii Shifting [SC Opt] - Sliding Window/Fixed Size Window
def checkInclusion(self, s1: str, s2: str) -> bool:
# Sliding Window (Fixed Size Window)
# Substring Representation:
# - Maintain window [left, right]
# - Window size always equals len(s1)
# - Frequency array: char -> frequency (size 26 for lowercase letters)
# Idea:
# - Build a frequency array 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 len(s1)
n1 = len(s1)
n2 = len(s2)
# If needle length is greater than haystack length:
if n2 < n1:
return False
# s1 Frequency
# tc: O(n)
# sc: O(1)
needleCharCount = [0] * 26
for c in s1:
needleCharCount[ord(c) - 97] += 1
# Substring window frequency
# sc: O(1)
windowCharCount = [0] * 26
# left and right boundary of current window
left = 0
right = 0
# tc: O(n)
for right in range(n2):
# Add char count to window
rightChar = s2[right]
windowCharCount[ord(rightChar) - 97] += 1
# Window length
currWindowLen = right - left + 1
# Shrink from left while window is invalid:
# - window length is greater than needle length
# - since we extend window by 1 char, we only need to decrease by 1 char
if n1 < currWindowLen:
# Decrease char count from window
leftChar = s2[left]
windowCharCount[ord(leftChar) - 97] -= 1
# No need to "remove" the 0 count chars
# since all empty chars are already set to 0
# Shrink window
left += 1
# If window frequency array matches the needle frequency array,
# valid permutation found
if windowCharCount == needleCharCount:
return True
# overall: tc O(n)
# overall: sc O(1)
return False1358. 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
Give the total number of valid substrings. A valid substring contains all 3 chars.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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
n = len(s)-1
# HashMap: char -> frequency in current window
# sc: O(1) at most 3 chars
count = defaultdict(int)
# Sliding Window Boundaries
left = 0
right = 0
# Total valid substrings found
res = 0
# tc: O(n)
while right <= n:
# Add char count to window
count[s[right]] += 1
# Shrink from left while window is valid:
# - window contains all 3 chars
while True:
# Check if window contains all 3 characters
windowValid = True
for c in "abc":
if count[c] <= 0:
windowValid = False
break
# Stop Shrinking
if not windowValid:
break
# Decrease char count
count[s[left]] -= 1
# Shrink window
left += 1
# Include all valid subarrays from 0 to left:
# [z, z, z, a b c] -> left is set to index 4, when window becomes invalid
# so there are 4 valid subarrays
# tc: O(1)
res += left
# Iterate window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return resSolution 2: [Last Seen Index] 3 MRI Variables - 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
# - Valid window contains all 3 chars
# Idea:
# - Expand variable size window over list by extending right
# - Ignore all other chars
# - Update abc index to mri
# - All valid windows start at the left most mri after full iteration
n = len(s) - 1
# Init mri to -1,
# if char is never encountered then count results in 0
mriA = mriB = mriC = -1
# Total valid substrings found
count = 0
right = 0
# tc: O(n)
while right <= n:
# Update last seen index for current char
if s[i] == 'a':
mriA = i
elif s[i] == 'b':
mriB = i
elif s[i] == 'c':
mriC = i
# Leftmost mri:
# [z, z, z, a b c] -> mri for a is 3,
# so there are 3 + 1 = 4 subarrays with all 4 chars
count += min(mriA, mriB, mriC) + 1
# Extend window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return count209. Minimum Size Subarray Sum ::2:: - Medium
Topics: Binary Search, Sliding Window, Prefix Sum
Intro
Given an array of positive integers nums and a positive integer target, return the minimal length of a whose sum is greater than or equal to target. If there is no such subarray, return 0 instead. Follow up: If you have figured out the O(n) solution, try coding another solution of which the time complexity is O(n log(n)).
| Example Input | Output |
|---|---|
| target = 7, nums = [2,3,1,2,4,3] | 2 |
| target = 4, nums = [1,4,4] | 1 |
| target = 11, nums = [1,1,1,1,1,1,1,1] | 0 |
Constraints:
1 ≤ target ≤ 10^9
1 ≤ nums.length ≤ 10^5
1 ≤ nums[i] 10^4
Abstraction
Given array of nums, return the min length subarray with a sum greater than or equal to target. If none exists just return 0
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Running Sum Window Shrink While Valid - Sliding Window/Variable Size Window
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# Sliding Window (Variable Size Window)
# Window Representation:
# - Running count tracks sum of window
# - Valid window has sum greater than or equal to target
# Idea:
# - Expand variable size window over list by extending right
# - Add new value to running sum
# - All nums are positive, sum always increases when extending and decreases when shrinking
# - If sum exceeds target, shrink from left until sum is valid again
# - For each valid window compare size to max window
n = len(nums)
left = 0
# Sum of current window
runningSum = 0
minLen = float('inf')
# tc: O(n)
for right in range(n):
# Add num to running count
windowSum += nums[right]
# Shrink from left while window is valid:
# - windowSum meets or exceeds target
# tc: O(1) amortized
while target <= windowSum:
# Valid window, compare to min length
currWindowLen = right - left + 1
minLen = min(minLen, currWindowLen)
# Remove left num from window sum
windowSum -= nums[left]
# Shrink window
left += 1
# No valid window was found
if minLen == float('inf'):
return 0
# overall: tc O(n)
# overall: sc O(1)
return minLen76. 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 s and t, of lengths m and n, return the minimum substring of s, such that every character in t including duplicates is within the window.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Needle and Window Count HashMap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window
def checkInclusion(self, s: str, t: str) -> bool:
# Sliding Window (Fixed Size Window)
# Window Representation:
# - Two Hashmaps:
# needleFreq -> required counts
# windowFreq -> counts in current window
# - Valid window has equal char counts needle chars between needle and window freq
# Idea:
# - Expand variable size window over list by extending right
# - Track how many required character are satisfied via needleFreq
# - Add new char count to windowFreq
# - While chars between needle and window freq are satisfied,
# shrink from left to minimize window
# - For each valid window compare size to min window
# haystack < needle:
# If haystack is smaller than needle,
# substring is not possible
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
left = 0
right = 0
# Char count for current window
windowCharCount = defaultdict(int)
# Length and starting index for min window
minLen = float('inf')
minStart = 0
# Flags for unique chars slots from needle hashmap length
needCharSlots = len(needleCharCount)
haveCharSlots = 0
# tc: O(n)
for right in range(len(s)):
# Expand window
rightChar = s[right]
windowCharCount[rightChar] += 1
# If char is a specific slot char
if rightChar in needleCharCount:
# If char has met requirements
if windowCharCount[rightChar] == needleCharCount[rightChar]:
haveCharSlots += 1
# Shrink from left while window is valid:
# - All specific slot chars have met requirements
while haveCharSlots == needCharSlots:
# Valid window, compare to min
windowLen = right - left + 1
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 window
left += 1
# If no valid substring was found
if minLen == float("inf"):
return ""
# Grab min substring
res = s[minStart:minStart + minLen]
# overall: tc O(n + m)
# overall: sc O(n)
return resSolution 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:
# - One HashMap:
# char -> remaining required count
# - targetCharsRemaining: tracks total chars with requirements still needed
# Idea:
# - Expand variable size window over list by extending right
# - Update char remaining needed count
# -
# - Decrease count of remaining required count
# - When targetCharsRemaining == 0, window is valid
# - Shrink from left to minimize window
# - Track smallest valid window
# Needle is longer than haystack:
# substring not possible
if len(s) < len(t):
return ""
# Build required frequency map
windowCharCount = defaultdict(int)
for ch in t:
windowCharCount[ch] += 1
# Total chars still needed
globalRemainingCountAcrossChars = len(t)
# Sliding Window Variables
left = 0
right = 0
# Min window substring
minWindowRight = float('inf')
minWindowLeft = 0
# tc: O(n)
for right in range(len(s)):
# Expand window
rightChar = s[right]
# windowCharCount:
# To start, hashmap will contains:
# - positive values of required needle chars
# - all other possible keys are set to 0
# Moving forward, all other possible keys will
# always 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 = windowCharCount[rightChar]
if stillNeedThisMany > 0:
globalRemainingCountAcrossChars -= 1
# Increase char count for windowCharCount
# regardless of it being a required or non-required char,
# both cases covered per above
windowCharCount[rightChar] -= 1
# Shrink from left while window is valid:
# - Total needle char count is covered
# tc: amortized O(n)
while globalRemainingCountAcrossChars == 0:
# Valid window, compare to min
currWindowLen = right - left + 1
minWindowLen = minWindowRight - minWindowLeft + 1
if currWindowLen < minWindowLen:
minWindowRight = right
minWindowLeft = left
# Remove left character, and to char required count,
# again per above:
# - required needle chars have a positive required count
# - non-required chars have a required count of 0
leftChar = s[left]
windowCharCount[leftChar] += 1
# If this char has a positive required count:
# - it is a required needle char
# - we need one more copy of the char, since we just lost a occurrence of it
# - add to global need count
stillNeedThisMany = windowCharCount[leftChar]
if stillNeedThisMany > 0:
globalRemainingCountAcrossChars += 1
# Shrink window
left += 1
# No valid window was found
if minWindowRight == float("inf"):
return ""
# Grab min window
res = s[minWindowLeft:minWindowRight + 1]
# overall: tc O(n + m)
# overall: sc O(n)
return res1234. 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
Given a string containing only 'QWER', return the minimum substring length that can be replaced to make the string balanced. A balanced string has each of the 4 characters appear exactly n/4 times, or in other words, each character appears no more than n/4 times.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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:
# - HashMap tracks char count OUTSIDE window
# - Valid window makes up for uneven outside count by changing chars INSIDE the window
# Idea:
# - Expand variable size window over list by extending right
# - Remove chars from OUTSIDE count when they enter the window
# - Once all outside frequencies <= target, window is valid
# - For each valid window compare size to min window
# s = "QQWE", target = 1
# outsideCount = {Q:2, W:1, E:1, R:0}
# When window is at the first Q,
# that Q is remove from the OUTSIDE char count,
# leaving the max(outsideCount.values()) -> 1
# which is equal to the target == 1,
# meaning that the current window is the smallest possible section
# we can replace to make the entire string balanced
n = len(s)
# Target char count:
# - s length is multiple of 4, and has 4 possible characters (Q W E R)
# - balanced s has each char appear n // 4 times
target = n // 4
# Sliding Window Boundaries
left = 0
right = 0
# Outside HashMap:
# - Stores frequencies of chars OUTSIDE current window
outsideCount = defaultdict(int)
for c in s:
outsideCount[c] += 1
# Minimum valid window size
minLen = float('inf')
# Balanced Check:
# - Current window length is 0
# - All chars are outside the window (we are checking the original string)
# - If balanced value == target, then the string is already balanced
# tc: O(1)
if max(outsideCount.values()) == target:
return 0
# Expand sliding window
# tc: O(n)
for right in range(n):
# Add char to window,
# remove new char from OUTSIDE count
rightChar = s[right]
outsideCount[rightChar] -= 1
# Shrink from left while window is valid:
# - OUTSIDE count balanced value is equal to the target
# - string is balanced
# - left has not passed right (left and catch up to right, leaving a window length of 1)
# tc: O(1) amortized
while left <= right and max(outsideCount.values()) == target:
# Valid window, compare to min
currWindowLen = right - left + 1
minLen = min(minLen, currWindowLen)
# Remove char from window,
# add char back to OUTSIDE count
leftChar = s[left]
outsideCount[leftChar] += 1
# Shrink window
left += 1
# overall: tc O(n)
# overall: sc O(1)
return minLen862. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] [Monotonic] [Deque] 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:
# - Prefix sum for each index in the array
# - 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
n = len(nums)
# Prefix sum array
# sc: O(n)
p = [0] * (n + 1)
# Build Prefix Sum Array:
# p[i] = sum of nums[0] to nums[i-1]
# p[i] = prefixSum of previous index + previous index's value
# = p[i-1] + nums[i-1]
# tc: O(n)
for i in range(1, n+1):
p[i] = p[i-1] + nums[i-1]
# Monotonic Increasing Deque:
#
# dq = [dq[0], dq[1], dq[2], ..., dq[-1]]
# ^ ^
# older (left most) index newer (right most) index
# small p[i] large p[]
# sc: O(n)
dq = deque()
# Min valid subarray length:
# set to larger than array to say we have not found a valid array yet
minLen = float('inf')
right = 0
# Need to check up to nth prefix sum (indices 0 through n, n+1 total prefix sums)
# tc: O(n)
while right <= n:
# Shrink from left while window is valid:
# - dq[0]: index of the oldest, smallest prefix sum still in the deque
# - p[i]: prefix sum up to (but not including) the current index i (sum of nums[0..i-1])
# - k: min sum required
# - Take the difference between the prefix sums to get the sum of the '+' array in between the indexes
#
# [ --------- +++++++ xxxxxxxxx ]
# dq[0] i
# p[i]
#
# p[i] - dq[0] => results in sum of the '+' area
# tc: O(1) amortized
while dq and k <= p[right] - p[dq[0]]:
# Valid window, compare to min:
# Grab left index of window
left = dq[0]
# Compute length of window:
# - no need for (right - left + 1)
# - prefixSum array is already 1-indexed relative to nums and accounts for the +1
subarrayLen = right - left
# Compare to min
if subarrayLen < minLen:
minLen = subarrayLen
# Shrink window
dq.popleft()
# Maintain Monotonic Increase:
# - remove the most recently added prefix sum that are larger than the current prefix
# - append newest large prefix sum to deque
# tc: O(1) amortized
while dq and p[right] <= p[dq[-1]]:
dq.pop()
# Extend window
dq.append(right)
right += 1
# No valid window found
if minLen == float('inf'):
return -1
# overall: tc O(n)
# overall: sc O(n)
return minLen2962. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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
# Window Representation:
# -
# 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
maxNum = max(nums)
# Sliding Window Boundaries
left = 0
right = 0
# Max within current window
maxCount = 0
# Total valid subarrays
count = 0
# tc: O(n)
while right < n:
# Add num count
if nums[right] == maxNum:
maxCount += 1
# Shrink from left while window is valid:
# - most occurring element appears at least least k times
# tc: O(1) amortized
while k <= maxCount:
# n - right:
# - Current window contains k occurrences of max num
# - All right extensions keep the condition valid
# - Include all subarrays from right to n-1:
#
# nums = [3, 3, 1], k = 2, n = 3
# index = 0 1 2 3 4
# ^ ^
# l r
#
# = n - right = 3 - 1 = 2 subarrays
#
# left=1: nums[0..1] = [3, 3] two 3's => value
# left=2: nums[0..2] = [3, 3, 1] two 3's => value
# tc: O(1)
count += n - right
# Remove num count
if nums[left] == maxNum:
maxCount -= 1
# Shrink window
left += 1
# Extend window
right += 1
# overall: tc O(n)
# overall: sc O(1)
return count992. 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
Get total count of subarrays that have exactly k unique integers.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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 unique integers
# - For each right pointer expansion:
# 1. Add nums[right] to window
# 2. Shrink left while unique count exceeds k
# 3. Add window length (right - left + 1) to total count
# - Exact k unique subarrays = atMostK(k) - atMostK(k-1)
# Count of subarrays with at most k unique integers
def atMostK(nums, currK):
# Sliding Window Data
count = defaultdict(int)
# Unique Values In Window
uniqueCharCount = 0
# Sliding Window Variables
left = 0
right = 0
n = len(nums)
res = 0
# tc: O(n)
while right < n:
# Expand window
rightNum = nums[right]
# Check if new num has been found
if count[rightNum] == 0:
uniqueCharCount += 1
# Increase num count
count[rightNum] += 1
# Shrink from left while window is invalid:
# - too many unique values
while currK < uniqueCharCount:
# Remove old value
leftNum = nums[left]
count[leftNum] -= 1
# Check if we lost a unique value
if count[leftNum] == 0:
uniqueCharCount -= 1
# Shrink window
left += 1
# Valid window found:
# - all subarrays within this valid subarray are valid
# - all subarrays are counted depending on where their right ends
# - shrink from left allows the length to get count of subarrays
res += right - left + 1
# 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
# atMost(k) - atMost(k-1) == subarrays with k unique integers
res = atMostK(nums, k) - atMostK(nums, k-1)
# overall: tc O(n)
# overall: sc O(n)
return resSolution 2: [Sliding Window] Single Pass, Single Map, Two-Phase Shrink (Optimized) - Sliding Window/Variable Size Window
def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window) — Single Pass, Single Map
# Window Representation:
# - Maintain one window [left, right] with one frequency map
# - distinct: number of distinct values currently in window
# - prefix: number of "free" extra left positions that also
# give exactly k distinct, beyond the minimal left boundary
# Idea (Two-Phase Shrink):
# Phase 1 — forced shrink while distinct > k (standard invalid shrink):
# moves left until window has at most k distinct; resets prefix
# once, after shrinking, since the window boundary genuinely changed
# Phase 2 — opportunistic shrink while distinct == k:
# keep removing nums[left] as long as its frequency > 1
# (removing it won't drop distinct count below k)
# each such removal is one more valid left boundary -> prefix += 1
# Why Phase 2 must run EVERY iteration where distinct == k:
# Adding a DUPLICATE value at right increases that value's frequency
# inside the window — which can retroactively make the current left
# element removable, even though `distinct` itself didn't change.
# So Phase 2 must re-attempt shrinking every time distinct == k,
# not just the first time k is reached.
# Window Example:
# nums = [1, 2, 1, 2, 3], k = 2
#
# right=0 (1): distinct=1, not k, ans += 0
# right=1 (2): distinct=2 == k, freq[nums[left]]=freq[1]=1, not >1, no shrink
# ans += prefix(0) + 1 = 1 -> ans=1
# right=2 (1, dup): distinct stays 2, freq[nums[left=0]]=freq[1]=2 > 1, shrink:
# left=1, prefix=1
# ans += prefix(1) + 1 = 2 -> ans=3
# right=3 (2, dup): distinct stays 2, freq[nums[left=1]]=freq[2]=2 > 1, shrink:
# left=2, prefix=2
# ans += prefix(2) + 1 = 3 -> ans=6
# right=4 (3, new): distinct=3 > k, Phase 1 shrink:
# remove nums[2]=1, freq[1]->0, distinct=2, left=3, prefix=0
# Phase 2: freq[nums[left=3]]=freq[2]=1, not >1, no shrink
# ans += prefix(0) + 1 = 1 -> ans=7
#
# ans = 7 (matches expected output for this input)
n = len(nums)
# Frequency of each value in current window
# sc: O(k)
freq = defaultdict(int)
# Sliding Window Boundary
left = 0
# Number of extra "free" valid left starts for the current right
prefix = 0
# Number of distinct values in window
distinct = 0
# Total valid subarrays found
ans = 0
# tc: O(n)
for right in range(n):
# Check if new distinct value found
if freq[nums[right]] == 0:
distinct += 1
freq[nums[right]] += 1
# Phase 1 — Shrink from left while window is invalid:
# - too many distinct values (distinct > k)
# - boundary genuinely moved, so reset prefix once after shrinking
# tc: O(1) amortized
if distinct > k:
while distinct > k:
freq[nums[left]] -= 1
if freq[nums[left]] == 0:
distinct -= 1
left += 1
prefix = 0
# Phase 2 — window has exactly k distinct:
# opportunistically shrink further while it doesn't cost distinctness
# tc: O(1) amortized
while distinct == k and freq[nums[left]] > 1:
freq[nums[left]] -= 1
left += 1
prefix += 1
# All left positions from (left - prefix) to left are valid starts
if distinct == k:
ans += prefix + 1
# overall: tc O(n)
# overall: sc O(k)
return ans1248. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() [SC Opt] - 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: O(n)
while right < n:
# Add num, check if odd
# tc: O(1)
if nums[right] % 2 == 1:
oddNums += 1
# Shrink from left while window is invalid:
# 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
# Valid window found:
# - all subarrays within this valid subarray are valid
# - all subarrays are counted depending on where their right ends
# - shrink from left allows the length to get count of subarrays # 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)
res = atMost(k) - atMost(k-1)
# overall: tc O(n)
# overall: sc O(1)
return resSolution 2: [Sliding Window] Single Pass With Start Gap [TC Opt] - 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: number of odd numbers in current window
# - start_gap: 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 (each is a valid left boundary)
# - start_gap persists across iterations: once we've found valid
# left boundaries for a window with k odds, any future right
# expansion that doesn't add another odd number still has the
# same valid left boundaries
# - Add start_gap to total at every right, not just when counter == k
# Why start_gap persists:
# nums = [1, 1, 2, 1, 1], k = 3
#
# rp=3 (odd): counter=3 == k, start_gap reset to 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 shrinking. total += 1
#
# rp=4 (odd): counter=3 == k, start_gap reset to 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 shrinking. total += 1
#
# total = 2
# valid subarrays: [1,1,2,1] and [1,1,2,1,1]
# Single pass — no need to call atMost twice
# Sliding Window Boundaries
# 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: 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 the new odd resets the boundary count
# tc: O(1) amortized
if counter == k:
start_gap = 0
# Shrink from left while window is 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,
# the same left boundaries remain valid from the previous valid window
# tc: O(1)
total += start_gap
# overall: tc O(n)
# overall: sc O(1)
return totalSolution 3: [Sliding Window] Deque + Threshold (Reference: index-arithmetic variant) - Sliding Window/Variable Size Window
def numberOfSubarrays(self, nums: List[int], k: int) -> int:
# Sliding Window (Variable Size Window) + Deque
# Window Representation:
# - ones_indicies: deque holding indices of the last k odd numbers seen
# (once k odds are tracked, the oldest odd index gets evicted)
# - threshold: index of the odd number just evicted (the boundary
# marking how far left a valid window could start)
# - count_ones: number of odd numbers currently tracked (capped at k)
# Idea:
# - Scan right pointer j across nums
# - Every time we see an odd number, record its index
# - If k odds are already tracked, evict the oldest odd index into
# threshold and drop count_ones by 1 (keeps at most k odds tracked)
# - Whenever count_ones == k, the window ending at j has exactly k odds;
# the number of valid left boundaries is the gap between the
# first tracked odd index and threshold
# - Add that gap to total at every j where count_ones == k
# (same idea as start_gap in Solution 2, computed via index
# arithmetic instead of a running counter)
# Why ones_indicies[0] - threshold works:
# - threshold marks the last position that, if included in the window,
# would push the odd count above k
# - Any left boundary in (threshold, ones_indicies[0]] keeps exactly
# k odds in [left, j]: starting any earlier includes one more odd
# (pushing past k), and starting later than the first tracked odd
# doesn't change the odd count
# - So there are (ones_indicies[0] - threshold) valid left starts
# Window Example:
# nums = [1, 1, 2, 1, 1], k = 3
#
# j=0 (odd): ones=[0], count_ones=1
# j=1 (odd): ones=[0,1], count_ones=2
# j=2 (even): count_ones still 2, skip
# j=3 (odd): ones=[0,1,3], count_ones=3 == k
# total += ones[0] - threshold = 0 - (-1) = 1 -> total=1
# j=4 (odd): count_ones == k already, evict oldest:
# threshold = ones.popleft() = 0, count_ones=2
# append 4 -> ones=[1,3,4], count_ones=3
# total += ones[0] - threshold = 1 - 0 = 1 -> total=2
#
# total = 2
# valid subarrays: [1,1,2,1] and [1,1,2,1,1]
# Total valid subarrays found
# sc: O(1)
total = 0
# Number of odd numbers currently tracked (capped at k)
# sc: O(1)
count_ones = 0
# Indices of the last (at most) k odd numbers seen
# sc: O(k)
ones_indicies = deque()
# Index just left of the earliest odd number still valid as a
# window start; -1 means no odd has been evicted yet
# sc: O(1)
threshold = -1
# tc: O(n)
for j in range(len(nums)):
# Track index of odd numbers only
# tc: O(1)
if nums[j] % 2 == 1:
ones_indicies.append(j)
# Already have k odds tracked - evict the oldest one
# so the deque never holds more than k indices
# tc: O(1) amortized
if count_ones == k:
threshold = ones_indicies.popleft()
count_ones -= 1
count_ones += 1
# Window ending at j has exactly k odds tracked -
# count how many left boundaries keep it that way
# tc: O(1)
if count_ones == k:
total += ones_indicies[0] - threshold
# overall: tc O(n)
# overall: sc O(k)
return total930. Binary Subarrays With Sum ::3:: - 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
Find the number of subarrays whose sum equal the target.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Prefix Sum] Prefix Sum With HashMap [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: 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 resSolution 2: [Prefix Sum] Prefix Sum With Bounded Array [Binary Input Opt] - 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: 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 resSolution 3: [Sliding Window] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal [Binary Input Opt] - Sliding Window/Variable Size Window
def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
# Variable Size Window (Expand Then Shrink While Invalid 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
# Monotonic Shrink Advantage:
# Since we know that when we shrink, the sum decreases predictably,
# we can safely shrink the window from the left without missing any valid subarrays,
# which allows us to the do the goal(i) and goal(i-1) trick
# 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: O(n)
for right in range(len(nums)):
# Expand window
currSum += nums[right]
# Shrink from left while window is invalid:
# Shrink window until sum <= goal
while goal < currSum:
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)