【LeetCode热题100】打卡第17天:接雨水&全排列&旋转图像

【LeetCode热题100】打卡第17天:接雨水&全排列&旋转图像

⛅前言

大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!

精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、字典树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。

博客主页💖:知识汲取者的博客

LeetCode热题100专栏🚀:LeetCode热题100

Gitee地址📁:知识汲取者 (aghp) - Gitee.com

Github地址📁:Chinafrfq · GitHub

题目来源📢:LeetCode 热题 100 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台

PS:作者水平有限,如有错误或描述不当的地方,恳请及时告诉作者,作者将不胜感激

接雨水

🔒题目

原题链接:42. 接雨水

image-20230609090940595

🔑题解

  • 解法一:按行求解(超出时间限制)

    按行求解主要思路:自底向上迭代得到最终的答案,难点在于左右边界的判定,左右边界需要使用一个变量来判定,简单粗暴

    • Step1:获取柱子的最大高度,用于遍历

    • Step2:遍历每一层中所有的柱子,如果出现了左边界,就使用一个临时变量(temp)对雨水体积进行自增1;如果同时出现了右边界,就将临时变量记录的雨水体积加入到结果变量(ans)中。其中左边界和右边界的判断是同一个条件,即当前柱子的高度大于等于当前层的高度(核心步骤

    • Step3:经过Step1和Steap2的迭代,最终就能够得到最终的结果

    下面是上述步骤的一个简单图解:

    image-20230620144457237

    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    class Solution {
        public int trap(int[] height) {
            int ans = 0;
            // 获取柱子的最大高度
            int maxHeight = getMaxHeight(height);
            // 遍历每一层高度
            for (int i = 1; i <= maxHeight; i++) {
                boolean f = false; // 用于判断是否存在左边界 true-存在左边界
                int temp = 0; // 用于记录当前边界能够接纳的雨水
                // 遍历每一个柱子
                for (int j = 0; j < height.length; j++) {
                    if (f && height[j] < i) {
                        // 左侧出现了边界,同时当前柱子的高度小于当前层的高度,说明可以接纳雨水
                        temp++;
                    }
                    if (height[j] >= i) {
                        // 当前柱子的高度大于等于当前层的高度
                        // 说明当前柱子可以作为左边界,同时也说明当前柱子可以作为上一个左边界的右边界
                        // 所以需要将上一个左边界记录的雨水体积加到 ans 中,同时重新开始记录
                        f = true;
                        ans += temp;
                        temp = 0;
                    }
                }
            }
            return ans;
        }
    
        private int getMaxHeight(int[] height) {
            int maxHeight = Integer.MIN_VALUE;
            for (int i = 0; i < height.length; i++) {
                if (height[i] > maxHeight) {
                    maxHeight = height[i];
                }
            }
            return maxHeight;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ∗ m ) O(n*m) O(nm)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数, m m m为柱子的最大高度

  • 解法二:按列求解(通过)

    案列求解的思路就要比按行求解简单多路,先枚举每一个柱子,分别找出当前柱子左右两侧的最大高度的柱子,然后分别比较两者取较小者,因为能接多少雨水只跟左右柱子的最短高度有关。

    image-20230620181624222

    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    class Solution {
        public int trap(int[] height) {
            int ans = 0;
            // 遍历所有的柱子
            for (int i = 0; i < height.length; i++) {
                // 寻找左侧最大高度
                int leftMax = Integer.MIN_VALUE;
                for (int j = i - 1; j >= 0; j--) {
                    if (height[j] > leftMax) {
                        leftMax = height[j];
                    }
                }
                // 寻找右侧最大高度
                int rightMax = Integer.MIN_VALUE;
                for (int j = i + 1; j < height.length; j++) {
                    if (height[j] > rightMax) {
                        rightMax = height[j];
                    }
                }
                // 计算当前柱子上能够接纳的雨水
                int min = Math.min(leftMax, rightMax);
                if (min > height[i]) {
                    // 只有较矮的柱子大于当前柱子高度才能够接纳雨水
                    ans += min - height[i];
                }
            }
            return ans;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

    很奇怪,这一个时间复杂度高达 n 2 n^2 n2居然也能过,解法一反而过不了,这个可能跟LeetCode官方提供的测试数据有关系吧!如果最大高度是大于柱子的个数,则解法二更优,最大高度小于柱子的数量,则解法一更优,很显然测试数据肯定是柱子的最大高度大于柱子的个数的,所以才导致解法一过不了,解法二能过O(∩_∩)O

  • 解法三:动态规划

    在解法二中,我们每次求解左右两侧的最大高度,都需要重新遍历一遍,很显然这存在很多重复的计算,所以我们可以使用动态规划来省略这些重复的计算,我们可以创建两个数组 left[i](表示第i个柱子左侧的最大高度) 和 right[i](表示第i给柱子右侧的最大高度),这样我们就只需要遍历一遍,就能得到每个柱子左侧的最大高度和右侧的的最大高度,话不多说,开整。

    动态规划的核心就是得到动态转移方程,这里涉及到两个动态转移方程:

    • 对于left[i],也就是第 i 个柱子左侧的最大高度,有两种情况可供选择,第一种情况,左侧最大高度就是左侧的柱子,第二种情况,左侧做大高度是左侧柱子的最大高度,总结起来就是:

      left[i] = Math.max(left[i-1], height[i-1]);
      
    • 对于right[i],也就是第 i 个柱子右侧的最大高度,同样有两种情况可供选择,第一种情况,右侧最大高度就是右侧的柱子,第二种情况,右侧最大高度是右侧柱子的最大高度,总结起来就是:

      right[i] = Math.max(right[i+1], heigth[i+1]);
      

    PS:解法三本质上是对解法二的一个优化

    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    class Solution {
        public int trap(int[] height) {
            int ans = 0;
            int[] left = new int[height.length];
            int[] right = new int[height.length];
            // 从左往右遍历,得到每一个柱子的左侧最大高度
            for (int i = 1; i < height.length; i++) {
                left[i] = Math.max(left[i - 1], height[i - 1]);
            }
            // 从右往左遍历,得到每一个柱子的右侧最大高度
            for (int i = height.length - 2; i >= 0; i--) {
                right[i] = Math.max(right[i + 1], height[i + 1]);
            }
            // 从左往右遍历每一个柱子
            for (int i = 0; i < height.length; i++) {
                // 得到当前柱子左右最大高度的较小者
                int min = Math.min(left[i], right[i]);
                if (min > height[i]) {
                    // 当 当前柱子左右最大高度的较小者 比当前柱子的高度还要高时
                    // 才计算当前柱子可接纳的雨水(如果最小者都小于当前柱子高度,则不能接纳雨水)
                    ans += min - height[i];
                }
            }
            return ans;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为数组中元素的个数

    代码优化:上面的动态规划,相较于按列求解,很大程度上优化了时间复杂度(从 O ( n 2 ) O(n^2) O(n2)优化到 O ( n ) O(n) O(n)),但是仍有优化空间。我们可以发现从左往右的遍历过程中,我们可以计算出left[i],并不需要单独使用一个数组来存储left[i],这样我们就可以省略left数组,改用一个简单的变量来记录左侧最大高度O(∩_∩)O

    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    class Solution {
        public int trap(int[] height) {
            int ans = 0;
            int left = Integer.MIN_VALUE;
            int[] right = new int[height.length];
            // 从右往左遍历,得到每一个柱子的右侧最大高度
            for (int i = height.length - 2; i >= 0; i--) {
                right[i] = Math.max(right[i + 1], height[i + 1]);
            }
            // 从左往右遍历每一个柱子
            for (int i = 0; i < height.length; i++) {
                // 计算出左侧最大高度
                left = Math.max(left, height[i]);
                // 得到当前柱子左右最大高度的较小者
                int min = Math.min(left, right[i]);
                if (min > height[i]) {
                    // 当 当前柱子左右最大高度的较小者 比当前柱子的高度还要高时
                    // 才计算当前柱子可接纳的雨水(如果最小者都小于当前柱子高度,则不能接纳雨水)
                    ans += min - height[i];
                }
            }
            return ans;
        }
    }
    
  • 解法四:单调栈

    单调栈应该是这题的究极解法了,单调栈的应用场景:在一群数组中,我们总数要找比当前数组更小或更大的一个元素,就可以考虑使用单调栈,单调栈分为单调递增栈单调递减栈两类,很显然本题考察的单调递减栈,因为只有当左侧柱子的高度大于当前柱子的高度,才能够接纳雨水,递增的柱子是绝对不可能接纳雨水的(这个过程类似于解法一)

    主要实现步骤:

    • Step1:遍历所有的柱子
    • Step2:构建递减栈。栈中的柱子是递减的,只要出现比上一个柱子还要高的柱子,则说明出现了右边界,说明此时存在接纳雨水的可能,所以我们需要进行计算

    PS:关于单调栈相关题目还可以参考【打卡第24天:柱状图中最大的矩形】

    这里还是画一个草图来加以理解吧,画个图相信一看就能够明白,千言万语抵不过一张图来的直观

    假设:height[]={3,2,1,2},则程序运行流程如下

    image-20230620193134765

    image-20230620194058044

    image-20230620194208618

    最终我们可以计算出ans的结果为1,所以height[]={3,2,1,2}时能最多接纳雨水的体积是1

    import java.util.ArrayDeque;
    import java.util.Deque;
    
    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    class Solution {
        public int trap(int[] height) {
            int ans = 0;
            Deque<Integer> stack = new ArrayDeque<>();
            for (int i = 0; i < height.length; i++) {
                while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                    // 当前柱子比上一个柱子要高,说明出现了右边界
                    // 则计算当前右边界与 最开始入栈的那个柱子(也就是左边界,栈是递减的) 构成的容器能容纳的雨水
                    int h = height[stack.pop()]; // 出栈得到当前准备接纳雨水的柱子高度
                    if (stack.isEmpty()) {
                        // 如果此时栈为空,则说明只剩最后两个柱子了(一个左边界,一个右边界)
                        // 无法构成接纳雨水的容器,直接退出循环
                        break;
                    }
                    // 计算左右边界之间的距离
                    int distance = i - stack.peek() - 1;
                    // 获取左右边界中较短的高度
                    int min = Math.min(height[stack.peek()], height[i]);
                    // 将当前柱子接纳的雨水加入 ans 中
                    ans += distance * (min - h);
                }
                stack.push(i);
            }
            return ans;
        }
    }
    

参考题解

在此感谢大佬的究极题解,让我如沐春风,醍醐灌顶,不由得沉浸其中,真的太棒啦🤭

全排列

🔒题目

原题链接:46. 全排列

image-20230609091131337

🔑题解

  • 解法一:递归+回溯

    看到这种全排列,一下就想到了BFS,这类题型算是很常规的题目了。这里就不再过多赘述了,毕竟时间宝贵

    import java.util.*;
    
    /**
     * @author ghp
     * @title 全排列
     */
    class Solution {
        public List<List<Integer>> permute(int[] nums) {
            List<List<Integer>> ans = new ArrayList<>(10);
            Deque<Integer> path = new LinkedList<>();
            boolean[] vis = new boolean[nums.length];
            bfs(ans, nums, path, vis);
            return ans;
        }
    
        private void bfs(List<List<Integer>> ans, int[] nums, Deque<Integer> path, boolean[] vis) {
            // 递归结束条件
            if (path.size() == nums.length) {
                // 此时已经遍历到最后一层了
                ans.add(new ArrayList<>(path));
                return;
            }
            for (int i = 0; i < nums.length; i++) {
                if (!vis[i]) {
                    // 当前元素没有没被遍历,则添加到path中
                    path.addLast(nums[i]);
                    vis[i] = true;
                    bfs(ans, nums, path, vis);
                    // 恢复现场,用于回溯
                    vis[i] = false;
                    path.removeLast();
                }
            }
        }
    }
    

    备注:这里有一点小疑问?明明LinkedList删除和新增的性能要高于ArrayList,但是为什么使用ArrayList提交能够击败100%,但是使用LinkedList只能击败92%?有懂的大佬吗,希望能够为我解答一下疑惑w(゚Д゚)w

  • 解法二

    这个解法是LeetCode官方提供的,我也是参考官方给出的思路,重新写了一遍。他是通过不断两两交换实现,思路比较新颖,十分值得借鉴。

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * @author ghp
     * @title 全排列
     */
    class Solution {
        public List<List<Integer>> permute(int[] nums) {
            List<List<Integer>> ans = new ArrayList<>(10);
            List<Integer> path = Arrays.stream(nums).boxed().collect(Collectors.toList());
            bfs(ans, path, 0);
            return ans;
        }
    
        private void bfs(List<List<Integer>> ans, List<Integer> path, int step) {
            // 递归结束条件
            if (step == path.size()) {
                // 此时已经遍历到最后一层了
                ans.add(new ArrayList<>(path));
                return;
            }
            for (int i = step; i < path.size(); i++) {
                // 交换当前层元素(也就是第step个元素)与当前第i个元素
                Collections.swap(path, step, i);
                // 遍历下一层
                bfs(ans, path, step+1);
                // 恢复现场,用于回溯
                Collections.swap(path, step, i);
            }
        }
    }
    

旋转图像

🔒题目

原题链接:48. 旋转图像

image-20230609105036285

🔑题解

  • 解法一:通过找出规律,进行替换

    通过枚举,可以发现替换的规律,如下所示:

    image-20230609105427980
    ( 0 , 0 ) → ( 0 , n − 1 ) ( 0 , 1 ) → ( 1 , n − 1 ) ( 0 , 2 ) → ( 2 , n − 1 ) ( 1 , 0 ) → ( 0 , n − 2 ) ( 1 , 1 ) → ( 1 , n − 2 ) ( 1 , 2 ) → ( 2 , n − 2 ) . . . (0,0) → (0, n-1) \\ (0,1) → (1, n-1) \\ (0,2) → (2, n-1) \\ \\ (1,0) → (0, n-2) \\ (1,1) → (1, n-2) \\ (1,2) → (2, n-2) \\ ... (0,0)(0,n1)(0,1)(1,n1)(0,2)(2,n1)(1,0)(0,n2)(1,1)(1,n2)(1,2)(2,n2)...
    经过枚举,我们可以得到替换公式: ( i , j ) → ( j , n − 1 − i ) (i, j)→(j, n-1-i) (i,j)(j,n1i)

    import java.util.Arrays;
    
    /**
     * @author ghp
     * @title 旋转图像
     */
    class Solution {
        public void rotate(int[][] matrix) {
            int n = matrix.length;
            int[][] tempArr = new int[matrix.length][matrix.length];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    int t = matrix[i][j];
                    tempArr[j][n-1-i] = matrix[i][j];
                }
            }
            for (int i = 0; i < n; i++) {
                matrix[i] = Arrays.copyOf(tempArr[i], tempArr[i].length);
            }
        }
    }
    

    复杂度分析

    时间复杂度: O ( n 2 ) O(n^2) O(n2)

    空间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 解法二:先水平翻转,后根据主对角线翻转

    不得不佩服,这个思路真的是太巧妙了😫我怎么就想不到呢

    class Solution {
        public void rotate(int[][] matrix) {
            int n = matrix.length;
            // 水平翻转
            for (int i = 0; i < n / 2; ++i) {
                for (int j = 0; j < n; ++j) {
                    int temp = matrix[i][j];
                    matrix[i][j] = matrix[n - i - 1][j];
                    matrix[n - i - 1][j] = temp;
                }
            }
            // 主对角线翻转
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < i; ++j) {
                    int temp = matrix[i][j];
                    matrix[i][j] = matrix[j][i];
                    matrix[j][i] = temp;
                }
            }
        }
    }
    
    作者:LeetCode-Solution
    链接:https://leetcode.cn/problems/rotate-image/solution/xuan-zhuan-tu-xiang-by-leetcode-solution-vu3m/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    

    复杂度分析

    时间复杂度: O ( n 2 ) O(n^2) O(n2)

    空间复杂度: O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

知识汲取者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值