目录
理论基础
滑动窗口
(可参见双指针篇)
双指针同时从一端出发,一个指针作为窗口的左边界,另一个指针作为右边界,将问题分解为两步:
- 扩大窗口:通过增加窗口右边界,尝试找到一个满足条件的子串。
- 缩小窗口:当窗口不满足条件时,通过移动左边界缩小窗口,直到条件再次满足
适用于求子数组或子串的最大值、最小值或满足某些条件的长度等。
子串题型
子串:连续的字符片段。例如,字符串 "abcdef"
的子串包括 "abc"
和 "def"
,但不包括 "ace"
最长无重复字符的子串
在给定字符串中,寻找没有重复字符的最长子串。
- 滑动窗口,同时使用一个哈希集合来跟踪当前窗口中的字符。
- 初始化两个指针
L
和R
,分别表示窗口的左端和右端。 - 移动
R
扩展窗口,遇到重复字符时,移动L
收缩窗口。 - 记录最大长度
- 初始化两个指针
包含所有字符的最小子串
给定两个字符串 s
和 t
,找到 s
中包含 t
所有字符的最短子串。
- 使用滑动窗口和一个字符计数的哈希表来跟踪是否满足条件。
- 初始化两个指针
L
和R
,维护一个哈希表来记录字符的频率。 - 当窗口包含
t
的所有字符时,收缩窗口以寻找最短子串。 - 记录最小长度。
- 初始化两个指针
固定长度的滑动窗口最大值
给定一个数组,找到所有长度为 k
的滑动窗口的最大值。
- 使用双端队列来维护当前窗口中的最大元素。
- 使用队列存储当前窗口中可能成为最大值的元素。
- 每次移动窗口时,移除过期的元素,并更新当前最大值。
3.无重复最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长
子串
的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
Map<Character,Integer> m=new HashMap<>();//键是字母,值是对应索引
int L=-1,l=s.length(),res=0;//length是方法
for(int R=0;R<l;R++){
if(m.containsKey(s.charAt(R))) L=Math.max(L,m.get(s.charAt(R)));
//m.get(s.charAt(R))字符的上次出现位置
//L跳到上一次字符的位置
m.put(s.charAt(R),R);//R遍历,哈希表记录
res=Math.max(res,R-L);
}
return res;
}
}
438.所有异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104
s 和 p 仅包含小写字母
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
int sl = s.length(), pl = p.length();
if (pl>sl) return res;
int[] c1 = new int[26], c2 = new int[26];// c1: s窗口内字符的频次,c2: p 中字符的频次
for (int i = 0; i < pl; i++) c2[p.charAt(i) - 'a']++;//统计 p 中每个字符出现的频次
for (int l = 0, r = 0; r < sl; r++) {
c1[s.charAt(r) - 'a']++; // 右边加入窗口
if (r - l + 1 > pl) c1[s.charAt(l++) - 'a']--; // 窗口的大小大于 pl,l右移缩小窗口
if (check(c1, c2)) res.add(l); //左边界l为异位词开始索引
}
return res;
}
boolean check(int[] c1, int[] c2) {//字符频次一致?
for (int i = 0; i < 26; i++) {
if (c1[i] != c2[i]) return false;
}
return true;
}
}
560.和为K子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
前缀和ps是数组中从第一个元素开始,到当前元素为止所有元素的和
要找的子数组的和是 k,要找的子数组的前缀和应该是 sum - k
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> ps = new HashMap<>();
ps.put(0, 1); // 前缀和为0的次数初始化为1
int sum = 0,res=0;
for (int n: nums) {
sum += n;//总计和
if (ps.containsKey(sum - k)) res += ps.get(sum - k);//检查 ps 中是否存在 sum - k,存在加入res
ps.put(sum, ps.getOrDefault(sum, 0) + 1);//更新 ps
}
return res;
}
}
239.窗口最大值h
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int l=nums.length;
if(l== 0 || k == 0) return new int[0];
Deque<Integer> d = new LinkedList<>(); // 递减双端队列
int[] res = new int[l - k + 1]; // l - k + 1 是窗口在数组中滑动时的次数
for(int R = 0, L = 1 - k; R < l; L++, R++) {
if(L > 0 && d.peekFirst()==nums[L - 1]) d.removeFirst();//删除滑出窗口的元素(检查队最大值是否超出窗口范围)
while(!d.isEmpty() && d.peekLast() < nums[R]) d.removeLast();//移除所有比 nums[R] 小的元素(未来不可能最大值),保持d递减
d.addLast(nums[R]);//将当前元素加入队列
if(L >= 0) res[L] = d.peekFirst(); //存当前窗口的最大值
}
return res;
}
}
76.最小覆盖子串h
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length
n == t.length
1 <= m, n <= 105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
class Solution {
public String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}
HashMap<Character, Integer> count = new HashMap<>();
// 统计组成t字符串的每个字符数量
// count[n]<0:滑动窗口缺少多少个n字符
// count[n]==0:滑动窗口刚好包含多少个n字符
// count[n]>0:滑动窗口超过多少个n字符
for (char c : t.toCharArray()) {
count.put(c, count.getOrDefault(c, 0) - 1);
}
int formed = 0; // 已形成的字符数量
int start = 0; // 记录最小覆盖子串的起始位置
int length = Integer.MAX_VALUE; // 记录最小覆盖子串的长度
for (int left = 0, right = 0, required = t.length(); right < s.length(); right++) {
char c = s.charAt(right);
// 更新窗口中的字符计数
if (count.containsKey(c)) {
if (count.get(c) < 0) {
formed++;
}
count.put(c, count.get(c) + 1);
}
// 当窗口中的字符满足条件时,尝试缩小窗口
while (formed == required) {
if (right - left + 1 < length) {
start = left;
length = right - left + 1;
}
char d = s.charAt(left);
left++;
if (count.containsKey(d)) {
count.put(d, count.get(d) - 1);
if (count.get(d) < 0) {
formed--;
}
}
}
}
return length == Integer.MAX_VALUE ? "" : s.substring(start, start + length);
}
}