1. 问题描述:
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。
示例:
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:
如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-window-substring
2. 思路分析:
① 对于这种题目来说,假如使用模拟的方法肯定超时而且容易出错,使用滑动窗口的方法来解决就再适合不过了,因为是第一次接触滑动窗口所以看了一下官方的题解,一开始的时候感觉思路倒是不难理解,但是代码一开始的时候没有理解清楚,所以自己使用idea的debug进行调试,并且结合了官方的题解进行代码的理解,下面是我自己看了题解之后自己整理的思路
② 滑动窗口简单来说可以分为两个步骤:
1)先是找到满足题目要求的范围
2)在确定的范围内进行范围的缩小,使得得到的结果进一步缩小
对于这道题目来说,首先是要确定在S字符串中包含着T字符串的子串,比如S中的子串ADOBEC就包含了T中的所有字母,第二个是找到所有满足包含T的所有字符的子串,可以发现存在ADOBEC、BECODEBA、BANC这些子串,在子串中找到最短的那个就是我们要求解的答案,所以来说思路还是比较清楚的
3)所以我们首先要找出所有包含ABC的子串,这里可以使用数据结构List来进行映射,List中的元素可以为Pair类型(类似于Map类型),里面存放字符串T中字母在S中出现的位置,使用Pair类型的话那么可以很方便地知道T中的字母在S中出现的位置在哪里,使用这样的数据结构来映射的话可以很容易计算出字符串的从哪些位置开始是包含着所有字母的,下面是使用debug截的图可以很清楚看到映射关系:
4)使用List方法找到满足T中所有字母的肯定是当前子串中最短的,因为我们是使用S中包含的T的字母的位置来计算的,此外我们还需要在一开始的时候就map记录在字符串T中出现的字母的次数,这样在寻找下一个满足包含所有T字符的子串的时候才好计算,比如第一个找到的子串肯定是ADOBEC,肯定是当前最短的,因为我们找到时候是根据T中出现的字母找的,下一次尝试找到包含所有的,所以从List中找到T中出现的下一个字母的B的位置开始寻找(这个在List中可以很方便找到)但是需要删除的是上一个字母,删除的时候需要将另外一个记录出现的字母次数的map中映射的字母数量减1,这样在下一次寻找包含所有的才是正确的,下一次是BEC但是这个时候不满足包含所有所以需要扩展右边在循环中找到包含T中ABC字符的子串,可以发现是BECODEBA,所以这个时候需要使用两个while循环,第一个while循环是用来扩展右边界的找到符合所有ABC字母的子串,第二个是缩小子串的范围,找到更短长度的
5)所以进入第二个while循环的时候肯定是找到了满足条件的,这个时候缩小子串范围,看一下删除掉上一个满足条件的位置的字母是否有影响,比如找到第一个子串ADOBEC,尝试删除掉A是否满足包含T中所有字母,假如满足条件那么继续删除可以发现这里不行(因为缺少了字母A),在删除左边字母的时候有时候会更短,因为像找到的子串假如为AAOBEC,删除掉第一个A没有什么影响,但是得到的是更短的子串,所以这样在删除的没有影响的话继续删除,删除之后有影响的话那么需要扩展右边界
6)可以使用一个三个元素的数组来更新最短子串的长度,子串的开始位置与结束位置,这样在最后的话可以得到最短子串的位置
7)其实理解整个思路之后还是比较简单的,关键是使用对应的数据结构,所以我自己对官方的题解领悟之后按照自己的思路重新写了一遍
8)核心是List<Pair<Character, Integer>>可以找到包含所有T字母的子串,Map<Character, Integer>用来统计T中字母出现次数这样在删除字母的时候才好计算并且比较好找掉包含T中全部字母的
9)首先是需要理解官方给出的代码,捋清思路假如不能理解清楚,借助于idea的debug进行调试,逐步执行,观察结果这样可能理解会快一点,理解之后可以学习一下其中的数据结构,比如像Pair,很像Map只是在使用的时候好像不能够修改但是存值与取值都是一样的,看懂别人的代码也是一种能力,看懂之后需要自己写一遍,需要自己将错误修改过来这样对于自己理解代码才是比较有帮助的
3. 代码如下:
我自己写的:
import java.util.*;
public class Solution {
public String minWindow(String s, String t) {
/*首先是计算出给出的t字符串中各个字符出现的次数这样在接下来循环删除上一个满足条件的字母的时候才好计算*/
Map<Character, Integer> charTimes = new HashMap<>();
for (int i = 0; i < t.length(); ++i){
/*计算每个字符出现的次数*/
char c = t.charAt(i);
//getOrDefault方法可以不用判断之前是否键是否存在避免空指针的异常
charTimes.put(c, charTimes.getOrDefault(c, 0) + 1);
}
/*接下来通过计算字符串中t中每一个字符出现的位置在哪里, 这些位置都是包含着t字符串中的字符的*/
List<Pair<Character, Integer>> charPos = new ArrayList<>();
for (int i = 0; i < s.length(); ++i){
//计算出位置
char c = s.charAt(i);
if (charTimes.containsKey(c)){
//将每个在t字符串中出现的字符标记在list中这样在计算是否包含所有字符串的时候才方便一点
charPos.add(new Pair<>(c, i));
}
}
/*往字符串进行往右扩展, cur变量用来存储当前在t中出现的匹配的字母个数*/
int l = 0, r = 0, totalChars = t.length(), cur = 0;
/*用来记录历史上的最小子串*/
int ans[] = {-1, 0, 0};
/*使用一个pair来计算出中间过程中出现的字符个数, pair的功能比map要少一点适合于不用修改的数据*/
Map<Character, Integer> times = new HashMap<>();
while (r < charPos.size()){
/*先是计算出包含着所有字母子串然后再对得到的子串进行缩小*/
/*获取当前出现字母的位置, pair的位置*/
char c = charPos.get(r).getKey();
/*当出现的字母的次数小于当前map对应的字母的个数说明还没有凑够还需要再找*/
if (times.getOrDefault(c, 0) < charTimes.get(c)) cur++;
/*下面一句代码需要放到上一个判断的后面*/
times.put(c, times.getOrDefault(c, 0) + 1);
/*从子串左端减少字符删除一些不必要的字符, 进入这个循环表示l-r包含着所有的字符
* 并且循环的条件为删除的字符是没有用的对于构成最短的子串没有什么贡献
* */
while (l <= r && cur == totalChars){
int start = charPos.get(l).getValue();
int end = charPos.get(r).getValue();
if (ans[0] == -1 || end - start + 1 < ans[0]){
ans[0] = end - start + 1;
ans[1] = start;
ans[2] = end;
}
/*注意下面的是start而不是l*/
char c1 = s.charAt(start);
int curCharTimes = times.getOrDefault(c1, 0);
++l;
times.put(c1, curCharTimes - 1);
/*检查删除当前字符之后是否有影响*/
if (times.get(c1) < charTimes.get(c1)) {
/*表明删除当前的字符有影响因为之前自增了l, 所以需要减去上一个t中含有的字母*/
--cur;
break;
}
}
/*往右扩展*/
++r;
}
return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
}
}
官方代码:
class Solution {
public String minWindow(String s, String t) {
if (s.length() == 0 || t.length() == 0) {
return "";
}
Map<Character, Integer> dictT = new HashMap<Character, Integer>();
for (int i = 0; i < t.length(); i++) {
int count = dictT.getOrDefault(t.charAt(i), 0);
dictT.put(t.charAt(i), count + 1);
}
int required = dictT.size();
int l = 0, r = 0;
int formed = 0;
Map<Character, Integer> windowCounts = new HashMap<Character, Integer>();
int[] ans = {-1, 0, 0};
while (r < s.length()) {
char c = s.charAt(r);
int count = windowCounts.getOrDefault(c, 0);
windowCounts.put(c, count + 1);
if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
formed++;
}
while (l <= r && formed == required) {
c = s.charAt(l);
// Save the smallest window until now.
if (ans[0] == -1 || r - l + 1 < ans[0]) {
ans[0] = r - l + 1;
ans[1] = l;
ans[2] = r;
}
windowCounts.put(c, windowCounts.get(c) - 1);
if (dictT.containsKey(c) && windowCounts.get(c).intValue() < dictT.get(c).intValue()) {
formed--;
}
l++;
}
r++;
}
return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
}
}