Leetcode總結(jié)之遞歸 & 分治法/二分法

  1. 遞歸
  2. 分治法/二分法

1. 遞歸

1.1 遞歸思路的主要小思想

主要分成三步

  1. 定義一個與題意相關(guān)的函數(shù),要知道這個函數(shù)作用,函數(shù)的參數(shù)以及函數(shù)的返回值
  2. 要知道遞歸的最基礎(chǔ)的結(jié)束條件
  3. 要讓函數(shù)往遞歸的結(jié)束條件靠近,一點點縮小參數(shù)的范圍,縮小之后,我們可以通過一些輔助的變量或者操作,使原函數(shù)的結(jié)果不變。我們要找到父遞歸函數(shù)和子遞歸函數(shù)的關(guān)系,找到對應(yīng)的等價關(guān)系式。

1.2 Leetcode實例

q21 合并兩個有序鏈表

    /**
     * 使用遞歸的思路:
     * 1. 首先思考這個函數(shù)的作用: 輸入兩個鏈表的首節(jié)點,返回回來兩個鏈表合并之后的首節(jié)點
     * 2. 函數(shù)基本返回條件是其中一個node為空時返回另一個node
     *
     * 流程是:
     * 1. 先將基礎(chǔ)的終止條件寫出來
     * 2. 然后比較兩個node的值,需要遞歸更新狀態(tài)調(diào)用子函數(shù),
     *    例如如果l1.val < l2. val, 遞歸調(diào)用l1.next = mergeTwoListsTwo(l1.next, l2) (其實也相當(dāng)于更新狀態(tài))
     * @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 對稱二叉樹

    /**
     * 關(guān)于二叉樹注意根節(jié)點是個特殊點
     *
     * 使用遞歸的思路
     * 1. 當(dāng)前樹是否對稱取決于其左右子節(jié)點是否和其相對稱的節(jié)點是否對稱
     * 2. 增加一層null的葉子節(jié)點
     * 3. 對稱的思想主要在體現(xiàn)在對比對應(yīng)的節(jié)點, 可以聯(lián)想有兩棵樹找對應(yīng)節(jié)點就比較簡單
     *
     * 所以要借助函數(shù)isMirror(TreeNode t1, TreeNode t2)
     * 函數(shù)的意義是輸入需要進(jìn)行比對的兩個對稱節(jié)點, 判斷該節(jié)點以及子節(jié)點是否滿足對稱的要求
     * @param root
     * @return
     */
    public boolean isSymmetricTwo(TreeNode root) {
        return isMirrorTwo(root, root);
    }

    /**
     * 使用遞歸的思路:
     * 1. 因為都為樹增加了一層null的葉子節(jié)點,所以基本條件是兩個節(jié)點都為空返回true,若只有一個節(jié)點為空返回false
     * 2. 然后比對當(dāng)前節(jié)點的value,對比對應(yīng)節(jié)點的子節(jié)點是否是對稱的(遞歸,更新狀態(tài))
     * @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. 當(dāng)前樹的高度是否左右子樹高度的最大值+1得到的,這里使用了遞歸
     * 2. 基本結(jié)束條件是如果節(jié)點為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 翻轉(zhuǎn)二叉樹

    /**
     * 使用遞歸的思想
     * 1. 當(dāng)前樹進(jìn)行反轉(zhuǎn)的條件是交換已經(jīng)反轉(zhuǎn)好的左右子節(jié)點的位置
     *
     * 這里的注意事項是:
     * 需要把遞歸完之后返回的根節(jié)點進(jìn)行交換
     */
    public TreeNode invertTreeTwo(TreeNode root){
        if (root==null) return root;
        /**
         * 這樣寫是不ok的,因為第一行的遞歸把root.left更新了,所以結(jié)果是錯的 = =
        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;
    }


    /**
     * 借助一個函數(shù)遍歷整顆樹的節(jié)點判斷該節(jié)點的子節(jié)點或者本身是不是包含目標(biāo)節(jié)點
     * @param root
     * @param p
     * @param q
     * @return 返回公共的父節(jié)點
     */
    public TreeNode lowestCommonAncestorTwo(TreeNode root, TreeNode p, TreeNode q) {
        recurseTreeTwo(root,p,q);
        return ans;
    }

    /**
     * 使用遞歸的思想
     * 判斷當(dāng)前節(jié)點的子節(jié)點們或者本身是不是包含目標(biāo)節(jié)點的前提是自己的左右子樹是不是也同樣符合此條件
     *
     * 并且在遍歷的過程中如果發(fā)現(xiàn)有一個節(jié)點left + right + mid >= 2, 該節(jié)點就是我們目標(biāo)要找的公共祖先節(jié)點
     * @param currentNode
     * @param p
     * @param q
     * @return 當(dāng)前節(jié)點的子節(jié)點或者本身節(jié)點是不是就包含著目標(biāo)節(jié)點
     */
    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;
    }

