美文网首页java学习之路
leetCode进阶算法题+解析(三十一)

leetCode进阶算法题+解析(三十一)

作者: 唯有努力不欺人丶 | 来源:发表于2020-03-30 19:33 被阅读0次

    二叉树的最近公共祖先

    题目:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
    题目截图

    思路:额,分两种情况:一种是pq存不存在上下级关系,存在则答案就是上级的那个。不存在则找最近的交叉点。我目前有个想法:首先判断这两个节点在根节点的左右哪边。如果是一边则继续判断root.left或root.right。如果是两边则直接返回根节点。虽然我觉得这样做可能时间复杂度比较高,但是目前为止没有太好的办法,我先去实现再说别的。
    卧槽,居然实现了!!!我自己都惊呆了啊,毕竟写着写着觉得越来越不对。。。哈哈,我直接贴代码:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            if(root == p || root == q) return root;
            //都在左递归左
            if(isContains(root.left,p) && isContains(root.left,q)){
                return lowestCommonAncestor(root.left,p,q);
            }
            //都在右递归右
            if(isContains(root.right,p) && isContains(root.right,q)){
                return lowestCommonAncestor(root.right,p,q);
            }
            //一左一右,返回当前节点
            return root;
        }
        public boolean isContains(TreeNode root,TreeNode sub){
            if(root == null) return false;
            if(root == sub) return true;
            return isContains(root.right,sub) || isContains(root.left,sub);
        }
    }
    
    性能截图

    讲真,我想知道我后面那百分之五的兄弟都是什么思路啊。哈哈,这个写着写着发现了问题,该说不说我绝对是做了double的判断啊。应该一次递归实现?我去试着优化了。
    好了, 我现在感觉贼准确。之前思路还凑合,但是处理绝对有问题,我要先用contains遍历一遍,再用主方法的递归再遍历一遍。现在合成一次遍历了。我直接贴代码:

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode(int x) { val = x; }
     * }
     */
    class Solution {
        public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            if(root == null) return null;
            if(root == p || root == q) return root;
            TreeNode left = lowestCommonAncestor(root.left,p,q);
            TreeNode right = lowestCommonAncestor(root.right,p,q);
            if(left != null && right != null) return root;
            if(left != null) return left;
            if(right != null) return right;
            return null;
        }
    }
    

    如上代码,如果p,q分布在根节点两边,则left,right都不是空,则返回根节点。否则说明分布在一边。然后分别以左右节点为根遍历,直到遍历到分布在某一节点的两边(包括自身是其中一个节点)。逻辑其实很清楚,尽量去理解就行了,全靠脑补。
    多说两句,其实二叉树这一块的题我觉得一个好的解决办法就是脑补,毕竟代码中哪怕调试也不是很明了清晰的。或者随意画画图,只要自己理解就好。树这块的题目我觉得不是很难,毕竟不是客观基础上的不会,不知道,比较不偏向于数学吧。多尝试,多想想,就这样,下一题。

    除自身以外数组的乘积

    题目:给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

    示例:
    输入: [1,2,3,4]
    输出: [24,12,8,6]
    提示:题目数据保证数组之中任意元素的全部前缀元素和后缀(甚至是整个数组)的乘积都在 32 位整数范围内。
    说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。
    进阶:
    你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

    思路:这道题怎么说呢,说实话看到输入输出我瞬间的想法是求出数组整个的乘积,然后到某个元素这个乘积除某个元素。还挺美,毕竟这么想时间复杂度也是O(n),然后下一行告诉我们不要用除法,真的是,我记得不直接用除法的话位移是个好选择。我去试试吧。讲真,位运算永远是短板。
    额,这个题我看了题解。然后发现其实也没那么难,比如正常算要从头乘到尾然后除当前数。但是我们也可以直接出了当前数别的相乘。也算是动归的一种,用数组记录中间过程。我直接去写代码,然后再来说。

    class Solution {
        public int[] productExceptSelf(int[] nums) {
            int n = nums.length;
            int[] res = new int[n];
            int val = 1;
            //这一层循环让res对应的值是nums对应元素前面的元素乘积
            for(int i = 0;i<n;i++){
                res[i] = val;
                val *= nums[i];
            }
            val = 1;
            //这一层循环是将res对应的值变成前面元素乘积乘后面元素乘积
            for(int i = n-1;i>=0;i--){
                res[i] *= val;
                val *= nums[i]; 
            }
            return res;
        }
    }
    

    只能说思路真的很重要,说实话这道题我自己的想法就是位运算啊。代替除法啊。但是看了题解,真的只看了文字部分,扫了一眼大概是前后乘积相乘。然后手撸代码五分钟写完。这就说明思路是多重要了。而且完美的符合题目要求,常量额外空间。O(n)时间复杂(O(2n)是可以看成O(n)的)。并且这段代码性能超过百分百。
    多做题其实不一定在代码上有什么进步,除了一些api的调用剩下大多数都是对思路的开拓。继续下一题了!

    搜索二维矩阵2

    题目:编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
    每行的元素从左到右升序排列。
    每列的元素从上到下升序排列。

    示例:
    现有矩阵 matrix 如下:
    [
    [1, 4, 7, 11, 15],
    [2, 5, 8, 12, 19],
    [3, 6, 9, 16, 22],
    [10, 13, 14, 17, 24],
    [18, 21, 23, 26, 30]
    ]
    给定 target = 5,返回 true。
    给定 target = 20,返回 false。

    思路:额,我很烦这种靠感觉做题的。因为没有明确的标准。怎么说呢,单纯的实现一次遍历怎么都ok了,但是重点是高效。是不是只要不是全遍历就算高效了?两个升序条件,可以确定了这个数的范围。小于0 ,0大于m-1,n-1就肯定是不存在的。其次就只能顺着找了,因为两个数组都是从小到大排序的,所以想要确定一个数的范围想要越来越小只能从下往上逼近。我目前的想法从最后一行开始定位,如果比给定值小则往上走,最终走不下去是false,不然就找到给定值了true。我去代码实现下。

    class Solution {
        public boolean searchMatrix(int[][] matrix, int target) {       
            if(matrix.length==0 || matrix[0].length == 0) return false;
            if(target<matrix[0][0] || target>matrix[matrix.length-1][matrix[0].length-1]) return false;
            int l = matrix.length-1;
            int r = 0;
            while(r<matrix[0].length && l>=0){
                if(matrix[l][r]>target){//当前值大了,往上缩圈
                    l--;
                }else if(matrix[l][r]<target){//当前值小了,往右缩圈
                    r++;
                }else{//不大于不小于说明就是当前值
                    return true;
                }
            } 
            return false;
        }
    }
    

    性能超过百分之九十,将将及格,思路没问题。就是这个范围我是多次测试。比较麻烦。一开始我是想用最后一个元素作为起始坐标。后来发现往前走可以往上也可以往前,出现岔路了,所以改成最后一排第一个了,这样大了小了都只有一条路可以走。反正最终版就是上面的代码,我去看看性能排行第一的是思路比较好还是代码的细节处理比较好。
    有点神奇啊,你们都想不到性能排行第一的代码居然就是硬生生的遍历???我直接贴出来:

    class Solution {
        public boolean searchMatrix(int[][] matrix, int target) {
            if(matrix.length == 0 || matrix[0].length == 0){
                return false;
            }
            int n = matrix[0].length - 1;
             for(int i = 0; i < matrix.length; i++){
                 for(; n >=0; n--){
                     if(matrix[i][n] == target){
                         return true;
                     }else if(matrix[i][n] < target){
                         break;
                     }else{
                         continue;
                     }
                 }
             }
            return false;
        }
    }
    

    并且同样的代码我提交也是7ms,和我上面的性能是一样的。有点奇怪。不过这个题看起来好像也就这样了,所以过了,下一题。

    为运算表达式设置优先级

    题目:给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。

    示例 1:
    输入: "2-1-1"
    输出: [0, 2]
    解释:
    ((2-1)-1) = 0
    (2-(1-1)) = 2
    示例 2:
    输入: "23-45"
    输出: [-34, -14, -10, -10, 10]
    解释:
    (2(3-(45))) = -34
    ((23)-(45)) = -14
    ((2(3-4))5) = -10
    (2((3-4)5)) = -10
    (((23)-4)5) = 10

    思路:额,怎么说呢,我第一反应是回溯。dp,然后才是具体这个题。我感觉没两个数字可以分为两种情况,直接运算,不直接运算。所以用dp是可以解决的。如果不说结果的话,首先两个数只有一种结果!其次三个数,可以先计算两个数的值,剩下的又是两个数了。只不过三个数中先计算两个数有两种可能,所以三个数会有两个结果。再之后是四位数。四位数选出两个数先计算,是有三种可能选出这两个数的。正常来讲四个数应该会有3乘2,也就是六个结果。但是我仔细看了下,有一些式子会出现多次,比如上面示例2的第二个解释,先算2乘3 和先算 4乘5都会走到这个式子。所以具体情况我觉得还是要再判断一下。我现在能想到的最low的办法就是放进set,然后再挨个计算,最后放到结果集,我先去试试
    好吧,我第一反应是dp,但是真正到了写代码的时候却发现dp麻烦的不行。尤其是判重环节还要处理,所以临时又有了新的思路,分治。其实也不能算新的思路,我一直在看这个图中规律,其实也可以看成两个数两个数的分别治理。两个数是某个符号左边的和右边的。这么说思路就很清楚了。也就是每个符号可以将整个式子分成两个,左边的和右边的。如果某边还有多个数继续分,反正我感觉这个思路的代码好写的多,我继续去试试。
    第一版本的代码出来了,果然分治比较容易写,一会儿我再用dp实现下比较比较性能:

    class Solution {
        public List<Integer> diffWaysToCompute(String input) {
            List<Integer> res = new ArrayList<Integer>(); 
            //当前部分不含有符号,说明是一个数字了
            if(!input.contains("+") && !input.contains("-") && !input.contains("*")){
                res.add(Integer.valueOf(input));
                return res;
            }
            for(int i = 0;i<input.length();i++){
                if(input.charAt(i)<'0'){
                    for(int left : diffWaysToCompute(input.substring(0,i))){//含头不含尾,把符号去掉了
                        for(int right : diffWaysToCompute(input.substring(i+1))){
                            if(input.charAt(i)=='+'){
                                res.add(left+right);
                            }else if(input.charAt(i)=='-'){
                                res.add(left-right);
                            }else{//只剩下乘法了
                                res.add(left*right);
                            }
                        }
                    }
                }
            }
            return res;
        }
    }
    

    思路就是上面 说的思路。任何一个式子都可以分两部分,把所有的组合情况用双层循环列出来,再分别组合。感觉这个逻辑稍微好理解一点。好了,我再去用dp试试怎么实现,我觉得这个题dp肯定是没问题的。
    咳咳,大型打脸现场~~我写了半个小时,dp还是没实现。。总有问题,然后要下班了,我也不想调试了。这个题就这样吧,不过我还是要说这个题绝对dp是可以实现的。也算是留下个作业,啥时候我有时间了想起来了会继续做的(大概率是就此烂尾了)。
    今天的笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,顺便祝大家工作顺顺利利!生活健健康康!

    相关文章

      网友评论

        本文标题:leetCode进阶算法题+解析(三十一)

        本文链接:https://www.haomeiwen.com/subject/hmdmuhtx.html