Leetcode总结之递归 & 分治法/二分法

  1. 递归
  2. 分治法/二分法

1. 递归

1.1 递归思路的主要小思想

主要分成三步

  1. 定义一个与题意相关的函数,要知道这个函数作用,函数的参数以及函数的返回值
  2. 要知道递归的最基础的结束条件
  3. 要让函数往递归的结束条件靠近,一点点缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。我们要找到父递归函数和子递归函数的关系,找到对应的等价关系式。

1.2 Leetcode实例

q21 合并两个有序链表

    /**
     * 使用递归的思路:
     * 1. 首先思考这个函数的作用: 输入两个链表的首节点,返回回来两个链表合并之后的首节点
     * 2. 函数基本返回条件是其中一个node为空时返回另一个node
     *
     * 流程是:
     * 1. 先将基础的终止条件写出来
     * 2. 然后比较两个node的值,需要递归更新状态调用子函数,
     *    例如如果l1.val < l2. val, 递归调用l1.next = mergeTwoListsTwo(l1.next, l2) (其实也相当于更新状态)
     * @param l1
     * @param l2
     * @return
     */
    public ListNode mergeTwoListsTwo(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;

        if (l1.val < l2.val){
            l1.next = mergeTwoListsTwo(l1.next,l2);
            return l1;
        }else {
            l2.next = mergeTwoListsTwo(l1,l2.next);
            return l2;
        }
    }

q101 对称二叉树

    /**
     * 关于二叉树注意根节点是个特殊点
     *
     * 使用递归的思路
     * 1. 当前树是否对称取决于其左右子节点是否和其相对称的节点是否对称
     * 2. 增加一层null的叶子节点
     * 3. 对称的思想主要在体现在对比对应的节点, 可以联想有两棵树找对应节点就比较简单
     *
     * 所以要借助函数isMirror(TreeNode t1, TreeNode t2)
     * 函数的意义是输入需要进行比对的两个对称节点, 判断该节点以及子节点是否满足对称的要求
     * @param root
     * @return
     */
    public boolean isSymmetricTwo(TreeNode root) {
        return isMirrorTwo(root, root);
    }

    /**
     * 使用递归的思路:
     * 1. 因为都为树增加了一层null的叶子节点,所以基本条件是两个节点都为空返回true,若只有一个节点为空返回false
     * 2. 然后比对当前节点的value,对比对应节点的子节点是否是对称的(递归,更新状态)
     * @param t1
     * @param t2
     * @return
     */
    public boolean isMirrorTwo(TreeNode t1, TreeNode t2){
        if (t1==null && t2==null) return true;
        if (t1==null || t2==null) return false;

        return (t1.val == t2.val) &&
                (isMirrorTwo(t1.right, t2.left))&&
                (isMirrorTwo(t1.left, t2.right));
    }

q104 二叉树的最大深度

    /**
     * 使用递归的思路:
     * 1. 当前树的高度是否左右子树高度的最大值+1得到的,这里使用了递归
     * 2. 基本结束条件是如果节点为null返回0
     * @param root
     * @return
     */
    public int maxDepthTwo(TreeNode root){
        if (root == null) return 0;
        return Math.max(maxDepthTwo(root.left),maxDepthTwo(root.right))+1;
    }

q226 翻转二叉树

    /**
     * 使用递归的思想
     * 1. 当前树进行反转的条件是交换已经反转好的左右子节点的位置
     *
     * 这里的注意事项是:
     * 需要把递归完之后返回的根节点进行交换
     */
    public TreeNode invertTreeTwo(TreeNode root){
        if (root==null) return root;
        /**
         * 这样写是不ok的,因为第一行的递归把root.left更新了,所以结果是错的 = =
        root.left = invertTreeTwo(root.right);
        root.right = invertTreeTwo(root.left);*/
        TreeNode t1 = invertTreeTwo(root.right);
        TreeNode t2 = invertTreeTwo(root.left);
        root.left = t1;
        root.right = t2;
        return root;
    }

