文章目录
双指针
1.快慢指针
- 判定链表中是否含有环
- 已知链表中含有环,返回这个环的起始位置
- 寻找链表的中点
- 寻找链表的倒数第 k 个元素
2.左右指针
- 二分查找
- 两数之和
- 反转数组
- 滑动窗口
3.其他
- 原地删除
- 删除数组中的重复项
滑动窗口
滑动窗口算法的思路是这样:
- 我们在字符串S中使用双指针中的左右指针技巧,初始化
left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。 - 我们先不断地
增加right
指针扩大窗口
[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)。 - 此时,我们
停止增加right
,转而不断增加left
指针缩小窗口
[left, right),直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同时,每次增加left,我们都要更新一轮结果。 - 重复第 2 和第 3 步,直到right到达字符串S的尽头。
int left = 0, right = 0;
while (right < s.size()) {`
// 增大窗口
window.add(s[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
}
}
初始化window和need两个哈希表,记录窗口中的字符和需要凑齐的字符。
76. 最小覆盖子串
76. 最小覆盖子串
题解: leetcode题解
//使用数组代替map
class Solution {
public String minWindow(String s, String t) {
if (s == null || s == "" || t == null || t == ""
|| s.length() < t.length()) {
return "";
}
//用来统计t中每个字符出现次数
int[] needs = new int[128];
//用来统计滑动窗口中每个字符出现次数
int[] window = new int[128];
for (int i = 0; i < t.length(); i++) {
needs[t.charAt(i)]++;
}
int left = 0;
int right = 0;
String res = "";
//目前有多少个字符
int count = 0;
//用来记录最短需要多少个字符。
int minLength = s.length() + 1;
while (right < s.length()) {
char ch = s.charAt(right);
window[ch]++;
if (needs[ch] > 0 && needs[ch] >= window[ch]) {
count++;
}
//移动到不满足条件为止
while (count == t.length()) {
ch = s.charAt(left);
if (needs[ch] > 0 && needs[ch] >= window[ch]) {
count--;
}
if (right - left + 1 < minLength) {
minLength = right - left + 1;
res = s.substring(left, right + 1);
}
window[ch]--;
left++;
}
right++;
}
return res;
}
}
567. 字符串的排列
class Solution {
public boolean checkInclusion(String s1, String s2) {
int len1 = s1.length(), len2 = s2.length();
if (len1 > len2) return false;
int[] ch_count1 = new int[26], ch_count2 = new int[26];
for (int i = 0; i < len1; i++) {
ch_count1[s1.charAt(i) - 'a']++;
ch_count2[s2.charAt(i) - 'a']++;
}
for (int i = len1; i < len2; i++) {
if (isEqual(ch_count1, ch_count2)) return true;
ch_count2[s2.charAt(i - len1) - 'a']--;
ch_count2[s2.charAt(i) - 'a']++;
}
return isEqual(ch_count1, ch_count2);
}
private boolean isEqual(int[] ch_count1, int[] ch_count2) {
for (int i = 0; i < 26; i++)
if (ch_count1[i] != ch_count2[i])
return false;
return true;
}
}
438. 找到字符串中所有字母异位词
438. 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 用于返回字母异位词的起始索引
List<Integer> res = new ArrayList<>();
// 用 map 存储目标值中各个单词出现的次数
HashMap<Character, Integer> map = new HashMap<>();
for (Character c : p.toCharArray()) map.put(c, map.getOrDefault(c, 0) + 1);
// 用另外一个 map 存储滑动窗口中有效字符出现的次数
HashMap<Character, Integer> window = new HashMap<>();
int left = 0; // 左指针
int right = 0; // 右指针
int valid = p.length(); // 只有当 valid == 0 时,才说明 window 中包含了目标子串
while (right < s.length()) {
// 如果目标子串中包含了该字符,才存入 window 中
char ch = s.charAt(right);
if (map.containsKey(ch)) {
window.put(ch, window.getOrDefault(ch, 0) + 1);
// 只有当 window 中该有效字符数量不大于map中该字符数量,才能算一次有效包含
if (window.get(ch) <= map.get(ch)) {
valid--;
}
}
// 如果 window 符合要求,即两个 map 存储的有效字符相同,就可以移动左指针了
// 但是只有二个map存储的数据完全相同,才可以记录当前的起始索引,也就是left指针所在位置
while (valid == 0) {
if (right - left + 1 == p.length()) res.add(left);
// 如果左指针指的是有效字符,需要更改 window 中的 key 对应的 value
// 如果 有效字符对应的数量比目标子串少,说明无法匹配了
char c = s.charAt(left);
if (map.containsKey(c)) {
window.put(c, window.get(c) - 1);
if (window.get(c) < map.get(c)) {
valid++;
}
}
left++;
}
right++;
}
return res;
}
}
class Solution {
public List<Integer> findAnagrams(String s, String p) {
char[] arrS = s.toCharArray();
char[] arrP = p.toCharArray();
// 接收最后返回的结果
List<Integer> ans = new ArrayList<>();
// 定义一个 needs 数组来看 arrP 中包含元素的个数
int[] needs = new int[26];
// 定义一个 window 数组来看滑动窗口中是否有 arrP 中的元素,并记录出现的个数
int[] window = new int[26];
// 先将 arrP 中的元素保存到 needs 数组中
for (int i = 0; i < arrP.length; i++) {
needs[arrP[i] - 'a'] += 1;
}
// 定义滑动窗口的两端
int left = 0;
int right = 0;
// 右窗口开始不断向右移动
while (right < arrS.length) {
int curR = arrS[right] - 'a';
right++;
// 将右窗口当前访问到的元素 curR 个数加 1
window[curR] += 1;
// 当 window 数组中 curR 比 needs 数组中对应元素的个数要多的时候就该移动左窗口指针
while (window[curR] > needs[curR]) {
int curL = arrS[left] - 'a';
left++;
// 将左窗口当前访问到的元素 curL 个数减 1
window[curL] -= 1;
}
// 这里将所有符合要求的左窗口索引放入到了接收结果的 List 中
if (right - left == arrP.length) {
ans.add(left);
}
}
return ans;
}
}
单调栈
单调栈主要是用来解决下一个更好的数的问题「Next Greater Number」。
496. 下一个更大元素 I
给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
示例 1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。
对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。
对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。
这个题可以直接暴力,也可以通过单调栈来做
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Stack<Integer> stack = new Stack();
Map<Integer, Integer> map = new HashMap();
int[] res = new int[nums1.length];
//单调栈代码
for(int i = 0; i < nums2.length; i++){
while(!stack.isEmpty() && nums2[i] > stack.peek()){
map.put(stack.pop(),nums2[i]);
}
stack.push(nums2[i]);
}
while(!stack.empty()){ //将不存在下一个更大的数放入map
map.put(stack.pop(),-1);
}
for(int i = 0; i < nums1.length; i++){
res[i] = map.get(nums1[i]);
}
return res;
}
}
739. 每日温度
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
class Solution {
public int[] dailyTemperatures(int[] T) {
Stack<Integer> stack = new Stack();
int[] res = new int[T.length];
for(int i = 0; i < T.length; i++){
while(!stack.isEmpty() && T[i] > T[stack.peek()]){
int tmp = stack.pop();
res[tmp] = i - tmp;
}
stack.push(i);
}
return res;
}
}
这个题和上面那个题都是用的单调栈,唯一的区别就是上面是记录的值,下面记录的索引。
单调队列
其实栈或队列无关紧要,和单调维护方式一样,都是使用方法罢了。
239. 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
单调栈的思维很精巧也很高效,是比较高级的栈维护技巧。以下题目类似:
907. 子数组的最小值之和
503. 下一个更大元素 II
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int lo = 0, hi = 0;
int[] res = new int[nums.length - k + 1];
ArrayDeque<Integer> q = new ArrayDeque<>();
while (hi < nums.length) {
if (hi - lo < k) {
offer(q, nums, hi++);
} else {
res[lo] = nums[q.getFirst()];
if (q.getFirst() == lo++) {
q.removeFirst();
}
}
}
res[lo] = nums[q.getFirst()];
return res;
}
// monotonous queue
private void offer(ArrayDeque<Integer> q, int[] nums, int i) {
while (!q.isEmpty() && nums[q.getLast()] < nums[i]) {
q.removeLast();
}
q.offer(i);
}
}
参考
如果当前元素比队列的最后一个元素大,那么就将最后一个元素出队,重复这步直到当前元素小于队列的最后一个元素或者队列为空。进行下一步。
如果当前元素小于等于队列的最后一个元素或者队列为空,那么就直接将当前元素入队。
按照上边的方法添加元素,队列中的元素就刚好是一个单调递减的序列,而最大值就刚好是队头的元素。
当队列的元素等于窗口的大小的时候,由于添加元素的时候我们进行了出队操作,所以我们不能像解法二那样每次都删除第一个元素,需要先判断一下队头元素是否是我们要删除的元素。
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> max = new ArrayDeque<>();
int n = nums.length;
if (n == 0) {
return nums;
}
int result[] = new int[n - k + 1];
int index = 0;
for (int i = 0; i < n; i++) {
if (i >= k) {
if (max.peekFirst() == nums[i - k]) {
max.removeFirst();
}
}
while (!max.isEmpty() && nums[i] > max.peekLast()) {
max.removeLast();
}
max.addLast(nums[i]);
if (i >= k - 1) {
result[index++] = max.peekFirst();
}
}
return result;
}