CrackedRuby CrackedRuby

Overview

Sliding window provides an efficient approach for solving problems that involve sequential data where answers depend on contiguous subarrays or substrings. The technique reduces time complexity by avoiding redundant computations as the window moves through the data structure.

The core concept involves maintaining two pointers that define a range of elements. As these pointers move, they create a "window" that slides across the data. Operations performed on elements entering or leaving the window enable incremental computation of aggregate values without recalculating from scratch.

Problems suited for sliding window share common characteristics: they involve arrays or strings, require processing contiguous elements, and ask for optimization (maximum, minimum, or specific conditions) over all possible windows. Examples include finding the longest substring without repeating characters, maximum sum subarray of fixed size, or smallest subarray with sum greater than a target value.

The technique appears in string processing, time series analysis, network packet processing, and anywhere sequential data requires efficient range-based computation.

# Basic fixed-size window: maximum sum of k consecutive elements
def max_sum_fixed_window(arr, k)
  return nil if arr.length < k
  
  # Calculate initial window sum
  window_sum = arr[0...k].sum
  max_sum = window_sum
  
  # Slide window: remove leftmost, add new rightmost
  (k...arr.length).each do |i|
    window_sum = window_sum - arr[i - k] + arr[i]
    max_sum = [max_sum, window_sum].max
  end
  
  max_sum
end

max_sum_fixed_window([2, 1, 5, 1, 3, 2], 3)
# => 9 (subarray [5, 1, 3])

Key Principles

The sliding window technique operates on the principle of incremental computation. Instead of recalculating values for each possible subarray, the algorithm maintains state as the window moves. When the window shifts by one position, only two operations occur: remove the element leaving the window and add the element entering the window.

Window Boundaries

Two indices define the window boundaries. The left pointer marks the start, the right pointer marks the end. The window size equals right - left + 1 for inclusive boundaries or right - left for exclusive right boundaries. The choice between inclusive and exclusive boundaries affects loop conditions and index calculations.

State Maintenance

The window maintains aggregate state that updates incrementally. For sum-based problems, the state holds the current sum. For substring problems, the state might track character frequencies using a hash. For maximum/minimum problems, the state could use a deque to maintain elements in sorted order.

Window Movement Patterns

Fixed-size windows move both pointers together, maintaining constant size. The right pointer advances, a new element enters, the left pointer advances, an old element exits. Variable-size windows expand and contract based on conditions. The right pointer extends the window when conditions allow, the left pointer contracts when constraints violated.

Termination Conditions

Fixed-size windows terminate when the right pointer reaches the end. Variable-size windows terminate when no valid expansion remains possible. Some problems require processing after the main loop to handle edge cases where the optimal window exists at the end.

Time Complexity Foundation

Naive approaches checking all subarrays run in O(n²) or O(n³) time. Sliding window reduces this to O(n) because each element enters and exits the window exactly once. The work per element remains constant, leading to linear time complexity.

# Variable-size window: longest substring without repeating characters
def longest_unique_substring(s)
  char_index = {}
  left = 0
  max_length = 0
  
  s.each_char.with_index do |char, right|
    # If character seen and within current window
    if char_index[char] && char_index[char] >= left
      left = char_index[char] + 1
    end
    
    char_index[char] = right
    max_length = [max_length, right - left + 1].max
  end
  
  max_length
end

longest_unique_substring("abcabcbb")
# => 3 (substring "abc")

Ruby Implementation

Ruby's enumerable methods and hash capabilities make sliding window implementations concise. The language provides multiple approaches depending on problem requirements and readability preferences.

Fixed Window with Array Slicing

Array slicing creates readable fixed-window code but generates intermediate arrays. This approach works well for small windows or when clarity outweighs performance.

def average_of_subarrays(arr, k)
  return [] if arr.length < k
  
  (0..arr.length - k).map do |i|
    arr[i, k].sum / k.to_f
  end
end

average_of_subarrays([1, 3, 2, 6, -1, 4, 1, 8, 2], 5)
# => [2.2, 2.8, 2.4, 3.6, 2.8]

Fixed Window with Manual State

Manual state management avoids intermediate arrays and provides better performance. The pattern maintains window state and updates incrementally.