q236 二叉树的最近公共祖先

    private TreeNode ans;

    public LowestCommonAncestorOfBinaryTree(){
        this.ans = null;
    }


    /**
     * 借助一个函数遍历整颗树的节点判断该节点的子节点或者本身是不是包含目标节点
     * @param root
     * @param p
     * @param q
     * @return 返回公共的父节点
     */
    public TreeNode lowestCommonAncestorTwo(TreeNode root, TreeNode p, TreeNode q) {
        recurseTreeTwo(root,p,q);
        return ans;
    }

    /**
     * 使用递归的思想
     * 判断当前节点的子节点们或者本身是不是包含目标节点的前提是自己的左右子树是不是也同样符合此条件
     *
     * 并且在遍历的过程中如果发现有一个节点left + right + mid >= 2, 该节点就是我们目标要找的公共祖先节点
     * @param currentNode
     * @param p
     * @param q
     * @return 当前节点的子节点或者本身节点是不是就包含着目标节点
     */
    public boolean recurseTreeTwo(TreeNode currentNode, TreeNode p, TreeNode q){
        if (currentNode == null) return false;

        int left = recurseTreeTwo(currentNode.left,p,q)?1:0;
        int right = recurseTreeTwo(currentNode.right,p,q)?1:0;

        int mid = (currentNode == p || currentNode == q)?1:0;

        if (left+right+mid >=2) this.ans =currentNode;

        return (left+right+mid)>0;
    }

这道题除了递归还有一种比较简单的思路:
首先使用深度优先遍历遍历整棵树,在遍历的过程中使用map记录每个节点的父子关系,map的key是子节点,value是对应的父节点。
之后使用hashset记录其中一个节点的所有祖先节点,然后遍历另一个节点的祖先节点直到找到两个祖先节点集合的交集就是找到了目标结果。

2. 分治法/二分法

2.1 分治法与二分法的适用场景和基本思想

1. 分治法

分治法的基本思想是:把一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破。

每一层递归上有三个步骤:

  • 分解:将原问题分解为若干个规模较小,相互独立并且与原问题形式相同的子问题
  • 解决:若子问题规模较小且容易被解决则直接解决,否则递归地解决各个子问题
  • 合并:合并子问题的解为原问题的解
2. 二分法

使用二分法的场景通常是数组中元素是有序的,然后根据中间元素的值和目标值进行比对,判断下一步应该在哪一半(舍弃另一半)查找,从而使得时间复杂度在O(log(n)).

二分法重点在于:

  • 决定查找的边界,左闭右开或者左闭右闭,根据查找的区间来决定边界的更新以及循环结束的条件(区间中至少有一个元素或者已经找到了临界值)
  • 要确定自己二分查找的结果的临界条件

2.2 Leetcode实例

q23 合并K个排序链表

    /**
     * 按分治的思路进行求解
     * 1. 这个已经细分成非常小的list了,可以将合并k个list转换成先进行小范围的两两合并
     * 2. 两两合并之后,扩大list之间间隙,继续进行两两合并,这时候两两一组的数量缩小了一半
     * 3. 重复上述步骤,直到间隙超过了两个数组之间的长度
     */
    public ListNode mergeKListsTwo(ListNode[] lists) {
        int interval = 1;
        while (interval < lists.length){
            for (int i=0;i<lists.length-interval;i+=2*interval){
                lists[i] = merge2List(lists[i], lists[i+interval]);
            }
            interval *=2;
        }

        return lists.length>0?lists[0]:null;
    }

    public ListNode merge2List(ListNode l1, ListNode l2){
        ListNode head = new ListNode(0);
        ListNode point = head;

        while (l1 != null && l2 != null){
            if (l1.val <= l2.val){
                point.next = l1;
                l1 = l1.next;
            }else {
                point.next = l2;
                l2 = l2.next;
            }
            point = point.next;
        }

        if (l1 == null){
            point.next = l2;
        } else {
            point.next = l1;
        }

        return head.next;
    }

这道题还有个思路我觉得很好:
就是首先通过把所有list之中首元素加入到PriorityQueue之中,维护一个大小小于等于所有lists大小的队列,然后循环把最小的元素pop出来,然后如果最小的那个元素的下一个不为空,就把他放入到队列之中,重复整个流程直到Queue为空就得到了答案。

