Java题解LeetCode——76.最小覆盖子串
题目
76.最小覆盖子串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:
如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
解析
今天仔细看了题解,原来子串是连续的,以前我以为是不连续的。
注意,在官方题解中说字符串T中可能出现重复字符,所以在包含字符的同时次数也要相等。
用滑动窗口去做,可以理解为双指针,指针中间就是窗口,两个指针的起始位置都在字符串的最左边。先让右指针右移,判断满足题目条件后,让左指针右移,目的是看看能不能缩小窗口(减小子串长度),distance 的作用是记录窗口内 t 串字符出现的频数,初始化为0,当右指针右移时,一旦发现指针对应的字符和 t 串中的字符匹配上了distance++, 当频数满足题意时,就是distance == t 串的长度,这个窗口(子串)就确定了,然后让左指针右移,看看窗口能不能缩小,左指针的指针移动逻辑和右指针相同,就是一旦发现左指针对应的字符串匹配上了,distance --,这样 distance != t 串的长度,不满足条件那么就不能让左指针右移了。
代码
//滑动窗口,可以理解为双指针,一左一右,中间就是窗口
class Solution {
public String minWindow(String s, String t) {
int sLen = s.length();
int tLen = t.length();
if (sLen == 0 || tLen == 0 || sLen < tLen){
return "";
}
char[] sCharArray = s.toCharArray();
char[] tCharArray = t.toCharArray();
// ascii('z') = 122,存字母对应的ASCII码
int[] winFreq = new int[128]; //用来记录窗口内(也就是当前子串)的字符情况
int[] tFreq = new int[128]; //用来记录t字符的频数
for (char c : tCharArray){
tFreq[c]++; //桶排序中统计出现次数的思想
}
//滑动窗口内部包含多少T中的字符,对应字符频数超过不重复计算
int distance = 0; //用来记录t中字符在窗口中出现的频数
int minLen = sLen + 1; //最小子串的长度,用于最后的substring
int begin = 0; //开始下标,用于最后的substring
//左右两个指针,也就是窗口的两个边界
int left = 0;
int right = 0;
//左闭右开区间[left,right)
while (right < sLen){ //当右指针没移动到头时
//右指针向右移动逻辑
//这一步可以省略,建议加上,更符合逻辑
if (tFreq[sCharArray[right]] == 0){ //右指针的字符不是t中字符时
right++; //向右移动
continue;
}
if (winFreq[sCharArray[right]]<tFreq[sCharArray[right]]){
distance++; //t中字符在窗口中出现的频数+1
}
winFreq[sCharArray[right]]++; //窗口内(也就是当前子串)当前右指针对应的字符频次+1
right++; //指针右移
//执行该循环就说明,窗口的右边界已经找到了一个符合条件的窗口,但是左边界没有动,接下来尝试右移左边界缩小窗口,看看能不让子串更小
while(distance == tLen){ //当t中字符在窗口中出现的频数满足题意时
//更新最小子串长度
if (right - left < minLen){
minLen = right - left;
begin = left;
}
//左指针向右移动的逻辑
//这一个步可以省略,建议加上,更符合逻辑
if (tFreq[sCharArray[left]] == 0){ //左指针的字符不是t中字符时
left++; //向右移动
continue;
}
//当前左指针对应的字符频次和t串对应的字符频次相等
if (winFreq[sCharArray[left]] == tFreq[sCharArray[left]]){
distance--; //t中字符在窗口中出现的频数-1(因为左指针要右移,所以频次会减少)
}
winFreq[sCharArray[left]]--; //窗口内(也就是当前子串)当前左指针对应的字符频次-1
left++; //左指针右移
}
}
if (minLen == sLen+1){ //最小长度没有变化,说明无法匹配,直接返回空串
return "";
}
return s.substring(begin, begin+minLen);
}
}
代码小优化
就做了一个小调整,让时间少了1ms
把上面的代码中
sCharArray[right] 这个值提取出来
char sCharRight = sCharArray[right]; //拿到当前右指针对应的字符
同理左指针的也一样
调整后代码整体如下
//滑动窗口,可以理解为双指针,一左一右,中间就是窗口
class Solution {
public String minWindow(String s, String t) {
int sLen = s.length();
int tLen = t.length();
if (sLen == 0 || tLen == 0 || sLen < tLen){
return "";
}
char[] sCharArray = s.toCharArray();
char[] tCharArray = t.toCharArray();
// ascii('z') = 122,存字母对应的ASCII码
int[] winFreq = new int[128]; //用来记录窗口内(也就是当前子串)的字符情况
int[] tFreq = new int[128]; //用来记录t字符的频数
for (char c : tCharArray){
tFreq[c]++; //桶排序中统计出现次数的思想
}
//滑动窗口内部包含多少T中的字符,对应字符频数超过不重复计算
int distance = 0; //用来记录t中字符在窗口中出现的频数
int minLen = sLen + 1; //最小子串的长度,用于最后的substring
int begin = 0; //开始下标,用于最后的substring
//左右两个指针,也就是窗口的两个边界
int left = 0;
int right = 0;
//左闭右开区间[left,right)
while (right < sLen){ //当右指针没移动到头时
//右指针向右移动逻辑
char sCharRight = sCharArray[right]; //拿到当前右指针对应的字符
if (tFreq[sCharRight] == 0){ //右指针的字符不是t中字符时
right++; //向右移动
continue;
}
if (winFreq[sCharRight]<tFreq[sCharRight]){
distance++; //t中字符在窗口中出现的频数+1
}
winFreq[sCharRight]++; //窗口内(也就是当前子串)当前右指针对应的字符频次+1
right++; //指针右移
//执行该循环就说明,窗口的右边界已经找到了一个符合条件的窗口,但是左边界没有动,接下来尝试右移左边界缩小窗口,看看能不让子串更小
while(distance == tLen){ //当t中字符在窗口中出现的频数满足题意时
//更新最小子串长度
if (right - left < minLen){
minLen = right - left;
begin = left;
}
//左指针向右移动的逻辑
char sCharLeft = sCharArray[left]; //拿到当前左指针对应的字符
if (tFreq[sCharLeft] == 0){ //左指针的字符不是t中字符时
left++; //向右移动
continue;
}
//当前左指针对应的字符频次和t串对应的字符频次相等
if (winFreq[sCharLeft] == tFreq[sCharLeft]){
distance--; //t中字符在窗口中出现的频数-1(因为左指针要右移,所以频次会减少)
}
winFreq[sCharLeft]--; //窗口内(也就是当前子串)当前左指针对应的字符频次-1
left++; //左指针右移
}
}
if (minLen == sLen+1){ //最小长度没有变化,说明无法匹配,直接返回空串
return "";
}
return s.substring(begin, begin+minLen);
}
}