从递归性质开始思考二叉树的遍历过程(递归代码实现、通过手动模拟递归栈将递归转为迭代)

二叉树的递归性质

"二叉树"这个概念本身就是可以用递归的形式进行定义的:

二叉树的递归定义

  1. 一个二叉树可以是空树,即它不包含任何节点。
  2. 一个非空的二叉树由一个根节点和两个子树组成:
  • 左子树本身是一个二叉树。
  • 右子树本身也是一个二叉树。

这种定义方式强调了二叉树在结构上的递归性质:二叉树可以看作是一个根节点加上两棵子树(左子树和右子树),而这两棵子树本身也是二叉树

递归性质使得我们能够将复杂的问题分解为更小的、相似的子问题。通过递归地解决这些子问题,我们可以逐步构建出整个问题的解决方案。

在二叉树中,这种分解通常表现为将某个与以V为根的子树相关的问题,转化为与V的左子树和V的右子树相关的问题。

  • V的左子树V的右子树也是二叉树,并且它们相比于以V为根的子树规模更小
  • 所以这是将一个规模较大的问题转换为规模较小的相似的子问题的过程,是典型的"分治"思想
  • 基本问题一般对应于"空树"或"只有一个节点的子树",此时问题可以直接求解

基于二叉树的递归性质解决遍历问题

遍历问题的递归分解

对二叉树的遍历问题进行递归分解:

对以V为根的子树的遍历,可以转化为:

  • 对节点V的访问
  • 遍历V的左子树
  • 遍历对V的右子树

基本问题:当遇到空树时不用遍历直接返回

前序、中序和后序遍历

根据节点v的访问时机,遍历又可以分为:前序、中序和后序遍历。

代码实现(递归)

我们可以很容易地实现用递归函数实现二叉树的遍历。

前序遍历

java 复制代码
// 前序遍历以v为根的子树
public void preorder_recur(TreeNode v){
    /* 递归终点 */
    if(v == null) return;
    
    /* 分治过程 */
    visit(v);                // 访问节点v
    preorder_recur(v.left);  // 遍历v的左子树
    preorder_recur(v.right); // 遍历v的右子树
}

中序遍历

java 复制代码
// 中序遍历以v为根的子树
public void preorder_recur(TreeNode v){
    /* 递归终点 */
    if(v == null) return;
    
    /* 分治过程 */
    preorder_recur(v.left);  // 遍历v的左子树
    visit(v);                // 访问节点v
    preorder_recur(v.right); // 遍历v的右子树
}

后序遍历

java 复制代码
// 后序遍历以v为根的子树
public void preorder_recur(TreeNode v){
    /* 递归终点 */
    if(v == null) return;
    
    /* 分治过程 */
    preorder_recur(v.left);  // 遍历v的左子树
    preorder_recur(v.right); // 遍历v的右子树
    visit(v);                // 访问节点v
}

代码实现(递归转迭代:手动模拟递归调用栈)

参考文章:知乎-xxChan:递归改写成迭代的通用方法------妈妈再也不用担心我不会写二叉树非递归遍历了

使用通用的递归转迭代方法(手动模拟递归调用栈),可以将遍历的递归实现转换成迭代实现。

函数的调用过程是如何实现的?

如果在函数a的执行过程中调用了其他函数(记为函数b),就需要先将函数a的当前运行状态保存下来(保存现场),然后转去执行函数b。等函数b执行结束后,再回过头来根据之前保存的函数a的断点处运行状态继续执行函数a。

在计算机中,函数的调用过程是通过函数调用栈 来实现的。用栈帧记录每一个函数运行时的状态信息,包括参数的值、局部变量的值以及当前执行到的位置(程序计数器PC的值)。每当发生函数调用时,就创建一个栈帧并压入栈顶,随着函数的执行同步更新栈帧中的内容。如果某个函数执行结束,就将它的栈帧从栈中弹出。

当函数a将要执行函数b的调用语句时,此时栈顶还是函数a的栈帧,并且同步记录者函数a的当前运行状态。当函数a执行到函数b的调用语句时,函数b的栈帧会压入栈顶,接下来开始函数b的执行。执行完函数b之后,将函数b的栈帧弹出,此时栈顶又变成了函数a的栈帧。这时我们就可以从函数a的栈帧中读取它在调用函数b之前的运行状态,然后就可以继续执行函数a了。

手动模拟递归调用栈

以中序遍历为例,下面是中序遍历的递归实现代码:

java 复制代码
// 中序遍历以v为根的子树
public void preorder_recur(TreeNode v){
    /* 递归终点 */
    if(v == null) return;    // pc = 0
    
    /* 分治过程 */
    preorder_recur(v.left);  // pc = 1
    visit(v);                // pc = 2
    preorder_recur(v.right); // pc = 3
                             // pc = 4
}

