1. 程式人生 > 其它 >演算法-遞迴&分治

演算法-遞迴&分治

一、遞迴

0、遞迴概述

為什麼要用遞迴而不用迴圈:

​ 以n的階乘為例,確實使用迴圈會更方便,但是使用遞迴的場景,一般是比較難以確認推導路徑的,例如一棵樹,要獲取所有節點值的和,這樣就不能使用迴圈了,就需要使用遞迴。

遞迴三要素:

​ 定義需要遞迴的問題(重疊的子問題)

​ 確定遞迴邊界

​ 保護和還原現場

虛擬碼如下

void doSomething(int level, int param){
	// 具有邊界
	if(level > MAX_VALUE){
		return;
	}
	// 每層的處理邏輯是一樣的
	process(level, param);
	// 使用新的引數呼叫該方法
	doSomething(level+1, new_param);
}

1、子集(中等)

題目地址:https://leetcode.cn/problems/subsets/

給你一個整數陣列 nums ,陣列中的元素 互不相同 。返回該陣列所有可能的子集(冪集)。

解集 不能 包含重複的子集。你可以按 任意順序 返回解集。

示例 1:

輸入:nums = [1,2,3]
輸出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

輸入:nums = [0]
輸出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

解答

​ 解題思路就是迴圈 nums 的每一個數據,該資料要或者不要都是一個選項,那麼就可以使用遞迴的模板:

​ 遞迴邊界:當走完nums陣列,則已經到了邊界,將陣列儲存

​ 處理邏輯:當前節點不要是一種情況,當前節點要是一種情況

​ 遞迴:繼續向下一個節點移動

​ 根據上面的分析,不要時,直接將index加一遞迴呼叫即可,如果要,則先將該節點的資料放入集合,然後再將index 加一遞迴呼叫

​ 為了不太多的佔用空間,因此可以使用一個共享的集合儲存臨時放入的值,只有到最後需要放入返回集合時,才新建一個集合並將其放入返回集合中。

​ 由於使用的是共享的集合,因此要還原現場,那麼在遞迴呼叫後,要把本次的影響清除,即remove掉本次新增的節點。

​ 這裡為什麼要清除本次的影響,可能不太好理解,舉例說明:

​ 以題目中的1,2,3為例,當前共享的集合為[],那麼:

​ 遞迴下標0,選擇不要,集合為[]

​ 遞迴下標1,選擇不要,集合為[]

​ 遞迴下標2,選擇不要,集合為[]

​ 遞迴下標3:到達邊界,出現一個結果 []

​ 迴歸遞迴2,選擇要,集合為[3]

​ 遞迴下標3:到達邊界,出現一個結果[3]

如果不清除當前新增3的影響,即不刪除當前新增的3

​ 迴歸遞迴1:選擇要,集合為[3,2]

​ 遞迴下標2,選擇不要,集合為[3,2]

​ 遞迴下標3:到達邊界,出現一個結果 [3,2]

​ 迴歸遞迴2,選擇要,集合為[3,2,3]

​ 遞迴下標3:到達邊界,出現一個結果[3,2,3]

這裡明顯已經錯誤了

如果清除當前新增3的影響,即不刪除當前新增的3

​ 迴歸下標2時,先清楚了最後一個元素,變為[]

​ 迴歸遞迴1:選擇要,集合為[2]

​ 遞迴下標2,選擇不要,集合為[2]

​ 遞迴下標3:到達邊界,出現一個結果 [2]

​ 迴歸遞迴2,選擇要,集合為[2,3]

​ 遞迴下標3:到達邊界,出現一個結果[2,3]

class Solution {
    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int[] nums;

    public List<List<Integer>> subsets(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.nums = nums;
        subsets(0);
        return ans;
    }

    private void subsets(int index){
        if(index == nums.length){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        subsets(index+1);
        choose.add(nums[index]);
        subsets(index+1);
        choose.remove(choose.size()-1);
    }
}

2、組合(中等)

題目地址:https://leetcode.cn/problems/combinations/description/

給定兩個整數 nk,返回範圍 [1, n] 中所有可能的 k 個數的組合。

你可以按 任何順序 返回答案。

示例 1:

輸入:n = 4, k = 2
輸出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

輸入:n = 1, k = 1
輸出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

解答

這道題的解題思路和上面的一致,區別就是如何判斷邊界條件,上面是如果超過了nums長度,則到了邊界,這道題是長度為k,那麼集合的長度到了k,就到了邊界,將集合放入結果集中,還有另外一種情況,就是index已經超過了n的邊界,但是集合的長度還沒到,這種也到了邊界,但是不將集合放入結果集中。

class Solution {

    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int n;
    private int k;

