Author avatar

Recnac

Algorithm Templates: Two Pointers - Part 3

Recnac

  • Sep 3, 2020
  • 13 Min read
  • 10,648 Views
  • Sep 3, 2020
  • 13 Min read
  • 10,648 Views

Introduction

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:

  1. Describing the concept by algorithm template and flow chart
  2. Providing typical examples to help you understand application scenarios and how to use it

Start and End of Sliding Window

Template

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)
Python

two pointers - start end of sliding window

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
python

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:

two pointers - sliding window flowchart

From the perspective of process flow, the algorithm is divided into the outer loop and inner loop.

  • As we mentioned before, the end pointer grows in each outer iteration while the start pointer grows in each inner iteration.
  • The outer loop handles the logic of the end pointer. We update counter[s[end]], which affects the update of count. In that way, it will affect the inner loop indirectly.
  • The inner loop handles the logic of the start pointer. We update counter[s[start]] and count in the same way.
  • If finding minimum, we update 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.

Examples

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 ''
python

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
python

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
python

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
python

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.

Three Pointers

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
python

Conclusion

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]