def max_sum_subarray(arr, k)
  return nil if arr.empty? || k > arr.length || k <= 0
  
  current_sum = arr[0...k].sum
  max_sum = current_sum
  
  (k...arr.length).each do |i|
    current_sum += arr[i] - arr[i - k]
    max_sum = current_sum if current_sum > max_sum
  end
  
  max_sum
end

Variable Window with Hash State

Variable windows often require hash maps to track element frequencies, positions, or counts within the window.

def min_window_substring(s, t)
  return "" if s.empty? || t.empty?
  
  target_freq = t.each_char.tally
  window_freq = Hash.new(0)
  
  left = 0
  min_len = Float::INFINITY
  min_start = 0
  matched = 0
  
  s.each_char.with_index do |char, right|
    window_freq[char] += 1
    matched += 1 if target_freq[char] && window_freq[char] == target_freq[char]
    
    while matched == target_freq.size
      if right - left + 1 < min_len
        min_len = right - left + 1
        min_start = left
      end
      
      left_char = s[left]
      window_freq[left_char] -= 1
      matched -= 1 if target_freq[left_char] && window_freq[left_char] < target_freq[left_char]
      left += 1
    end
  end
  
  min_len == Float::INFINITY ? "" : s[min_start, min_len]
end

min_window_substring("ADOBECODEBANC", "ABC")
# => "BANC"

Deque for Maximum/Minimum in Window

When the window must track maximum or minimum values, a deque maintains elements in sorted order. Ruby lacks a built-in deque, but arrays provide the necessary operations.

def max_sliding_window(arr, k)
  return [] if arr.empty? || k <= 0
  
  result = []
  deque = [] # Stores indices
  
  arr.each_with_index do |num, i|
    # Remove elements outside window
    deque.shift while !deque.empty? && deque.first < i - k + 1
    
    # Remove elements smaller than current (they can't be max)
    deque.pop while !deque.empty? && arr[deque.last] < num
    
    deque.push(i)
    result.push(arr[deque.first]) if i >= k - 1
  end
  
  result
end

max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3)
# => [3, 3, 5, 5, 6, 7]

Using Enumerator for Complex Iterations

Enumerators enable custom iteration patterns for complex sliding window scenarios.

def each_window(arr, size)
  return enum_for(:each_window, arr, size) unless block_given?
  
  return if arr.length < size
  
  (0..arr.length - size).each do |i|
    yield arr[i, size], i
  end
end

# Usage
sums = []
each_window([1, 2, 3, 4, 5], 3) do |window, index|
  sums << window.sum
end
# sums => [6, 9, 12]

Common Patterns

Several sliding window patterns recur across different problem types. Recognition of these patterns accelerates problem-solving and implementation.

Fixed-Size Window with Aggregate

This pattern computes an aggregate value (sum, average, product) over all windows of size k. The window moves one element at a time, updating the aggregate incrementally.

def max_average_subarray(nums, k)
  window_sum = nums[0...k].sum
  max_sum = window_sum
  
  (k...nums.length).each do |i|
    window_sum += nums[i] - nums[i - k]
    max_sum = [max_sum, window_sum].max
  end
  
  max_sum / k.to_f
end

Expanding Window with Constraint

The window expands by moving the right pointer until a constraint breaks. When violated, the left pointer advances to restore validity. This pattern handles problems like "longest substring with at most k distinct characters."

def length_of_longest_substring_k_distinct(s, k)
  return 0 if s.empty? || k == 0
  
  char_count = Hash.new(0)
  left = 0
  max_length = 0
  
  s.each_char.with_index do |char, right|
    char_count[char] += 1
    
    while char_count.size > k
      left_char = s[left]
      char_count[left_char] -= 1
      char_count.delete(left_char) if char_count[left_char] == 0
      left += 1
    end
    
    max_length = [max_length, right - left + 1].max
  end
  
  max_length
end

length_of_longest_substring_k_distinct("eceba", 2)
# => 3 ("ece")

Shrinking Window with Target

The window expands until meeting a target, then shrinks to find the minimum size that still satisfies the target. This handles "smallest subarray with sum >= target."