    public List<List<Integer>> combine(int n, int k) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.n = n;
        this.k = k;
        combine(1);
        return ans;
    }


    private void combine(int index){
        if(index > n && choose.size() < k){
            return;
        }
        if(choose.size() == k){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        combine(index+1);
        choose.add(index);
        combine(index+1);
        choose.remove(choose.size()-1);
    }
}

3、全排列(中等)

題目地址:https://leetcode.cn/problems/permutations/description/

給定一個不含重複數字的陣列 nums ,返回其 所有可能的全排列 。你可以 按任意順序 返回答案。

示例 1:

輸入:nums = [1,2,3]
輸出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

輸入:nums = [0,1]
輸出:[[0,1],[1,0]]

示例 3:

輸入:nums = [1]
輸出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整數 互不相同

解答

​ 以題目的1,2,3為例,第一位可以是1,2,3的其中一個

​ 如果第一位是1,由於1已經使用,那麼第二位可以是2,3

​ 如果第二位是2,由於1,2已經使用,那麼第三位只能是3

​ 第三位是3,出一個結果 1,2,3

​ 如果第二位是3,由於1,3已經使用,那麼第三位只能是2

​ 第三位是2,出一個結果 1,3,2

​ 如果第一位是2....

​ 可以發現,每一個位置的值都是從第一位開始判斷,如果當前節點已經被使用了,那麼就不能用

​ 因此可以定義一個共享的used陣列,長度與nums一致,用以表示nums中的數值哪一個被使用過了

​ 根據以上分析:

​ 邊界:當下標已經超過nums的下標時,表示已經得到了一個值,進行處理

​ 具體的處理:

​ 由於每個節點都是迴圈nums,從中取一個沒有用過的值,因此迴圈nums,根據 used 判斷當前節點是否已使用,如果沒有使用,則當前節點使用該值,如果已經使用,則當前節點不使用該值,繼續向後遞迴。

​ 如果使用該值,就將used的當前節點置位true,表示已使用

​ 處理完畢後,需要還原現場,因此需要將used當前節點改為false;

class Solution {

    private List<List<Integer>> ans;
    private List<Integer> choose;
    private boolean[] used;
    private int[] nums;

    public List<List<Integer>> permute(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        used = new boolean[nums.length];
        this.nums = nums;
        permute(0);
        return ans;
    }


    private void permute(int index){
        if(index == nums.length){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        for(int i=0; i<nums.length; i++){
            if(!used[i]){
                choose.add(nums[i]);
                used[i] = true;
                permute(index + 1);
                used[i] = false;
                choose.remove(choose.size()-1);
            }
        }
    }
}

4、全排列 II(中等)

題目地址:https://leetcode.cn/problems/permutations-ii/description/

給定一個可包含重複數字的序列 nums按任意順序 返回所有不重複的全排列。

示例 1:

輸入:nums = [1,1,2]
輸出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

輸入:nums = [1,2,3]
輸出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

解答

​ 與上一道題一樣,添加了一個 雜湊表去重。

class Solution {
    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int[] nums;
    private boolean[] used;
    private Map<String, String> map;
    public List<List<Integer>> permuteUnique(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.nums = nums;
        used = new boolean[nums.length];
        map = new HashMap();
        permuteUnique(0);
        return ans;
    }

    private void permuteUnique(int index){
        if(index == nums.length){
            if(!map.containsKey(geneKey())){
                List<Integer> list = new ArrayList(choose);
                ans.add(list);
                map.put(geneKey(), "");
            }
            return;
        }

        for(int i=0; i<nums.length; i++){
            if(!used[i]){
                used[i] = true;
                choose.add(nums[i]);
                permuteUnique(index + 1);
                used[i] = false;
                choose.remove(choose.size()-1);
            }
        }
    }

