Jc-alt logo
jc
data structures and algorithms

LeetCode: Two Pointers II Sliding Window

LeetCode: Two Pointers II Sliding Window
108 min read
#data structures and algorithms
Table Of Contents

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:

  1. Fixed size (constant length) window
  2. 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) = 24

Sliding 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 = 3

219. Contains Duplicate II ::3:: - Easy

Topics: Array, Hash Table, Sliding Window

Intro

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) lte k.

Example InputOutput
nums = [1,2,3,1], k = 3true
nums = [1,0,1,1], k = 1true
nums = [1,2,3,1,2,3], k = 2false

Constraints:

1 ≤ nums.length ≤ 10^5

-10^9 ≤ nums[i] ≤ 10^9

0 ≤ k ≤ 10^9

Abstraction

duplicate! duplicate!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: (iterative)

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointers] Sort List With Matching Index List LeetCode Runtime Space Analysis - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:  

        # Space Optimization:
        #   - LeetCode measures peak memory usage at runtime, not just asymptotic SC
        #   - HashMap/HashSet solutions are O(n), but carry large constant factors:
        #       hash table buckets, key-value pairs, pointers, hashing overhead, etc
        #   - A list of integers in contiguous in memory with no overhead,
        #     making it significantly lighter in practice despite same O(n) SC
        #   - No early exist via set to preserve this space advantage

        # Sort By Value + Index Comparison

        # Representation:
        #   - Sort indices by their values in nums
        #   - Adjacent indices in sorted order are the closest value matches

        # Idea:
        #   - If any duplicate values exist, they will be adjacent after sorting
        #   - Sort indices based on original values
        #   - If duplicate is found, check if original indexes are within k
        #   - Avoids storing a window or hashmap entirely

        n = len(nums)

        # Empty Case:
        # if not enough days to have a duplicate
        if n <= 1 or k <= 0:
            return False

        # Create list of indices
        # tc: O(n)
        # sc: O(n)
        index = list(range(n))

        # Creates an array of indexes that maps to the new sorted array
        # ex:
        #      nums = [3,1,2], idx=[0,1,2]
        #      nums = [3,1,2], idx=[1,2,0]
        # tc: O(n log n)
        index.sort(key=lambda i: nums[i])

        # Check adjacent pairs in sorted order
        # tc: O(n)
        for i in range(1, n):

            currIndex = index[i]
            prevIndex = index[i-1]

            # Check: if adjacent values are duplicates
            if nums[currIndex] == nums[prevIndex]:

                # Boundaries of original indices
                left = min(currIndex, prevIndex)
                right = max(currIndex, prevIndex)

                # Distance between original indices
                distance = right - left

                # Check: if original indices are within k distance
                if distance <= k:
                    return True

        # No duplicates found within distance k

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

Solution 2: [Sliding Window] MRI HashMap With Dynamic Left Based On Right and K - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window + HashMap

        # Window Representation:
        #   - Width of [left, right]
        #   - Window size of max k
        #   - HashSet stores most recent index appearance for nums

        # Idea:
        #   - Slide window over num list and add index at right boundary
        #   - Check mri to determine if duplicate exists within window boundaries
    
        # Track more recent index appearance for num
        # sc: O(n)
        mostRecentIndex = {}

        # Sliding Window Variables
        # sc: O(1)

        # right boundary
        n = len(nums)
        right = 0

        # Empty Case:
        # if not enough days to have a duplicate
        if n <= 1 or k <= 0:
            return False

        # tc: iterate over n O(n)
        for right in range(n):

            currNum = nums[right]

            # Check: if num has already been seen
            # tc: O(1)
            if currNum in mostRecentIndex:

                #     k distance
                #  | ------------ |
                # left          right 

                # the furthest left index that is still within k distance
                left = right - k

                # previous occurrence of currNum
                prevIndex = mostRecentIndex[currNum]

                # Check: if previous num copy is within current window:
                if left <= prevIndex <= right:
                    return True

            # Implies: no duplicate exists
            # Then: update mri for num
            mostRecentIndex[currNum] = right

 
        # No duplicates found within distance k

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

Solution 3: [Sliding Window] HashSet Tracking Nums Currently Within Window Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window + HashSet

        # Window Representation:
        #   - Width of [left, right]
        #   - Window size of max k
        #   - HashSet stores elements currently inside window

        # Idea:
        #   - Slide window over num list and add num at right boundary
        #   - Check if num we added is already within window boundary
        #   - Keep max k elements within window boundaries
        #   - Remove elements from left boundary once window is large than size k

        n = len(nums)

        # Empty Case:
        # if not enough days to buy and sell in time
        if n <= 1 or k <= 0:
            return False

        # Sliding Window Data:
        # sc: up to O(k)
        window = set()

        # Sliding Window Boundaries:
        # sc: O(1)
        left = 0
        right = 0

        # Early exit: if all elements are unique, no duplicates possible
        # tc: O(n)
        # sc: O(n)
        if len(window) == n:
            return False

        # tc: iterate over n O(n)
        for right in range(n):

            # Check: if num already exists within window range
            if nums[right] in window:
                return True

            # Add right index to sliding window and iterate:
            window.add(nums[right])

            # Remove left index from sliding window, if we've reached capacity, and iterate:
            if len(window) > k:
                window.remove(nums[left])
                left += 1

        # No duplicates found within distance k

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

121. Best Time to Buy and Sell Stock ::4:: - Easy

Topics: Array, Dynamic Programming, Sliding Window

Intro

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example InputOutput
prices = [7,1,5,3,6,4]5
prices = [7,6,4,3,1]0

Constraints:

1 ≤ prices.length ≤ 105

0 ≤ prices[i] ≤ 104

Abstraction

Given an array of stock values, find the highest profit possible.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: (iterative)

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Tracking Left Min Buy Price And Right Max Profit Index - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Sliding Window (Variable Size)

        # Stock Representation:
        #   - Treat each day as a potential sell day
        #   - Maintain a window [left, right] where:
        #        left: best buy day so far (lowest price seen)
        #        right: current sell day candidate (highest prices seen)

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # Sliding Window Representation:

        # Data: max profit seen so far
        # sc: O(1)
        maxProfit = 0

        # Left Boundary: lowest buy price seen so far
        # tc: O(1)
        left = 0

        # Right Boundary: current price scanner
        # tc: O(1)
        right = 0

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

            # Grab current window boundaries
            # sc: O(1)
            buyPrice = prices[left]
            sellPrice = prices[right]

            # Check: can we make a profit (is current price higher than our current buy)
            # Then: calculate and compare profit
            if buyPrice < sellPrice:
                
                # Calculate profit
                profit = sellPrice - buyPrice

                # Compare profit
                maxProfit = max(maxProfit, profit)

            # Check: have we found a better buy price (is current price lower than our current buy)
            # Then: update our lowest buy (our left boundary)
            else:
                left = right

            # iterate current price scanner
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Iterate over array of n prices O(n)No additional memory allocation for iteration O(1)
Profit checkO(1)O(1)Profit calculation in constant O(1)No additional memory allocation for calculation O(1)
OverallO(n)O(1)Iteration over array dominates O(n)No additional memory allocation O(1)

Solution 2: [Sliding Window] Single Variable Sliding Window Efficient Max Profit [TC Opt] - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Sliding Window (Variable Size)

        # Stock Representation:
        #   - Treat each day as a potential sell day
        #   - Maintain a window [left, right] where:
        #        left: best buy day so far (lowest price seen)
        #        right: current sell day candidate (highest prices seen)

        # Idea:
        #   - Track lowest price
        #   - If current price is lower than buy price, we have found better buy price, reset window
        #   - If current price is higher than buy price, check if higher profit found

        # Sliding Window Representation:

        # Data: max profit seen so far
        maxProfit = 0

        # Left Boundary: lowest buy price seen so far
        minPrice = float('inf')

        # tc: iterate over n O(n)
        for price in prices:

            # if today has a lower buy price
            if price < minPrice:
                minPrice = price

            # if today has a better profit
            elif price - minPrice > maxProfit:
                maxProfit = price - minPrice

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Iterate over array of n prices O(n)No additional memory allocation for iteration O(1)
Profit checkO(1)O(1)Profit calculation in constant O(1)No additional memory allocation for calculation O(1)
OverallO(n)O(1)Iteration over array dominates O(n)No additional memory allocation O(1)

Solution 3: [Dynamic Programming] Track Min Buy And Max Profit Up To Day i Table - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (Full DP Table)

        # Window Representation:
        #   - Maintain window within [i]
        #   - buyPrice[i][0]: best buy day so far:
        #       we represent this by min(-buyPrice) for easier later calculations
        #
        #   - buyPrice[i][1]: best profit price up to day i
        #       

        # Idea:
        #   - Always buy before you sell (<= index 1)
        #   - Check if new buy day found
        #   - Check if new best profit found


        # Empty check: no profit to be made
        if not prices:
            return 0

        n = len(prices)

        # Initialize DP:
        # sc: O(n)
        dp = []
        
        # Fill dp table:
        #   - dp[i][0]: best buy price up to day i
        #   - dp[i][1]: best profit price up to day i
        # tc: O(n)
        for i in range(n):
            dp.append([0, 0])

        
        # Initialize DP:
        # i = day
        # dp[i][0]: set best buy price to day 0
        # dp[i][1]: set best profit made up to day 0 
        #           (cannot buy and sell on same day, so profit on day 0 is 0)
        # tc: O(1)
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        # start from second day
        # tc: iterate across n days O(n)
        for i in range(1, n):

            # Check: grab todays buy price
            # Use profit to explain our buy price:
            #     profit = hold + sellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: grab best buy price up to yesterday
            bestBuyUpToYesterday = dp[i-1][0]

            # Check: grab new buy price
            dp[i][0] = max(bestBuyUpToYesterday, todayPrice)     

            # Check: todays profit
            todayProfit = bestBuyUpToYesterday + prices[i]
            
            # Check: grab new best profit, cannot sell today, so use yesterdays price    
            dp[i][1] = max(dp[i-1][1], todayProfit)


        # overall: tc O(n)
        # overall: sc O(n)
        return dp[-1][1]
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 4: [Dynamic Programming] Track Min Buy And Max Profit State Compression - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (State Compression)

        # Stock Representation:
        #   - Treat each day as potential sell day
        #   - Maintain a state tracking where:
        #        buyPrice: best buy price so far (lowest price seen)
        #        profit: most profit made so far (highest profit made using each day as a sell day)

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # set up compressed dynamic programming table first iteration:        
        # [i][0] -> min buying price up to day i
        # [i][1] -> max profit up to day i

        # Initialize Compressed DP:
        #   - min price up to day 0
        #   - max profit up to day 0
        bestBuyPrice = -prices[0]
        maxProfit = 0

        # start from second day
        # tc: iterate over n days O(n)
        for i in range(1, n):
            
            # Put buy price as negative,
            # to ensure we always subtract it from out buy price
            #     profit = hold + sellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: compare best buy price up to yesterday to today buy price
            bestBuyPrice = max(bestBuyPrice, todayPrice)

            todayProfit = bestBuyPrice + prices[i]

            # profit = hold + sellPrice
            #        = (-3) + 10
            #        = 7
            maxProfit = max(maxProfit, todayProfit)  

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

