演算法-遞迴&分治
一、遞迴
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/
給定兩個整數 n
和 k
,返回範圍 [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;
}
}