    private String geneKey(){
        String key = "";
        for(int str : choose){
            key += "-" + str;
        }
        return key;
    }
}

5、反轉二叉樹(簡單)

題目地址:https://leetcode.cn/problems/invert-binary-tree/description/

給你一棵二叉樹的根節點 root ,翻轉這棵二叉樹,並返回其根節點。

示例 1:

輸入:root = [4,2,7,1,3,6,9]
輸出:[4,7,2,9,6,3,1]

示例 2:

輸入:root = [2,1,3]
輸出:[2,3,1]

示例 3:

輸入:root = []
輸出:[]

提示:

  • 樹中節點數目範圍在 [0, 100]
  • -100 <= Node.val <= 100

解答

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(root == null){
            return root;
        }
        TreeNode left = root.left;
        root.left = root.right;
        root.right = left;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
}

6、驗證二叉搜尋樹(中等)

題目地址:https://leetcode.cn/problems/validate-binary-search-tree/

給你一個二叉樹的根節點 root ,判斷其是否是一個有效的二叉搜尋樹。

有效 二叉搜尋樹定義如下:

  • 節點的左子樹只包含 小於 當前節點的數。
  • 節點的右子樹只包含 大於 當前節點的數。
  • 所有左子樹和右子樹自身必須也是二叉搜尋樹。

示例 1:

輸入:root = [2,1,3]
輸出:true

示例 2:

輸入:root = [5,1,4,null,null,3,6]
輸出:false
解釋:根節點的值是 5 ,但是右子節點的值是 4 。

解答

​ 定義一個最大值一個最小值,那麼當前節點應該在這個區間內,左子節點應該在最小值與當前節點值的區間內,右子節點應該在當前節點與最大值的區間內。

​ 這裡主要是節點值的區間是從int的最小值到最大值,因此定義範圍時就不能用int,應該用long

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean isValidBST(TreeNode root) {
        return isValidBST(root, Long.valueOf(Integer.MIN_VALUE) -1, Long.valueOf(Integer.MAX_VALUE) +1);
    }

    public boolean isValidBST(TreeNode root, long min, long max) {
        if(root == null){
            return true;
        }
        if(root.val<=min || root.val>=max){
            return false;
        }
        return isValidBST(root.left, min, root.val)
            && isValidBST(root.right, root.val, max);
    }
}

7、二叉樹的最大深度(簡單)

題目地址:https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/

給定一個二叉樹,找出其最大深度。

二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。

說明: 葉子節點是指沒有子節點的節點。

示例:
給定二叉樹 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

解答

​ 返回左子節點和右子節點的最大值即可

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        return maxDepth(root, 0);
    }

    private int maxDepth(TreeNode node, int depth){
        if(node == null){
            return depth;
        }
        return Math.max(maxDepth(node.left, depth + 1), maxDepth(node.right, depth + 1));
    }
}

8、二叉樹的最小深度(簡單)

題目地址:https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/

給定一個二叉樹,找出其最小深度。

最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。

說明:葉子節點是指沒有子節點的節點。

示例 1:

輸入:root = [3,9,20,null,null,15,7]
輸出:2

示例 2:

輸入:root = [2,null,3,null,4,null,5,null,6]
輸出:5

解答

​ 返回左子節點和右子節點的最小值

​ 題目中定義了葉子節點是指沒有子節點的節點,因此要處理只有一個子節點的情況,這種情況就不能到此節點位置,應該返回另一側的值。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int minDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        return minDepth(root, 0);
    }

    private int minDepth(TreeNode node, int depth){
        if(node.left == null && node.right == null){
            return depth+1;
        }
        if(node.left == null){
            return minDepth(node.right, depth+1);
        }
        if(node.right == null){
            return minDepth(node.left, depth + 1);
        }
        return Math.min(minDepth(node.left, depth+1), minDepth(node.right, depth+1));
    }
}

二、分治

0、分治概述

分治,即“分而治之”,就是把原問題劃分成若干個同類子問題,分別解決後,再把結果合併起來

關鍵點:原問題和各個子問題都是重複的(同類的)————遞迴定義

除了向下遞迴“問題”,還要向上合併“結果”

分治演算法一般用遞迴實現

1、括號生成(中等)

題目地址:https://leetcode.cn/problems/generate-parentheses/description/

數字 n 代表生成括號的對數,請你設計一個函式,用於能夠生成所有可能的並且 有效的 括號組合。

示例 1:

輸入:n = 3
輸出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

輸入:n = 1
輸出:["()"]