3. Longest Substring Without Repeating Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s, find the length of the longest without duplicate characters.

Example InputOutput
s = "abcabcbb"3
s = "bbbbb"1
s = "pwwkew"3

Constraints:

0 ≤ s.length ≤ 5 * 104

s consists of English letters, digits, symbols and spaces.

Abstraction

Given a string, find longest substring without duplicates

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] MRI HashMap With Jumping Left If Unique Flag Is Broken - Sliding Window/Variable Size Window

    def lengthOfLongestSubstring(self, s: str) -> int:

        # Sliding Window (Variable Size)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashSet stores most recent index appearance for nums
        #   - Window will contain no duplicates

        # Idea:
        #   - Expand right pointer to explore new characters
        #   - If new character results in a duplicate,
        #     move the left window boundary to 1 in front of the duplicate

        n = len(s)-1

        # Track more recent index appearance for char
        mostRecentIndex = {} 

        # Sliding Window Variables
        # sc: O(1)

        # left boundary of current substring
        left = 0

        # right boundary of current substring
        right = 0

        # max length so far
        maxLen = 0

        # tc: iterate over n O(n)
        while right <= n:
                
                # character we just added to substring
                newChar = s[right]

                # If duplicate exists within current window
                # tc: O(1)
                if newChar in mostRecentIndex:
                    
                    prevIndex = mostRecentIndex[newChar]
                    
                    if left <= prevIndex <= right:

                        # Move left pointer to remove previous duplicate from window
                        left = prevIndex + 1

                # Update most recent index for character
                # tc: O(1)
                mostRecentIndex[newChar] = right

                # Update window length
                # tc: O(1)
                currLen = right - left + 1

                # Check max length
                maxLen = max(maxLen, currLen)

                # Iterate right pointer to continue window shifting
                # tc: O(1)
                right += 1
            
        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return maxLen
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
HashmapO(n)O(n)Iteration over string O(n)Worst case will store n characters for completely unique string
IterationO(n)O(1)Iteration over string visited once by end pointer O(n)No additional memory allocation for end pointer iteration O(1)
Hashmap operationsO(1)O(1)Lookup and insertion in constant O(1)Constant length for 128-258 ascii characters O(1)
Window shiftingO(n)O(1)Iteration over string visited at most once by start pointer O(n)No additional memory allocation for start pointer iteration O(1)
OverallO(n)O(1)Iteration over string for start and end pointer dominates, leading to O(n)Constant space for hashmap char indexes dominates, leading to O(n)

Solution 2: [Sliding Window] HashSet Tracking Chars Currently Within Window Boundaries And Removing From Left While Unique Flag Is Broken - Sliding Window/Variable Size Window

def lengthOfLongestSubstring(self, s: str) -> int:

    # Sliding Window (Variable Size Window)

    # Window Representation:
    #   - Maintain window [left, right]
    #   - HashSet stores unique characters currently inside window

    # Idea:
    #   - Expand right to add new characters
    #   - If duplicate found, shrink from left until duplicate is removed
    #   - Track longest window seen with no duplicates

    n = len(s)

    # Sliding Window Data:
    # sc: O(k)
    window = set()

    # Sliding Window Boundaries:
    # sc: O(1)
    left = 0

    # Track longest valid window
    maxLen = 0

    # tc: iterate over n O(n)
    for right in range(n):

        # Shrink from left until duplicate is removed
        # tc: O(1) amortized
        while s[right] in window:
            window.remove(s[left])
            left += 1

        # Add incoming character to window
        # tc: O(1)
        window.add(s[right])

        # Update max length if current window is larger
        # tc: O(1)
        maxLen = max(maxLen, len(window))

    # overall: tc O(n)
    # overall: sc O(min(n, k))
    return maxLen
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

424. Longest Repeating Character Replacement ::1:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times. Return the length of the longest substring containing the same letter you can get after performing the above operations.

Example InputOutput
s = "ABAB", k = 24
s = "AABABBA", k = 14

Constraints:

1 ≤ s.length ≤ 105

s consists of only uppercase English letters.

0 ≤ k ≤ s.length

Abstraction

Given a string, and count k, return the longest substring of matching characters you can get after replacing k or less characters.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: (iterative)

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] HashMap Tracking CharCount In Window To Find MostOccur To Verify AllOtherChars In Window Are Replaceable Or Shrink From Left Until AllOtherChars Are Replaceable Less Than Or Equal To K - Sliding Window/Variable Size Window

    def characterReplacement(self, s: str, k: int) -> int:
        
        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - Hashmap: char -> count within window

        # Idea:
        #   - Expand right pointer to explore new character
        #   - maxFreq will hold the mostOccur char in window
        #   - Window length of size right - left + 1
        
        # Trick:
        #   - We need allOtherChars in window to be <= k, so that we know
        #     we can replace allOtherChars with the mostOccur
        #   - If allOtherChars is greater than k (allOtherChars > k),
        #     we simply shrink from left until we can replace allOtherChars,
        #     when allOtherChars is less than or equal to k, (allOtherChars <= k)

        n = len(s)

        # char count within window boundaries
        count = defaultdict(int)

        # Sliding Window Variables
        # sc: O(1)
        # left and right window boundaries
        left = 0
        right = 0

        # Sliding Window Data
        # sc: O(1)
        # mostOccur: most occurring character in window
        mostOccur = 0  
        # max length seen so far
        maxLen = 0

        # tc: iterate over n O(n)
        for right in range(n):

            # Character we just encountered
            # tc: O(1)
            currChar = s[right]
            charCount[currChar] += 1

            # Update mostOccur in window
            # tc: O(1)
            mostOccur = max(mostOccur, charCount[currChar])

            # length of curr window
            currWindowLen = right - left + 1

            # how many other chars exists, (anything that is not mostOccur)
            allOtherChars = currWindowLen - mostOccur

            # We need allOtherChars to be less than k (num of chars we can replace),
            # so that we can replace allOtherChars with the mostOccur.
            # thus, shrink window from left by 1 until allOtherChars is valid
            # tc: O(1) amortized
            while k < allOtherChars:

                # Note: we do NOT update mostOccur here
                # While shrinking, the mostOccur may become stale
                #   - true max mostOccur may have decreased
                #   - mostOccur char may have shifted to a different character

                # This is fine because:
                #   - for the current window from [left:right], we have already agreed that
                #     mostOccur is the highest char
                #   - so even if we shrink and remove
                
                # A stale mostOccur will make allOtherChars smaller than actual 
                #   - underestimating allOtherChars
                
                # This is safe because: 
                #   - No char can exceed the current mostOccur while shrinking,
                #     since we are only shrinking from the left and not modifying right
                #   - Regardless, we need to shrink because an invalid window 
                #     won't count towards our maxLen
                
                # Thus mostOccur:
                #   - not acting as a true most occurring character
                #   - acting as a flag for the original mostOccur for original window
                #     that existed before shrinking
                
                # Remove left character from window
                charCount[s[left]] -= 1
                left += 1

                # Recompute currWindowLen and allOtherChars after shrink
                currWindowLen = right - left + 1
                allOtherChars = currWindowLen - mostOccur


            
            # Once allOtherChars is valid, check maxLen
            maxLen = max(maxLen, currWindowLen)

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
IterationO(n)O(1)Iterate right pointer over each character at least once for n length O(n)No additional memory allocation for iteration O(1)
Frequency map operationsO(1)O(1)Insert in constant O(1)Constant frequency map size for 26 uppercase O(1)
Window expansion and shrinkingO(n)O(1)Each character added and removed at most once from window O(n)No additional memory allocation for virtual window O(1)
OverallO(n)O(1)Iteration over string of n length dominates, leading to O(n)Constant space frequency map dominates, leading to O(1)

567. Permutation in String ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise. In other words, return true if one of s1's permutations is the substring of s2.

Example InputOutput
s1 = "ab", s2 = "eidbaooo"true
s1 = "ab", s2 = "eidboaoo"false

Constraints:

1 ≤ s1.length, s2.length ≤ 104

s1 and s2 consist of lowercase English letters.

Abstraction

