昨天看到一个老兄在搞双指针,也就是所谓的滑动窗口,兴致暴起,自己也搞一搞,感觉还不错,希望能和大家一起分享。
前言
本题是LeetCode第76题,官方定义的难度系数为Hard,但是题目很有意思,这是我第一次遇到我自己的暴力解法和官方的优秀解法相差不大的题目。
才疏学浅,多多指教。
一、题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
示例:
二、解题思想和具体步骤
1.解题思想
- 先不谦虚的给大家分享一下我第一眼看到本题时的思路:
看到字符串类的问题首先想到的算法肯定是动态规划、回溯、分治以及双指针。剖析本体可以发现,本体貌似并不存在动态规划的首要条件----最优子结构性质,也不存在分治算法的相关特性,与回溯算法不能说是较为相似,而是压根就没任何关系。几种算法只剩下双指针!
先用图例来分享一下我自己的思路:假设 s=“DADBCADC” , t=“ABC”
1.先制作一个和 t 长度相等的滑动框,置于 s 内的起始位置(索引为 0 与滑动框左侧重合)
2.判断滑动框内的元素是否包含 t 中的所有元素,如果是,记录此时的字符串,并将滑动框恢复为原长度后整体向右移(但要保证滑动框的右侧不能从 s 的右侧出界),若没包含完全,那么就从右侧增加滑动框的长度。
3.重复上面的操作,直到遍历所有的情况,得出最小值,或返回空字符串。
- 看过官方题解后,结合本人思路与官方题解,得出最终思路:
具体思路:
1.设置 i ,j 来分别表示滑动窗口的左右边界;
2.不断增加左右边界,直到包含 t 中的所有元素;
3.不断增加 i 值,直到碰到一个必须包含的元素,记录此时的字符串和长度。这里的必须包含的意思是:如果继续增加 i 的值,滑动窗口就不能包含 t 的所有元素;
4.让 i 再增加一个位置,这个时候滑动窗口肯定不满足条件了,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到 j 超出了字符串 s 范围。
2.具体步骤
同样,我们假设 s 和 t 的值,并用图例来说明上文中的解题思路:
起始:
滑动寻找:
未包含全部,继续寻找:
继续:
仍未包含全部,继续
包含全部,停止向右扩张,开始增加 i 的值,即向左扩张,注意条件:
找到不可扔掉的值,记录此时的字符串和长度,并在记录后继续增加 i 值:
重复第一步,直到遍历完所有结果:
······其余步骤和上面一样,此处省略
三、存储方式的选择
起初我是打算用数组来存储每次的遍历结果,用一个变量的值来存储长度,但是发现在使用途中对步骤中的各条件的判断束手无策,比如:判断滑动框是否包含 t 中的所有元素等,就很麻烦。
看过一位大佬解法之后,拍手叫好,他采用python中的字典来进行存储每个元素在滑动框中的数量,借此来判断是否包含等其他判断条件。
四、代码实现
python代码:
def minWindow(self, s: str, t: str) -> str:
need=collections.defaultdict(int)
for c in t:
need[c]+=1
needCnt=len(t)
i=0
res=(0,float('inf'))
for j,c in enumerate(s):
if need[c]>0:
needCnt-=1
need[c]-=1
if needCnt==0: #步骤一:滑动窗口包含了所有T元素
while True: #步骤二:增加i,排除多余元素
c=s[i]
if need[c]==0:
break
need[c]+=1
i+=1
if j-i<res[1]-res[0]: #记录结果
res=(i,j)
need[s[i]]+=1 #步骤三:i增加一个位置,寻找新的满足条件滑动窗口
needCnt+=1
i+=1
return '' if res[1]>len(s) else s[res[0]:res[1]+1] #如果res始终没被更新过,代表无满足条件的结果
C++代码:代码地址
class Solution {
public:
unordered_map <char, int> ori, cnt;
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
java代码:代码地址
class Solution {
Map<Character, Integer> ori = new HashMap<Character, Integer>();
Map<Character, Integer> cnt = new HashMap<Character, Integer>();
public String minWindow(String s, String t) {
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int l = 0, r = -1;
int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
int sLen = s.length();
while (r < sLen) {
++r;
if (r < sLen && ori.containsKey(s.charAt(r))) {
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
ansR = l + len;
}
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
public boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}
}
C语言代码:代码地址
// [A-Z][a-z]
#define NUM_CHARS 26
#define MAX_LENGTH (NUM_CHARS * 2)
int char2offset(char c)
{
if (c >= 'a' && c <= 'z') {
return c - 'a';
} else if (c >= 'A' && c <= 'Z') {
return c - 'A' + NUM_CHARS;
}
return -1;
}
char *formatAnswer(const char *ans, int min)
{
char *res = NULL;
if (ans == NULL) {
res = (char *)malloc(sizeof(char) * 1);
res[0] = '\0';
} else {
res = (char *)malloc(sizeof(char) * (min + 1));
for (int i = 0; i < min; i++) {
res[i] = ans[i];
}
res[min] = '\0';
}
return res;
}
void getFreq(char *t, int freq[])
{
// Calculate the frequence of each characters.
const char *r = t;
while (*r != '\0') {
freq[char2offset(*r)]++;
r++;
}
}
// Sliding window
char *minWindow(char *s, char *t)
{
// INT_MAX or any value > strlen(s)
int min = strlen(s) + 1;
int freq[MAX_LENGTH] = { 0 };
getFreq(t, freq);
int tlen = strlen(t);
const char *right = s;
const char *left = s;
const char *ans = NULL;
// 统计t中的字符已经找到了的个数
int count = 0;
// 注意左边界要处理到整个字符串的最右边
while (*left != '\0') {
// just move left pointer
if (left < right) {
// 如果字符在t中,计数表示该字符需要在答案中出现的次数。
// 如果它不在t中,一定是右边界扩展时给它放进了区间,计数增加也不会超过0。
freq[char2offset(*left)]++;
if (freq[char2offset(*left)] > 0) {
// 字符在t中,计数才可能大于0。
// 说明本来已经找到的字符被移出答案了,需要重新找它。
// 所以,找到的个数要减少。
count--;
}
left++;
}
// 窗口左边界缩小了,也有可能获得合法的答案,所以要检查一下。
// If [left, right) is valid, [Left + 1, right) may also be valid.
if (count >= tlen) {
if (right - left < min) {
min = right - left;
ans = left;
}
}
// 移动右边界,直到t中的字符都被找到
while (*right != '\0' && count < tlen) {
// 如果字符在t中,计数大于0,说明找到了一次。找到的个数要增加。
// 如果它不在t中,计数不会大于0。怎么保证呢?
// (1) 初始化的时候它肯定是0。
// (2) 移动右边界时会给它递减,也不会大于0。
// (3) 移动左边界时会给它递增,但是递增的数值一定不会超过递减的数值。
// 因为它能出现在区间内,一定是右边界扩展的时候给移进去并递增过。
// 所以它也不会大于0。
if (freq[char2offset(*right)] > 0) {
count++;
}
// 需要找的计数递减。
freq[char2offset(*right)]--;
right++;
}
// 右边界扩大,需要检查是否找到合法的答案。
if (count >= tlen) {
if (right - left < min) {
min = right - left;
ans = left;
}
}
}
return formatAnswer(ans, min);
}
总结
本题在细节上的考虑还是非常重要的,比如很多地方的判断条件等。希望这篇文章能帮到大家,若有错误,请务必联系本人进行更改,将感激不尽。