LeetCode 热题 100 题解(二):双指针部分(2)| 滑动窗口部分(1)

题目四:接雨水(No. 43)

题目链接:https://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-100-liked

难度:困难


给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

在这里插入图片描述

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105

题解

首先来观察对于一个格子来说,它的容量是多少:

在这里插入图片描述

比如上图中的红色部分,它存储水的容量其实就是 左边的最大高度右边的最大高度 的 最小值,减去这里的格子高度(案例中为 0),在上面的案例中,左边的最高高度为 2,右边的最高高度为 3。

所以本题的暴力解法就比较好想了,找到其左边最大高度,再找到其右边的最大高度,通过上面的规律进行运算。

class Solution {
    public int trap(int[] height) {
        int res = 0;
        for (int i = 1; i < height.length; i++) {
            int left = i - 1;
            int right = i + 1;
            int maxLeft = 0;
            int maxRight = 0;
            while (left >= 0) { // 去左边寻找最大的
                maxLeft = Math.max(maxLeft, height[left--]);
            }
            while (right < height.length) { // 去右边寻找最大的
                maxRight = Math.max(maxRight, height[right++]);
            }
            int temp = Math.min(maxLeft, maxRight) - height[i]; // 执行运算逻辑
            res += temp > 0 ? temp : 0;
        }
        return res;
    }
}

这个方法的时间复杂度无疑是非常高的,因为每遍历到一个节点的时候都需要遍历整张表,接下来考虑如何对上面的方法进行优化。

首先来看左边的最大值,考虑一下,我们真的有必要每次去遍历来求得最大值吗?

可以将之前遍历过的节点的最大值保存下来,比如说这么一个高度数组 4,2,0,3,2,5 ,我们从第二个数字开始遍历,当执行完这个数字的逻辑之后,就可以拿它和之前的最大高度作比较,用作下一个节点的左边最大高度,这样不断更新就能保证每次求得的都是准确的。

class Solution {
    public int trap(int[] height) {
        int res = 0;
        int maxLeft = height[0];
        for (int i = 1; i < height.length; i++) {
            int left = i - 1;
            int right = i + 1;
            int maxRight = 0;
            while (right < height.length) { // 找到右边最大高度
                maxRight = Math.max(maxRight, height[right++]);
            }
            int temp = Math.min(maxLeft, maxRight) - height[i];
            res += temp > 0 ? temp : 0;
            maxLeft = Math.max(height[i], maxLeft); // 维护一个更新的 maxLeft
        }
        return res;
    }
}

既然对左边可以进行优化,那对右边是否可以优化呢?

因为是从前向后遍历的,所以右边肯定不可能像左边这样简单的更新;所以可以考虑提前处理,如果从后向前遍历的话,就可以类似左边那样求出 每个节点 的右最大值,所以可以在进入循环之前,先遍历一次高度数组,求出一个存储着每个节点右最大值的数组。

class Solution {
    public int trap(int[] height) {
        int res = 0;
        int maxLeft = height[0];
        int k = 2;
        int[] maxRight = new int[height.length];
        int m = height[maxRight.length - 1];
        for (int j = maxRight.length - 2; j >= 0; j--) { // 从后向前遍历,维护一个存储每个节点最大值的数组
            maxRight[j] = m;
            m = Math.max(m, height[j]);
        }
        for (int i = 1; i < height.length; i++) {
            int temp = Math.min(maxLeft, maxRight[i]) - height[i]; // 使用数组中的值
            res += temp > 0 ? temp : 0;
            maxLeft = Math.max(height[i], maxLeft);
        }
        return res;
    }
}

题目一:无重复字符的最长字串(No. 3)

题目链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/?envType=study-plan-v2&envId=top-100-liked

题目难度:中等


给定一个字符串 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 由英文字母、数字、符号和空格组成

题解

首先来看第一种方法,提到最长子串,想到的第一个方法就是动态规划。

先来尝试找一下状态,题目中问的是没有重复字符的最长的子串,可以将某个节点的状态定义为:以这个节点为结尾的,不含有重复字符的字串的最大长度,这种方式在处理子串的问题中非常常见。

再来思考这个状态应该如何转移。对于一个节点其实有这些选择

  • 从这个节点开始(重新开始)
  • 接续前面的子串

本题接续前面子串的时候要考虑不能含有重复的元素,所以并不像之前的题目那样就简单的做一个加一,但总而言之状态是可以转移的,那就来尝试一下。

先来确定 dp 数组,根据上面的推理,dp 数组只需要一维就即可, dp[i] 的含义为以 s.charAt(i) 为结尾的,不含有重复子串的最大长度。

然后就是确定状态转移方程了,对于一个下标为 x 的节点,它所代表的子串就是从 x - dp[i] 到 x 这个范围内内容;比如我们现在遍历到了下标为 x + 1 的节点,如果想要接续上前面的内容,就需要上面的范围中不含有 s.charAt(x + 1) 这个字符,此时需要通过遍历来确定是否有这个字符。

如果没有发现,那 dp[i] = dp[i - 1] + 1

如果发现了重复的字符串,比如下面的情况

在这里插入图片描述

此时要求得的是绿色的 b 的值,前一个节点最大子串的长度为 3,但当遍历到图中粉色的节点的时候发现了重复,此时 b 的长度应该为多少呢?是 2
画个图来更直观的了解一下:

在这里插入图片描述

当发现了相同的节点之后,这个值应该就是从被发现的位置索引 + 1 一直到新节点的长度

此时就确定了两种情况的递推公式,下面来讨论一下初始化

因为需要前一个节点的情况,所以 dp[0] 是要被初始化的,按照上面的含义,该位置应该被初始化为 1。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length() == 0) return 0;
        int[] dp = new int[s.length()];
        dp[0] = 1;
        int res = 1;
        for (int i = 1; i < s.length(); i++) {
            int field = dp[i - 1]; // 需要查询重复的范围
            char c = s.charAt(i);
            boolean findSame = false; // 是否找到了相同的节点
            while (field > 0) {
                int index = i - (field--);
                if (s.charAt(index) == c) { // 找到了相同的节点
                    int m = i - index - 1 + 1;
                    dp[i] = m;
                    findSame = true;
                    break;
                }
            }
            if (!findSame) dp[i] = dp[i - 1] + 1;
            res = Math.max(dp[i], res); // 动态更新 res
        }
        return res;
    }
}

接下来来看一下滑动窗口方案

所谓的滑动窗口其实就是用索引去限定一个范围,通过不断移动左范围和右范围的方式来调整窗口的范围,从而收集信息。

当滑动窗口的内容满足要求的时候就不断 移动右边界,来扩展滑动窗口的大小,如果发现不满足要求,也就是出现了重复的情况,就通过 收缩左边界 最终使得窗口内的元素始终满足要求,然后记录滑动窗口的长度。

滑动窗口方法的核心和上面的动态规划方法相同,都是通过遍历每个元素为 右节点 的情况,来不断更新最大值。

class Solution {
    char[] charArray;
    public int lengthOfLongestSubstring(String s) {
        if (s.length() == 0) return 0;
        charArray = s.toCharArray();
        int left = 0, right; // 左范围,右范围
        int res = 1;
        for (int i = 1; i < charArray.length; i++) {
            right = i;
            while (examine(left, right, charArray[i])) {
                left++;
            }
            res = Math.max(right - left + 1, res); // 更新结果
        }
        return res;
    }
    // 检测范围内是否有和此时右范围相同的元素
    public boolean examine(int left, int right, char c) {
        for (int i = left; i <= right - 1; i++) {
            if (charArray[i] == c) return true;
        }
        return false;
    }
}
  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值