【算法通关村 Day7】递归与二叉树遍历

递归与二叉树遍历青铜挑战

理解递归

递归算法是指一个方法在其执行过程中调用自身。它通常用于将一个问题分解为更小的子问题,通过重复调用相同的方法来解决这些子问题,直到达到基准情况(终止条件)。

递归算法通常包括两个主要部分:

  1. 基准情况(也叫递归终止条件):当问题规模足够小,递归可以停止,通常返回一个简单的结果。
  2. 递归部分:将问题分解成更小的子问题,并在递归过程中调用自身。

为了更清晰地说明递归,我给你一个经典的例子:阶乘计算。阶乘是一个整数和它以下所有整数的乘积。记作:n! = n * (n-1) * ... * 1,而递归的数学定义是:

  • n! = n * (n-1)!
  • 基本情况:1! = 10! = 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. 前序遍历的迭代实现

前序遍历顺序: 根节点 → 左子树 → 右子树

前序遍历的迭代实现我们使用栈来模拟递归过程,下面是详细步骤。

  • 初始化栈: 我们首先将根节点入栈,因为我们从根节点开始遍历。
  • 循环遍历:
    1. 每次从栈中弹出一个节点,访问它的值。
    2. 访问节点之后,需要按照前序遍历的规则,先将右子树入栈,再将左子树入栈。这样做的目的是保证左子树会先被访问到。
    3. 如果节点有右子树或左子树,就按顺序将它们入栈,栈是先进后出的结构,所以下次弹出的节点会先访问到左子树。
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);  // 如果左子树不为空,再将左子树入栈
        }
    }
}

2. 中序遍历的迭代实现

中序遍历顺序: 左子树 → 根节点 → 右子树

中序遍历的迭代实现使用一个栈来模拟递归过程,具体过程如下。

  • 步骤 1: 我们从根节点开始,逐层将左子树的节点入栈。栈会保存当前节点,并且我们一直往左走,直到遇到最左的节点。
  • 步骤 2: 如果当前节点为空(说明已经到达叶子节点的左子树),就弹出栈顶元素并访问它,访问完后转到右子树。
  • 步骤 3: 访问完当前节点后,将指针转向其右子树,继续执行类似的过程。
  • 栈的作用: 栈帮助我们记录从根到最左叶节点的路径,并确保访问完左子树后再访问根节点,再访问右子树。
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;  // 处理右子树
    }
}

3. 后序遍历的迭代实现

后序遍历顺序: 左子树 → 右子树 → 根节点

后序遍历的迭代实现稍微复杂一些,因为我们需要逆序访问根、右子树、左子树。为了实现这一点,我们可以使用两个栈来模拟递归过程。

  • 栈 1(stack1): 用来存储节点,遍历顺序是根 → 右子树 → 左子树。我们先将根节点入栈,然后每次弹出栈顶节点并将其左右子树入栈(右子树先入栈)。
  • 栈 2(stack2): 用来存储节点的访问顺序。因为栈是后进先出的,所以访问的顺序是根 → 右子树 → 左子树。最终,我们需要从 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 中的元素并访问
    }
}

你可能感兴趣的:(算法,数据结构)