This is the third part of a three-part series on Algorithm Templates: Two Pointers. In the second part, we delved into two other types of two pointers technique. Check it out if you missed it.
In this guide (Part 3), we will focus on the last advanced usage: sliding window. We will also talk about three pointers as an extension.
For each type, we will learn about it through two steps:
Sliding window is a useful algorithm technique for typical scenarios. Using the template can greatly simplify the logic of the algorithm. There is a premise: we should determine whether it is suitable for our particular scenario, and we need to make a reasonable abstraction. This template is inspired by these two discussions in LeetCode:
The template for sliding window is a little difficult to understand. So we'll start with a simple version, which focuses only on the two pointers. We use start
and end
pointers to make up a window. Then we move these two pointers to make the window slide.
1# start & end of sliding window: |start-> ... end->|
2# short version of sliding window, focus on two pointers
3def start_end_sliding_window(self, seq):
4 start, end = 0, 0
5 while end < len(seq):
6 # end pointer grows in the outer loop
7 end += 1
8
9 # start pointer grows with some restrict
10 while self.start_condition(start):
11 # process logic before pointers movement
12 self.process_logic1(start, end)
13 # start grows in the inner loop
14 start += 1
15
16 # or process logic after pointers movement
17 self.process_logic2(start, end)
From the above template and diagram, we can see that the end
pointer grows in the outer loop while the start
pointer grows in the inner loop. The logic of sliding window should be processed either before the start pointer grows or at the end of the outer loop. We will discuss the specific logic in the following template.
Here is a more specific version of sliding window:
1# more specific version of sliding window
2# s - source sequence, p - pattern or restrict sequence
3def sliding_window_template_with_examples(s, p):
4 # initialize the hash map
5 # counter is used to record current state, usually use Counter or defaultdict
6 counter = Counter(p)
7 # two pointers, boundary of sliding window
8 start, end = 0, 0
9 # condition checker, update it when trigger some key changes
10 # the initial value depend on your situation
11 count = 0
12 # result, return int (such as max or min) or list (such as all index)
13 res = 0
14
15 # loop the source sequence from begin to end
16 while end < len(s):
17 counter[s[end]] += 1
18 # update count based on some condition
19 if counter[s[end]] > 1:
20 count += 1
21 # end pointer grows in the outer loop
22 end += 1
23
24 # count condition, the condition may be different
25 while count > 0:
26 '''
27 update res here if finding minimum
28 '''
29 # increase start pointer to make it invalid or valid again
30 counter[s[start]] -= 1
31 # update count based on some condition
32 if counter[s[start]] > 0:
33 count -= 1
34 # start pointer grows in the inner loop
35 start += 1
36 '''
37 update res here if finding maximum
38 '''
39 # the result logic may be different
40 res = max(res, end - start)
41 return res
Based on the first version, we added more logical components in this template.
counter
is usually defined as a hashmap (defaultdict
or Counter
in Python). It is used to record the current state. count
works as a condition checker. We update it when triggering some key changes. It is mainly used for the condition of the inner loop.res
represents the result to achieve. The data type of res
depends on the requirement, such as int (max or min) or list (get all index).To make the process more clear, I made a flow chart:
From the perspective of process flow, the algorithm is divided into the outer loop and inner loop.
end
pointer grows in each outer iteration while the start
pointer grows in each inner iteration.end
pointer. We update counter[s[end]]
, which affects the update of count
. In that way, it will affect the inner loop indirectly.start
pointer. We update counter[s[start]]
and count
in the same way.res
logic at the beginning of the inner loop. If finding maximum, we update res
logic at the end of the outer loop.It should be noted that this template is only for the most common processes. It demonstrates the core idea of the algorithm, but it needs to be reformed or decorated for a more complex scene. Many parts need to be adjusted according to the actual scenario, such as the definition of the inner loop condition, the way to update counter
, and how to calculate res
.
Maybe you're still confused about how to use the sliding window technique. Don't worry. We'll get familiar with it through a couple of typical examples with variations.
The sliding window technique is mainly used to solve the substring search problem. First, let's look at an example of finding minimum. As the template shows, we should put the result logic at the beginning of the inner loop.
Given a string S and a string T, find the minimum window in S which will contain all the characters in T.
1# [76] https://leetcode.com/problems/minimum-window-substring/
2def minWindow(s: str, t: str) -> str:
3 counter = Counter(t)
4 count, start, end, res = len(t), 0, 0, [float('inf'), 0]
5 while end < len(s):
6 counter[s[end]] -= 1
7 # consider duplicate char in t
8 if counter[s[end]] >= 0:
9 count -= 1
10 end += 1
11 # valid in while
12 while count == 0:
13 # update minimum here, inner while loop
14 if end - start < res[0]:
15 res = (end - start, start)
16
17 counter[s[start]] += 1
18 if counter[s[start]] > 0:
19 count += 1
20 start += 1
21 return s[res[1]:res[0] + res[1]] if res[0] != float('inf') else ''
Here is an example with no pattern involved:
Given a string, find the length of the longest substring without repeating characters.
1# [3] https://leetcode.com/problems/longest-substring-without-repeating-characters/
2def lengthOfLongestSubstring(s: str) -> int:
3 # create a default dict to maintain state
4 counter = defaultdict(int)
5 count, start, end, res = 0, 0, 0, 0
6 while end < len(s):
7 counter[s[end]] += 1
8 if counter[s[end]] > 1:
9 count += 1
10 end += 1
11 while count > 0:
12 counter[s[start]] -= 1
13 if counter[s[start]] > 0:
14 count -= 1
15 start += 1
16 # update maximum here
17 res = max(res, end - start)
18 return res
Besides substring problems, we can also apply sliding window to other similar scenarios.
You have two baskets, and each basket can carry any quantity of fruit, but you want each basket to only carry one type of fruit each.
What is the total amount of fruit you can collect with this procedure?
In this problem, we need to find the length of the longest sub-array with no more than two distinct integers (fruit types).
1# [904] https://leetcode.com/problems/fruit-into-baskets/
2def totalFruit(tree: 'List[int]') -> int:
3 counter = defaultdict(int)
4 count, start, end, res = 0, 0, 0, 0
5
6 while end < len(tree):
7 counter[tree[end]] += 1
8 if counter[tree[end]] == 1:
9 count += 1
10 end += 1
11 while count > 2:
12 counter[tree[start]] -= 1
13 if counter[tree[start]] == 0:
14 count -= 1
15 start += 1
16 res = max(res, end - start)
17 return res
A more complex example with complex match policy:
Given a non-empty string containing only digits, determine the total number of ways to decode it.
1# [30] https://leetcode.com/problems/substring-with-concatenation-of-all-words/
2def findSubstring(s: str, words: 'List[str]') -> 'List[int]':
3 if not words:
4 return []
5 word_len, res = len(words[0]), []
6
7 # start offset from 0 to word_len, and step is word_len
8 for i in range(word_len):
9 # reset state every epoch
10 counter = Counter(words)
11 start, < len<, count = i, i, len(words)
12 while end < len(s):
13 cur_word = s[end:end + word_len]
14 # check is not necessary here, just for performance
15 if cur_word in counter:
16 counter[cur_word] -= 1
17 if counter[cur_word] >= 0:
18 count -= 1
19 end += word_len
20 if count == 0:
21 res.append(start)
22 # not use a while, use length restriction to ensure consecutive words
23 if end - start == word_len * len(words):
24 cur_word = s[start:start + word_len]
25 if cur_word in counter:
26 counter[cur_word] += 1
27 if counter[cur_word] > 0:
28 count += 1
29 start += word_len
30 return res
From the above examples, we can see that the sliding window technique simplifies the complexity of the algorithm problem and breaks down the problem into logical parts of the template.
Based on two pointers, we can extend to more variations, such as three pointers. The idea is similar. With the abstraction of pointers, we simplify the logic and improve the efficiency of the program.
Let's take a classic example of three pointers.
Given an array with n objects colored red, white or blue, sort them in-place so that objects of the same color are adjacent, with the colors in the order red, white and blue.
1# [75] https://leetcode.com/problems/sort-colors/
2def sortColors(nums):
3 red, white, blue = 0, 0, len(nums) - 1
4
5 while white <= blue:
6 if nums[white] == 0:
7 nums[red], nums[white] = nums[white], nums[red]
8 white += 1
9 red += 1
10 elif nums[white] == 1:
11 white += 1
12 else:
13 nums[white], nums[blue] = nums[blue], nums[white]
14 blue -= 1
In this guide, we learned about the final type of the two pointers technique: sliding window. This template is a bit difficult but very useful in particular scenarios. In addition, we learned three pointers as an extension of two pointers.
To access the complete code, you can download Algorithm Templates from Github. I hope some of them will be useful for you.
If you like this series, please stay tuned. I will update more algorithm templates in the future.
This guide is part of a series on the two pointers technique:
I hope you enjoyed it. If you have any questions, you're welcome to contact me at [email protected].