這道題除了遞歸還有一種比較簡單的思路:
首先使用深度優(yōu)先遍歷遍歷整棵樹,在遍歷的過程中使用map記錄每個節(jié)點的父子關(guān)系,map的key是子節(jié)點,value是對應(yīng)的父節(jié)點。
之后使用hashset記錄其中一個節(jié)點的所有祖先節(jié)點,然后遍歷另一個節(jié)點的祖先節(jié)點直到找到兩個祖先節(jié)點集合的交集就是找到了目標(biāo)結(jié)果。

2. 分治法/二分法

2.1 分治法與二分法的適用場景和基本思想

1. 分治法

分治法的基本思想是:把一個難以直接解決的大問題,分割成一些規(guī)模較小的相同問題,以便各個擊破。

每一層遞歸上有三個步驟:

  • 分解:將原問題分解為若干個規(guī)模較小,相互獨立并且與原問題形式相同的子問題
  • 解決:若子問題規(guī)模較小且容易被解決則直接解決,否則遞歸地解決各個子問題
  • 合并:合并子問題的解為原問題的解
2. 二分法

使用二分法的場景通常是數(shù)組中元素是有序的,然后根據(jù)中間元素的值和目標(biāo)值進(jìn)行比對,判斷下一步應(yīng)該在哪一半(舍棄另一半)查找,從而使得時間復(fù)雜度在O(log(n)).

二分法重點在于:

  • 決定查找的邊界,左閉右開或者左閉右閉,根據(jù)查找的區(qū)間來決定邊界的更新以及循環(huán)結(jié)束的條件(區(qū)間中至少有一個元素或者已經(jīng)找到了臨界值)
  • 要確定自己二分查找的結(jié)果的臨界條件

2.2 Leetcode實例

q23 合并K個排序鏈表

    /**
     * 按分治的思路進(jìn)行求解
     * 1. 這個已經(jīng)細(xì)分成非常小的list了,可以將合并k個list轉(zhuǎn)換成先進(jìn)行小范圍的兩兩合并
     * 2. 兩兩合并之后,擴(kuò)大list之間間隙,繼續(xù)進(jìn)行兩兩合并,這時候兩兩一組的數(shù)量縮小了一半
     * 3. 重復(fù)上述步驟,直到間隙超過了兩個數(shù)組之間的長度
     */
    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;
    }

這道題還有個思路我覺得很好:
就是首先通過把所有l(wèi)ist之中首元素加入到PriorityQueue之中,維護(hù)一個大小小于等于所有l(wèi)ists大小的隊列,然后循環(huán)把最小的元素pop出來,然后如果最小的那個元素的下一個不為空,就把他放入到隊列之中,重復(fù)整個流程直到Queue為空就得到了答案。

