递归算法是指一个方法在其执行过程中调用自身。它通常用于将一个问题分解为更小的子问题,通过重复调用相同的方法来解决这些子问题,直到达到基准情况(终止条件)。
递归算法通常包括两个主要部分:
为了更清晰地说明递归,我给你一个经典的例子:阶乘计算。阶乘是一个整数和它以下所有整数的乘积。记作:n! = n * (n-1) * ... * 1
,而递归的数学定义是:
n! = n * (n-1)!
1! = 1
或 0! = 1
下面是一个使用Java编写的递归算法来计算阶乘的示例代码:
class Factorial {
public static int factorial(int n){
//基准情况
if(n == 0 || n == 1){
return 1;
}
//递归部分
return n * factorial(n - 1);
}
public static void main(String[] args){
int num = 5;
int result = factorial(num);
System.err.println(num + "! = " + result);
}
}
我们之前进行链表反转使用的是迭代法,回顾一下:
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next; // 临时保存下一个节点
curr.next = prev; // 反转当前节点的指针
prev = curr; // 前移 prev 和 curr 指针
curr = nextTemp;
}
return prev; // 返回新的头结点
}
链表反转同样可以通过递归法实现,
public ListNode reverseList(ListNode head) {
// 基准情况
if (head == null || head.next == null) {
return head;
}
// 递归调用
ListNode newHead = reverseList(head.next);
// 反转当前节点和下一个节点的指向
head.next.next = head; // 当前节点的下一个节点指向当前节点
head.next = null; // 当前节点的 next 指向 null
// 返回新的头节点
return newHead;
}
通过递归方法反转链表简洁且易于理解,但需注意其空间复杂度较高(O(n)),因为每次递归都会增加调用栈的空间消耗。相比之下,迭代法的空间复杂度更低(O(1)),但在代码可读性上稍逊于递归法。
递归实现二叉树的前序、中序、后序遍历的思路是基于树的深度优先搜索(DFS)。以下是递归实现这三种遍历方式的代码,并附有解释:
首先,定义一个二叉树节点(TreeNode)类:
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
前序遍历的顺序是:根节点 → 左子树 → 右子树
public void preorderTraversal(TreeNode root) {
if (root == null) {
return; // 递归终止条件
}
System.out.print(root.val + " "); // 访问根节点
preorderTraversal(root.left); // 递归遍历左子树
preorderTraversal(root.right); // 递归遍历右子树
}
解释:
中序遍历的顺序是:左子树 → 根节点 → 右子树
public void inorderTraversal(TreeNode root) {
if (root == null) {
return; // 递归终止条件
}
inorderTraversal(root.left); // 递归遍历左子树
System.out.print(root.val + " "); // 访问根节点
inorderTraversal(root.right); // 递归遍历右子树
}
解释:
后序遍历的顺序是:左子树 → 右子树 → 根节点
public void postorderTraversal(TreeNode root) {
if (root == null) {
return; // 递归终止条件
}
postorderTraversal(root.left); // 递归遍历左子树
postorderTraversal(root.right); // 递归遍历右子树
System.out.print(root.val + " "); // 访问根节点
}
递归实现的核心在于每次对树的左右子树进行递归操作,递归的终止条件是节点为空。当节点不为空时,根据遍历顺序访问当前节点的值。
假设我们有以下的二叉树:
1
/ \
2 3
/ \
4 5
用以下代码来测试遍历:
public class BinaryTreeTraversal {
public static void main(String[] args) {
// 创建二叉树
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
BinaryTreeTraversal tree = new BinaryTreeTraversal();
System.out.println("Preorder Traversal:");
tree.preorderTraversal(root);
System.out.println("\nInorder Traversal:");
tree.inorderTraversal(root);
System.out.println("\nPostorder Traversal:");
tree.postorderTraversal(root);
}
}
输出结果:
Preorder Traversal:
1 2 4 5 3
Inorder Traversal:
4 2 5 1 3
Postorder Traversal:
4 5 2 3 1
这三种迭代实现都利用栈来模拟递归过程,栈的先进后出特性在遍历过程中起到了关键作用。
前序遍历顺序: 根节点 → 左子树 → 右子树
前序遍历的迭代实现我们使用栈来模拟递归过程,下面是详细步骤。
public void preorderTraversal(TreeNode root) {
if (root == null) {
return; // 如果树为空,直接返回
}
Stack stack = new Stack<>(); // 创建一个栈来存储节点
stack.push(root); // 将根节点入栈
while (!stack.isEmpty()) { // 当栈不为空时,继续循环
TreeNode node = stack.pop(); // 弹出栈顶元素(当前节点)
System.out.print(node.val + " "); // 访问当前节点
// 先右子树入栈,再左子树入栈,保证左子树先被访问
if (node.right != null) {
stack.push(node.right); // 如果右子树不为空,先将右子树入栈
}
if (node.left != null) {
stack.push(node.left); // 如果左子树不为空,再将左子树入栈
}
}
}
中序遍历顺序: 左子树 → 根节点 → 右子树
中序遍历的迭代实现使用一个栈来模拟递归过程,具体过程如下。
public void inorderTraversal(TreeNode root) {
Stack stack = new Stack<>();
TreeNode current = root; // 从根节点开始
while (current != null || !stack.isEmpty()) { // 当栈不为空,或者当前节点不为空时,继续遍历
// 1. 将当前节点及其所有左子树入栈
while (current != null) {
stack.push(current); // 将当前节点入栈
current = current.left; // 然后将当前节点移到左子节点
}
// 2. 弹出栈顶元素并访问
current = stack.pop(); // 弹出栈顶元素
System.out.print(current.val + " "); // 访问当前节点
// 3. 转到右子树
current = current.right; // 处理右子树
}
}
后序遍历顺序: 左子树 → 右子树 → 根节点
后序遍历的迭代实现稍微复杂一些,因为我们需要逆序访问根、右子树、左子树。为了实现这一点,我们可以使用两个栈来模拟递归过程。
stack2
中弹出节点,才能得到正确的后序遍历顺序(左子树 → 右子树 → 根节点)。public void postorderTraversal(TreeNode root) {
if (root == null) {
return; // 如果树为空,直接返回
}
Stack stack1 = new Stack<>(); // 用于存储遍历的节点
Stack stack2 = new Stack<>(); // 用于存储节点的访问顺序
stack1.push(root); // 将根节点入栈
while (!stack1.isEmpty()) { // 当 stack1 不为空时继续循环
TreeNode node = stack1.pop(); // 弹出栈顶元素
stack2.push(node); // 将该节点放入 stack2
// 先左子树入栈,再右子树入栈
if (node.left != null) {
stack1.push(node.left);
}
if (node.right != null) {
stack1.push(node.right);
}
}
// stack2 中存放的是根、右子树、左子树的顺序,我们需要反转输出
while (!stack2.isEmpty()) {
System.out.print(stack2.pop().val + " "); // 弹出 stack2 中的元素并访问
}
}