Given a string 1 and string 2, return true of string 1 is a permutation within string 2.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Needle and Window HashMap For CharCount And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window

    def checkInclusion(s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Window size always equals len(s1)
        #   - Hashmap: char -> frequency

        # Idea:
        #   - Build a frequency map for s1
        #   - Expand right pointer to build window in s2
        #   - If window size exceeds len(s1), shrink from left
        #   - If window frequency == s1 frequency, permutation exists

        # Since window size is fixed, left moves whenever
        # window grows larger than lens1)

        n1 = len(s1)
        n2 = len(s2)

        # s2 < s1: permutation not possible
        # tc: O(1)
        if n2 < n1:
            return False

        # s1 Frequency
        # tc: O(n)
        # sc: O(n)
        needleCharCount = defaultdict(int)
        for c in s1:
            needleCharCount[c] += 1

        # Substring window frequency
        # sc: O(1)
        windowCharCount = defaultdict(int)
        
        # left boundary of current substring
        left = 0

        # right boundary of current substring
        right = 0

        # tc: iterate over n O(n)
        for right in range(n2):

            # Add new character to substring
            # tc: O(1)
            rightChar = s2[right]
            windowCharCount[rightChar] += 1

            currWindowLen = right - left + 1

            # Optimization:
            # While window size is longer than  needle size, shrink
            # tc: O(1)
            while n1 < currWindowLen:

                # Remove character from substring
                leftChar = s2[left]

                # Update substring character count
                windowCharCount[leftChar] -= 1

                # Remove zero frequency keys
                if windowCharCount[leftChar] == 0:
                    # need to remove empty char counts, 
                    # so we can match window hashmap to needle hashmap 
                    del windowCharCount[leftChar]
                
                # Shrink substring from left
                # tc: O(1)
                left += 1

                currWindowLen = right - left + 1
            
            # Window is valid size,
            # check if permutation found
            if windowCharCount == needleCharCount:
                return True

        # overall: tc O(n) 
        # overall: sc O(n)
        return False
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
s1 FrequencyO(m)O(1)Iterate over s1 to generate frequency count O(m)Constant frequency count up to 26 characters O(1)
IterationO(n)O(1)Iterate over s2 to generate sliding window frequency count O(n)Constant frequency count up to 26 characters O(1)
HashmapO(1)O(1)Comparison in constant O(1)No additional memory allocation for hashmap comparison O(1)
OverallO(n + m)O(1)Iteration over s1 and s2 dominate, leading to O(n + m)Constant frequency count for s1 and sliding window O(1)

Solution 2: [Sliding Window] Needle and Window Array With Lowercase Ascii Shifting Via 97 To Index 0 [SC Opt] - Sliding Window/Fixed Size Window

    def checkInclusion(self, s1: str, s2: str) -> bool:
        
        # Sliding Window (Fixed Size Window)

        # Window Representation:
        #   - Fixed window of size len(s1)
        #   - Two frequency arrays of size 26 (one per lowercase letter)
        #   - needleCharCount: frequency of each char in s1
        #   - fixedWindowCharCount: frequency of each char in current window of s2

        # Idea:
        #   - Build needleCharCount fixedWindowCharCount from s1
        #   - Seed first window of s2 into fixedWindowCharCount
        #   - Slide window across s2: remove leftmost char, add new rightmost char
        #   - If fixedWindowCharCount == needleCharCount at any point, window is a permutation of s1

        # Lowercase Ascii Index Shifting:
        # lowercase chars start 97, so if we want 'a' to go from 97 to 0,
        # we just subtract 97 from any and all chars.
        # Now instead of 97 -> 122, we get 0 -> 26

        # s2 too short to contain s1
        # tc: O(1)
        if len(s2) < len(s1):
            return False

        # Fixed Window Iteration:
        # Create a fixed window size based on the needle size.
        # Calculate the window on the leftmost indexes of the haystack.
        # Iterate the fixed window over the haystack while simply
        # adding the right char and removing the left char.
        # Compare each iteration to expected needleCharCount

        # Frequency arrays for each lowercase letter a-z
        # sc: O(1) fixed size 26
        needleCharCount = [0] * 26
        fixedWindowCharCount = [0] * 26

        # Fixed window right boundary
        fixedWindowRight = len(s1)

        # Seed needleCharCount and first window of s2
        # tc: O(k) where k = len(s1)
        for i in range(fixedWindowRight):
            # populate the expected needle char count
            needleCharCount[ord(s1[i]) - 97] += 1
            # populate the first [0, right] chars in the haystack
            fixedWindowCharCount[ord(s2[i]) - 97] += 1

        # Check if first fixed window char count matches needle char count
        # tc: O(1)
        if fixedWindowCharCount == needleCharCount:
            return True

        # Slide window across remaining chars in s2
        # tc: O(n)
        for right in range(fixedWindowRight, len(s2)):

            # Grab left boundary index for fixed window
            left = right - fixedWindowRight

            # Iterate fixed window, remove left char, add right char
            fixedWindowCharCount[ord(s2[left]) - 97] -= 1
            fixedWindowCharCount[ord(s2[right]) - 97] += 1

            # Check if window char count matches needle char count
            # tc: O(1)
            if fixedWindowCharCount == needleCharCount:
                return True

        # No permutation found
        
        # overall: tc O(n + k)
        # overall: sc O(1)
        return False
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

76. Minimum Window Substring ::2:: - Hard

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s and t of lengths m and n respectively, return the minimum window of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string "". The testcases will be generated such that the answer is unique.

Example InputOutput
s = "ADOBECODEBANC", t = "ABC""BANC"
s = "a", t = "a""a"
s = "a", t = "aa"""

Constraints:

m == s.length

n == t.length

1 ≤ m, n ≤ 105

s and t consist of uppercase and lowercase English letters.

Abstraction

Given two strings s1 and s2, of lengths m and n, return the minimum substring of s, such that every character including duplicates is in the window.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Needle and Window Hashmap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window

    def checkInclusion(self, s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Two Hashmaps:
        #       needleFreq -> required counts
        #       windowFreq -> current window counts

        # Idea:
        #   - Expand right pointer to explore new character
        #   - Track how many required character are satisfied via needleFreq
        #   - While required characters are satisfied, shrink from left to minimize window
        #   - Track smallest valid window found

        # sLen < tLen: substring not possible
        # tc: O(1)
        if len(s) < len(t):
            return ""

        # Build needle character map
        # sc: O(m)
        needleCharCount = defaultdict(int)
        for c in t:
            needleCharCount[c] += 1

        # Sliding Window Variables
        # sc: O(1)
        left = 0
        right = 0

        # Sliding Window Data (3)
        # Frequency map for curr chars in window (1)
        # sc: O(1)
        windowCharCount = defaultdict(int)

        # length and starting index of smallest window so far (2)
        minLen = float('inf')
        minStart = 0

        # Flags for unique chars slots from needle hashmap length (3)
        needCharSlots = len(needleCharCount) 
        haveCharSlots = 0                 

        # tc: iterate over n O(n)
        for right in range(len(s)):

            # Expand window
            # tc: O(1)
            rightChar = s[right]
            windowCharCount[rightChar] += 1

            # If char is a slot char with requirements
            if rightChar in needleCharCount:

                # If char has met requirements
                if windowCharCount[rightChar] == needleCharCount[rightChar]:
                    haveCharSlots += 1


            # Shrink window while valid
            # tc: amortized O(n)
            while haveCharSlots == needCharSlots:
                
                # Substring size
                windowLen = right - left + 1
                
                # Update minimum window
                if windowLen < minLen:
                    minLen = windowLen
                    minStart = left

                # Remove left character
                leftChar = s[left]
                windowCharCount[leftChar] -= 1
                
                # If char is a slot char with requirements
                if leftChar in needleCharCount:

                    # If char no longer meets requirements
                    if  windowCharCount[leftChar] < needleCharCount[leftChar]:
                        haveCharSlots -= 1

                # Shrink substring from left
                # tc: O(1)
                left += 1

        # If no valid substring was found
        if minLen == float("inf"):
            return ""

        # Splice min substring
        res = s[minStart:(minStart + minLen)]

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

Solution 2: [Sliding Window] Needle Hashmap And stillNeedThisMany And GlobalRemainingCountAcrossChars Indicate Valid Window Shrink While Valid - Sliding Window/Variable Size Window

    def minWindow(self, s: str, t: str) -> str:

        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Single hashmap: char -> remaining required count
        #   - targetCharsRemaining: tracks total chars still needed

        # Idea:
        #   - Expand right pointer to explore new character
        #   - Decrease count of remaining required count
        #   - When targetCharsRemaining == 0, window is valid
        #   - Shrink from left to minimize window
        #   - Track smallest valid window

        # sLen < tLen: substring not possible
        # tc: O(1)        
        if len(s) < len(t):
            return ""

        # Build required frequency map
        # sc: O(1)
        needleCharCount = defaultdict(int)

        # tc: O(m)
        for ch in t:
            needleCharCount[ch] += 1

        # Total chars still needed
        globalRemainingCountAcrossChars = len(t)         

        # Sliding Window Variables
        # sc: O(1)
        left = 0
        right = 0
        minWindow = (0, float("inf"))          

        # tc: iterate over n O(n)
        for right in range(len(s)):

            # Expand window
            # tc: O(1)
            rightChar = s[right]
            
            # Note:
            # to start, needleCharCount hashmap will only contain values of chars that 
            # we need for the needle.
            # This means all other possible keys, are set to 0, and moving forward,
            # can only be less than or equal to 0 (regardless of adding or removing).
            # Thus, to track chars that are in the needle,
            # we only count chars that at some point have a positive count,
            # to contribute towards decreasing or increasing the global remaining 
            stillNeedThisMany = needleCharCount[rightChar]

            if stillNeedThisMany > 0:
                globalRemainingCountAcrossChars -= 1

            needleCharCount[rightChar] -= 1

            # Shrink window while its valid, so we can find the min valid window
            # tc: amortized O(n)
            while globalRemainingCountAcrossChars == 0:

                currWindowLen = right - left + 1
                currMinWindowLen = minWindow[1] - minWindow[0] + 1

                # If smaller min window found, update min
                if currWindowLen < currMinWindowLen:
                    minWindow = (left, right)

                # Remove left character
                leftChar = s[left]
                needleCharCount[leftChar] += 1

                # If after excluding this char we now need copies of this char,
                # we can count it towards hurting and increasing the global counter
                stillNeedThisMany = needleCharCount[leftChar]

                if stillNeedThisMany > 0:
                    globalRemainingCountAcrossChars += 1

                # Shrink substring from left
                # tc: O(1)
                left += 1

        # If no valid window was found
        if minWindow[1] == float("inf"):
            return ""

        # Grab min window
        res = s[minWindow[0]:minWindow[1] + 1]

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

239. Sliding Window Maximum ::4:: - Hard

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Monotonic Queue

Intro

You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Example InputOutput
nums = [1,3,-1,-3,5,3,6,7], k = 3Output: [3,3,5,5,6,7]
nums = [1], k = 1[1]

Constraints:

1 ≤ nums.length ≤ 105

-104 ≤ nums[i] ≤ 104

1 ≤ k ≤ nums.length

Abstraction

Given list of nums and sliding window size k, return array of largest sum for every window within the list.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointers] Two Pointer And MaxHeap Priority Queue [TC Slow] - Sliding Window/Fixed Size Window

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

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window of size k
        #   - MaxHeap storing (-value, index)
        #   - Heap top always holds current window maximum

        # Idea:
        #   - Push first k elements into heap
        #   - For each new element:
        #       1. Push new element
        #       2. Remove stale elements (outside window) (index <= (right - k))
        #       3. Heap top is max of current window


        # Empty check:
        # tc: O(1)
        if not nums or k == 0:
            return []

        n = len(nums)

        # List of max values for each window
        # sc: O(n)
        result = []

        # MaxHeap():
        #   - a Python minHeap with negative values, resulting in a maxHeap
        #   - O(log n) insertion and removal 

        # MaxHeap data representation: (-value, index)
        #   - Need the -value to turn MinHeap into MaxHeap 
        #   - Need the index to check if top of heap is outside window
        maxHeap = []  

        # MaxHeap top representation (maxHeap[0]):
        #   - Top: largest element index in current window
        #   - All other: no guarantee for any other element, 
        #              we are only guaranteed that the top is the current max in the MaxHeap
        # 

        # Sliding Window Variables
        # sc: O(1)
        right = 0

        # Initialize first window:
        # push first k elements to maxHeap
        # tc: O(k)
        while right < k:
            
            # tc: O(log k)
            heapq.heappush(maxHeap, (-nums[right], right))

            # Expand substring
            # tc: O(1)
            right += 1

        # MaxHeap:
        # root of maxHeap now holds the max of the current window,
        # so its holding the max for the first window,
        # we append to the result list
        peekMax = -maxHeap[0][0]
        result.append(peekMax)

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

            # Expand window
            newElem = nums[right]

            # Push new element: (-value, index)
            heapq.heappush(maxHeap, (-newElem, right))

            # Remove top of maxHeap if it is stale (outside window)
            while maxHeap[0][1] <= (right - k):
                heapq.heappop(maxHeap)

            # MaxHeap:
            # root of maxHeap now holds the max of the current window,
            # we append to the result list
            peekMax = -maxHeap[0][0]
            result.append(peekMax)

            # Expand substring
            # tc: O(1)
            right += 1

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

Solution 2: [Dynamic Programming] Tracking Max Value By Dynamic Programming K Block Partitioning Encountered L To R and R to L Then Sliding Fixed Window And Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window

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

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Partition array into blocks of size k
        #   - leftMax[i]  = max from block start to i
        #   - rightMax[i] = max from block end to i (reverse scan)

        # Empty check:
        if not nums or k == 0:
            return []

        # We will split the original nums array into k sized blocks:
        # index:   0  1  2    3  4  5    6  7
        # nums:    1  3 -1   -3  5  3    6  7
        # blocks: [------]  [------]   [----]
        #           B0         B1        B2

        # Blocks are based only on index position and k size
        # They are not based on the direction of the scan 

        n = len(nums)

        # L to R Max Values, to L Max Values
        leftMax = [0] * n
        rightMax = [0] * n

        # Fill leftMax: find the running max on a per block basis L to R
        for i in range(n):

            # Start of new block: max is first element of block
            if i % k == 0:
                leftMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                leftMax[i] = max(leftMax[i-1], nums[i])

        # Fill rightMax: find the running max on a per block basis R to L
        for i in range(n-1, -1, -1):

            # Start of new block: max is first element of block
            if (i+1) % k == 0 or i == n-1:
                rightMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                rightMax[i] = max(rightMax[i+1], nums[i])

    
        # Compute sliding fixed window max 
        res = []
        left = 0
        right = k - 1

    
        # -----------------------------
        # Combining Precomputed with Fixed Window:
        # we are iterating a fixed window of size k:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #  

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]


        # -----------------------------
        # Grabbing Farthest Data Available To Use From Left and Right:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R           L

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R           L

        # -----------------------------
        # Window Max Coverage:
        #       R = index max from iterating Right to Left (<---)
        #       L = index max from iterating Left to Right (--->)

        # Case 1: Entire Window Is Accounted For
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R --------- L        
        #  (<---)     (--->)

        # Case 2: Entire Window Is Accounted For
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R --------- L        
        #    (<---)     (--->)
        
        # tc: iterate over nums array O(n)
        while right < n:

            # Re: see above diagrams
            res.append(max(rightMax[left], leftMax[right]))
            left += 1
            right +=1

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

