递归、搜索与回溯算法 - 1 ( 递归 二叉树 8000 字详解 )
一:递归
1.1 汉诺塔
题目链接:汉诺塔
class Solution {
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
dfs(A, A.size(), B, C);
}
// a 是源柱,b 是辅助柱,c 是目的柱,把 a 柱上的 n 个元素借助 b 柱放在 c 住上
public void dfs(List<Integer> a, int n, List<Integer> b, List<Integer> c){
// 递归出口
if(n == 1){
int t = a.size();
Integer tmp = a.remove(t - 1); // 获取最后一个元素的同时还要移除,所以用 remove
c.add(tmp);
return; // 当 n == 1 时开始函数的归过程
}
dfs(a, n - 1, c, b); // 开始函数的递归过程
int t = a.size();
Integer tmp = a.remove(t - 1); // 获取最后一个元素的同时还要移除,所以用 remove
c.add(tmp);
dfs(b, n - 1, a, c); // 开始函数的递归过程
}
}
class Solution {
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
for(int x :A) C.add(x);
}
}
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 {
/**
* 合并两个有序链表,让新链表也有序
* @param list1 第一个升序链表的头节点
* @param list2 第二个升序链表的头节点
* @return 合并后的升序链表的头节点
*/
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 函数的出口
if(list1 == null) return list2;
if(list2 == null) return list1;
// 重复子问题
if(list1.val < list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1; // 返回经过 mergeTwoLists 作用后的有序列表和 list1 较小节点合并后的有序列表
}else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
}
1.3 反转链表
题目链接:反转链表
/**
* 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 {
/**
* 反转以 head 为起点的子链表,返回反转后的头节点
* @param head 单链表的头节点
* @return 反转后的单链表的头节点
*/
public ListNode reverseList(ListNode head) {
// 先处理两个特殊情况,一个是 head 为空,一个是 head 只有一个元素,这两种情况下都不用进行逆序
if(head == null || head.next == null) return head;
// 重复子问题
ListNode newNode = reverseList(head.next); // reverseList 返回头节点是为了我们能直接把返回值返回给程序,虽然这个头节点在递归中没啥用
head.next.next = head;
head.next = null;
return newNode;
}
}
1.4 两两交换链表中的结点
题目链接:两两交换链表中的结点
/**
* 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 {
/**
* 两两交换链表中的节点(递归实现)
* @param head 链表的头节点
* @return 交换后的链表的头节点
*/
public ListNode swapPairs(ListNode head) {
// 先处理 head 只有为空和 head 只有一个元素的情况
if(head == null || head.next == null) return head;
// 接下来就是重复子问题了,先获取一下逆置后的头节点,用于返回
ListNode tmp = swapPairs(head.next.next);
ListNode swap = head.next; // swap 是 head 的下一个节点
swap.next = head;
head.next = tmp;
return swap; // 返回交换后的头节点
}
}
1.5 Pow(x, n) - 快速幂
题目链接:Pow(x, n) - 快速幂
class Solution {
public double myPow(double x, int n) {
// 如果 n < 0,计算 1.0 / (x 的 -n 次幂);否则直接计算 x 的 n 次幂
return n < 0 ? 1.0 / pow(x, -n) : pow(x, n);
}
/**
* 快速幂的递归实现
* @param x 底数
* @param n 指数(非负整数)
* @return 计算结果 x 的 n 次幂
*/
public double pow(double x, int n){
// 递归函数的出口
if(n == 0) return 1.0;
// 递归求解 x 的 n/2 次幂
double tmp = pow(x, n / 2);
return n % 2 == 0 ? tmp * tmp : tmp * tmp * x;
}
}
二:二叉树
2.1 计算布尔二叉树的值
题目链接:计算布尔二叉树的值
/**
* 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 {
/**
* 递归方法评估二叉布尔树的值
* @param root 二叉布尔树的根节点
* @return 二叉布尔树的布尔值结果
*/
public boolean evaluateTree(TreeNode root) {
// 函数的出口,因为二叉树是完整的二叉树,所以我们只需要判断左子树是否为空即可
if(root.left == null) return root.val == 1 ? true : false;
boolean left = evaluateTree(root.left);
boolean right = evaluateTree(root.right);
return root.val == 2 ? left || right : left && right;
}
}
2.2 求根节点到叶节点数字之和
题目链接:求根节点到叶节点数字之和
/**
* 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 sumNumbers(TreeNode root){
// 调用深度优先搜索(DFS)方法,初始路径和 preSum 为 0
return dfs(root, 0);
}
/**
* 深度优先搜索(DFS)方法
* @param root 当前节点
* @param preSum 从根节点到当前节点路径组成的数字
* @return 从当前节点到所有叶子节点路径组成的数字之和
*/
public int dfs(TreeNode root, int preSum){
preSum = preSum * 10 + root.val;
if(root.left == null && root.right == null) return preSum;
int ret = 0;
if (root.left != null) ret += dfs(root.left, preSum);
if (root.right != null) ret += dfs(root.right, preSum);
return ret;
}
}
2.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 {
/**
* 给定一个二叉树,如果一个节点的值为 0,且其所有子树都被修剪为空,则该节点也需要被修剪。
* 最终返回修剪后的二叉树。
* @param root 二叉树的根节点
* @return 修剪后的二叉树的根节点
*/
public TreeNode pruneTree(TreeNode root) {
// 递归出口
if(root == null) return null;
// 如果不为空就递归遍历左右子树
root.left = pruneTree(root.left);
root.right = pruneTree(root.right);
if(root.left == null && root.right == null && root.val == 0) root = null; // 如果左右子树递归后变为空了,并且当前节点的值也为 0 ,那么就返回空,否则返回这个树的头节点
return root;
}
}
2.4 验证二叉搜索树
题目链接:验证二叉搜索树
/**
* 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 {
// 定义一个全局变量,用于记录上一个访问的节点值
long prev = Long.MIN_VALUE;
/**
* 验证二叉搜索树的有效性
* @param root 二叉树的根节点
* @return 如果是有效的二叉搜索树,返回 true;否则返回 false
*/
public boolean isValidBST(TreeNode root) {
// 递归出口:如果节点为空,返回 true,叶子节点天然是一个搜索二叉树
if (root == null) return true;
// 递归验证左子树是否为有效的二叉搜索树
boolean left = isValidBST(root.left);
// 剪枝:如果左子树不是有效的二叉搜索树,直接返回 false
if (left == false) return false;
// 判断当前节点的值是否大于上一个访问的节点值
boolean cur = false;
if (root.val > prev) cur = true;
if (cur == false) return false; // 如果当前节点值不满足条件,返回 false,否则更新 prev 的值
else prev = root.val;
// 递归验证右子树是否为有效的二叉搜索树
boolean right = isValidBST(root.right);
//右子树的递归调用依赖于当前节点的合法性。如果当前节点不合法,递归自然不会进入右子树,等价于隐式剪枝。
// 只有当左子树、当前节点、右子树均有效时,才返回 true
return left && cur && right;
}
}
2.5 二叉搜索树中第 K 小的元素
题目链接:二叉搜索树中第 K 小的元素
/**
* 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 {
// 先定义两个全局变量,count 用来记录遍历的节点个数,ret 存储最终结果
int count, ret;
public int kthSmallest(TreeNode root, int k) {
count = k;
bfs(root);
return ret;
}
public void bfs(TreeNode root){
// 递归的出口
if(root == null) return;
// 接下来开始左中右的遍历
bfs(root.left);
// 因为给出的 root 一定是一个二叉搜索数,所以中的过程我们直接让 count-- 即可
count--;
if(count == 0){
ret = root.val;
return;
}
bfs(root.right);
}
}
2.6 二叉树的所有路径
题目链接:二叉树的所有路径
/**
* 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 {
// 用 ret 存储最终的结果
List<String> ret;
public List<String> binaryTreePaths(TreeNode root) {
ret = new ArrayList<>();
dfs(root, new StringBuffer());
return ret;
}
/**
* 深度优先搜索方法,用于遍历二叉树并生成路径
* @param root 当前节点
* @param _path 当前路径(到达当前节点时的路径)
*/
void dfs(TreeNode root, StringBuffer _path){
// 第一步,先拼接上这个节点的值
StringBuffer path = new StringBuffer(_path); // 要根据传入的 _path 构造 path
path.append(Integer.toString(root.val));
// 接下来判断一下是不是叶子节点,如果是叶子节点的话就不用加上 -> 了,直接把 pagth 保存在 ret 中并返回
if(root.left == null && root.right == null){
ret.add(path.toString());
return;
}else{
path.append("->");
}
// 接着继续去左子树和右子树继续找路径
if (root.left != null) dfs(root.left, path);
if (root.right != null) dfs(root.right, path);
}
}
三:穷举 vs 深搜
3.1 全排列
题目链接:全排列
class Solution {
// 先定义三个全局变量,ret 用来存储最终结果,path 用来记录路径,check 用来判断某个数字是否被使用
List<List<Integer>> ret;
List<Integer> path;
boolean[] check;
public List<List<Integer>> permute(int[] nums){
// 先初始化一下三个全局变量
ret = new ArrayList<>();
path = new ArrayList<>();
check = new boolean[nums.length]; // check 的长度要和 nums 数组的长度一样,这样才能保证每个数字都存在于 check 中
dfs(nums);
return ret;
}
/**
* 深度优先搜索方法,生成所有排列
* @param nums 输入的整数数组
*/
public void dfs(int[] nums){
// 递归的出口
if(path.size() == nums.length){
ret.add(new ArrayList(path));
return;
}
// 接着只关注某一个子问题
for(int i = 0; i < check.length; i++){
// 如果当前的数字没有被使用的话,就把这个数字放在 path 中
if(check[i] == false){
path.add(nums[i]);
check[i] = true;
dfs(nums);
// 在函数归的过程时,要把 path 中最后一个元素弄掉,并把 check 中的元素置为 false
check[i] = false;
path.remove(path.size() - 1);
}
}
}
}
3.2 子集
题目链接:子集
class Solution {
// 先定义两个全局变量,ret 用于存储最终的结果,path 用于记录路径
List<List<Integer>> ret;
List<Integer> path;
public List<List<Integer>> subsets(int[] nums){
// 先初始化一下两个全局变量
ret = new ArrayList<>();
path = new ArrayList<>();
dfs(nums, 0);
return ret;
}
/**
* 深度优先搜索方法,生成所有子集
* @param nums 输入的整数数组
* @param pos 从哪开始枚举,比如说 1, 2, 3
*/
public void dfs(int[] nums, int pos){
// 首先先把当前层次的 path 加入到 ret 中
ret.add(new ArrayList(path));
// 接着开始把 pos 后面的数字后加到 path 中
for(int i = pos; i < nums.length; i++){
path.add(nums[i]);
// 接着继续把这个数字往深处递归
dfs(nums, i + 1);
// 回溯,把最后一个元素移除
path.remove(path.size() - 1);
}
}
}