|
| 1 | +## Minimum Window Substring(最小窗口覆盖子串) |
| 2 | + |
| 3 | +### 一、题目 |
| 4 | + |
| 5 | +#### 1. 英文版 |
| 6 | + |
| 7 | +【Minimum Window Substring】 |
| 8 | +Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n). |
| 9 | +Example: |
| 10 | + |
| 11 | +Input: S = "ADOBECODEBANC", T = "ABC" |
| 12 | +Output: "BANC" |
| 13 | + |
| 14 | +Note: |
| 15 | +· If there is no such window in S that covers all characters in T, return the empty string "". |
| 16 | +· If there is such window, you are guaranteed that there will always be only one unique minimum window in S. |
| 17 | + |
| 18 | +#### 2. 中文版 |
| 19 | + |
| 20 | +【最小窗口覆盖子串】 |
| 21 | +给你一个字符串S、一个字符串T,请在字符串S里面找出:包含T所有字母的最小子串,要求算法的时间复杂度为O(n)。 |
| 22 | + |
| 23 | +示例: |
| 24 | +输入: S = "ADOBECODEBANC", T = "ABC" |
| 25 | +输出: "BANC" |
| 26 | + |
| 27 | +说明: |
| 28 | +如果S中不存这样的子串,则返回空字符串""。 |
| 29 | +如果S中存在这样的子串,我们保证它是唯一的答案。 |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +### 二、题目理解 |
| 34 | + |
| 35 | +> 1.在S(source)中找到包含T(target)中**全部字母**的一个子串; |
| 36 | +
|
| 37 | +> 2.子串T中**字符不去重**,即 T = "ABC"和 T = "ABAC"是不同的子串,后者需要找到包含两个A的子串,包含"ABAC"的最小窗口覆盖子串为"ADOBECODEBA"; |
| 38 | +
|
| 39 | + |
| 40 | + |
| 41 | +> 3.**顺序无所谓**,但这个子串一定是所有可能子串中最短的。 |
| 42 | +
|
| 43 | + |
| 44 | + |
| 45 | +### 三、解法 |
| 46 | + |
| 47 | +#### 1.暴力解法 |
| 48 | + |
| 49 | +```C++ |
| 50 | +for (int i = 0; i < source.size(); i++) |
| 51 | + for (int j = i + 1; j < source.size(); j++) |
| 52 | + if source[i:j] 包含 t 的所有字母: |
| 53 | + 更新答案 |
| 54 | +``` |
| 55 | + |
| 56 | +#### 2.滑动窗口 |
| 57 | + |
| 58 | +在滑动窗口类型的问题中都会有两个指针。一个用于延伸现有窗口的right指针,和一个用于收缩窗口的left指针。在任意时刻,只有一个指针运动,而另一个保持静止。 |
| 59 | + |
| 60 | +而该题目需要找到包含子串的最小窗口,我们可以通过移动right指针不断扩张窗口。当窗口包含全部所需的字符后,如果能收缩,我们就收缩窗口知道得到最小窗口,最终可以得到最小的可行窗口。 |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | + |
| 78 | +> 1)初始化:left和right指针都指向S的第一个元素A; |
| 79 | +
|
| 80 | +> 2)移动right指针:向右扩张,直到得到一个可行窗口; |
| 81 | +
|
| 82 | +> 3)得到可行的窗口后,将left指针逐个右移,如果得到窗口依然可行,则更新最小窗口大小; |
| 83 | +
|
| 84 | +> 4)如果窗口不再可行,则跳转到第2)步; |
| 85 | +
|
| 86 | +> 5)重复以上步骤,直到遍历完S。 |
| 87 | +
|
| 88 | +重点关注: |
| 89 | +我们更高效地判断第2)步,即判断当前left和right指向的窗口是否可行,也就是是否包含T? |
| 90 | + |
| 91 | + |
| 92 | + |
| 93 | +代码:(C++) |
| 94 | +```C++ |
| 95 | +#include <iostream> |
| 96 | +#include <unordered_map> |
| 97 | + |
| 98 | +std::string FindMinimumWindowSubstring(std::string source, |
| 99 | + std::string target) { |
| 100 | + |
| 101 | + if (source.empty() || target.empty()) { |
| 102 | + return ""; |
| 103 | + } |
| 104 | + |
| 105 | + size_t source_len = source.length(); |
| 106 | + size_t target_len = target.length(); |
| 107 | + |
| 108 | + if (source_len < target_len) { |
| 109 | + return ""; |
| 110 | + } |
| 111 | + |
| 112 | + size_t start = 0; //符合条件的最小窗口的字符串位置 |
| 113 | + size_t min_len = UINT_MAX; //符合条件的最小窗口字符长度 |
| 114 | + |
| 115 | + //1)初始化 |
| 116 | + size_t left = 0; //left指针 |
| 117 | + size_t right = 0; //right指针 |
| 118 | + |
| 119 | + std::unordered_map<char, size_t> window_char_map; //记录当前子串对应char |
| 120 | + std::unordered_map<char, size_t> target_char_map; //记录目标子串对应char |
| 121 | + |
| 122 | + for (char c : target) { |
| 123 | + target_char_map[c]++; |
| 124 | + } |
| 125 | + |
| 126 | + size_t target_char_map_len = target_char_map.size(); //target子串中包含字符的个数(去重后) |
| 127 | + |
| 128 | + size_t match = 0; //记录已经匹配的字符个数(去重后) |
| 129 | + |
| 130 | + //4)重复:直到S串末尾 |
| 131 | + while (right < source_len) { |
| 132 | + |
| 133 | + //2)找到可行窗口,即包含T中所有字符的窗口。 |
| 134 | + char current_char = source[right]; |
| 135 | + |
| 136 | + //如果target包含该字符 |
| 137 | + if (target_char_map.count(current_char)) { |
| 138 | + |
| 139 | + //在当前子串标记匹配该字符 |
| 140 | + window_char_map[current_char]++; |
| 141 | + |
| 142 | + if (window_char_map[current_char] == target_char_map[current_char]) { |
| 143 | + match++; //累加已经匹配字符个数 |
| 144 | + } |
| 145 | + } |
| 146 | + right++; //right指针右移 |
| 147 | + |
| 148 | + //3)如果当前窗口可行,则想办法右移left指针。 |
| 149 | + while (match == target_char_map_len) { |
| 150 | + |
| 151 | + //如果符合条件的最小窗口len发生变化,则更新 |
| 152 | + if (right - left < min_len) { |
| 153 | + |
| 154 | + //移动记录符合条件的start位置 |
| 155 | + start = left; |
| 156 | + |
| 157 | + //更新符合条件的最小窗口len |
| 158 | + min_len = right - left; |
| 159 | + } |
| 160 | + |
| 161 | + char left_char = source[left]; |
| 162 | + |
| 163 | + //如果left位置的字符在target中 |
| 164 | + if (target_char_map.count(left_char)) { |
| 165 | + |
| 166 | + //从当前窗口中减掉left位置的字符 |
| 167 | + if (window_char_map.count(left_char)) { |
| 168 | + window_char_map[left_char]--; |
| 169 | + } |
| 170 | + |
| 171 | + //如果left位置的字符,在target中是必须的,则match - 1,这会导致下一个while循环终止,即left指针右移操作终止; |
| 172 | + //直到找到可行窗口后,再执行第3)步。 |
| 173 | + if (window_char_map[left_char] < target_char_map[left_char]) { |
| 174 | + if (match > 0) { |
| 175 | + match--; |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + left++; //left指针右移 |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + return min_len == UINT_MAX ? "" : source.substr(start, min_len); |
| 185 | +} |
| 186 | + |
| 187 | +int main(int argc, const char * argv[]) { |
| 188 | + |
| 189 | + std::string source = "ADOBECODEBANC"; |
| 190 | + std::string target = "ABC"; |
| 191 | + printf("\nsource: %s\ntarget: %s\nMinimum window substring is: %s\n", source.c_str(), target.c_str(), FindMinimumWindowSubstring(source, target).c_str()); |
| 192 | + |
| 193 | + source = "ADOBECODEBANC"; |
| 194 | + target = "ABAC"; |
| 195 | + printf("\nsource: %s\ntarget: %s\nMinimum window substring is: %s\n", source.c_str(), target.c_str(), FindMinimumWindowSubstring(source, target).c_str()); |
| 196 | + |
| 197 | + source = "ADOBECODEBANC"; |
| 198 | + target = "BECE"; |
| 199 | + printf("\nsource: %s\ntarget: %s\nMinimum window substring is: %s\n", source.c_str(), target.c_str(), FindMinimumWindowSubstring(source, target).c_str()); |
| 200 | + |
| 201 | + source = "ADOBECODEBANC"; |
| 202 | + target = "ABCAC"; |
| 203 | + printf("\nsource: %s\ntarget: %s\nMinimum window substring is: %s\n", source.c_str(), target.c_str(), FindMinimumWindowSubstring(source, target).c_str()); |
| 204 | + |
| 205 | + return 0; |
| 206 | +} |
| 207 | +``` |
| 208 | +
|
| 209 | +#### 3.算法的时间复杂度 |
| 210 | +
|
| 211 | +> O(M + N),M和N分别是S和T的长度; |
| 212 | +> 其中初始化T每个字符出现个数,时间复杂度为O(N); |
| 213 | +> 遍历S while循环为M次,里面嵌套的while循环总计最多为M次,时间复杂度为O(M)。 |
| 214 | +
|
| 215 | +### 四、参考资料 |
| 216 | +> 1.[LeetCode最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring) |
| 217 | +> 2.[查找包含子串的最小子串](https://www.geeksforgeeks.org/find-the-smallest-window-in-a-string-containing-all-characters-of-another-string/) |
| 218 | +
|
0 commit comments