Solution 3: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Values Unsafe For Duplicates [TC Opt] - Sliding Window/Variable Size Window

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

        # Sliding Window (Fixed Size Window) + Monotonic Deque
        
        # Window Representation:
        #   - Maintain a monotonic decreasing deque
        #   - Deque stores actual values (not indices)
        #   - Front of deque is always the max of current window

        # Idea:
        #   - Before adding new element, pop from back any values
        #     smaller than it (they can never be the max)
        #   - When window slides, pop from front if it equals
        #     the element being removed (nums[i])
        #   - Front of deque is always current window max

        # Unsafe For Duplicates:
        #   - Stores values instead of indices
        #   - When removing outgoing element, we compare by value not position
        #   - If duplicate values exist, we may accidentally remove the wrong copy

        # Empty check:
        if not nums or k == 0:
            return []

        n = len(nums)

        # result list for each window
        # sc: O(n)
        res = []

        # Deque():
        #   - a double ended queue
        #   - O(1) insertion and removal from both sides

        # Deque left and right ends representation:
        #   - Front (left end): largest element value in current window
        #     dq[0] always holds the current window max value
        #   - Back (right end): most recently added value
        #     dq[-1] holds the smallest element seen so far

        # Monotonic decreasing order means:
        #   High -> Low

        # left (high)             right (low)
        # [ val=8,  val=6,  val=3,  val=1 ]  Our Queue: Monotonic values always decreasing

        # stores values in monotonic decreasing order
        # sc: O(k)
        dq = deque()

        # Initialize first window:
        # Grab first k elements and place into deque
        # tc: O(k)
        for i in nums[:k]:

            # Monotonic Property:
            # Remove elements smaller than new num from right side to keep Monotonic High -> Low
            # tc: O(1) amortized
            while dq and i > dq[-1]:
                dq.pop()

            # Append new value after ensuring Monotonic High -> Low
            dq.append(i)

        # Deque front now holds max value of first window
        res = [dq[0]]

        # Slide window across remaining elements
        # tc: O(n - k)
        for i, x in enumerate(nums[k:]):

            # Sliding Window Single Iteration:
            # since at most, we are only adding 1 new element,
            # since we have a fixed window size,
            # at most, only 1 old element can go stale (out of bounds of the window)

            # Unsafe Duplicate Stale Check:
            # Since we store values instead of indices,
            # we identify the outgoing element by value comparison
            # nums[i] is the element leaving the window
            # Risk: if duplicates exist, may remove the wrong copy
            # tc: O(1)
            outgoingVal = nums[i]
            if dq[0] == outgoingVal:
                dq.popleft()

            # Monotonic Property:
            # Remove elements smaller than new num from right side to keep Monotonic High -> Low
            # tc: O(1) amortized
            while dq and x > dq[-1]:
                dq.pop()

            # Append new value after ensuring Monotonic High -> Low
            dq.append(x)

            # Deque front now holds max value of current window
            res.append(dq[0])

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

Solution 4: [Two Pointers] Two Pointer And Monotonic Decreasing Deque For Indices Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window

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

        # Sliding Window (Fixed Size Window)

        # Substring Representation
        #   - Maintain window of size k [left, right]
        #   - Monotonic Decreasing Deque storing indices
        #   - Front of deque always holds max element index

        # Idea:
        #   - Remove elements smaller than incoming element (from back)
        #   - Remove front if it exits window
        #   - Front of deque is always window max

        # Safe For Duplicates:
        #   - Stores indices instead of values
        #   - When removing outgoing element, we compare by index not value
        #   - Each index is unique, so we always know exactly which element is leaving
        #   - Duplicate values are handled correctly since we never confuse two
        #     elements at different positions

        # Empty check:
        if not nums or k == 0:
            return []

        n = len(nums)

        # result list for each window
        # sc: O(n)
        res = []

        # Deque():
        #   - a double ended queue
        #   - O(1) insertion and removal from both sides

        # Deque left and right ends representation:
        #   - Front (left end): largest element index in current window
        #     dq[0] always holds the index of the current window max
        #   - Back (right end): most recently added index
        #     dp[-1] holds the index of the smallest element seen so far 

        # Monotonic decreasing order means:
        #   High -> Low

        # left (high)             right (low)
        # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
        # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing


        # stores indices in monotonic decreasing order of their values
        # sc: O(k)
        dq = deque()

        # Initialize first window:
        # Grab first k elements and place into deque
        # tc: O(k)
        for right in range(k):

            # Monotonic Property:
            # Remove elements smaller than new num from right side to keep Monotonic High -> Low
            # tc: O(1) amortized
            while dq and nums[dq[-1]] < nums[right]:
                dq.pop()

        # Append new index after ensuring Monotonic High -> Low
        dq.append(right)


        # tc: iterate over n O(n)
        for right in range(n):

            
            # Monotonic Property:
            # Remove elements smaller new num from the right side to keep Monotonic High -> Low
            # tc: O(1) amortized
            while dq and nums[dq[-1]] < nums[right]:
                dq.pop()

            # Append new num after ensuring Monotonic High -> Low
            dq.append(right)

            # Sliding Window Single Iteration:
            # since at most, we are only adding 1 new element, 
            # since we have a fixed window size,
            # at most, only 1 old element can go stale (out of bounds of the window)
            
            # Monotonic Trick:
            # Since we keeping both:
            #   - Monotonic High -> Low
            #   - Iterating by 1 Index at a time

            # left (high)             right (low)
            # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
            # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing

            # So we only have to check if the highest value,
            # which by definition will have the lowest index (oldest value in the deque)
            # is now out of bounds of the fixed window

            left = right - k
            HighestValueIndex = dq[0]

            # Remove stale highest number from front
            # tc: O(1)
            if not left <= HighestValueIndex <= right:
                dq.popleft()

            # Deque front now holds max index of current window
            res.append(nums[dq[0]])


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

930. Binary Subarrays With Sum ::2:: - Medium

Topics: Array, Hash Table, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer goal, return the number of non-empty subarrays with a sum goal. A subarray is a contiguous part of the array.

Example InputOutput
nums = [1,0,1,0,1], goal = 24
nums = [0,0,0,0,0], goal = 015

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

nums[i] is either 0 or 1

0 ≤ goal ≤ nums.length

Abstraction

