算法-递归&分治
一、递归
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;
}
}
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~