递归与二叉树遍历青铜挑战
理解递归
递归算法是指一个方法在其执行过程中调用自身。它通常用于将一个问题分解为更小的子问题,通过重复调用相同的方法来解决这些子问题,直到达到基准情况(终止条件)。
递归算法通常包括两个主要部分:
- 基准情况(也叫递归终止条件):当问题规模足够小,递归可以停止,通常返回一个简单的结果。
- 递归部分:将问题分解成更小的子问题,并在递归过程中调用自身。
为了更清晰地说明递归,我给你一个经典的例子:阶乘计算。阶乘是一个整数和它以下所有整数的乘积。记作: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; // 返回新的头结点
}
- 时间复杂度:O(N),需要遍历整个链表一次。
- 空间复杂度:O(1),仅使用了固定数量的额外空间。
链表反转同样可以通过递归法实现,
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)。以下是递归实现这三种遍历方式的代码,并附有解释:
1. 二叉树节点定义
首先,定义一个二叉树节点(TreeNode)类:
static class TreeNode {int val;TreeNode left;TreeNode right;TreeNode(int val) {this.val = val;this.left = null;this.right = null;}
}
2. 前序遍历(Preorder Traversal)
前序遍历的顺序是:根节点 → 左子树 → 右子树
public void preorderTraversal(TreeNode root) {if (root == null) {return; // 递归终止条件}System.out.print(root.val + " "); // 访问根节点preorderTraversal(root.left); // 递归遍历左子树preorderTraversal(root.right); // 递归遍历右子树
}
解释:
- 首先访问根节点,然后递归遍历左子树,再递归遍历右子树。
3. 中序遍历(Inorder Traversal)
中序遍历的顺序是:左子树 → 根节点 → 右子树
public void inorderTraversal(TreeNode root) {if (root == null) {return; // 递归终止条件}inorderTraversal(root.left); // 递归遍历左子树System.out.print(root.val + " "); // 访问根节点inorderTraversal(root.right); // 递归遍历右子树
}
解释:
- 先递归遍历左子树,然后访问根节点,最后递归遍历右子树。
4. 后序遍历(Postorder Traversal)
后序遍历的顺序是:左子树 → 右子树 → 根节点
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
递归与二叉树遍历黄金挑战
二叉树遍历的迭代写法
- 前序遍历:通过栈控制顺序,根节点先访问,再左子树,最后右子树。
- 中序遍历:使用栈模拟递归,把左子树入栈后访问根节点,再访问右子树。
- 后序遍历:使用两个栈,第一个栈负责遍历节点,第二个栈记录节点的访问顺序,最后输出。
这三种迭代实现都利用栈来模拟递归过程,栈的先进后出特性在遍历过程中起到了关键作用。
1. 前序遍历的迭代实现
前序遍历顺序: 根节点 → 左子树 → 右子树
前序遍历的迭代实现我们使用栈来模拟递归过程,下面是详细步骤。
- 初始化栈: 我们首先将根节点入栈,因为我们从根节点开始遍历。
- 循环遍历:
- 每次从栈中弹出一个节点,访问它的值。
- 访问节点之后,需要按照前序遍历的规则,先将右子树入栈,再将左子树入栈。这样做的目的是保证左子树会先被访问到。
- 如果节点有右子树或左子树,就按顺序将它们入栈,栈是先进后出的结构,所以下次弹出的节点会先访问到左子树。
public void preorderTraversal(TreeNode root) {if (root == null) {return; // 如果树为空,直接返回}Stack<TreeNode> 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); // 如果左子树不为空,再将左子树入栈}}
}
2. 中序遍历的迭代实现
中序遍历顺序: 左子树 → 根节点 → 右子树
中序遍历的迭代实现使用一个栈来模拟递归过程,具体过程如下。
- 步骤 1: 我们从根节点开始,逐层将左子树的节点入栈。栈会保存当前节点,并且我们一直往左走,直到遇到最左的节点。
- 步骤 2: 如果当前节点为空(说明已经到达叶子节点的左子树),就弹出栈顶元素并访问它,访问完后转到右子树。
- 步骤 3: 访问完当前节点后,将指针转向其右子树,继续执行类似的过程。
- 栈的作用: 栈帮助我们记录从根到最左叶节点的路径,并确保访问完左子树后再访问根节点,再访问右子树。
public void inorderTraversal(TreeNode root) {Stack<TreeNode> 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; // 处理右子树}
}
3. 后序遍历的迭代实现
后序遍历顺序: 左子树 → 右子树 → 根节点
后序遍历的迭代实现稍微复杂一些,因为我们需要逆序访问根、右子树、左子树。为了实现这一点,我们可以使用两个栈来模拟递归过程。
- 栈 1(stack1): 用来存储节点,遍历顺序是根 → 右子树 → 左子树。我们先将根节点入栈,然后每次弹出栈顶节点并将其左右子树入栈(右子树先入栈)。
- 栈 2(stack2): 用来存储节点的访问顺序。因为栈是后进先出的,所以访问的顺序是根 → 右子树 → 左子树。最终,我们需要从
stack2
中弹出节点,才能得到正确的后序遍历顺序(左子树 → 右子树 → 根节点)。 - 两个栈的作用: 第一个栈负责遍历,第二个栈负责记录节点的访问顺序,最终通过第二个栈实现后序遍历的输出。
public void postorderTraversal(TreeNode root) {if (root == null) {return; // 如果树为空,直接返回}Stack<TreeNode> stack1 = new Stack<>(); // 用于存储遍历的节点Stack<TreeNode> 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 中的元素并访问}
}