滑动窗口
长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
我的解答:o(n^2)复杂度,双遍历
官方解法:
关键在于利用全为正这个性质
Java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// nums和target全为正
// 核心在于维护一个和大于traget的滑动窗口
// 则对于一个子数组集合[left,right],如果大于traget了
// 然后就right + 1,而缩小left到不能再小为止
// 这样left和right都是o(n)
int n = nums.length;
int ans = n + 1;
int left = 0;
int sum = 0;
for(int right = 0; right < n; right++){
sum += nums[right];
while(sum - nums[left] >= target){
sum -= nums[left];
left++;
}
if( (sum >= target) && (ans > right - left + 1)){
ans = right - left + 1;
}
}
return ans == n+1 ? 0 : ans;
}
}
无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
我的解答:
Java
class Solution {
public int lengthOfLongestSubstring(String s) {
// 思路类似,也是维护一个当前最长无重复子串
// 判断是否重复可以维护一个哈希表,这样就是o(1)的判断复杂度
// 如果有重复,则移动left到重复的下一个
// 记录最长长度
int n = s.length();
if(n < 2){
return n;
}
int ans = 0;
Map<Character,Integer> map = new HashMap<Character,Integer>();
int left = 0;
// 初始化
map.put(s.charAt(left),left);
for(int right = 1; right < n; right++){
char c = s.charAt(right);
Integer index = map.get(c);
if(index == null){
// 加入当前不会重复
map.put(c,right);
}else{
// 加入会重复
while(left <= index){
map.remove(s.charAt(left));
left++;
}
map.put(c,right);
}
if(right - left + 1 > ans){
ans = right - left + 1;
}
}
return ans;
}
}
还可以优化,去掉while循环
java
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
// key: 字符, value: 该字符下一次可以开始的位置 (index + 1)
Map<Character, Integer> map = new HashMap<>();
for (int right = 0, left = 0; right < n; right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
// 核心:如果字符在map里,left跳到重复字符上次出现位置的下一个
// 注意:必须用 Math.max,防止 left 往回跳
// (跳到当前窗口左侧的老数据上)
left = Math.max(left, map.get(c));
}
ans = Math.max(ans, right - left + 1);
map.put(c, right + 1); // 存入 right + 1,方便下次直接赋值给 left
}
return ans;
}
}
串联所有单词的子串
给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。
s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。
返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。
我的想法
Java
// 使用kmp算法得到words里面每个单词在s里面的索引,这样就是一个int数组了
// 然后使用滑动窗口(窗口大小为words数组的长度)滑一次,就得到结果了
存在问题:
Java
// 一个单词可能在 s 中多次出现,甚至与其他单词的位置重叠。
// 例如 s = "aaaaa", words = ["aaa", "aa"]。
// 这样的匹配就解决不了了
字符串的排列
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的 排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
我的做法:
Java
class Solution {
public boolean checkInclusion(String s1, String s2) {
// 排列,就是s1的字母及其个数,在s2中都有
int n1 = s1.length();
int n2 = s2.length();
if(n1 > n2){
return false;
}
// s1中的字母个数
// 题目限定仅仅包含小写字母
Map<Character,Integer> s1CharCount = new HashMap<>();
for(char c : s1.toCharArray()){
Integer count = s1CharCount.get(c);
if(count == null){
s1CharCount.put(c,1);
}else{
s1CharCount.put(c,count+1);
}
}
// 滑动窗口中各个单词的出现次数
Map<Character,Integer> windowCharCount = new HashMap<>();
int left = 0;
for(int right = 0; right < n2; right++){
// 新滑进来一个
char c = s2.charAt(right);
Integer count = windowCharCount.get(c);
if(count == null){
windowCharCount.put(c,1);
}else{
windowCharCount.put(c,count+1);
}
// 没够继续滑
if(right - left + 1 < n1){
continue;
}
// 够了判断是否满足排列
if(s1CharCount.equals(windowCharCount)){
return true;
}
// 不是排列,最后面的滑出去
c = s2.charAt(left);
count = windowCharCount.get(c);
if(count == 1){
windowCharCount.remove(c);
}else{
windowCharCount.put(c,count-1);
}
left++;
}
return false;
}
}
通过了,但是执行效率不高。核心在于每次都需要调用map的equals方法,而且由于每次只有小写字母,可以直接用数组
Java
class Solution {
public boolean checkInclusion(String s1, String s2) {
// 排列,就是s1的字母及其个数,在s2中都有
int n1 = s1.length();
int n2 = s2.length();
if(n1 > n2){
return false;
}
// s1Count来记录每一个字符缺失的数目
int[] s1Count = new int[26];
// 使用diff来记录s1和s2中不匹配的字符的个数
int diff = 0;
for(char c : s1.toCharArray()){
if(s1Count[c - 'a'] == 0){
// 第一次出现
diff++;
}
s1Count[c - 'a']++;
}
char[] s2Chars = s2.toCharArray();
for(int i = 0; i < n2; i++){
int c = s2Chars[i] - 'a';
s1Count[c]--;
if(s1Count[c] == 0){
// 消除了一个不匹配
// 第c号字符的数目已经相同
diff--;
}
if(i < n1 - 1){
// 窗口大小不够
continue;
}
if(diff == 0){
// 匹配了
return true;
}
int out = s2Chars[i - n1 + 1] - 'a';
if(s1Count[out] == 0){
// 原来是匹配的,现在移走了
diff++;
}
s1Count[out]++;
}
return false;
}
}
第一步是确定子串的开头在哪里,依次遍历
但是可以知道开头最长就到每个单词的长度那里
因为再往后实际上已经在之前切分过了。
第二步就是通过滑动窗口判断当前窗口内是否满足,如果不满足则向右滑动,然后再次判断。直至终点。
原来自己写的时候c == null直接continue了,任务不符合的单词就不应该被加进来。
但是对于对于输入"barfoothefoobarman",["foo","bar"] 会只输出了[0]而不是[0,9]
因为当前窗口有"the",不代表以后窗口不能满足
我看答案之后自己的解答:
Java
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
if(words.length == 0){
return new ArrayList();
}
int wordLen = words[0].length();
int windowLen = wordLen * words.length;
Map<String,Integer> targetCount = new HashMap<>();
// 遍历查看words数组中各个单词的出现次数
for(String word : words){
targetCount.merge(word,1,Integer::sum);
}
List<Integer> ans = new ArrayList<>();
// 枚举滑动窗口的起点
for(int start = 0; start < wordLen; start++){
// 当前窗口计数,直接使用浅拷贝即可
Map<String,Integer> nowCount = new HashMap<>(targetCount);
// 窗口与目标单词个数不符合的计数
int diff = words.length;
// right为开,即窗口为[start,right)
for(int right = start + wordLen; right <= s.length(); right += wordLen){
// 当前新加入窗口的单词word
String word = s.substring(right - wordLen,right);
Integer c = nowCount.get(word);
if(c != null){
// 如果c为null,则出现了不存在的单词,不管
if(c > 1){
// 新进来了一个缺的单词,更新nowCount
nowCount.put(word,c-1);
}else if(c == 1){
// 缺且刚好能满足,不同的个数减一
nowCount.put(word,0);
diff--;
} else if(c == 0){
// 不缺了,但是又进来一个
nowCount.put(word,c-1);
diff++;
}
}
// 窗口左端点
int left = right - windowLen;
if(left < 0){
// 窗口还没到个数,继续滑,不需要出
continue;
}
// 判断当前是否满足条件了
if(diff == 0){
ans.add(left);
}
// 出最左边的
String outWord = s.substring(left,left+wordLen);
c = nowCount.get(outWord);
if(c != null){
if(c == -1){
diff--;
}else if(c == 0){
diff++;
}
nowCount.put(outWord,c+1);
}
}
}
return ans;
}
}
但是是存在问题的,对于"wordgoodgoodgoodbestword",["word","good","best","good"],不能得到8,因为
再第三个good的时候diff变成了3,然后word再出,diff就变成了4,而后无法满足了
Java
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
if(words.length == 0){
return new ArrayList();
}
int wordLen = words[0].length();
int windowLen = wordLen * words.length;
Map<String,Integer> targetCount = new HashMap<>();
// 遍历查看words数组中各个单词的出现次数
for(String word : words){
targetCount.merge(word,1,Integer::sum);
}
List<Integer> ans = new ArrayList<>();
// 枚举滑动窗口的起点
for(int start = 0; start < wordLen; start++){
// 应该去记录多出来的单词,而不应该忽略
// 最重要的是,原来我记录的是不等于0的数,就丢失了原来的数应该是多少
// 现在记录的是目标是多少和我现在有多少
Map<String,Integer> nowCount = new HashMap<>();
// 窗口与目标单词个数不符合的计数
int diff = targetCount.size();
// right为开,即窗口为[start,right)
for(int right = start + wordLen; right <= s.length(); right += wordLen){
// 当前新加入窗口的单词word
String inWord = s.substring(right - wordLen,right);
int oldInCount = nowCount.getOrDefault(inWord,0);
int targetInCount = targetCount.getOrDefault(inWord,0);
// 对于不存在的词也进行计数
nowCount.put(inWord, oldInCount + 1);
if(oldInCount + 1 == targetInCount){
diff--;
}else if(oldInCount == targetInCount){
diff++;
}
// 窗口左端点
int left = right - windowLen;
if(left < 0){
// 窗口还没到个数,继续滑,不需要出
continue;
}
// 判断当前是否满足条件了
if(diff == 0){
ans.add(left);
}
// 出最左边的
String outWord = s.substring(left,left+wordLen);
int oldOutCount = nowCount.get(outWord);
int targetOutCount = targetCount.getOrDefault(outWord,0);
nowCount.put(outWord,oldOutCount - 1);
if(oldOutCount - 1 == targetOutCount){
diff--;
}else if(oldOutCount == targetOutCount){
diff++;
}
}
}
return ans;
}
}
最小覆盖子串
给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""。
测试用例保证答案唯一。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
我看答案之后的解答:
Java
class Solution {
public String minWindow(String s, String t) {
// 可变滑动窗口从左到右滑动,如果满足了window涵盖t,则不断缩小
// 记录最小的位置
int m = s.length();
int n = t.length();
int ansLeft = -1;
int ansRight = m - 1;
// s和t由英文字符构成,直接用数组
int[] countS = new int[128];
int[] countT = new int[128];
// 初始化t中的字符个数
for(char c : t.toCharArray()){
countT[c]++;
}
// 遍历窗口
int left = 0;
char[] sCharArray = s.toCharArray();
for(int right = 0; right < m; right++){
//right 进入
char inChar = sCharArray[right];
countS[inChar]++;
while(contain(countS,countT)){
// 涵盖
if(right - left < ansRight - ansLeft){
// 找到更小的满足的窗口
ansLeft = left;
ansRight = right;
}
// 不断移出左侧的字符,寻找更小的满足的窗口
countS[sCharArray[left]]--;
left++;
}
}
return ansLeft < 0 ? "" : s.substring(ansLeft,ansRight+1);
}
// 判断s数组中的个数是不是都大于t
private boolean contain(int[] countS, int[] countT){
for(int i = 'A'; i <= 'Z'; i++){
if(countS[i] < countT[i]){
return false;
}
}
for(int i = 'a'; i <= 'z'; i++){
if(countS[i] < countT[i]){
return false;
}
}
return true;
}
}
还可以优化,使用diff来消除每次都需要调用contain函数。
Java
class Solution {
public String minWindow(String s, String t) {
// 可变滑动窗口从左到右滑动,如果满足了window涵盖t,则不断缩小
// 记录最小的位置
// 还可以进行优化,每次都要进行判断两个数组是否涵盖
// 则仍旧可以使用一个diff变量来记录当前不同的个数
int m = s.length();
int n = t.length();
int ansLeft = -1;
int ansRight = m - 1;
// s和t由英文字符构成,直接用数组
int[] countS = new int[128];
int[] countT = new int[128];
// 初始化t中的字符个数
for(char c : t.toCharArray()){
countT[c]++;
}
// 初始化diff
int diff = 0;
for(int i : countT){
if(i != 0){
diff++;
}
}
// 遍历窗口
int left = 0;
char[] sCharArray = s.toCharArray();
for(int right = 0; right < m; right++){
//right 进入
char inChar = sCharArray[right];
countS[inChar]++;
if(countT[inChar] != 0 && countS[inChar] == countT[inChar]){
// 当前的right进入消除了某一个字符的不同
diff--;
}
while(diff == 0){
// 涵盖
if(right - left < ansRight - ansLeft){
// 找到更小的满足的窗口
ansLeft = left;
ansRight = right;
}
// 不断移出左侧的字符,寻找更小的满足的窗口
char outChar = sCharArray[left];
if(countT[outChar] != 0 && countS[outChar] == countT[outChar]){
// 当前的left移出导致了某一个字符的不同
diff++;
}
countS[outChar]--;
left++;
}
}
return ansLeft < 0 ? "" : s.substring(ansLeft,ansRight+1);
}
// 判断s数组中的个数是不是都大于t
private boolean contain(int[] countS, int[] countT){
for(int i = 'A'; i <= 'Z'; i++){
if(countS[i] < countT[i]){
return false;
}
}
for(int i = 'a'; i <= 'z'; i++){
if(countS[i] < countT[i]){
return false;
}
}
return true;
}
}