def min_subarray_sum(target, nums)
  return 0 if nums.empty?
  
  left = 0
  current_sum = 0
  min_length = Float::INFINITY
  
  nums.each_with_index do |num, right|
    current_sum += num
    
    while current_sum >= target
      min_length = [min_length, right - left + 1].min
      current_sum -= nums[left]
      left += 1
    end
  end
  
  min_length == Float::INFINITY ? 0 : min_length
end

min_subarray_sum(7, [2, 3, 1, 2, 4, 3])
# => 2 (subarray [4, 3])

Two-Pointer with Frequency Map

Maintains character or element frequencies within the window using a hash. Common for anagram, permutation, or pattern-matching problems.

def find_anagrams(s, p)
  result = []
  return result if s.length < p.length
  
  p_count = p.each_char.tally
  window_count = Hash.new(0)
  
  # Initialize first window
  s[0...p.length].each_char { |c| window_count[c] += 1 }
  result << 0 if window_count == p_count
  
  # Slide window
  (p.length...s.length).each do |i|
    # Add new character
    window_count[s[i]] += 1
    
    # Remove old character
    left_char = s[i - p.length]
    window_count[left_char] -= 1
    window_count.delete(left_char) if window_count[left_char] == 0
    
    result << i - p.length + 1 if window_count == p_count
  end
  
  result
end

find_anagrams("cbaebabacd", "abc")
# => [0, 6] (anagrams at indices 0 and 6)

Maximum/Minimum in Sliding Window

Maintains the maximum or minimum element as the window slides. Uses a deque to track potential candidates in sorted order.

def min_sliding_window(nums, k)
  result = []
  deque = []
  
  nums.each_with_index do |num, i|
    deque.shift if !deque.empty? && deque.first < i - k + 1
    deque.pop while !deque.empty? && nums[deque.last] > num
    deque.push(i)
    result.push(nums[deque.first]) if i >= k - 1
  end
  
  result
end

Distinct Count Window

Tracks the number of distinct elements in the window, often requiring a hash to maintain counts.

def count_distinct_windows(arr, k)
  return [] if arr.length < k
  
  freq = Hash.new(0)
  result = []
  
  # First window
  arr[0...k].each { |num| freq[num] += 1 }
  result << freq.size
  
  # Slide window
  (k...arr.length).each do |i|
    # Add new element
    freq[arr[i]] += 1
    
    # Remove old element
    old = arr[i - k]
    freq[old] -= 1
    freq.delete(old) if freq[old] == 0
    
    result << freq.size
  end
  
  result
end

count_distinct_windows([1, 2, 1, 3, 4, 2, 3], 4)
# => [3, 4, 4, 3]

Practical Examples

Problem: Maximum Points from Cards

Given an array of card values, select exactly k cards from either end to maximize points. This requires a sliding window approach where the window represents cards NOT selected.

def max_score(card_points, k)
  n = card_points.length
  window_size = n - k
  
  # If selecting all cards
  return card_points.sum if window_size == 0
  
  total = card_points.sum
  window_sum = card_points[0...window_size].sum
  min_window = window_sum
  
  # Find minimum window (cards not selected)
  (window_size...n).each do |i|
    window_sum += card_points[i] - card_points[i - window_size]
    min_window = [min_window, window_sum].min
  end
  
  total - min_window
end

max_score([1, 2, 3, 4, 5, 6, 1], 3)
# => 12 (take [6, 1] from right and [1] from left)

Problem: Longest Repeating Character Replacement

Find the longest substring where at most k characters can be replaced to make all characters the same.

def character_replacement(s, k)
  char_count = Hash.new(0)
  left = 0
  max_count = 0
  max_length = 0
  
  s.each_char.with_index do |char, right|
    char_count[char] += 1
    max_count = [max_count, char_count[char]].max
    
    # Window size - most frequent character count = replacements needed
    while right - left + 1 - max_count > k
      char_count[s[left]] -= 1
      left += 1
    end
    
    max_length = [max_length, right - left + 1].max
  end
  
  max_length
end

character_replacement("AABABBA", 1)
# => 4 (replace one B in "AABA" to get "AAAA")

Problem: Fruits into Baskets

