算法学习之双指针、滑动窗口、单调队列、单调栈

双指针

1.快慢指针

  • 判定链表中是否含有环
  • 已知链表中含有环,返回这个环的起始位置
  • 寻找链表的中点
  • 寻找链表的倒数第 k 个元素

2.左右指针

  • 二分查找
  • 两数之和
  • 反转数组
  • 滑动窗口

3.其他

  • 原地删除
  • 删除数组中的重复项

滑动窗口

滑动窗口算法的思路是这样:

  1. 我们在字符串S中使用双指针中的左右指针技巧,初始化left = right = 0,把索引左闭右开区间[left, right)称为一个「窗口」。
  2. 我们先不断地增加right指针扩大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)。
  3. 此时,我们停止增加right,转而不断增加left指针缩小窗口[left, right),直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同时,每次增加left,我们都要更新一轮结果。
  4. 重复第 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. 字符串的排列

567. 字符串的排列
题解:leetcode题解

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;
    }
}

leetcode题解

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;
    }
}

3. 无重复字符的最长子串

单调栈

单调栈主要是用来解决下一个更好的数的问题「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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值