提示:

  • 1 <= n <= 8

解答

​ 按照左右分為兩部分,分別計算,然後拼裝

​ 使用分治,如果是3,可以分為 0-3,1-2,2-1,3-0

​ 對於 1-2,1只能是 只能是 (),對於2,可以是 ()(),也可以是 (()),那麼就可以合併為 ()()() 和 ()(()) 兩種情況

​ 對於 2-1, 2可以是 ()(),也可以是 (()),1只能是 () ,那麼就可以合併為 ()()() 和 (()) () 兩種情況

​ 對於 3-0,就是 () 裡面套了2個,即 ((())) 或者 (()()),0-3也是一樣的效果

​ 對於上述分析,可以發現,出現了重複項,為了保證不出重複項,就可以把左側看為一個完整的整體,即肯定是使用 () 包括起來的,就不會出現重複了,例如 1 必須是 (),2 必須是 (()),3必須是 (()()) 或 (()()) ,這樣左側肯定是一個不可分割的整體,就不會出現重複項

​ 那如何保證他是一個整體呢,就是一定讓其是被括號包括起來的,如果左側的長度為 left,右側長度為 right,那麼就可以使用 "(" + lStr + ")" + rStr 來計算,其中 lStr 是左側的括號,rStr 是右側的括號,但是左側括號因為外部又包了一個括號,因此是使用的 left -1 來計算的。

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList();
        if(n == 0){
            list.add("");
            return list;
        }
        for(int i=1;i<=n;i++){
            List<String> left = generateParenthesis(i-1);
            List<String> right = generateParenthesis(n-i);
            for(String lStr : left){
                for(String rStr : right){
                    list.add("(" + lStr + ")" + rStr);
                }
            }
        }
        return list;   
    }
}

2、合併K個升序連結串列(困難)

題目地址:https://leetcode.cn/problems/merge-k-sorted-lists/description/

給你一個連結串列陣列,每個連結串列都已經按升序排列。

請你將所有連結串列合併到一個升序連結串列中,返回合併後的連結串列。

示例 1:

輸入:lists = [[1,4,5],[1,3,4],[2,6]]
輸出:[1,1,2,3,4,4,5,6]
解釋:連結串列陣列如下:
[
  1->4->5,
  1->3->4,
  2->6
]
將它們合併到一個有序連結串列中得到。
1->1->2->3->4->4->5->6

示例 2:

輸入:lists = []
輸出:[]

示例 3:

輸入:lists = [[]]
輸出:[]

解答

​ 進行分治處理,每次取中間節點,將陣列分為左右兩個陣列,將左右兩個數組合併為兩個連結串列,再對這兩個連結串列進行合併,這部分使用遞迴

​ 在遞迴方法中,首先,如果陣列長度為0,返回null,如果為1,返回當前連結串列,如果為2,返回這兩個連結串列的合併結果,否則,繼續拆分為左右陣列進行合併。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0){
            return null;
        }
        if(lists.length == 1){
            return lists[0];
        }
        return mergeKLists(lists, 0, lists.length-1);
    }


    private ListNode mergeKLists(ListNode[] lists, int left, int right) {
        if(right - left < 0){
            return null;
        }
        if(right - left == 0){
            return lists[left];
        }
        if(right - left == 1){
            return mergeTwo(lists[left], lists[right]);
        }
        int mid = (right - left)/2 + left;
        ListNode leftNode = mergeKLists(lists, left, mid); // 0,1
        ListNode rightNode = mergeKLists(lists, mid+1, right); // 2,2
        return mergeTwo(leftNode, rightNode);
    }
    

    private ListNode mergeTwo(ListNode leftNode, ListNode rightNode){
        ListNode headNode = new ListNode();
        ListNode tempNode = headNode;
        while(leftNode != null || rightNode != null){
            if(leftNode == null){
                tempNode.next = rightNode;
                break;
            }
            if(rightNode == null){
                tempNode.next = leftNode;
                break;
            }
            if(leftNode.val < rightNode.val){
                tempNode.next = leftNode;
                tempNode = tempNode.next;
                leftNode = leftNode.next;
            }else{
                tempNode.next = rightNode;
                tempNode = tempNode.next;
                rightNode = rightNode.next;
            }
        }
        tempNode = headNode;
        return headNode.next;
    }
}