Collect maximum fruits from trees where each basket holds one fruit type, and baskets hold only two types total. This reduces to longest subarray with at most two distinct elements.

def total_fruit(fruits)
  basket = Hash.new(0)
  left = 0
  max_fruits = 0
  
  fruits.each_with_index do |fruit, right|
    basket[fruit] += 1
    
    while basket.size > 2
      basket[fruits[left]] -= 1
      basket.delete(fruits[left]) if basket[fruits[left]] == 0
      left += 1
    end
    
    max_fruits = [max_fruits, right - left + 1].max
  end
  
  max_fruits
end

total_fruit([1, 2, 1, 2, 3, 1, 1])
# => 5 (collect [2, 3, 1, 1, 1] or [1, 1, 2, 1, 2])

Problem: Permutation in String

Check if one string contains a permutation of another string as a substring.

def check_inclusion(s1, s2)
  return false if s1.length > s2.length
  
  s1_count = s1.each_char.tally
  window_count = Hash.new(0)
  
  # Initialize window
  s2[0...s1.length].each_char { |c| window_count[c] += 1 }
  return true if window_count == s1_count
  
  # Slide window
  (s1.length...s2.length).each do |i|
    window_count[s2[i]] += 1
    
    left_char = s2[i - s1.length]
    window_count[left_char] -= 1
    window_count.delete(left_char) if window_count[left_char] == 0
    
    return true if window_count == s1_count
  end
  
  false
end

check_inclusion("ab", "eidbaooo")
# => true (contains "ba")

Problem: Minimum Size Subarray Sum

Find the minimal length of a contiguous subarray where sum >= target.

def min_sub_array_len(target, nums)
  left = 0
  sum = 0
  min_len = Float::INFINITY
  
  nums.each_with_index do |num, right|
    sum += num
    
    while sum >= target
      min_len = [min_len, right - left + 1].min
      sum -= nums[left]
      left += 1
    end
  end
  
  min_len == Float::INFINITY ? 0 : min_len
end

min_sub_array_len(7, [2, 3, 1, 2, 4, 3])
# => 2 ([4, 3])

Performance Considerations

Sliding window transforms problems from O(n²) or worse to O(n) time complexity by eliminating redundant computation. Understanding when this optimization applies and its implementation costs guides effective usage.

Time Complexity Analysis

Fixed-size window algorithms run in O(n) time where n is the array length. Each element enters and exits the window exactly once. Operations per element remain constant: hash lookup, arithmetic update, comparison. Variable-size windows also achieve O(n) despite the nested while loop. The left pointer moves at most n times across the entire algorithm, not per iteration.

The deque-based maximum/minimum window runs in O(n) amortized time. Each element enters and exits the deque at most once. Individual operations might take O(k) where k is the deque size, but total operations across all elements remain O(n).

Space Complexity Patterns

Fixed-size windows with simple aggregates (sum, count) use O(1) space. The state consists of a few variables regardless of input size. Windows tracking frequencies or unique elements use O(k) space where k is the window size or number of distinct elements. Hash maps grow with distinct elements in the window.

Deque-based approaches use O(k) space in the worst case where k is the window size. The deque size never exceeds the window size because elements outside the window get removed.

Ruby-Specific Performance

Array slicing creates new array objects. For performance-critical code processing large arrays, avoid slicing inside loops. Manual index tracking with state variables outperforms slicing by eliminating object allocation.

# Slower: creates k new arrays
def slow_window_sum(arr, k)
  (0..arr.length - k).map { |i| arr[i, k].sum }
end

# Faster: maintains single sum variable
def fast_window_sum(arr, k)
  result = []
  sum = arr[0...k].sum
  result << sum
  
  (k...arr.length).each do |i|
    sum += arr[i] - arr[i - k]
    result << sum
  end
  
  result
end

Hash operations in Ruby run in O(1) average time. However, hash creation and manipulation add constant overhead. For windows tracking only presence/absence, consider alternative data structures. For frequency counting, hashes remain the idiomatic choice.

Optimization Techniques

Early Termination: Some problems allow stopping before processing all elements. If finding any valid window suffices, return immediately upon discovery.