wow!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Two Pointer] Sliding Window With atMost() of Goal Minus atMost() of Goal Minus 1 Helper Only Shrink Left When Surpassed Goal (Optimized For Binary Input) - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
               
        # Two Pointer (Variable Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - curr_sum tracks sum of current window

        # Idea:
        #   - Directly counting subarrays with sum == goal is hard
        #     with two pointers because we can't know when to stop shrinking
        #   - Instead, count subarrays with sum <= goal via atMost()
        #   - exactly(goal) = atMost(goal) - atMost(goal - 1)
        #   - Inside atMost: expand right, shrink left when sum > goal,
        #     right - left + 1 counts all valid subarrays ending at right

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # --------------------------------------
        # atMost(2): all windows with sum <= 2
        # nums = [1, 0, 1, 0, 1]
        #         LR                currSum=1, res += 1 (1 subarray  ending at R)
        #         L  R              currSum=1, res += 2 (2 subarrays ending at R)
        #         L     R           currSum=2, res += 3 (3 subarrays ending at R)
        #         L        R        currSum=2, res += 4 (4 subarrays ending at R)
        #            L        R     currSum=3 -> shrink -> currSum=2, res += 4 (4 subarrays ending at R)
        # atMost(2) total = 1+2+3+4+4 = 14
        #
        # --------------------------------------
        # atMost(1): all windows with sum <= 1
        # nums = [1, 0, 1, 0, 1]
        #         LR                currSum=1, res += 1 (1 subarray  ending at R)
        #         L  R              currSum=1, res += 2 (2 subarrays ending at R)
        #            L  R           currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
        #            L     R        currSum=1, res += 3 (3 subarrays ending at R)
        #               L     R     currSum=2 -> shrink -> currSum=1, res += 2 (2 subarrays ending at R)
        # atMost(1) total = 1+2+2+3+2 = 10
        # --------------------------------------
        # exactly(2) = atMost(2) - atMost(1) = 14 - 10 = 4
        #   [1, 0, 1]
        #   [1, 0, 1, 0]
        #   [0, 1, 0, 1]
        #   [1, 0, 1]

        # Only valid because nums contains only 0 and 1:
        # shrinking window always decreases sum predictably
        # tc: O(1)
        def atMost(goal) -> int:

            # Guard against negative goal
            if goal < 0:
                return 0

            # Sliding Window Variables
            # sc: O(1)
            left = 0
            right = 0

            currSum = 0
            res = 0

            # tc: iterate over n O(n)
            for right in range(len(nums)):

                # Expand window
                # tc: O(1)
                currSum += nums[right]

                # Shrink window until sum <= goal
                # tc: O(1) amortized
                while currSum > goal:
                    currSum -= nums[left]
                    left += 1

                # All subarrays ending at right with sum <= goal
                # tc: O(1)
                windowLen = right - left + 1

                # Add to total res
                res += windowLen

            return res

        # exactly(goal) = atMost(goal) - atMost(goal - 1)
        # overall: tc O(n)
        # overall: sc O(1)
        return atMost(goal) - atMost(goal - 1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Prefix Sum] Prefix Sum With HashMap (Works For Any Input) - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + HashMap

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1


        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # HashMap: prefixSum -> number of occurrences
        # sc: O(n)
        prefixSumCounts = defaultdict(int)

        # Base case: empty prefix has sum 0, seen once
        # tc: O(1)
        prefixSumCounts[0] = 1

        # Running prefix sum
        currSum = 0

        # Total valid subarrays found
        res = 0

        # right boundary of prefixSum [0, right]
        right = 0

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

            # Expand prefix sum
            # tc: O(1)
            currSum += nums[right]

            # If: prefixSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our prefixSum back down to goal
            # tc: O(1)
            if currSum >= goal:

                # Check Target:
                # if at any point when iterating left to right,
                # the prefixSum added up to this target
                excludingSum = currSum - goal

                # Check Times:
                # number of times we encountered the target,
                # this tells us how many places we can shift left
                # to exclude numbers to get our prefixSum back down to goal
                excludingSumTimes = prefixSumCounts[excludingSum]

                # Add Times:
                # each location we can move left to,
                # is another prefixSum window that can be restricted via left to achieve goal
                res += excludingSumTimes

            # Record current prefixSum to occurrence count
            # tc: O(1)
            prefixSumCounts[currSum] += 1

            # Move right pointer
            right += 1

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

Solution 3: [Prefix Sum] Prefix Sum With Bounded Array (Optimized For Binary Input) - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + Array of (N+1)

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1

        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # Tracks for up to all possible sums (everything set to 1)
        # how many times we've seen a certain sum
        # sc: O(n)
        prefixSumCount = [0] * (n + 1)

        # Base case: we encounter the sum 0, for the first time
        # tc: O(1)
        prefixSumCount[0] = 1

        # Sum from left up to right
        currSum = 0

        # Total valid subarrays found
        res = 0

        # tc: iterate over n O(n)
        for num in nums:

            # Expand prefix sum
            # tc: O(1)
            currSum += num

            # If: currSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our currSum back down to goal
            # tc: O(1)
            if currSum >= goal:
                
                # Check Target: 
                # if at any point when iterating left to right,
                # the currSum added up to this target
                exludingSum = currSum - goal 

                # Check Times:
                # check number of times we encountered the target,
                # this tells us how many places we can shift left to exlucde numbers to get our currSum back down to goal
                exludingSumTimes = prefixSumCount[exludingSum]

                # Add Times: 
                # each location we can move left to, 
                # is another currSum window that can be restricted via left to achieve goal
                res += exludingSumTimes

            # Record current prefix sum to use in future
            # tc: O(1)
            prefixSumCount[currSum] += 1

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

1234. Replace the Substring for Balanced String :::: - Medium

Topics: String, Sliding Window

Intro

You are given a string s of length n containing only four kinds of characters: 'Q', 'W', 'E', and 'R'. A string is said to be balanced if each of its characters appears n / 4 times where n is the length of the string. Return the minimum length of the substring that can be replaced with any other string of the same length to make s balanced. If s is already balanced, return 0.

Example InputOutput
s = "QWER"0
s = "QQWE"1
s = "QQQW"2

Constraints:

n == s.length

4 ≤ n ≤ 10^5

n is a multiple of 4