q33 搜索旋转排序数组

    /**
     * 想要让时间复杂度是O(logN),要使用二分的方式进行查找
     * 二分的特点在于根据区间特点和目标值,可以确定目标节点落在哪一半的区间上,
     * 每经历一次查找,要查找区间的长度就会缩短一半
     *
     * 二分查找的时候必须要注意寻找的区间和目标值
     * 这道题用的区间是左闭合右闭合的区间,所以保证区间中有元素的条件是low<=high
     * 在遍历的过程中判断元素看target在哪个区间,然后缩小区间范围
     *
     * 这道题的思路是:
     * 1. 在每次划分的时候其实都是可能划分成一个有拐点的区间和一个正常的区间
     * 2. 要判断在这两种区间中target出现时具备的条件
     * @param nums 输入的原数组
     * @param target 在原数组中要查找的目标值
     * @return 目标值在数组中的index,没找到返回-1
     */
    public static int searchTwo(int[] nums, int target){
        if (nums.length == 0) return -1;
        int i=0, j= nums.length-1;
        while (i<=j){
            int mid = (i+j)/2;
            if (nums[mid] == target){
                return mid;
            } else if (nums[mid] < nums[i]){ //说明区间的左半边有拐点
                if (target<nums[i] && target>nums[mid]){ //target落到右边连续区间的特点
                    i = mid+1;
                }else {
                    j = mid-1;
                }
            } else {//说明区间左边是连续的
                if (target>=nums[i] && target<nums[mid]){
                    j=mid-1;
                }else {
                    i = mid+1;
                }
            }
        }

        return -1;
    }

q34 在排序数组中查找元素的第一个和最后一个位置

    public int[] searchRangeTwo(int[] nums, int target) {
        int[] res = {-1,-1};
        int leftIdx = findLeftestIndex(nums,target);
        if (leftIdx == nums.length || nums[leftIdx] != target){
            return res;
        }

        res[0] = leftIdx;
        res[1] = findRightestIndex(nums, target)-1;
        return res;
    }

    /**
     * 使用二分法查找到最左边与目标值一样的元素
     *
     * 所选择的区间是左闭右开的
     * 结束的条件是l=r, 也就是区间之中没有元素, 看r最后停在哪里
     *
     * 1. 当mid的值是>=目标值的时候,右半边的区域其实也不用查找了,但是r所在的位置可能是与目标值相等的
     *    但是需要看看左半边还有没有和目标值一样的,如果没有就会到l=r的结束条件,返回结果
     * 2. 当mid值如果小于目标值,肯定左半边都舍弃掉了,从mid+1开始查找
     * @param nums
     * @param target
     * @return
     */
    public int findLeftestIndex(int[] nums, int target){
        int left =0, right = nums.length;
        while (left<right){
            int mid = (left+right)/2;
            if (nums[mid]>=target){
                right = mid;
            }else {
                left = mid+1;
            }
        }
        return left;
    }

    /**
     * 使用二分查找法找到第一个比目标值大的元素
     *
     * 所选择的区间是左闭右开
     * 结束的条件也是l=r, 也就是区间之中没有元素, 看r最后停在哪里
     *
     * 1. 当mid的值是>目标值的时候,右半边的区域其实也不用查找了,但是r所在的位置可能是第一个比目标值大的
     * 但是需要看看左半边还有没有比目标值大的元素,如果没有就会到l=r的结束条件,返回结果
     * 2. 当mid值如果小于目标值,肯定左半边都舍弃掉了,从mid+1开始查找
     * @param nums
     * @param target
     * @return
     */
    public int findRightestIndex(int[] nums, int target){
        int left =0, right = nums.length;
        while (left<right){
            int mid = (left+right)/2;
            if (nums[mid]>target){
                right = mid;
            }else {
                left = mid+1;
            }
        }
        return left;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 上一篇我们总结了链表题目的常见题型和套路,本章我们再来看看二分。实话实说,二分的题目通常来说都比链表题目复杂一些,...
    suoga阅读 1,225评论 0 0
  • 首页目录 点击查看[https://www.jianshu.com/p/c390b7d89e35] 第一题 难度:...
    Benzic阅读 683评论 0 0
  • 1、分治策略分治法是采用分而治之,逐个解决的策略。孙子兵法曰:凡治众如寡者,分数是也。 采用分治求解的问题必须具有...
    不困于情阅读 693评论 0 0
  • 为什么学习数据结构与算法? 关于数据结构和算法,以前只是看过一些零散的文章或者介绍,从来都没有系统的去学习过。随着...
    李大酱的大脖子阅读 923评论 0 0
  • 动态规划 111. 爬楼梯思路类似斐波那契数列注意考虑第 0 阶的特殊情况 272. 爬楼梯 II思路类似上题,只...
    6默默Welsh阅读 2,411评论 0 1