def contains_duplicate_within_k(nums, k)
  seen = {}
  
  nums.each_with_index do |num, i|
    return true if seen[num] && i - seen[num] <= k
    seen[num] = i
  end
  
  false
end

Batch Processing: When the window operation is expensive, batch multiple operations. Instead of checking conditions on every iteration, check periodically or only when state changes significantly.

State Representation: Choose state representation based on query requirements. Tracking sum requires one integer. Tracking maximum requires a deque. Tracking median requires two heaps. Match state complexity to problem needs.

Memory Locality: Fixed-size windows with sequential access exhibit good cache performance. Variable-size windows with hash lookups may incur cache misses. For performance-critical applications processing large datasets, consider cache effects.

Benchmarking Example

require 'benchmark'

arr = (1..100_000).to_a
k = 1000

Benchmark.bm do |x|
  x.report("slicing:") do
    (0..arr.length - k).each { |i| arr[i, k].sum }
  end
  
  x.report("sliding:") do
    sum = arr[0...k].sum
    (k...arr.length).each { |i| sum += arr[i] - arr[i - k] }
  end
end

# Results show sliding window significantly faster

Common Pitfalls

Off-by-One Errors in Window Bounds

Incorrect boundary calculations cause elements to be processed multiple times or skipped. The window size calculation right - left + 1 assumes both indices are inclusive. Exclusive right boundaries use right - left.

# Incorrect: double-counts elements
def wrong_window(arr, k)
  left = 0
  arr.each_with_index do |_, right|
    window_size = right - left  # Wrong for inclusive bounds
    left += 1 if window_size > k
  end
end

# Correct
def correct_window(arr, k)
  left = 0
  arr.each_with_index do |_, right|
    window_size = right - left + 1  # Correct for inclusive bounds
    left += 1 if window_size > k
  end
end

Forgetting to Update State on Window Shrink

When the left pointer moves, the element exiting the window must update the state. Forgetting this creates incorrect results.

# Incorrect: doesn't remove exiting element from hash
def wrong_distinct_count(arr, k)
  freq = Hash.new(0)
  
  arr.each_with_index do |num, right|
    freq[num] += 1
    
    if right >= k
      # Missing: freq[arr[right - k]] -= 1
    end
  end
end

# Correct
def correct_distinct_count(arr, k)
  freq = Hash.new(0)
  
  arr.each_with_index do |num, right|
    freq[num] += 1
    
    if right >= k
      old = arr[right - k]
      freq[old] -= 1
      freq.delete(old) if freq[old] == 0
    end
  end
end

Not Handling Empty Hash Values

Ruby hashes return nil for missing keys unless a default value is set. When decrementing counts, removing zero-count entries prevents the hash from growing unbounded.

# Problematic: hash grows with zero values
def bad_frequency_tracking(s, k)
  freq = {}
  
  s.each_char.with_index do |c, i|
    freq[c] = (freq[c] || 0) + 1
    
    if i >= k
      old = s[i - k]
      freq[old] -= 1  # Leaves zero values
    end
  end
end

# Better: removes zero counts
def good_frequency_tracking(s, k)
  freq = Hash.new(0)
  
  s.each_char.with_index do |c, i|
    freq[c] += 1
    
    if i >= k
      old = s[i - k]
      freq[old] -= 1
      freq.delete(old) if freq[old] == 0
    end
  end
end

Incorrect Maximum Count Tracking

When tracking the most frequent element in a variable window, the maximum count may not decrease when the window shrinks. This optimization requires careful handling.

# Bug: max_count never decreases
def buggy_replacement(s, k)
  freq = Hash.new(0)
  left = 0
  max_count = 0
  
  s.each_char.with_index do |c, right|
    freq[c] += 1
    max_count = [max_count, freq[c]].max
    
    # max_count doesn't update when window shrinks
    while right - left + 1 - max_count > k
      freq[s[left]] -= 1
      left += 1
    end
  end
end

# Correct approach: recalculate or use different strategy
def correct_replacement(s, k)
  freq = Hash.new(0)
  left = 0
  result = 0
  
  s.each_char.with_index do |c, right|
    freq[c] += 1
    
    while right - left + 1 - freq.values.max > k
      freq[s[left]] -= 1
      left += 1
    end
    
    result = [result, right - left + 1].max
  end
  
  result