s contains only 'Q', 'W', 'E', and 'R'.

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Full Frequency HashMap Tracking All Chars Outside Window - Sliding Window/Variable Size Window

    def balancedString(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # String Representation:
        #   - Maintain variable window [left, right]
        #   - Current window represents the substring we will replace
        #   - Frequencies OUTSIDE the window must all be <= target

        # Idea:
        #   - Store frequencies of chars OUTSIDE current window
        #   - Expand right to bring chars INTO window (removing from outside freq)
        #   - Once all outside frequencies <= target, window is valid
        #   - Shrink left to minimize valid window
        #   - Track smallest valid window seen

        # Balanced String:
        #   - 4 possible characters: Q W E R
        #   - Each must appear exactly len(s) // 4 times
        #   - Any character appearing more than target times needs replacement
        #
        #   e.g. s = "QWER", target = 1
        #        outsideCount = {Q:1, W:1, E:1, R:1} -> already balanced
        #
        #   e.g. s = "QQWE", target = 1
        #        outsideCount = {Q:2, W:1, E:1, R:0}
        #        Q exceeds target by 1, need to replace 1 char in window

        n = len(s)

        # Target frequency for each character:
        #   - The string s has n characters, and 4 possible characters (Q W E R)
        #   - A perfectly balanced string would have each character appear n // 4 times
        # tc: O(1)
        # sc: O(1)
        target = n // 4

        # Sliding Window Boundaries
        # sc: O(1)
        left = 0
        right = 0

        # Frequency HashMap:
        #   - Stores frequencies of chars OUTSIDE current window
        #   - Only 4 possible characters Q W E R
        # sc: O(1)
        outsideCount = defaultdict(int)
        for c in s:
            outsideCount[c] += 1

        # Minimum valid window size
        minLen = float('inf')

        # Early Exit:
        #   - If no character exceeds target balance count
        #   - Then all characters == target, 
        #     since you can't have any char count above target
        #     without another char count below target to compensate leading to unbalance
        # tc: O(1)
        if max(outsideCount.values()) <= target:
            return 0

        # Expand sliding window
        # tc: O(n)
        for right in range(n):

            # New character has entered window,
            # remove count from outside window tracker
            rightChar = s[right]
            outsideCount[rightChar] -= 1

            # Shrink from left while valid outside conditions:
            #   - All outside frequencies <= target => string is balanced
            #   - Window pointers are still valid
            # tc: O(1) amortized
            while max(outsideCount.values()) <= target and left <= right:

                # Check curr valid window
                currWindowLen = right - left + 1
                minLen = min(minLen, currWindowLen)

                # Remove char from window,
                # add it back to outside count frequencies
                # tc: O(1)
                leftChar = s[left]
                outsideCount[leftChar] += 1

                # Shrink left pointer
                # tc: O(1)
                left += 1

        # MinLen:
        # smallest window available, with a valid outside count

        # The window needs to be just large enough to absorb all excess, minLen,
        # which tells you the minimum number of characters you need to replace.
        # You don't need to know exactly what to replace them with, just that 
        # a replacement of that size exists that can fix the balance

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

Solution 2: [Sliding Window] Excess Count HashMap Tracking Overages Outside Window - Sliding Window/Variable Size Window

    def balancedString(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - Maintain window [left, right] representing the substring we can replace
        #   - Goal: Find minimum length substring which, when replaced, balances the string
        #   - Count of each character outside window should be <= n // 4
    
    
        n = len(s)
        target = n // 4

        # Count total occurrences of each character in s
        totalCount = defaultdict(int)
        for c in s:
            totalCount[c] += 1
        
        # Only keep excess counts,
        # characters that already <= target are ignored
        excess = defaultdict(int) 
        for c in "QWER":

            # Current total occurrences in string
            currCount = totalCount[c]

            # Extra occurrences beyond allowed target
            extraCount = currCount - target

            # If character does not exceed target,
            # no replacement is needed
            excess[c] = max(0, extraCount)      
        
        # Check whether any character still has excess frequency
        hasExcess = False

        for count in excess.values():

            # Found a character occurring
            # more than the allowed target count
            if count > 0:
                hasExcess = True
                break

        # If no character has excess frequency,
        # the string is already balanced
        if not hasExcess:
            return 0

        # Sliding Window Variables
        # sc: O(1)
        left = 0
        right = 0

        # Initialize min length as length of full original string
        minLen = n 

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

            # Expand window: 
            # decrease count for current character if it's in excess
            if s[right] in excess:
                excess[s[right]] -= 1

            while True:

                # A character is considered "balanced"
                # once the window has absorbed enough of its excess count
                #
                # Why <= 0 instead of == 0 ?
                #   excess[c] tracks how many more of character c
                #   still need to be included inside the window
                #
                #   Example:
                #       excess['Q'] = 2
                #
                #   After including 2 Q's in window:
                #       excess['Q'] = 0
                #
                #   After including 3 Q's in window:
                #       excess['Q'] = -1
                #
                # Negative values are still valid because the window
                # contains MORE than enough of that character
                qBalanced = (excess['Q'] <= 0)
                wBalanced = (excess['W'] <= 0)
                eBalanced = (excess['E'] <= 0)
                rBalanced = (excess['R'] <= 0)

                # If any character excess is still uncovered,
                # stop shrinking
                if not (qBalanced and wBalanced and eBalanced and rBalanced):
                    break

                # Current window is valid
                minLen = min(minLen, right - left + 1)

                # Remove left char from window
                # and restore its excess requirement
                leftChar = s[left]
                if leftChar in excess:
                    excess[leftChar] += 1

                # Iterate left window
                left += 1

            # Expand right pointer
            right += 1

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

1208. Get Equal Substrings Within Budget ::1:: - Medium

Topics: String, Sliding Window

Intro

You are given two strings s and t of the same length and an integer maxCost. You want to change s to t. Changing the ith character of s to ith character of t costs |s[i] - t[i]| (i.e., the absolute difference between the ASCII values of the characters). Return the maximum length of a substring of s that can be changed to be the same as the corresponding substring of t with a cost less than or equal to maxCost. If there is no substring from s that can be changed to its corresponding substring from t, return 0.

Example InputOutput
s = "abcd", t = "bcdf", maxCost = 33
s = "abcd", t = "cdef", maxCost = 31
s = "abcd", t = "acde", maxCost = 01

Constraints:

1 ≤ s.length ≤ 10^5

t.length == s.length

0 ≤ maxCost ≤ 10^6

s and t consist of only lowercase English letters.

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
        
        # Sliding Window + Two Pointers
        
        # Substring Representation:
        #   - Maintain window [left, right] representing the current substring of s
        #   - The window is valid if totalCost <= maxCost
        #   - Find the max length valid window

        # Idea:
        #   - For each character at index 'right', compute cost to change s[right] -> t[right]
        #   - Expand window by moving right pointer and accumulating totalCost
        #   - If totalCost exceeds maxCost, shrink window from left until valid
        #   - Track max window length during iteration

        n = len(s)
        
        # Sliding Window Variables
        # sc: O(1)
        left = 0
        right = 0

        # MaxLen
        # sc: O(1)
        totalCost = 0
        maxLen = 0

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

            # Add new character
            rightCharS = s[right]
            rightCharT = t[right]

            # Get Cost:
            # calculate cost to change S to T, we need to spend this money now
            cost = abs(ord(rightCharS) - ord(rightCharT))
            totalCost += cost

            # Shrink window while totalCost is invalid
            while maxCost < totalCost:

                # Remove old character
                leftCharS = s[right]
                leftCharT = t[right]

                # Get Cost:
                # calculate cost to change S to T, we are saving this money now
                totalCost -= abs(ord(s[left]) - ord(t[left]))

                # Iterate window
                left += 1
            
            # TotalCost is now valid:
            # valid window found, compare to max length
            windowLen = (right - left) + 1
            maxLen = max(maxLen, windowLen)

            # Iterate window
            right += 1

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

992. Subarrays with K Different Integers ::1:: - Hard

Topics: Array, Hash Table, Sliding Window, Counting

Intro

Given an integer array nums and an integer k, return the number of good subarrays of nums. A good array is an array where the number of different integers in that array is exactly k. For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1,2,1,2,3], k = 27
nums = [1,2,1,3,4], k = 33

Constraints:

1 ≤ nums.length ≤ 2 * 10^4

1 ≤ nums[i], k ≤ nums.length

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window

    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
                
        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - Maintain window [left, right] representing current subarray
        #   - Window is valid if it contains <= k distinct numbers
        #   - For exactly k distinct: count = atMostK(k) - atMostK(k-1)

        # Idea:
        #   - Use a sliding window to count number of subarrays
        #      with at most k distinct integers
        #   - For each right pointer expansion:
        #       1. Add nums[right] to window
        #       2. Shrink left while distinct count exceeds k
        #       3. Add window length (right - left + 1) to total count
        #   - Exact k distinct subarrays = atMostK(k) - atMostK(k-1)

        # Helper: count subarrays with at most k distinct integers
        # overall: tc O(n)
        # overall: sc O(n)
        def atMostK(nums, currK):

            # Sliding Window Data
            # sc: O(n)
            count = defaultdict(int)

            # Tracking Unique Values In Window
            distinct = 0

            # Sliding Window Variables
            # sc: O(1)
            left = 0
            right = 0

            n = len(nums)

            res = 0

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

                # Expand window
                rightNum = nums[right]

                # Check if new num has been found
                if count[rightNum] == 0:
                    distinct += 1

                # Increase num count 
                count[rightNum] += 1

                # Shrink window while distinct count is invalid
                while currK < distinct:

                    # Remove old value
                    leftNum = nums[left]
                    count[leftNum] -= 1

                    # Check if we lost a unique value
                    if count[leftNum] == 0:
                        distinct -= 1

                    # Shrink window
                    # tc: O(1)
                    left += 1

                # Valid window found:
                # All subarrays within this valid subarray are valid as well,
                # so the length of the larger subarray is the number of valid subarrays we have found
                res += (right - left) + 1

                # We don't double count because each subarray is counted exactly once at its right endpoint

                # Expand right pointer
                right += 1

            # tc: O(n)
            # sc: O(n)
            return res

        # atMost(k):   counts all subarrays with <= k distinct elements
        # atMost(k-1): counts all subarrays with <= k-1

        # so doing atMost(l) - atMost(k-1) removes all the answers that were <= k-1
        # which leaves only the answers that were == k
        res = atMostK(nums, k) - atMostK(nums, k-1)

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

904. Fruit Into Baskets ::2:: - Medium

Topics: Array, Hash Table, Sliding Window

Intro

You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces. You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow: You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.

Example InputOutput
fruits = [1,2,1]3
fruits = [0,1,2,2]3
fruits = [1,2,3,2,2]4

Constraints:

1 ≤ fruits.length ≤ 10^5

0 ≤ fruits[i] < fruits.length

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Frequency Fruit Count With Shrink From Left While More Than 2 Fruit In Window - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Hashmap
        
        # Idea:
        #   - Use a sliding window to maintain at most 2 distinct fruit types.
        #   - Expand right; shrink left when more than 2 distinct types appear.
        #   - Track maximum window length.
        
        # Rules:
        #   1. Maintain a hashmap: fruit type -> count in current window.
        #   2. Expand right pointer, increment count.
        #   3. If distinct fruits > 2, shrink window from left until <= 2.
        #   4. Update maximum length for each right.
        
        # Fruit Count:
        # at most 2 types of fruit in hashmap
        # sc: O(1)
        count = defaultdict(int)

        # Window Representation:
        # sc: O(1)
        left = 0
        right = 0

        n = len(fruits)
        maxLen = 0

        # Iterate over array
        # tc: O(n)
        while right < n:

            # Add right num
            rightNum = fruits[right]
            count[rightNum] += 1

            # Shrink window while window is invalid
            #   - more than 2 distinct fruits in window
            while len(count) > 2:
                
                # Remove left num
                leftNum = fruits[left]
                count[leftNum] -= 1

                # Check if ran out of a certain type of fruit, then remove
                if count[fruits[left]] == 0:
                    del count[fruits[left]]

                # Iterate window
                left += 1

            # Update maximum number of fruits collected
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

            # Iterate window
            right += 1

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

Solution 2: [Sliding Window] Last Seen Index Tracking With Shrink From Left - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Last Seen Index Map

        # Idea:
        #   - Maintain a window [l, r]
        #   - Track last seen index of each fruit type
        #   - If we ever have > 2 fruit types:
        #       remove the fruit with the smallest last seen index
        #       and move left pointer just past it

        # Invariant:
        #   - window always contains at most 2 fruit types

        last_seen = {}

        l = 0
        res = 0

        for r in range(len(fruits)):

            # Update last seen index of current fruit
            last_seen[fruits[r]] = r

            # If we have more than 2 fruit types,
            # remove the one that was seen least recently
            if len(last_seen) > 2:

                # Find fruit with smallest last seen index
                fruit_to_remove = min(last_seen, key=last_seen.get)

                # Move left pointer past it
                l = last_seen[fruit_to_remove] + 1

                # Remove it from map
                del last_seen[fruit_to_remove]

            # Update result
            res = max(res, r - l + 1)

        # 
        return res
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit ::1:: - Medium

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Ordered Set, Monotonic Queue

Intro

Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.

Example InputOutput
nums = [8,2,4,7], limit = 42
nums = [10,1,2,4,7,2], limit = 54
nums = [4,2,2,2,4,4,2,2], limit = 03

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^9

0 ≤ limit ≤ 10^9

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def longestSubarray(self, nums: List[int], limit: int) -> int:
        # Sliding Window + Monotonic Deques
    
        # Idea:
        #   - Maintain two deques:
        #       max_d: decreasing deque for maximum
        #       min_d: increasing deque for minimum
        #   - Expand right pointer; shrink left if max - min > limit.
        #   - Track maximum window length.
        
        # Rules:
        #   1. Append nums[right] to both deques, maintaining monotonic quality
        #   2. While current window violates limit (max - min > limit), shrink from left
        #   3. Update maximum window length

        # Sliding Window Data
        # sc: O(n)
        maxDeque = deque()  # stores elements in decreasing order
        minDeque = deque()  # stores elements in increasing order

        # Window Representation:
        # sc: O(1)
        left = 0
        right = 0


        maxLen = 0
        n = len(nums)

        # Iterate over window 
        while right < n:
            
            # Add new num to window
            rightNum = nums[right]

            # Maintain Monotonic decreasing max deque
            while maxDeque and rightNum > maxDeque[-1]:
                maxDeque.pop()

            # put num in corresponding location in maxDeque
            maxDeque.append(rightNum)

            # Maintain Monotonic increasing min deque
            while minDeque and rightNum < minDeque[-1]:
                minDeque.pop()

            # put num in corresponding location in minDeque
            minDeque.append(rightNum)

            difference = maxDeque[0] - minDeque[0]

            # Shrink window if difference exceeds limit
            while limit < difference:

                # if current max is the left most num, remove from maxDeque from window
                if nums[left] == maxDeque[0]:
                    maxDeque.popleft()
                
                # if current min is the left most num, remove from minDeque from window
                if nums[left] == minDeque[0]:
                    minDeque.popleft()

                # Iterate window
                left += 1

                # grab new difference
                difference = maxDeque[0] - minDeque[0]

            # Update maximum window length
            maxLen = max(maxLen, right - left + 1)

            # Iterate window 
            right += 1

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

658. Find K Closest Elements ::2:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given a sorted integer array arr, two integers k and x, return the k closest integers to x in the array. The result should also be sorted in ascending order. An integer a is closer to x than an integer b if:

  • |a - x| lt |b - x|, or
  • |a - x| == |b - x| and a lt b
Example InputOutput
arr = [1,2,3,4,5], k = 4, x = 3[1,2,3,4]
arr = [1,1,2,3,4,5], k = 4, x = -1[1,1,2,3]

Constraints:

1 ≤ k ≤ arr.length

1 ≤ arr.length ≤ 10^4

arr is sorted in ascending order

-10^4 ≤ arr[i], c ≤ 10^4

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Simple Window Version - Sliding Window/Variable Size Window

    def findClosestElements(self, arr, k, x):
        
        n = len(arr) - 1

        # Sliding Window Boundaries
        # sc: O(1)
        left = 0
        right = n

        currWindowLen = right - left + 1

        # Shrink window from n down to k elements
        # At each step, remove the element farther from x
        while k < currWindowLen:

            # value difference from left and right nums to target x
            leftNum = arr[left]
            leftValue  = abs(leftNum - x)

            rightNum = arr[right]
            rightValue = abs(rightNum - x)

            # Between left and right, remove farther num from target
            # If equal, default to remove right
            # (keep left, problem prefers smaller)
            if leftValue <= rightValue:
                right -= 1
            else:
                left += 1

            # Check new window length
            currWindowLen = right - left + 1

        # left is now at the best location
        res = arr[left:(right + 1)]

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

Solution 2: [Sliding Window] Binary Search To Create Sliding Window - Sliding Window/Variable Size Window

    def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
                
        # Binary Search + Sliding Window Approach

        # Idea:
        # 1. The closest k elements must form a contiguous subarray of length k.
        # 2. Use binary search to find the left boundary of that window.
        # 3. Compare distances from x for potential windows, and shrink towards the closest.
        
        n = len(arr)
        
        # Binary search boundaries for left index of window
        left = 0
        right = n - k  # window of size k, so left can go at most n-k
        
        # Binary search over possible left boundaries
        # tc: O(log(n-k))
        while left < right:

            # grab mid
            mid = (left + right) // 2

            # Compare distances of window edges to x
            leftEdgeDist  = x - arr[mid]
            rightEdgeDist = arr[mid + k] - x

            # Left edge is farther from x than the right edge
            # Shift left boundary to get left edge closer
            # tc: O(1)
            if rightEdgeDist < leftEdgeDist:
                left = mid + 1

            # Right edge is farther or equal to from x than the left edge
            # Shift left boundary to get right edge closer
            # tc: O(1)
            else:
                right = mid

        # Left now points to optimal left boundary because:
        #   - Every position to the left was eliminated (left edge was too far)
        #   - Every position to the right was eliminated (right edge was too far)
        #   - left == right means only one candidate remains that was never eliminated
        #   - This is the position where the window [left, left+k] is closest to x
        # Splice window of size k
        # tc: O(k)
        res = arr[left:left + k]

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

1358. Number of Substrings Containing All Three Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s consisting only of characters a, b and c. Return the number of substrings containing at least one occurrence of all these characters a, b and c.

Example InputOutput
s = "abcabc"10
s = "aaacb"3
s = "abc"1

Constraints:

3 ≤ s.length ≤ 5 * 10^4

s only consists of a, b or c characters

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each char in current window
        #   - Window is valid when it contains at least one of each: a, b, c

        # Idea:
        #   - Expand right to add new characters
        #   - Once window is valid (has all 3 chars), shrink from left
        #     to find the smallest valid window ending at right
        #   - Every starting index from [0, left-1] also forms a valid substring
        #     ending at right, since adding more chars to the left keeps it valid
        #   - So add left to result at each right

        # Counting Valid Substrings:
        #   s = "abcabc", right = 2 (window = "abc")
        #
        #   s = [a, b, c, a, b, c]
        #        [.........]
        #        0         2
        #   Window "abc" is valid, shrink left until invalid:
        #   left=0 -> shrink -> left=1, window="bc" invalid
        #   left=1, add left=1 to res
        #   This counts 1 valid substring ending at right=2: ["abc"]
        #
        #   s = [a, b, c, a, b, c]
        #        [...............]
        #        0               5
        #   right=5: left shrinks to 3, add left=3 to res
        #   This counts 3 valid substrings ending at right=5:
        #   ["abcabc", "bcabc", "cabc"]

        n = len(s)

        # HashMap: char -> frequency in current window
        # sc: O(1) at most 3 chars
        count = defaultdict(int)

        # Sliding Window Boundaries
        # sc: O(1)
        left = 0

        # Total valid substrings found
        res = 0

        # tc: iterate over n O(n)
        for right in range(n):

            # Add incoming character to window
            # tc: O(1)
            count[s[right]] += 1

            # Shrink from left while window contains all 3 chars
            # Every position we shrink past becomes a valid starting index
            # tc: O(1) amortized
            while all(count[c] > 0 for c in "abc"):
                count[s[left]] -= 1
                left += 1

            # All starting indices [0, left-1] form valid substrings ending at right
            # since adding more chars to the left of the window keeps it valid
            # tc: O(1)
            res += left

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

Solution 2: [Last Seen Index] Track Last Seen Index For Each Char - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Last Seen Index Tracking (Single Pass)

        # Representation:
        #   - Track the last seen index of each char: a, b, c
        #   - last_a, last_b, last_c = most recent index of each char seen so far

        # Idea:
        #   - For each index i, the earliest we can start a valid substring
        #     ending at i is min(last_a, last_b, last_c)
        #   - Any starting index from [0, min(last_a, last_b, last_c)]
        #     forms a valid substring ending at i
        #   - So add min(last_a, last_b, last_c) + 1 to count at each i

        # Why min(last_a, last_b, last_c) + 1?
        #   s = "abcabc"
        #
        #   i=2 (c): last_a=0, last_b=1, last_c=2
        #   min = 0, count += 0 + 1 = 1
        #   valid substrings ending at i=2: ["abc"]
        #
        #   i=3 (a): last_a=3, last_b=1, last_c=2
        #   min = 1, count += 1 + 1 = 2
        #   valid substrings ending at i=3: ["abca", "bca"]
        #
        #   i=5 (c): last_a=3, last_b=4, last_c=5
        #   min = 3, count += 3 + 1 = 4
        #   valid substrings ending at i=5: ["abcabc", "bcabc", "cabc", "abc"]

        # Last seen index for each character
        # initialized to -1 meaning not yet seen
        # sc: O(1)
        last_a = last_b = last_c = -1

        # Total valid substrings found
        count = 0

        # tc: iterate over n O(n)
        for i in range(len(s)):

            # Update last seen index for current char
            # tc: O(1)
            if s[i] == 'a':
                last_a = i
            elif s[i] == 'b':
                last_b = i
            else:
                last_c = i

            # Earliest valid start = min of all last seen indices
            # All starting indices [0, min+1] form valid substrings ending at i
            # +1 because index is 0-based
            # tc: O(1)
            count += min(last_a, last_b, last_c) + 1

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

1004. Max Consecutive Ones III ::2:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer k, return the maximum number of consecutive 1's in the array if you can flip at most k 0's.

Example InputOutput
nums = [1,1,1,0,0,0,1,1,1,1,0], k = 26
nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], k = 310

