Sliding Window#
Fix Sized Window#
Find maximum (or minimum) sum of a subarray of size k#
Given an array (list) nums
consisted of only non-negative integers, find the
largest sum among all subarrays of length k
in nums
.
For example, if the input is nums = [1, 2, 3, 7, 4, 1]
, k = 3
, then the
output would be 14
as the largest length 3
subarray sum is given by
[3, 7, 4]
which sums to 14
.
Solution 1 (Not Optimal)#
1def subarray_sum_fixed(nums: List[int], k: int) -> int:
2 if len(nums) < k:
3 return 0
4
5 maximum = float("-inf")
6 total_num = len(nums)
7
8 for index in range(total_num):
9 if index == total_num - k: # no need + 1 since start from 0
10 break
11 subarray = nums[index:index + k]
12 sum_ = sum(subarray)
13 if sum_ >= maximum:
14 maximum = sum_
15 return maximum
Test#
1nums = [1, 2, 3, 7, 4, 1]
2k = 3
3
4compare_test_cases(
5 actual_list = [subarray_sum_fixed(nums, k)],
6 expected_list = [14],
7 description_list = ["subarray_sum_fixed"],
8)
Time Complexity#
At first glance, it seems like the time complexity of the above algorithm will be \(\mathcal{O}(n)\) where \(n\) is the total number of elements in the input since we are iterating through each element of the input array up till \(n-k+1\).
But there is a hidden factor in the above algorithm: the call to sum
for each
subarray of size \(k\). Remember, the sum
function takes \(\mathcal{O}(k)\) time
for each subarray of size \(k\). Therefore, the overall time complexity of our
algorithm will become \(\mathcal{O}(n*k)\).
And let’s consider where \(k << n\), then the time complexity will still be \(\mathcal{O}(n)\) since \(k\) is small.
If \(k\) is large or approaching \(n\), then the time complexity will be \(\mathcal{O}(n^2)\)? Not really, since the maximum number of subarrays of size \(k\) in an array of size \(n\) is \(n-k+1\). Therefore, the time complexity of our algorithm will be \(\mathcal{O}((n-k+1)*k)\), which is asymptotically equivalent to \(\mathcal{O}(k)\) which is same as \(\mathcal{O}(n)\).
Consider the case where \(k=n\):
If \(k = n\), where \(n\) is the length of the array, then there would only be one subarray of length \(n\) (the entire array itself). Therefore, the algorithm would only need to compute the sum of this one subarray, which would result in a time complexity of \(\mathcal{O}(n)\), not \(\mathcal{O}(n^2)\).
However, in the general case where \(k < n\), the time complexity of the algorithm would be \(\mathcal{O}(n*k)\), because the algorithm needs to compute the sum of each possible subarray of length \(k\) in the array.
So while the worst-case time complexity of the algorithm is \(\mathcal{O}(n*k)\), if \(k = n\), the time complexity would actually be \(\mathcal{O}(n)\).
Furthermore, the above solution may not be faithful to the “sliding window” approach.
Space Complexity#
The space (auxiliary) complexity of the given subarray_sum_fixed
function is
\(\mathcal{O}(k)\), where \(k\) is the size of the subarray being considered.
Here’s a breakdown of the space usage in the function:
maximum
variable: This variable is used to track the maximum sum encountered so far. Its space requirement is constant, as it only stores a single integer value.total_num
variable: This variable stores the total number of elements in thenums
list. Its space requirement is constant, as it only stores a single integer value.subarray
variable: This variable is used to store a subarray of size k. In each iteration of the loop, a new subarray is created and stored in this variable. The space requirement for thesubarray
variable is \(\mathcal{O}(k)\) since it stores k elements.sum_
variable: This variable stores the sum of the elements in the subarray. Its space requirement is constant, as it only stores a single integer value.
Overall, the dominant factor affecting the space complexity is the size of the subarray, which is \(k\). Therefore, the space (auxiliary) complexity of the function is \(\mathcal{O}(k)\).
But this is a bit tricky since strictly speaking, the space complexity does not depend on the input size. It depends on the size of the subarray being considered, which is \(k\).
Solution 2 (Optimal)#
The approach described is called a “sliding window” approach because we’re essentially sliding a window of a fixed size (k) along the array, and calculating the sum of the elements within that window.
Here’s a step-by-step explanation:
We start by defining a window of size
k
at the leftmost part of the array. The sum of the elements in this window is calculated and stored in a variable calledwindow_sum
.The maximum sum found so far,
largest
, is initially set to thewindow_sum
.We then “slide” this window one position to the right. To calculate the new
window_sum
, we add the value of the new element on the right and subtract the value of the element that was previously on the leftmost position of the window.If the new
window_sum
is larger than thelargest
sum found so far, we updatelargest
to be the newwindow_sum
.Repeat steps 3 and 4 until we’ve slid the window to the end of the array. The
largest
sum at the end of this process is the largest sum ofk
consecutive elements in the array.
Here’s an example:
Consider an array [1, 3, 2, 6, -1, 4, 1, 8, 2] and k = 5
.
In the beginning, our window of size 5 includes the elements [1, 3, 2, 6, -1].
The window_sum
is 11 (1+3+2+6-1). So initially, largest = window_sum = 11
.
The window slides to the right to include the next element 4 and exclude the
leftmost element 1. Now, the window includes the elements [3, 2, 6, -1, 4]. The
new window_sum
is 14 (11 - 1 + 4), which is larger than largest
, so we
update largest
to 14.
We continue this process until we reach the end of the array. The final
largest
value will be the maximum sum of k
consecutive elements.
Here’s a diagram to illustrate this process:
Array: [1, 3, 2, 6, -1, 4, 1, 8, 2]
k = 5
Steps:
# Window position window_sum largest
{1, 3, 2, 6, -1}, 4, 1, 8, 2 11 11
1, {3, 2, 6, -1, 4}, 1, 8, 2 14 14
1, 3, {2, 6, -1, 4, 1}, 8, 2 12 14
1, 3, 2, {6, -1, 4, 1, 8}, 2 18 18
1, 3, 2, 6, {-1, 4, 1, 8, 2} 14 18
At the end, the maximum sum of 5 consecutive elements in the array is 18.
It is worth noting that that the total number of subarrays of size \(k\) in an array of size \(n\) is \(n-k+1\). Why? Because the first subarray of size \(k\) will start at index 0 and end at index \(k-1\). The second subarray of size \(k\) will start at index 1 and end at index \(k\). The third subarray of size \(k\) will start at index 2 and end at index \(k+1\). And so on. The last subarray of size \(k\) will start at index \(n-k\) and end at index \(n-1\).
Therefore, the total number of subarrays of size \(k\) in an array of size \(n\) is \(n - k + 1\). This is because for each starting index of the subarray, we “shift” it one position to the right, until we reach the point where the end of the subarray is at the last index of the array.
Here’s a visualization for better understanding:
Consider an array of size \(n = 7\) and we want subarrays of size \(k = 3\).
Array: [a, b, c, d, e, f, g]
Subarrays of size 3:
[a, b, c], d, e, f, g (starts at index 0, ends at index 2)
a, [b, c, d], e, f, g (starts at index 1, ends at index 3)
a, b, [c, d, e], f, g (starts at index 2, ends at index 4)
a, b, c, [d, e, f], g (starts at index 3, ends at index 5)
a, b, c, d, [e, f, g] (starts at index 4, ends at index 6)
As you can see, the last subarray starts at index \(n - k = 7 - 3 = 4\) and ends at index \(n - 1 = 6\).
So, we have 5 subarrays of size 3 in an array of size 7, which matches with the formula \(n - k + 1 = 7 - 3 + 1 = 5\).
Implementation#
1from typing import List
2
3def subarray_sum_fixed(nums, k):
4 window_sum = 0
5 for i in range(k):
6 window_sum += nums[i]
7 largest = window_sum
8 for right in range(k, len(nums)):
9 left = right - k
10 window_sum -= nums[left]
11 window_sum += nums[right]
12 largest = max(largest, window_sum)
13 return largest
In a mathematical text, the sliding window algorithm can be represented using summations and indexing of the input list. Here’s how we can explain it:
Consider a sequence of numbers \(a = [a_1, a_2, ..., a_n]\) where \(n\) is the total number of elements in the sequence, and a window size \(k\).
The sum of elements in the initial window (first \(k\) elements) is given by:
This sum \(S\) can also be considered as the maximum sum of subarray of length \(k\) found so far.
As we slide the window by one position to the right, the sum of the new window can be calculated by subtracting the first element of the previous window (\(a_i\)) and adding the first element not included in the previous window (\(a_{i+k}\)):
This operation is repeated until the window reaches the end of the list, i.e., \(i = n - k + 1\).
At each step, we update the maximum sum found so far:
This will eventually give us the maximum sum of any subarray of length \(k\) in the sequence \(a\). The key advantage of this approach is that each update operation is done in constant time, leading to a linear time complexity (\(O(n)\)) for the entire algorithm.
The Sliding Window Technique#
The sliding window technique, a specific application of the two pointers technique, is an algorithmic strategy used to solve a variety of problems that involve searching for subsequences in an array or list that satisfy a particular condition. The so-called “window” is defined by two pointers or indices into the array.
Generalized Sliding Window Technique#
Consider a sequence \(S = [s_1, s_2, \ldots, s_n]\) of \(n\) elements where \(s*i \in \mathbb{R}\) for all \(1 \leq i \leq n\). We define two pointers, \(i\) and \(j\) (with \(1 \leq i \leq j \leq n\)), which demarcate the boundaries of the “window”. This window represents a contiguous subsequence \(s_i, s*{i+1}, \ldots, s_j\) of the sequence, or more commonly denoted as:
The objective is often to find a window that satisfies a specific condition \(C(S, i, j)\). This condition is a function of the sequence and the current window, and varies depending on the problem. At the start, we set \(i = j = 1\), which corresponds to a window that only includes the first element of the sequence.
We then iteratively adjust the positions of \(i\) and \(j\) based on the condition \(C\) and the specific problem requirements. This process continues until we meet a suitable stopping condition. This is often when the right pointer \(j\) has traversed the entire sequence.
Fixed Window Size Sliding Window Technique#
In the fixed-size sliding window technique, we start by determining the window size \(k\). The window is defined by the pointers \(i\) and \(j\) such that \(j - i + 1 = k\), and it remains constant throughout the algorithm.
The algorithm is as follows:
Decide the window size \(k\).
Initialize \(i = 1\) and \(j = k\).
Repeat the following until a stopping condition is met (usually \(j > n\)):
Evaluate \(C(S, i, j)\) and process the window as needed.
Increment both \(i\) and \(j\) synchronously to slide the window.
In this variant, the window slides through the sequence as \(i\) and \(j\) are incremented together, maintaining the same size \(k\).
Variable Window Size Sliding Window Technique#
In the variable-size sliding window technique, the size of the window can change dynamically during the execution of the algorithm. When the condition \(C\) is satisfied, we expand the window by incrementing \(j\). When the condition \(C\) is not satisfied, we shrink the window from the left by incrementing \(i\).
The algorithm is as follows:
Initialize \(i = j = 1\).
Repeat the following until a stopping condition is met (usually \(j > n\)):
If \(C(S, i, j)\) is satisfied, increment \(j\) to expand the window.
Else, increment \(i\) to shrink the window.
This variant allows for more flexibility and is often useful in problems where the optimal window size is not known beforehand and depends on the specific condition \(C\).