@ -60,7 +60,7 @@ This command specifies the `english` branch and limit the depth of clone, get ri
|
||||
* [Interval Scheduling: Intersections of Intervals](think_like_computer/IntervalIntersection.md)
|
||||
* [String Multiplication](think_like_computer/string_multiplication.md)
|
||||
* [Pancake Soring Algorithm](think_like_computer/PancakesSorting.md)
|
||||
* [滑动窗口技巧](think_like_computer/滑动窗口技巧.md)
|
||||
* [Sliding Window Technique](think_like_computer/滑动窗口技巧.md)
|
||||
* [常用的位操作](think_like_computer/常用的位操作.md)
|
||||
* [信封嵌套问题](think_like_computer/信封嵌套问题.md)
|
||||
* [回溯算法团灭排列、组合、子集问题](interview/子集排列组合.md)
|
||||
|
BIN
pictures/Sliding_window/0.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
pictures/Sliding_window/1.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
pictures/Sliding_window/2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
pictures/Sliding_window/3.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
pictures/Sliding_window/title1.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
pictures/Sliding_window/title2.jpg
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
pictures/Sliding_window/title3.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 84 KiB |
295
think_like_computer/SlidingWindowTechnique.md
Normal file
@ -0,0 +1,295 @@
|
||||
# Sliding Window Technique
|
||||
|
||||
**Translator: [floatLig](https://github.com/floatLig)**
|
||||
|
||||
**Author: [labuladong](https://github.com/labuladong)**
|
||||
|
||||
This article shows you the magic template for "sliding window" with two pointers: the left and right of the window. With this, you can easily solve several difficult substring matching problems.
|
||||
|
||||
There are at least 9 problems in LeetCode that can be solved efficiently using this method. In this article, we choose three problems with the most votes, more classical to explain. The first question, in order for the reader to master the algorithm template, the last two questions are easy to answer according to the template.
|
||||
|
||||
This article code for C++ implementation, will not use any programming quirks, but still briefly introduce some of the data structure used, in case some readers because of the language details of the problem hindering the understanding of the algorithm idea:
|
||||
|
||||
`unordered_map` is `hashmap`, one of its methods, `count(key)`, corresponds to `containsKey(key)` to determine whether the key exists or not.
|
||||
|
||||
`Map [key]` can be used to access the corresponding `value` of the `key`. Note that if the key does not exist, C++ automatically creates the key and assigns the `map[key]` value to 0.
|
||||
|
||||
`map[key]++`, which appears many times in the code, is equivalent to `map.put(key, map.getordefault (key, 0) + 1)` in Java.
|
||||
|
||||
Now let's get to the point.
|
||||
|
||||
### 1. Minimum Window Substring
|
||||
|
||||

|
||||
|
||||
The question asks us to return the minimum substring from the string S (Source) which has all the characters of the string T (Target). Let us call a substring desirable if it has all the characters from T.
|
||||
|
||||
If you don't use any optimization, the code would look like this:
|
||||
|
||||
```java
|
||||
for (int i = 0; i < s.size(); i++)
|
||||
for (int j = i + 1; j < s.size(); j++)
|
||||
if s[i:j] contains all letters of t:
|
||||
update answer
|
||||
```
|
||||
|
||||
Although the idea is very straightforward, but the *time complexity* of this algorithm is O(N^2).
|
||||
|
||||
We can solve it with sliding window. The sliding window algorithm idea is like this:
|
||||
|
||||
1. We start with two pointers, *left and right* initially pointing to the first element of the string S.
|
||||
|
||||
2. We use the right pointer to expand the window [left, right] until we get a desirable window that contains all of the characters of T.
|
||||
|
||||
3. Once we have a window with all the characters, we can move the left pointer ahead one by one. If the window is still a desirable one we keep on updating the minimum window size.
|
||||
|
||||
4. If the window is not desirable any more, we repeat step 2 onwards.
|
||||
|
||||
This idea actually not difficult. **Move right pointer to find a valid window. When a valid window is found, move left pointer to find a smaller window (optimal solution)**.
|
||||
|
||||
Now let's graph it. `needs` and `window` act as counters. `needs` record the number of occurrences of characters in T, and `window` record the number of occurrences of the corresponding character.
|
||||
|
||||
Initial State:
|
||||
|
||||

|
||||
|
||||
Moving the right pointer until the window has all the elements from string T.
|
||||
|
||||

|
||||
|
||||
Now move the left pointer. Notice the window is still desirable and smaller than the previous window.
|
||||
|
||||

|
||||
|
||||
After moving left pointer again, the window is no more desirable.
|
||||
|
||||

|
||||
|
||||
We need to increment the right pointer and left pointer to look for another desirable window until the right pointer reaches the end of the string S (the algorithm ends).
|
||||
|
||||
If you can understand the above process, congratulations, you have fully mastered the idea of the sliding window algorithm.
|
||||
|
||||
Here comes the simple pseudocode.
|
||||
|
||||
```cpp
|
||||
string s, t;
|
||||
// Looking for the "minimum substring" of t in s
|
||||
int left = 0, right = 0;
|
||||
string res = s;
|
||||
|
||||
while(right < s.size()) {
|
||||
window.add(s[right]);
|
||||
right++;
|
||||
// When we found a valid window, move left to find smaller window.
|
||||
while (found a valid window) {
|
||||
// If the window's substring is shorter, update the res
|
||||
res = minLen(res, window);
|
||||
window.remove(s[left]);
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
```
|
||||
|
||||
If you can understand the code above, you are one step closer to solving the problem. Now comes the tricky question: how do you tell if the window (substring s[left...right]) meets the requirements (contains all characters of t)?
|
||||
|
||||
A general way is to use two hashmap as counters. To check if a window is valid, we use a map `needs` to store `(char, count)` for chars in t. And use counter `window` for the number of chars of t to be found in s. If `window` contains all the keys in `needs`, and the value of these keys is greater than or equal to the value in `needs`, we know that `window` meets the requirements and can start moving the left pointer.
|
||||
|
||||
Refinement pseudocode above.
|
||||
|
||||
```cpp
|
||||
string s, t;
|
||||
// Two pointers
|
||||
int left = 0, right = 0;
|
||||
string res = s;
|
||||
|
||||
// Initialize the map
|
||||
unordered_map<char, int> window;
|
||||
unordered_map<char, int> needs;
|
||||
for (char c : t) needs[c]++;
|
||||
|
||||
// The number of characters that meet the requirement
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++; // Add to window
|
||||
if (window[c1] == needs[c1])
|
||||
// The number of occurrences of the character c1 meets the requirement
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
// When we found a valid window
|
||||
while (match == needs.size()) {
|
||||
// Update res here if finding minimum
|
||||
res = minLen(res, window);
|
||||
// Increase left pointer to make it invalid/valid again
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--; // Remove from window
|
||||
if (window[c2] < needs[c2])
|
||||
// The number of occurrences of the character c2 no longer meets the requirement
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
```
|
||||
|
||||
The above code already has complete logic, only a pseudo-code, that is, update `res`, but this problem is too easy to solve, directly see the solution!
|
||||
|
||||
The code of solving this problem is below.
|
||||
|
||||
```cpp
|
||||
string minWindow(string s, string t) {
|
||||
// Records the starting position and length of the shortest substring
|
||||
int start = 0, minLen = INT_MAX;
|
||||
int left = 0, right = 0;
|
||||
|
||||
unordered_map<char, int> window;
|
||||
unordered_map<char, int> needs;
|
||||
for (char c : t) needs[c]++;
|
||||
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++;
|
||||
if (window[c1] == needs[c1])
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
while (match == needs.size()) {
|
||||
if (right - left < minLen) {
|
||||
// Updates the position and length of the smallest string
|
||||
start = left;
|
||||
minLen = right - left;
|
||||
}
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--;
|
||||
if (window[c2] < needs[c2])
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return minLen == INT_MAX ?
|
||||
"" : s.substr(start, minLen);
|
||||
}
|
||||
```
|
||||
|
||||
I think it would be hard for you to understand if you were presented with a large piece of code, but can you understand the logic of the algorithm by following up? Can you see clearly the structure of the algorithm?
|
||||
|
||||
**Time Complexity**: O(|S| + |T|) where |S| and |T| represent the lengths of strings S and T. In the worst case we might end up visiting every element of string S twice, once by left pointer and once by right pointer. ∣T∣ represents the length of string T.
|
||||
|
||||
The reader might think that the nested while loop complexity should be a square, but you can think of it this way, the number of while executions is the total distance that the double pointer left and right traveled, which is at most 2 meters.
|
||||
|
||||
### 2. Find All Anagrams in a String
|
||||
|
||||

|
||||
|
||||
The difficulty of this problem is medium, but using the above template, it should be easy.
|
||||
|
||||
If you update the res of the original code, you can get the answer to this problem.
|
||||
|
||||
```cpp
|
||||
vector<int> findAnagrams(string s, string t) {
|
||||
// Init a collection to save the result
|
||||
vector<int> res;
|
||||
int left = 0, right = 0;
|
||||
// Create a map to save the Characters of the target substring.
|
||||
unordered_map<char, int> needs;
|
||||
unordered_map<char, int> window;
|
||||
for (char c : t) needs[c]++;
|
||||
// Maintain a counter to check whether match the target string.
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++;
|
||||
if (window[c1] == needs[c1])
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
while (match == needs.size()) {
|
||||
// Update the result if find a target
|
||||
if (right - left == t.size()) {
|
||||
res.push_back(left);
|
||||
}
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--;
|
||||
if (window[c2] < needs[c2])
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
Since this problem is similar to the previous one, the `window` also needs to contain all the characters of the string t, but the last problem is to find the shortest substring. This problem is to find a substring of the same length.
|
||||
|
||||
### 3. Longest Substring Without Repeating Characters
|
||||
|
||||

|
||||
|
||||
When you encounter substring problems, the first thing that comes to mind is the sliding window technique.
|
||||
|
||||
Similar to the previous idea, use `window` as a counter to record the number of occurrences of characters in the window. Then move the right pointer to scan through the string. If the character is already in `window`, move the left pointer to the right of the same character last found.
|
||||
|
||||
```cpp
|
||||
int lengthOfLongestSubstring(string s) {
|
||||
int left = 0, right = 0;
|
||||
unordered_map<char, int> window;
|
||||
int res = 0; // Record maximum length
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
window[c1]++;
|
||||
right++;
|
||||
// If a duplicate character appears in the window
|
||||
// Move the left pointer
|
||||
while (window[c1] > 1) {
|
||||
char c2 = s[left];
|
||||
window[c2]--;
|
||||
left++;
|
||||
}
|
||||
res = max(res, right - left);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
One thing needs to be mentioned is that when asked to find maximum substring, we should update maximum after the inner while loop to guarantee that the substring is valid. On the other hand, when asked to find minimum substring, we should update minimum inside the inner while loop.
|
||||
|
||||
### Summarize
|
||||
|
||||
Through the above three questions, we can summarize the abstract idea of sliding window algorithm:
|
||||
|
||||
```java
|
||||
int left = 0, right = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
window.add(s[right]);
|
||||
right++;
|
||||
|
||||
while (valid) {
|
||||
window.remove(s[left]);
|
||||
left++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The data type of the window can vary depending on the situation, such as using the hash table as the counter, or you can use an array to do the same, since we only deal with English letters.
|
||||
|
||||
The slightly tricky part is the `valid` condition, and we might have to write a lot of code to get this updated in real time. For example, the first two problems, it seems that the solution is so long, in fact, the idea is still very simple, but most of the code is dealing with this problem.
|
@ -1,296 +0,0 @@
|
||||
# 滑动窗口技巧
|
||||
|
||||
本文详解「滑动窗口」这种高级双指针技巧的算法框架,带你秒杀几道高难度的子字符串匹配问题。
|
||||
|
||||
LeetCode 上至少有 9 道题目可以用此方法高效解决。但是有几道是 VIP 题目,有几道题目虽不难但太复杂,所以本文只选择点赞最高,较为经典的,最能够讲明白的三道题来讲解。第一题为了让读者掌握算法模板,篇幅相对长,后两题就基本秒杀了。
|
||||
|
||||
本文代码为 C++ 实现,不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解:
|
||||
|
||||
`unordered_map` 就是哈希表(字典),它的一个方法 count(key) 相当于 containsKey(key) 可以判断键 key 是否存在。
|
||||
|
||||
可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。
|
||||
|
||||
所以代码中多次出现的 `map[key]++` 相当于 Java 的 `map.put(key, map.getOrDefault(key, 0) + 1)`。
|
||||
|
||||
本文大部分代码都是图片形式,可以点开放大,更重要的是可以左右滑动方便对比代码。下面进入正题。
|
||||
|
||||
### 一、最小覆盖子串
|
||||
|
||||

|
||||
|
||||
题目不难理解,就是说要在 S(source) 中找到包含 T(target) 中全部字母的一个子串,顺序无所谓,但这个子串一定是所有可能子串中最短的。
|
||||
|
||||
如果我们使用暴力解法,代码大概是这样的:
|
||||
|
||||
```java
|
||||
for (int i = 0; i < s.size(); i++)
|
||||
for (int j = i + 1; j < s.size(); j++)
|
||||
if s[i:j] 包含 t 的所有字母:
|
||||
更新答案
|
||||
```
|
||||
|
||||
思路很直接吧,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。
|
||||
|
||||
滑动窗口算法的思路是这样:
|
||||
|
||||
1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
|
||||
|
||||
2、我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
|
||||
|
||||
3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
|
||||
|
||||
4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
|
||||
|
||||
这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。**左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
|
||||
|
||||
下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和窗口中的相应字符的出现次数。
|
||||
|
||||
初始状态:
|
||||
|
||||

|
||||
|
||||
增加 right,直到窗口 [left, right] 包含了 T 中所有字符:
|
||||
|
||||

|
||||
|
||||
|
||||
现在开始增加 left,缩小窗口 [left, right]。
|
||||
|
||||

|
||||
|
||||
直到窗口中的字符串不再符合要求,left 不再继续移动。
|
||||
|
||||

|
||||
|
||||
|
||||
之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。
|
||||
|
||||
如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。至于如何具体到问题,如何得出此题的答案,都是编程问题,等会提供一套模板,理解一下就会了。
|
||||
|
||||
上述过程可以简单地写出如下伪码框架:
|
||||
|
||||
```cpp
|
||||
string s, t;
|
||||
// 在 s 中寻找 t 的「最小覆盖子串」
|
||||
int left = 0, right = 0;
|
||||
string res = s;
|
||||
|
||||
while(right < s.size()) {
|
||||
window.add(s[right]);
|
||||
right++;
|
||||
// 如果符合要求,移动 left 缩小窗口
|
||||
while (window 符合要求) {
|
||||
// 如果这个窗口的子串更短,则更新 res
|
||||
res = minLen(res, window);
|
||||
window.remove(s[left]);
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
```
|
||||
|
||||
如果上述代码你也能够理解,那么你离解题更近了一步。现在就剩下一个比较棘手的问题:如何判断 window 即子串 s[left...right] 是否符合要求,是否包含 t 的所有字符呢?
|
||||
|
||||
可以用两个哈希表当作计数器解决。用一个哈希表 needs 记录字符串 t 中包含的字符及出现次数,用另一个哈希表 window 记录当前「窗口」中包含的字符及出现的次数,如果 window 包含所有 needs 中的键,且这些键对应的值都大于等于 needs 中的值,那么就可以知道当前「窗口」符合要求了,可以开始移动 left 指针了。
|
||||
|
||||
现在将上面的框架继续细化:
|
||||
|
||||
```cpp
|
||||
string s, t;
|
||||
// 在 s 中寻找 t 的「最小覆盖子串」
|
||||
int left = 0, right = 0;
|
||||
string res = s;
|
||||
|
||||
// 相当于两个计数器
|
||||
unordered_map<char, int> window;
|
||||
unordered_map<char, int> needs;
|
||||
for (char c : t) needs[c]++;
|
||||
|
||||
// 记录 window 中已经有多少字符符合要求了
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++; // 加入 window
|
||||
if (window[c1] == needs[c1])
|
||||
// 字符 c1 的出现次数符合要求了
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
// window 中的字符串已符合 needs 的要求了
|
||||
while (match == needs.size()) {
|
||||
// 更新结果 res
|
||||
res = minLen(res, window);
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--; // 移出 window
|
||||
if (window[c2] < needs[c2])
|
||||
// 字符 c2 出现次数不再符合要求
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
```
|
||||
|
||||
上述代码已经具备完整的逻辑了,只有一处伪码,即更新 res 的地方,不过这个问题太好解决了,直接看解法吧!
|
||||
|
||||
```cpp
|
||||
string minWindow(string s, string t) {
|
||||
// 记录最短子串的开始位置和长度
|
||||
int start = 0, minLen = INT_MAX;
|
||||
int left = 0, right = 0;
|
||||
|
||||
unordered_map<char, int> window;
|
||||
unordered_map<char, int> needs;
|
||||
for (char c : t) needs[c]++;
|
||||
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++;
|
||||
if (window[c1] == needs[c1])
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
while (match == needs.size()) {
|
||||
if (right - left < minLen) {
|
||||
// 更新最小子串的位置和长度
|
||||
start = left;
|
||||
minLen = right - left;
|
||||
}
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--;
|
||||
if (window[c2] < needs[c2])
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return minLen == INT_MAX ?
|
||||
"" : s.substr(start, minLen);
|
||||
}
|
||||
```
|
||||
|
||||
如果直接甩给你这么一大段代码,我想你的心态是爆炸的,但是通过之前的步步跟进,你是否能够理解这个算法的内在逻辑呢?你是否能清晰看出该算法的结构呢?
|
||||
|
||||
这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个 while 循环最多执行 2M 次,时间 O(M)。
|
||||
|
||||
读者也许认为嵌套的 while 循环复杂度应该是平方级,但是你这样想,while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 嘛。
|
||||
|
||||
|
||||
### 二、找到字符串中所有字母异位词
|
||||
|
||||

|
||||
|
||||
这道题的难度是 Easy,但是评论区点赞最多的一条是这样:
|
||||
|
||||
`How can this problem be marked as easy???`
|
||||
|
||||
实际上,这个 Easy 是属于了解双指针技巧的人的,只要把上一道题的代码改中更新 res 部分的代码稍加修改就成了这道题的解:
|
||||
|
||||
```cpp
|
||||
vector<int> findAnagrams(string s, string t) {
|
||||
// 用数组记录答案
|
||||
vector<int> res;
|
||||
int left = 0, right = 0;
|
||||
unordered_map<char, int> needs;
|
||||
unordered_map<char, int> window;
|
||||
for (char c : t) needs[c]++;
|
||||
int match = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
if (needs.count(c1)) {
|
||||
window[c1]++;
|
||||
if (window[c1] == needs[c1])
|
||||
match++;
|
||||
}
|
||||
right++;
|
||||
|
||||
while (match == needs.size()) {
|
||||
// 如果 window 的大小合适
|
||||
// 就把起始索引 left 加入结果
|
||||
if (right - left == t.size()) {
|
||||
res.push_back(left);
|
||||
}
|
||||
char c2 = s[left];
|
||||
if (needs.count(c2)) {
|
||||
window[c2]--;
|
||||
if (window[c2] < needs[c2])
|
||||
match--;
|
||||
}
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
因为这道题和上一道的场景类似,也需要 window 中包含串 t 的所有字符,但上一道题要找长度最短的子串,这道题要找长度相同的子串,也就是「字母异位词」嘛。
|
||||
|
||||
### 三、无重复字符的最长子串
|
||||
|
||||

|
||||
|
||||
遇到子串问题,首先想到的就是滑动窗口技巧。
|
||||
|
||||
类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复:
|
||||
|
||||
```cpp
|
||||
int lengthOfLongestSubstring(string s) {
|
||||
int left = 0, right = 0;
|
||||
unordered_map<char, int> window;
|
||||
int res = 0; // 记录最长长度
|
||||
|
||||
while (right < s.size()) {
|
||||
char c1 = s[right];
|
||||
window[c1]++;
|
||||
right++;
|
||||
// 如果 window 中出现重复字符
|
||||
// 开始移动 left 缩小窗口
|
||||
while (window[c1] > 1) {
|
||||
char c2 = s[left];
|
||||
window[c2]--;
|
||||
left++;
|
||||
}
|
||||
res = max(res, right - left);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
需要注意的是,因为我们要求的是最长子串,所以需要在每次移动 right 增大窗口时更新 res,而不是像之前的题目在移动 left 缩小窗口时更新 res。
|
||||
|
||||
### 最后总结
|
||||
|
||||
通过上面三道题,我们可以总结出滑动窗口算法的抽象思想:
|
||||
|
||||
```java
|
||||
int left = 0, right = 0;
|
||||
|
||||
while (right < s.size()) {
|
||||
window.add(s[right]);
|
||||
right++;
|
||||
|
||||
while (valid) {
|
||||
window.remove(s[left]);
|
||||
left++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 window 的数据类型可以视具体情况而定,比如上述题目都使用哈希表充当计数器,当然你也可以用一个数组实现同样效果,因为我们只处理英文字母。
|
||||
|
||||
稍微麻烦的地方就是这个 valid 条件,为了实现这个条件的实时更新,我们可能会写很多代码。比如前两道题,看起来解法篇幅那么长,实际上思想还是很简单,只是大多数代码都在处理这个问题而已。
|
||||
|
||||
如果本文对你有帮助,欢迎关注我的公众号 labuladong,致力于把算法问题讲清楚~
|
||||
|
||||

|