cookbook
- 左右指针不可以回头
- 找到描述窗口的数据结构
1、无重复字符的最长子串
// 用HashMap来维护窗口大小, 因为没有删除map里的value,所以窗口可能包含left左区间的值
// 需要注意left的取值问题, 案例"abba"
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) return 0;
int left = 0;
int max = 0;
HashMap map = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
if (map.containsKey(s.charAt(right))) {
left = Math.max(left, map.get(s.charAt(right)) + 1); // 注意这里
}
map.put(s.charAt(right), right);
max = Math.max(max, right - left + 1);
}
return max;
}
}
// 用set维护窗口,需要删除,较为麻烦
class Solution {
public int lengthOfLongestSubstring(String s) {
int i = 0;
int j = -1;
int len = j - i + 1;
Set set = new HashSet<>();
while (j < s.length()-1) {
j++;
char c = s.charAt(j);
if (!set.contains(c)) {
len = Math.max(len, j-i+1);
set.add(c);
} else {
while (s.charAt(i++) != c) {
set.remove(s.charAt(i-1));
}
}
}
return len;
}
}
2、串联所有单词的子串
class Solution {
public List findSubstring(String s, String[] words) {
Map counts = new HashMap<>();
for (String str : words) counts.put(str, counts.getOrDefault(str, 0)+1);
int n = s.length(); int len = words[0].length(); int num = words.length;
List res = new ArrayList<>();
for (int i = 0; i < n - num * len + 1; i++) {
Map seen = new HashMap<>();
int j = 0;
while (j < num) {
String curStr = s.substring(i + j * len, i + (j + 1) * len);
if (counts.containsKey(curStr)) {
seen.put(curStr, seen.getOrDefault(curStr, 0) + 1);
if (seen.get(curStr) > counts.get(curStr)) {
break;
}
} else {
break;
}
j++;
}
if (j == num) {
res.add(i); // 第一次居然把这里添加了j
}
}
return res;
}
}
3、最小覆盖子串
// 用count数组来计数, t的长度表示需要的距离,维护好这两个量
class Solution {
public String minWindow(String s, String t) {
int[] count = new int[128];
for (char c : t.toCharArray()) count[c]++;
int size = t.length();
int N = s.length();
int left = 0;
int right = 0;
int head = 0;
int minLen = N + 1;
while (right < N) {
if (count[s.charAt(right++)]-- > 0) size--;
while (size == 0) {
if (right - left < minLen) {
head = left;
minLen = right - head;
}
if (count[s.charAt(left++)]++ == 0) size++;
}
}
return minLen == N + 1 ? "" : s.substring(head, head + minLen);
}
}
4、 重复的DNA序列
class Solution {
public List findRepeatedDnaSequences(String s) {
Set set = new HashSet<>();
Set res = new HashSet<>();
for (int i = 10; i <= s.length(); i++) {
String cur = s.substring(i - 10, i);
if (set.contains(cur)) {
res.add(cur);
} else {
set.add(cur);
}
}
return new ArrayList<>(res);
}
}
5、长度最小的子数组
class Solution {
public int minSubArrayLen(int target, int[] nums) {
if (nums == null || nums.length <= 0) return 0;
int minLength = nums.length + 1;
int left = 0;
int right = 0;
int sum = 0;
while (right < nums.length) {
sum += nums[right++];
while (sum >= target) {
minLength = Math.min(minLength, right - left);
sum -= nums[left++];
}
}
return minLength == nums.length + 1 ? 0 : minLength;
}
}
6、存在重复元素 II
class Solution {
public boolean containsNearbyDuplicate(int[] nums, int k) {
HashSet set = new HashSet<>();
for(int i = 0; i < nums.length; i++) {
if(set.contains(nums[i])) {
return true;
}
set.add(nums[i]);
if(set.size() > k) {
set.remove(nums[i - k]);
}
}
return false;
}
}
7、存在重复元素 III,*
// 用hash桶来判断,比较巧妙,主要桶id的计算
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
long size = t + 1;
HashMap map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
long cur = nums[i];
long id = cur >= 0 ? cur / size : (cur + 1) / size - 1;
if (map.containsKey(id)) return true;
if (map.containsKey(id - 1) && cur - map.get(id - 1) <= t) return true;
if (map.containsKey(id + 1) && map.get(id + 1) - cur <= t) return true;
map.put(id, cur);
if (i >= k) {
long left = nums[i - k];
long pre = left >= 0 ? left / size : (left + 1) / size - 1;
map.remove(pre);
}
}
return false;
}
}
// 用TreeSet维护窗口,方便查找
class Solution0 {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
TreeSet set = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
Long ceiling = set.ceiling((long)nums[i] - (long)t);
if (ceiling != null && ceiling <= (long)nums[i] + (long)t) {
return true;
}
set.add((long)nums[i]);
if (i >= k) {
set.remove((long)nums[i-k]);
}
}
return false;
}
}
8、滑动窗口最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque deque = new ArrayDeque<>();
int[] res = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
int inputVal = nums[i];
while (!deque.isEmpty() && deque.peekLast() < inputVal) {
deque.pollLast();
}
deque.offer(inputVal);
if (i >= k -1) {
res[i - k + 1] = deque.peek();
if (deque.peek() == nums[i-k+1]) {
deque.poll();
}
}
}
return res;
}
}
9、至少有 K 个重复字符的最长子串,*
// 滑动窗口+递归,先统计s中数量,小于k的是分割线,不包含在子串中
class Solution {
public int longestSubstring(String s, int k) {
int N = s.length();
int[] counts = new int[26];
for (char c : s.toCharArray()) {
counts[c - 'a']++;
}
boolean flag = true;
for (int cnt : counts) {
if (cnt > 0 && cnt < k) flag = false;
}
if (flag) return N;
int left = 0;
int right = 0;
int res = 0;
while (right < N) {
int index = s.charAt(right) - 'a';
if (counts[index] < k) {
String sub = s.substring(left, right);
res = Math.max(res, longestSubstring(sub, k));
left = right + 1;
}
right++;
}
res = Math.max(res, longestSubstring(s.substring(left), k));
return res;
}
}
10、替换后的最长重复字符,*
// 用count数组描述窗口,curmax维护窗口的最大字符数量,right - left表示历史最大结果,只增不减
class Solution {
public int characterReplacement(String s, int k) {
int N = s.length();
int left = 0;
int right;
int[] count = new int[26];
int curmax = 0;
for (right = 0; right < N; right++) {
curmax = Math.max(curmax, ++count[s.charAt(right) - 'A']);
int curLength = right - left + 1;
if (curLength - curmax > k) {
count[s.charAt(left++) - 'A']--;
}
}
return right - left;
}
}
11、找到字符串中所有字母异位词,*
// 和76题最小覆盖字串相比,这题的长度和目标是一样的,所以无须维护distance,有一个不是p中的字符,就会从头开始
class Solution {
public List findAnagrams(String s, String p) {
List res = new ArrayList<>();
if (s.length() < p.length()) return res;
int[] needFreq = new int[26];
int[] windowFreq = new int[26];
for (int i = 0; i < p.length(); i++) {
needFreq[p.charAt(i) - 'a']++;
}
int left = 0;
int right = 0;
while (right < s.length()) {
int rightCharIndex = s.charAt(right++) - 'a';
windowFreq[rightCharIndex]++;
while (left < s.length() && windowFreq[rightCharIndex] > needFreq[rightCharIndex]) {
windowFreq[s.charAt(left) - 'a']--;
left++;
}
if (right - left == p.length()) {
res.add(left);
}
}
return res;
}
}
12、滑动窗口中位数,涉及堆,先遗留
13、字符串的排列
// 和"找到字符串中所有字母异位词"一样
class Solution {
public boolean checkInclusion(String s1, String s2) {
int[] needs = new int[26];
for (int i = 0; i < s1.length(); i++) {
needs[s1.charAt(i) - 'a']--;
}
int left = 0;
int right = 0;
while (right < s2.length()) {
needs[s2.charAt(right) - 'a']++;
while (needs[s2.charAt(right) - 'a'] > 0) {
needs[s2.charAt(left++) - 'a']--;
}
if (right - left + 1 == s1.length()) return true;
right++;
}
return false;
}
}
14、最小区间
class Solution {
public int[] smallestRange(List> nums) {
HashMap> insides = new HashMap<>(); // 区间值,nums元素下标映射
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
int size = nums.size();
for (int i = 0 ; i < size; i++) {
for (int num : nums.get(i)) {
List list = insides.getOrDefault(num, new ArrayList());
list.add(i);
insides.put(num, list);
max = Math.max(max, num);
min = Math.min(min, num);
}
}
int[] freq = new int[size]; // 数组下标出现的次数
int resLeft = min;
int resRight = max;
int count = 0;
int left = min;
int right = min;
while (right <= max) { // 注意这里是等于
if (insides.containsKey(right)) {
for (int index : insides.get(right)) {
freq[index]++;
if (freq[index] == 1) count++;
}
while (count == size) {
if (right - left < resRight - resLeft) { // 错误记录,既然把这里left和right顺序搞反了
resLeft = left;
resRight = right;
}
if (insides.containsKey(left)) {
for (int leftIndex : insides.get(left)) {
freq[leftIndex]--;
if (freq[leftIndex] == 0) count--;
}
}
left++;
}
}
right++;
}
return new int[] {resLeft, resRight};
}
}
15、子数组最大平均数 I
class Solution {
public double findMaxAverage(int[] nums, int k) {
int sum = 0;
int maxSum = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (i >= k - 1) {
maxSum = Math.max(maxSum, sum);
sum -= nums[i - k + 1];
}
}
return (maxSum * 1.0) / k;
}
}
16、乘积小于K的子数组
// 每次加上以右结尾子数组的个数
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
if (k <= 1) return 0;
int left = 0;
int cnt = 0;
int prod = 1;
for (int right = 0; right < nums.length; right++) {
prod *= nums[right];
while (prod >= k) {
prod /= nums[left++];
}
cnt += right - left + 1;
}
return cnt;
}
}
17、最长重复子数组
// 过程可以看成两把尺子的头尾相接并相想移动
class Solution {
public int findLength(int[] nums1, int[] nums2) {
return nums1.length < nums2.length ? findMax(nums1, nums2) : findMax(nums2, nums1);
}
public int findMax(int[] A, int[] B) { // 假定A.length <= B.length;
int res = 0;
int aLen = A.length;
int bLen = B.length;
for (int len = 1; len < aLen; len++) {
res = Math.max(res, findMax(A, 0, B, bLen - len, len));
}
for (int i = 1; i < bLen - aLen; i++) {
res = Math.max(res, findMax(A, 0, B, bLen - aLen - i, aLen));
}
for (int i = 0; i < aLen; i++) {
res = Math.max(res, findMax(A, i, B, 0, aLen - i));
}
return res;
}
public int findMax(int[] A, int aStart, int[] B, int bStart, int len) {
int res = 0;
int sum = 0;
for (int i = 0; i < len; i++) {
if (A[aStart + i] == B[bStart + i]) {
sum++;
} else if (sum > 0) {
res = Math.max(res, sum);
sum = 0;
}
}
return Math.max(res, sum); // 注意这里sum最后可能不为0;
}
}
18、和至少为 K 的最短子数组
// 利用前缀和转化为求窗口内两个值差大于K的最小子数组长度
class Solution {
public int shortestSubarray(int[] nums, int k) {
int N = nums.length;
long[] preSum = new long[N + 1];
for (int i = 0; i < N; i++) {
preSum[i + 1] = preSum[i] + (long)nums[i];
}
Deque deque = new LinkedList<>();
int min = N + 1;
int right = 0;
while (right < N + 1) {
long cur = preSum[right];
while (!deque.isEmpty() && preSum[deque.getLast()] >= cur) {
deque.removeLast();
}
while (!deque.isEmpty() && cur >= preSum[deque.getFirst()] + k) {
min = Math.min(min, right - deque.removeFirst());
}
deque.addLast(right);
right++;
}
return min < N + 1 ? min : -1;
}
}
19、水果成篮
class Solution {
public int totalFruit(int[] fruits) {
int left = 0;
int right = 0;
Map cnt = new HashMap<>();
for (; right < fruits.length;right++) {
cnt.put(fruits[right], cnt.getOrDefault(fruits[right], 0) + 1);
if (cnt.size() > 2) {
cnt.put(fruits[left], cnt.get(fruits[left]) - 1);
cnt.remove(fruits[left++], 0);
}
}
return right - left;
}
}
20、和相同的二元子数组 ,前缀和+滑动窗口待求解。
// 前缀和+哈希表解,转化为p[j] - p[i] = k
class Solution {
public int numSubarraysWithSum(int[] nums, int goal) {
int sum = 0;
int res = 0;
Map cnt = new HashMap<>(); // 前缀和 的cnt表
for (int i = 0; i < nums.length; i++) {
cnt.put(sum, cnt.getOrDefault(sum, 0) + 1);
sum += nums[i];
res += cnt.getOrDefault(sum - goal, 0);
}
return res;
}
}
// 转化为最多有goal个1的子数组问题求解
class Solution {
public int numSubarraysWithSum(int[] nums, int goal) {
if (goal <= 0) return solve(nums,0);
return solve(nums, goal) - solve(nums, goal - 1);
}
// 最多有goal个1的子数组
public int solve(int[] nums, int goal) {
int res = 0;
int sum = 0;
int left = 0;
int right = 0;
while (right < nums.length) {
sum += nums[right++];
while (sum > goal) {
sum -= nums[left++];
}
res += right - left;
}
return res;
}
}
21、最长湍流子数组
class Solution {
public int maxTurbulenceSize(int[] arr) {
if (arr.length == 0) {
return 0;
}
int left = 0;
int right = 1; // 注意right从1开始
int pre = 0;
int res = 1; // 这里注意为1
while (right < arr.length) {
int diff = arr[right] - arr[right - 1];
if ((pre < 0 && diff < 0) || (pre > 0 && diff > 0)) {
left = right - 1;
}
if (diff == 0) {
left = right;
}
right++;
res = Math.max(res, right - left);
pre = diff; // 记得改变pre的值
}
return res;
}
}
22、K 个不同整数的子数组, 缺差分数组解法
// 转化为求最多包含k个不同整数的子数组问题求解
class Solution {
public int subarraysWithKDistinct(int[] nums, int k) {
return mostKsubrrays(nums, k) - mostKsubrrays(nums, k - 1);
}
public int mostKsubrrays(int[] nums, int k) {
if (nums.length == 0 || k <= 0) return 0;
int[] freq = new int[nums.length+1];
int left = 0;
int right = 0;
int cnt = 0;
int res = 0;
for (; right < nums.length; right++) {
freq[nums[right]]++;
if (freq[nums[right]] == 1) cnt++;
while (cnt > k) {
freq[nums[left]]--;
if (freq[nums[left]] == 0) cnt--;
left++;
}
res += right - left + 1;
}
return res;
}
}
23、K 连续位的最小翻转次数,*
class Solution {
public int minKBitFlips(int[] nums, int k) {
Queue queue = new ArrayDeque<>();
int res = 0;
for (int i = 0; i < nums.length; i++) {
if (!queue.isEmpty() && i - queue.peek() > k - 1) {
queue.poll();
}
if (queue.size() % 2 == nums[i]) {
if (i + k > nums.length) return -1;
queue.add(i);
res++;
}
}
return res;
}
}
24、最大连续1的个数 III
// 维护k就行
class Solution {
public int longestOnes(int[] nums, int k) {
if (nums.length == 0) return 0;
int left = 0;
int right = 0;
while(right < nums.length) {
if (nums[right++] == 0) k--;
if (k < 0 && nums[left++] == 0) k++;
}
return right -left;
}
}
25、爱生气的书店老板
// 先加不生气的,然后把不生气的customer值变为0,转化为求固定窗口大小的最大值
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int N = customers.length;
int ans = 0;
for (int i = 0; i < N; i++) {
if (grumpy[i] == 0) {
ans += customers[i];
customers[i] = 0;
}
}
int left = 0;
int max = 0;
int cur = 0;
for (int right = 0; right < N; right++) {
cur += customers[right];
if (right - left + 1 > minutes) cur -= customers[left++];
max = Math.max(max, cur);
}
return max + ans;
}
}
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int left = 0;
int right = 0;
int max = 0;
int cur = 0;
for (int i = 0; i < grumpy.length; i++) {
cur += grumpy[i] == 0 ? customers[i] : 0;
}
for (; right < customers.length; right++) {
if (grumpy[right] == 1) {
cur += customers[right];
while (right - left + 1 > minutes) {
if (grumpy[left] == 1) {
cur -= customers[left];
}
left++;
}
}
max = Math.max(max, cur);
}
return max;
}
}
26、尽可能使字符串相等
// 常规滑动窗口的题,一次过
class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int N = s.length();
int[] diff = new int[N];
for (int i = 0; i < N; i++) {
diff[i] = Math.abs(s.charAt(i) - t.charAt(i));
}
int left = 0;
int right = 0;
int sum = 0;
int res = 0;
while (right < N) {
sum += diff[right++];
while (sum > maxCost) {
sum -= diff[left++];
}
res = Math.max(res, right - left);
}
return res;
}
}
27、替换子串得到平衡字符串
// 此题和76题类似,先把问题转化为求一个窗口内包含某些字符串的子串最短长度
class Solution {
public int balancedString(String s) {
int N = s.length();
int quarter = N / 4;
int[] cnt = new int[26];
int need = 0;
for (char c : s.toCharArray()) {
if (++cnt[c - 'A'] > quarter) need++;
}
if (need == 0) return 0;
int size = need;
int res = N + 1;
int left = 0;
int right = 0;
while (right < N) {
int cur = s.charAt(right++) - 'A';
if (cnt[cur]-- > quarter) need--;
while (need == 0) {
res = Math.min(res, right - left); // 第一次居然在这里把res写成size了,我该打
int leftIndex = s.charAt(left) - 'A';
if (++cnt[leftIndex] > quarter) need++;
left++;
}
}
return res;
}
}
28、可获得的最大点数
// 转化为求窗口的最小值
class Solution {
public int maxScore(int[] cardPoints, int k) {
int N = cardPoints.length;
int wSize = N - k;
int sum = 0;
for (int i = 0; i < N - k; i++) {
sum += cardPoints[i];
}
int minSum = sum;
for (int i = wSize; i < N; i++) {
sum = sum + cardPoints[i] - cardPoints[i - wSize];
minSum = Math.min(minSum, sum);
}
return Arrays.stream(cardPoints).sum() - minSum;
}
}
29、绝对差不超过限制的最长连续
class Solution {
public int longestSubarray(int[] nums, int limit) {
Deque minQue = new ArrayDeque<>();
Deque maxQue = new ArrayDeque<>();
int left = 0;
int right = 0;
int n = nums.length;
int res = 0;
while (right < n) {
int curVal = nums[right];
while (!minQue.isEmpty() && minQue.peekLast() > curVal) {
minQue.pollLast();
}
minQue.add(curVal);
while (!maxQue.isEmpty() && maxQue.peekLast() < curVal) {
maxQue.pollLast();
}
maxQue.add(curVal);
// 左移
while (!minQue.isEmpty() && !maxQue.isEmpty() && maxQue.peek() - minQue.peek() > limit) {
if (nums[left] == minQue.peek()) {
minQue.poll();
}
if (nums[left] == maxQue.peek()) {
maxQue.poll();
}
left++;
}
right++;
res = Math.max(res, right - left);
}
return res;
}
}
30、删除子数组的最大得分
class Solution {
public int maximumUniqueSubarray(int[] nums) {
Set set = new HashSet<>();
int maxSum = 0;
int sum = 0;
int left = 0;
int right = 0;
while (right < nums.length) {
sum += nums[right];
while (set.contains(nums[right])) {
set.remove(nums[left]);
sum -= nums[left++];
}
maxSum = Math.max(maxSum, sum);
set.add(nums[right++]);
}
return maxSum;
}
}
31、跳跃游戏 VI,涉及动态规划,遗留
32、至多包含两个不同字符的最长子串
class Solution {
// 用hashmap,维护窗口字符数量和个数
public int lengthOfLongestSubstringTwoDistinct(String s) {
int N = s.length();
int res = 0;
Map map = new HashMap<>();
int left = 0;
int right = 0;
while (right < N) {
char c = s.charAt(right);
map.put(c, map.getOrDefault(c, 0) + 1);
while (map.size() > 2) {
char leftChar = s.charAt(left);
map.put(leftChar, map.get(leftChar) - 1);
if (map.get(leftChar) == 0) map.remove(leftChar);
left++;
}
right++;
res = Math.max(res, right - left);
}
return res;
}
}
33、795. 区间子数组个数
// 计算结果时考虑需要增加的子数组的个数是多少
class Solution {
public int numSubarrayBoundedMax(int[] nums, int left, int right) {
int N = nums.length;
int i = 0;
int j = 0;
int res = 0;
int cnt = 0;
while(j < N) {
int cur = nums[j++];
if (cur > right) {
i = j;
cnt = 0;
} else if (cur < left){
res += cnt;
} else {
res += j - i;
cnt = j - i; // 一开始写成了cnt++,忽略了一些情况。只要其中包含一个满足条件的最大数即可。
}
}
return res;
}
}
34、1358. 包含所有三种字符的子字符串数目
class Solution {
// 自己想到的,每次加上以左边为起点的所有子字符串
public int numberOfSubstrings(String s) {
int N = s.length();
int[] cnt = new int[3];
int ans = 0;
int left = 0;
int right = 0;
while (right < N) {
cnt[s.charAt(right)-'a']++;
while(cnt[0]>0 && cnt[1]>0 && cnt[2]>0) {
ans += N - right; // 此时如果满足,则加上后面的也满足。
cnt[s.charAt(left++)-'a']--;
}
right++;
}
return ans;
}
}
35、467. 环绕字符串中唯一的子字符串, dp解法
36、340. 至多包含 K 个不同字符的最长子串
// 典型滑动窗口题,一次过100%
class Solution {
public int lengthOfLongestSubstringKDistinct(String s, int k) {
int N = s.length();
int[] cnt = new int[128];
int left = 0;
int right = 0;
int sum = 0;
int res = 0;
while (right < N) {
char cur = s.charAt(right++);
if (cnt[cur]++ == 0) sum++;
while (sum > k) {
if (cnt[s.charAt(left++)]-- == 1) sum--;
}
res = Math.max(res, right - left);
}
return res;
}
}