Constraints:

1 ≤ nums.length ≤ 10^5

nums[i] is either 0 or 1

0 ≤ k ≤ nums.length

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window With Shrinking Max Tracking - Sliding Window/Variable Size Window

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

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - zeros tracks number of 0s in current window
        #   - Window is valid when zeros <= k

        # Idea:
        #   - Expand right to add new elements
        #   - If new element is 0, increment zeros
        #   - If zeros > k, shrink from left until valid again
        #   - Track longest valid window seen

        # Window Example:
        #   nums = [1, 1, 0, 0, 1, 1, 0, 1], k = 2
        #
        #   right=2: zeros=1, window=[1,1,0]
        #   [1, 1, 0, 0, 1, 1, 0, 1]
        #    L        R
        #   valid, maxLen=3
        #
        #   right=3: zeros=2, window=[1,1,0,0]
        #   [1, 1, 0, 0, 1, 1, 0, 1]
        #    L           R
        #   valid, maxLen=4
        #
        #   right=6: zeros=3 > k, shrink left
        #   [1, 1, 0, 0, 1, 1, 0, 1]
        #             L           R
        #   zeros=2, maxLen=6

        n = len(nums)

        # Sliding Window Boundaries
        # sc: O(1)
        left = 0

        # Number of zeros in current window
        numZeros = 0

        # Longest valid window seen
        maxLen = 0

        # tc: iterate over n O(n)
        for right in range(n):

            # Add num
            # tc: O(1)
            if nums[right] == 0:
                numZeros += 1

            # Shrink from left while window is invalid:
            #   - num of 0's must be <= k, else window is invalid
            #   - shift until number of 0's is below k
            # tc: O(1) amortized
            while k < numZeros:
                if nums[left] == 0:
                    numZeros -= 1
                left += 1

            # Check curr window
            # tc: O(1)
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

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

Solution 2: [Sliding Window] Two Pointer Sliding Window Without Shrinking - Sliding Window/Variable Size Window

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

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - zeroCount tracks number of 0s in current window
        #   - Window never shrinks — left only moves when zeroCount > k

        # Idea:
        #   - Expand right to add new elements
        #   - If new element is 0, increment zeroCount
        #   - If zeroCount > k, slide window forward by 1 (move left by 1)
        #     without shrinking — window size stays the same or grows
        #   - Final answer is len(nums) - left (window size at end)

        # Key Insight vs Solution 1:
        #   - S1 shrinks window back down when invalid, tracking maxLen explicitly
        #   - S2 never shrinks — once we find a window of size w,
        #     we only move forward when we find something bigger
        #   - left only advances when zeroCount > k, keeping window size stable
        #   - At the end, len(nums) - left gives the largest window we ever reached

        # Window Example:
        #   nums = [1, 1, 0, 0, 1, 1, 0, 1], k = 2
        #
        #   right=3 (0): zeroCount=2, window=[1,1,0,0], left=0
        #   [1, 1, 0, 0, 1, 1, 0, 1]
        #    L           R
        #   valid, window size=4
        #
        #   right=6 (0): zeroCount=3 > k
        #   [1, 1, 0, 0, 1, 1, 0, 1]
        #             L           R
        #   slide left by 1, nums[left=2]=0 so zeroCount=2
        #   window size stays at 6, never shrinks
        #
        #   final: len(nums) - left = 8 - 2 = 6

        n = len(nums)

        # Sliding Window Boundaries
        # sc: O(1)
        left = 0

        # Number of zeros in current window
        numZeros = 0

        # tc: iterate over n O(n)
        for right in range(n):

            # Add incoming element to window
            # tc: O(1)
            if nums[right] == 0:
                numZeros += 1

            # Shrink from left while window is invalid:
            #   - num of 0's must be <= k, else window is invalid
            #   - shift until number of 0's is below k
            # tc: O(1) amortized
            if k < numZeros:
                if nums[left] == 0:
                    numZeros -= 1
                left += 1

        # Window never shrank, so largest window size = len(nums) - left
        res = n - left

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

1248. Count Number of Nice Subarrays ::3:: - Medium

Topics: Array, Hash Table, Math, Sliding Window, Prefix Sum

Intro

Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it. Return the number of nice sub-arrays.

Example InputOutput
nums = [1,1,2,1,1], k = 32
nums = [2,4,6], k = 10

Constraints:

1 ≤ nums.length ≤ 50000