end

Initial Window Setup Errors

Setting up the first window before entering the main loop requires careful index handling. Missing or incorrect initialization causes wrong results for the first window.

# Wrong: doesn't process first k elements correctly
def wrong_init(arr, k)
  sum = 0
  
  arr.each_with_index do |num, i|
    sum += num
    sum -= arr[i - k] if i >= k  # First window incomplete
  end
end

# Correct: initializes first window separately
def correct_init(arr, k)
  sum = arr[0...k].sum
  max_sum = sum
  
  (k...arr.length).each do |i|
    sum += arr[i] - arr[i - k]
    max_sum = [max_sum, sum].max
  end
  
  max_sum
end

Comparing Hashes for Equality

Ruby hash equality requires both keys and values to match. When comparing frequency maps, zero-count entries must be removed or the comparison fails.

# Fails when hashes have different zero-count keys
target = { 'a' => 2, 'b' => 1 }
window = { 'a' => 2, 'b' => 1, 'c' => 0 }

target == window  # => false, even though counts match for target keys

# Solution: remove zero counts
window.delete('c')
target == window  # => true

Infinite Loops in While Conditions

Variable windows use while loops to shrink. If the shrinking condition never becomes false, the loop runs infinitely. Verify that the left pointer advancement always makes progress toward satisfying the loop exit condition.

# Potential infinite loop if condition poorly designed
def risky_window(arr, k)
  left = 0
  
  arr.each_with_index do |num, right|
    while some_condition  # Verify this eventually becomes false
      left += 1
    end
  end
end

Reference

Sliding Window Types

Type Description Window Size Use Case
Fixed Both pointers move together Constant k Maximum sum of k elements, average of subarrays
Expanding Right grows, left conditionally shrinks Variable Longest substring with k distinct characters
Shrinking Right grows, left shrinks to minimize Variable Smallest subarray with sum >= target
Two Distinct Two separate moving windows Multiple Merge intervals, multiple ranges

Common Problem Categories

Category Typical Pattern Example
Maximum/Minimum Sum Fixed window with sum tracking Max sum of k consecutive elements
Substring Search Hash map with character frequencies Anagrams, permutations in string
Longest/Shortest Subarray Variable window with constraint Longest substring without repeating characters
Distinct Elements Hash map counting unique values At most k distinct characters
Window Maximum/Minimum Deque maintaining sorted order Maximum in each window of size k

Time Complexity Summary

Operation Complexity Notes
Fixed window O(n) Each element processed once
Variable window O(n) Left pointer moves at most n times total
With hash operations O(n) Hash operations O(1) average
With deque O(n) amortized Each element enters/exits deque once
Naive approach O(n²) or O(n³) Sliding window optimization target

Space Complexity Patterns

Data Structure Space Use Case
Simple aggregate O(1) Sum, count, single value
Hash map O(k) Frequencies, k = distinct elements
Deque O(k) Max/min in window, k = window size
Multiple structures O(k) Complex state tracking

Ruby Methods for Sliding Window

Method Purpose Example
each_with_index Iterate with position tracking arr.each_with_index { }
sum Calculate sum of range arr[i...j].sum
tally Count element frequencies str.each_char.tally
Hash.new(0) Initialize frequency hash freq = Hash.new(0)
shift/push Deque operations with array deque.shift; deque.push(val)

Decision Matrix

Problem Asks For Window Type Key State Pattern
Fixed k elements Fixed Sum/count/product Two pointers move together
Longest satisfying constraint Expanding Hash/count Expand right, shrink left when invalid
Shortest satisfying target Shrinking Sum/count Expand until valid, shrink while valid
All windows of size k Fixed Aggregate value Process each window position
Maximum/minimum per window Fixed with deque Sorted candidates Maintain monotonic deque

Edge Cases Checklist

Scenario Consideration
Empty input Return appropriate default value
Window larger than array Handle or return special value
k = 0 or k < 0 Validate input constraints
Single element Verify window logic works
All elements identical Check distinct counting logic
Negative numbers Verify sum/comparison logic
Window at array boundaries Check index calculations
Zero counts in hash Remove to prevent comparison issues