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
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.
counteris usually defined as a hashmap (
Counterin Python). It is used to record the current state.
countworks as a condition checker. We update it when triggering some key changes. It is mainly used for the condition of the inner loop.
resrepresents the result to achieve. The data type of
resdepends 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.
endpointer grows in each outer iteration while the
startpointer grows in each inner iteration.
endpointer. We update
counter[s[end]], which affects the update of
count. In that way, it will affect the inner loop indirectly.
startpointer. We update
countin the same way.
reslogic at the beginning of the inner loop. If finding maximum, we update
reslogic 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
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#  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: 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:res + res] if res != 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#  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#  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#  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),  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#  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]