1 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ nums.length

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost() - Sliding Window/Variable Size Window

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

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - oddCount tracks number of odd numbers in current window

        # Idea:
        #   - Directly counting subarrays with exactly k odds is hard
        #     with two pointers because we can't know when to stop shrinking
        #   - Instead, count subarrays with at most k odds via atMost()
        #   - exactly(k) = atMost(k) - atMost(k - 1)

        # Why atMost works:
        #   - atMost(k)     counts subarrays with 0, 1, 2, ... k odds
        #   - atMost(k - 1) counts subarrays with 0, 1, 2, ... k-1 odds
        #   - subtracting leaves only subarrays with exactly k odds

        # Window Example:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   atMost(3): all windows with oddCount <= 3
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3, res += 4
        #    L           R   oddCount=4 > 3, shrink -> left=1, oddCount=3, res += 4
        #   atMost(3) = 14
        #
        #   atMost(2): all windows with oddCount <= 2
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3 > 2, shrink -> left=1, oddCount=2, res += 3
        #       L        R   oddCount=3 > 2, shrink -> left=2, oddCount=2, res += 3
        #   atMost(2) = 12
        #
        #   exactly(3) = atMost(3) - atMost(2) = 14 - 12 = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        def atMost(k: int) -> int:

            # Sliding Window Boundaries
            # sc: O(1)
            left = 0
            right = 0

            n = len(nums)

            # Sliding Window Data:
            # Number of odds in current window
            # sc: O(1)
            oddNums = 0

            # Total valid subarrays found
            res = 0

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

                # Add num, check if odd
                # tc: O(1)
                if nums[right] % 2 == 1:
                    oddNums += 1

                # Shrink from left until oddCount <= k
                # tc: O(1) amortized
                while k < oddNums:
                    
                    # Remove left, check if odd
                    if nums[left] % 2 == 1:
                        oddNums -= 1

                    # Iterate window
                    left += 1

                # All subarrays within curr subarrays are also valid
                # tc: O(1)
                res += right - left + 1

                # Iterate window
                right += 1

            # tc: O(n)
            # sc: O(n)
            return res

        # exactly(k) = atMost(k) - atMost(k - 1)
        # overall: tc O(n)
        # overall: sc O(1)
        return atMost(k) - atMost(k - 1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [Sliding Window] Two Pointer Single Pass With Start Gap - Sliding Window/Variable Size Window

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

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [lp, rp]
        #   - counter tracks number of odd numbers in current window
        #   - start_gap tracks number of valid left boundaries for current window

        # Idea:
        #   - Expand right to add new elements
        #   - Once counter == k, shrink from left counting how many positions
        #     keep counter == k (these are all valid left boundaries)
        #   - start_gap persists across iterations — once we find valid left
        #     boundaries for a window of k odds, any future right expansion
        #     that doesn't add an odd number still has the same valid left boundaries
        #   - Add start_gap to total at each right

        # Why start_gap persists:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   rp=3 (odd): counter=3 == k, start_gap=0
        #   [1, 1, 2, 1, 1]
        #    L           R
        #   shrink: lp=0 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #       L        R
        #   counter != k, stop. total += 1
        #
        #   rp=4 (odd): counter=3 == k, start_gap=0
        #   [1, 1, 2, 1, 1]
        #       L           R
        #   shrink: lp=1 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #          L        R
        #   counter != k, stop. total += 1
        #
        #   total = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Single pass — no need to call atMost twice
        # sc: O(1)
        lp = 0

        # Number of odd numbers in current window
        counter = 0

        # Number of valid left boundaries for current window
        start_gap = 0

        # Total valid subarrays found
        total = 0

        # tc: iterate over n O(n)
        for rp in range(len(nums)):

            # Add incoming element to window
            # tc: O(1)
            if nums[rp] % 2 != 0:
                counter += 1

            # Once counter == k, count valid left boundaries
            # Reset start_gap since new odd resets the boundary count
            # tc: O(1) amortized
            if counter == k:
                start_gap = 0

                # Shrink from left while window stays valid (counter == k)
                # Each shrink that keeps counter == k is another valid left boundary
                while counter == k:
                    start_gap += 1
                    if nums[lp] % 2 != 0:
                        counter -= 1
                    lp += 1

            # start_gap persists: even if right adds an even number,
            # same left boundaries remain valid from previous valid window
            # tc: O(1)
            total += start_gap

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

Solution 3: [Sliding Window] Two Pointer Deque - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
        
        total = 0

        count_ones = 0
        ones_indicies = deque()
        threshold = -1

        for j in range(len(nums)):
            if nums[j] % 2 == 1:
                ones_indicies.append(j)

                if count_ones == k:
                    threshold = ones_indicies.popleft()
                    count_ones -= 1

                count_ones += 1

            if count_ones == k:
                total += ones_indicies[0] - threshold

        return total
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

862. Shortest Subarray with Sum at Least K ::1:: - Hard

Topics: Array, Binary Search, Queue, Sliding Window, Heap (Priority Queue), Prefix Sum, Monotonic Queue

Intro

Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1], k = 11
nums = [1,2], k = 4-1
nums = [2,-1,2], k = 33

Constraints:

1 ≤ nums.length ≤ 10^5

-10^5 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ 10^9

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Prefix Sum With Monotonic Deque - Sliding Window/Variable Size Window

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

        # Prefix Sum + Monotonic Increasing Deque

        # Representation:
        #   - P[i] = prefix sum from nums[0] to nums[i-1]
        #   - Subarray sum [i, j] = P[j+1] - P[i]
        #   - Deque stores indices of monotonic increasing prefix sums

        # Idea:
        #   - We want shortest subarray with sum >= k
        #   - Equivalent to finding shortest j - i where P[j] - P[i] >= k
        #   - Maintain monotonic increasing deque of prefix sum indices
        #   - For each j, pop from front when P[j] - P[dq[0]] >= k
        #     (valid subarray found, pop to find even shorter one)
        #   - Pop from back when P[j] <= P[dq[-1]]
        #     (current prefix is smaller, back element can never be a useful left boundary)

        # Why not standard sliding window?
        #   - nums can contain negative numbers
        #   - Expanding window doesn't guarantee increasing sum
        #   - Standard two pointer breaks down with negatives
        #   - Prefix sum + monotonic deque handles negatives correctly

        # Deque Representation:
        #   - Monotonic Increasing order means:
        #     Low -> High
        #
        #   left (low)               right (high)
        #   [ P[2], P[4], P[5], P[7] ]  prefix sums always increasing
        #   [ idx=2, idx=4, idx=5, idx=7 ]  indices always increasing

        # Window Example:
        #   nums = [2, -1, 2], k = 3
        #   P    = [0,  2,  1, 3]
        #
        #   i=0: dq=[], append 0        -> dq=[0]
        #   i=1: P[1]=2, P[1]-P[0]=2 < 3, no pop front
        #        append 1               -> dq=[0,1]
        #   i=2: P[2]=1 <= P[1]=2, pop back
        #        append 2               -> dq=[0,2]
        #   i=3: P[3]=3, P[3]-P[0]=3 >= k, res=3-0=3, pop front
        #        P[3]-P[2]=2 < k, stop  -> dq=[2]
        #        append 3               -> dq=[2,3]
        #   res = 3  (full array [2,-1,2] sums to 3)

        n = len(nums)

        # Build prefix sum array
        # P[i] = sum of nums[0..i-1]
        # sc: O(n)
        P = [0] * (n + 1)

        # tc: O(n)
        for i in range(n):
            P[i + 1] = P[i] + nums[i]

        # Monotonic Increasing Deque storing prefix sum indices
        # sc: O(n)
        dq = deque()

        # Shortest valid subarray length found
        res = n + 1

        # tc: iterate over n+1 prefix sums O(n)
        for i in range(n + 1):

            # Pop from front while valid subarray found
            # P[i] - P[dq[0]] >= k means subarray [dq[0], i] sums to >= k
            # Pop to find even shorter valid subarray
            # tc: O(1) amortized
            while dq and P[i] - P[dq[0]] >= k:
                res = min(res, i - dq.popleft())

            # Maintain monotonic increasing order from back
            # If current prefix <= back of deque, back can never be a useful left boundary
            # since current index is further right and has smaller/equal prefix sum
            # tc: O(1) amortized
            while dq and P[i] <= P[dq[-1]]:
                dq.pop()

            dq.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return res if res <= n else -1
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

713. Subarray Product Less Than K ::1:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given an array of integers nums and an integer k, return the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than k.

Example InputOutput
nums = [10,5,2,6], k = 1008
nums = [1,2,3], k = 00

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

1 ≤ nums[i] ≤ 1000

0 ≤ k ≤ 10^6

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Running Product Sliding Window - Sliding Window/Variable Size Window

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


        # Early Exit:
        if k <= 1:
            return 0

        # Sliding window boundaries
        left = 0

        # Running product of current window
        product = 1

        # Total valid subarrays
        maximus = 0

        # Expand right boundary
        # tc: O(n)
        for right in range(len(nums)):

            # Include current element into window
            product *= nums[right]

            # Shrink window until valid
            # product < k
            #
            # tc: O(1) amortized
            while product >= k:

                # Remove left element from product
                product //= nums[left]

                # Shrink window
                left += 1

            # Add all valid subarrays
            # ending at right
            maximus += right - left + 1

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

2962. Count Subarrays Where Max Element Appears at Least K Times ::1:: - Medium

Topics: Array, Sliding Window

Intro

You are given an integer array nums and a positive integer k. Return the number of subarrays where the maximum element of nums appears at least k times in that subarray. A subarray is a contiguous sequence of elements within an array.

Example InputOutput
nums = [1,3,2,3,3], k = 26
nums = [1,4,2,1], k = 30

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^6

1 ≤ k ≤ 10^5

Abstraction

wow! again

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Sliding Window] Sliding Window - Sliding Window/Variable Size Window

    def countSubarrays(self, nums: List[int], k: int) -> int:
        
        # Sliding Window / Variable Size Window

        # Idea:
        #   - Find global maximum element
        #   - Maintain sliding window
        #   - Track frequency of maximum element inside window
        #   - Once max frequency >= k:
        #       every subarray starting from left
        #       and ending at/right of current right is valid

        # Key Observation:
        #
        #   If current window already contains k occurrences of max_num,
        #   then extending further right keeps condition valid
        #
        #       all subarrays:
        #           [left ... right]
        #           [left ... right+1]
        #           ...
        #           [left ... n-1]
        #
        #   Therefore all subarrays from right -> n-1 are valid
        #   Count added: n - right

        n = len(nums)

        # Global maximum element
        # tc: O(n)
        maxNum = max(nums)

        # Left boundary
        left = 0

        # Frequency of maxNum inside window
        maxCount = 0

        # Total valid subarrays
        res = 0

        # Expand right boundary
        # tc: O(n)
        for right in range(n):

            # Include nums[right]
            if nums[right] == maxNum:
                maxCount += 1

            # Shrink while valid
            #
            # Every window starting at left
            # and ending at/right of right is valid
            #
            # tc: O(1) amortized
            while maxCount >= k:

                res += n - right

                # Remove nums[left]
                if nums[left] == maxNum:
                    maxCount -= 1

                left += 1

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