给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
题解一
题解二
图片转载自LeetCode-Soution
思路和算法
本问题要求我们返回字符串 s 中包含字符串 t 的全部字符的最小窗口。我们称包含 t 的全部字母的窗口为「可行」窗口。
我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 r 指针,和一个用于「收缩」窗口的 l 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
解题过程
一、 初始化两个字符数组have[]和need[]分别记录s和t中每个字符的出现次数,用变量pass记录t中字符的总个数,用passed记录s的子串中包含t中全部某个种类全部字符的个数
二、使用滑动窗口在s中遍历,通过两个指针left和right来维护窗口的左右边界,right循环遍历s字符串:
- 如果窗口此时此时have[right]=need[right],说明s中新覆盖了t中一个字符,passed++
- 当窗口内的字符种类数等于t中的字符种类数时(passed==pass),尝试循环移动左指针缩小窗口,同时更新最小窗口的边界。 即min=right-left+1,更新完最小窗口大小后 left++,have[l]–
- 更新完最小窗口大小后 right右移,窗口扩大
三、返回最小窗口对应的子字符串或空字符串(如果没有找到符合条件的窗口)。
复杂度
时间复杂度:O(n),其中n是字符串s的长度。每个字符最多被访问两次,一次作为窗口的右边界,一次作为窗口的左边界。
空间复杂度:O(1),使用了固定大小的数组来记录字符的出现次数,不随输入规模的变化而变化(这里假设字符集大小固定为128)。
class Solution {
public String minWindow(String s, String t) {
if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) return "";
//维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
//ASCII表总长128
int[] need = new int[128];
int[] have = new int[128];
//将目标字符串指定字符的出现次数记录
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
int pass=0;
for (int i = 0; i < need.length; i++) {
if(need[i]>0) pass++;
}
//分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
//已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
int left = 0, right = 0, min = s.length() + 1, passed = 0, start = 0;
while(right<s.length()){
char r = s.charAt(right);
have[r]++;
//注意这里是=而不是>=,避免重复计数
if(have[r]==need[r]) passed++;
//当子串中通过的字符数==t中包含的字符数
while(pass==passed){
char l=s.charAt(left);
//如果have[l]刚好等于need[l],那么左移left会使得pass的数减少,会退出左移的循环
//更新最小子串的长度和起始位置
if(min>right-left+1){
min=right-left+1;
start=left;
}
//右移左指针
if(have[l]==need[l]) passed--;
left++;
have[l]--;
}
//右指针右移
right++;
}
//如果最小长度还为初始值,说明没有符合条件的子串
if (min == s.length() + 1) {
return "";
}
//返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
return s.substring(start, start + min);
}
}