q33 搜索旋轉(zhuǎn)排序數(shù)組

    /**
     * 想要讓時間復(fù)雜度是O(logN),要使用二分的方式進(jìn)行查找
     * 二分的特點在于根據(jù)區(qū)間特點和目標(biāo)值,可以確定目標(biāo)節(jié)點落在哪一半的區(qū)間上,
     * 每經(jīng)歷一次查找,要查找區(qū)間的長度就會縮短一半
     *
     * 二分查找的時候必須要注意尋找的區(qū)間和目標(biāo)值
     * 這道題用的區(qū)間是左閉合右閉合的區(qū)間,所以保證區(qū)間中有元素的條件是low<=high
     * 在遍歷的過程中判斷元素看target在哪個區(qū)間,然后縮小區(qū)間范圍
     *
     * 這道題的思路是:
     * 1. 在每次劃分的時候其實都是可能劃分成一個有拐點的區(qū)間和一個正常的區(qū)間
     * 2. 要判斷在這兩種區(qū)間中target出現(xiàn)時具備的條件
     * @param nums 輸入的原數(shù)組
     * @param target 在原數(shù)組中要查找的目標(biāo)值
     * @return 目標(biāo)值在數(shù)組中的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]){ //說明區(qū)間的左半邊有拐點
                if (target<nums[i] && target>nums[mid]){ //target落到右邊連續(xù)區(qū)間的特點
                    i = mid+1;
                }else {
                    j = mid-1;
                }
            } else {//說明區(qū)間左邊是連續(xù)的
                if (target>=nums[i] && target<nums[mid]){
                    j=mid-1;
                }else {
                    i = mid+1;
                }
            }
        }

        return -1;
    }

q34 在排序數(shù)組中查找元素的第一個和最后一個位置

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

    /**
     * 使用二分法查找到最左邊與目標(biāo)值一樣的元素
     *
     * 所選擇的區(qū)間是左閉右開的
     * 結(jié)束的條件是l=r, 也就是區(qū)間之中沒有元素, 看r最后停在哪里
     *
     * 1. 當(dāng)mid的值是>=目標(biāo)值的時候,右半邊的區(qū)域其實也不用查找了,但是r所在的位置可能是與目標(biāo)值相等的
     *    但是需要看看左半邊還有沒有和目標(biāo)值一樣的,如果沒有就會到l=r的結(jié)束條件,返回結(jié)果
     * 2. 當(dāng)mid值如果小于目標(biāo)值,肯定左半邊都舍棄掉了,從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;
    }

    /**
     * 使用二分查找法找到第一個比目標(biāo)值大的元素
     *
     * 所選擇的區(qū)間是左閉右開
     * 結(jié)束的條件也是l=r, 也就是區(qū)間之中沒有元素, 看r最后停在哪里
     *
     * 1. 當(dāng)mid的值是>目標(biāo)值的時候,右半邊的區(qū)域其實也不用查找了,但是r所在的位置可能是第一個比目標(biāo)值大的
     * 但是需要看看左半邊還有沒有比目標(biāo)值大的元素,如果沒有就會到l=r的結(jié)束條件,返回結(jié)果
     * 2. 當(dāng)mid值如果小于目標(biāo)值,肯定左半邊都舍棄掉了,從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;
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 上一篇我們總結(jié)了鏈表題目的常見題型和套路,本章我們再來看看二分。實話實說,二分的題目通常來說都比鏈表題目復(fù)雜一些,...
    suoga閱讀 1,436評論 0 0
  • 首頁目錄 點擊查看[http://www.itdecent.cn/p/c390b7d89e35] 第一題 難度:...
    Benzic閱讀 791評論 0 0
  • 1、分治策略分治法是采用分而治之,逐個解決的策略。孫子兵法曰:凡治眾如寡者,分?jǐn)?shù)是也。 采用分治求解的問題必須具有...
    不困于情閱讀 834評論 0 0
  • 為什么學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)與算法? 關(guān)于數(shù)據(jù)結(jié)構(gòu)和算法,以前只是看過一些零散的文章或者介紹,從來都沒有系統(tǒng)的去學(xué)習(xí)過。隨著...
    李大醬的大脖子閱讀 1,018評論 0 0
  • 動態(tài)規(guī)劃 111. 爬樓梯思路類似斐波那契數(shù)列注意考慮第 0 階的特殊情況 272. 爬樓梯 II思路類似上題,只...
    6默默Welsh閱讀 2,600評論 0 1

友情鏈接更多精彩內(nèi)容