在函数preorder_recur(v)中:

  1. 递归调用函数preorder_recur(v.left)
  2. 返回preorder_recur(v)函数中,对节点v进行访问
  3. 递归调用函数preorder_recur(v.right)

首先,我们需要定义一个用于表示栈帧的数据结构StackFrame

java 复制代码
class Frame {
    TreeNode node; // 递归函数的参数
    int pc;        // 下一条要执行的语句

    public Frame(TreeNode node, int pc){
        this.node = node; 
        this.pc = pc;     
    }
}

下面给出中序遍历的迭代实现:

java 复制代码
public void inorder_iter1(TreeNode root) {
    Deque<Frame> stack = new LinkedList<>(); // 函数调用栈

    stack.push(new Frame(root, 0));

    while(!stack.isEmpty()){
        Frame curFrame = stack.peek();

        switch(curFrame.pc){
            case 0: {
                        if(curFrame.node == null){
                            stack.pop(); 
                        }else{
                            curFrame.pc = 1;
                        }
                        break;
                    }
            case 1: {
                        stack.push(new Frame(curFrame.node.left, 0));
                        curFrame.pc = 2;
                        break;
                    }
            case 2: {
                        visit(curFrame.node);
                        curFrame.pc = 3;
                        break;
                    }
            case 3: {
                        stack.push(new Frame(curFrame.node.right, 0));
                        curFrame.pc = 4;
                        break;
                    }
            case 4: {
                        stack.pop();
                        break;
                    }
        }
    }
}

在上述代码中,当curFrame.pc == 3时,已经到了当前函数的最后一行preorder_recur(v.right),当preorder_recur(v.right)执行完返回之后,已经没有需要执行的语句了。因此可以在将新函数的栈帧压栈前,就将当前函数的栈帧弹出,即将case 3case 4合并为:

java 复制代码
case 3: {
            stack.pop();
            stack.push(new Frame(curFrame.node.right, 0));
            // curFrame.pc = 4;
            break;
        }

除此之外,在case 0中,也不必先将curFrame.pc赋值为1,然后再进入一次循环判断curFrame.pc == 1后执行case 1的内容。可以直接将case 1的内容提前到赋值语句curFrame.pc = 1处:

java 复制代码
case 0: {
            if(curFrame.node == null){
                stack.pop(); 
            }else{
                // curFrame.pc = 1;
                stack.push(new Frame(curFrame.node.left, 0));
                curFrame.pc = 2;    
            }
            break;
        }
// case 1: {
//             stack.push(new Frame(curFrame.node.left, 0));
//             curFrame.pc = 2;
//             break;
//         }

同理,可以直接将case 3的内容提前到赋值语句curFrame.pc = 2处:

java 复制代码
case 2: {
            result.add(curFrame.node.val);
            // curFrame.pc = 3;
            stack.pop();
            stack.push(new Frame(curFrame.node.right, 0));
            break;
        }

此时只剩下case 0case 2了,既然是一个2值,我们不妨用一个布尔变量来存,用False来代表case 0、用True来代表case 2

java 复制代码
public void inorder_iter1(TreeNode root) {
    List<Integer> result = new LinkedList<>();

    Deque<Frame> stack = new LinkedList<>();
    stack.push(new Frame(root, false));

    while(!stack.isEmpty()){
        Frame curFrame = stack.peek();

        if(curFrame.pc == false){
            if(curFrame.node == null){
                stack.pop(); 
            }else{
                stack.push(new Frame(curFrame.node.left, false));
                curFrame.pc = true;    
            }
        }else{
            visit(curFrame.node);
            stack.pop();
            stack.push(new Frame(curFrame.node.right, false));
        }
    }
}
相关推荐
搬砖的小码农_Sky5 小时前
C语言:数组
c语言·数据结构
先鱼鲨生7 小时前
数据结构——栈、队列
数据结构
一念之坤7 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
IT 青年7 小时前
数据结构 (1)基本概念和术语
数据结构·算法
熬夜学编程的小王7 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
liujjjiyun8 小时前
小R的随机播放顺序
数据结构·c++·算法
Reese_Cool9 小时前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
djk888810 小时前
.net将List<实体1>的数据转到List<实体2>
数据结构·list·.net
搬砖的小码农_Sky10 小时前
C语言:结构体
c语言·数据结构
_OLi_12 小时前
力扣 LeetCode 106. 从中序与后序遍历序列构造二叉树(Day9:二叉树)
数据结构·算法·leetcode