深入理解二叉树遍历:递归与栈的双重视角

  • 二叉树的遍历
    • 前序遍历
    • 中序遍历
    • 后续遍历
    • 总结

二叉树的遍历

虽然用递归的方法遍历二叉树实现起来更简单,但是要想深入理解二叉树的遍历,我们还必须要掌握用栈遍历二叉树,递归其实就是利用了系统栈去遍历。特此记录一下如何用双重视角去看待二叉树的遍历,加深一下理解。

前序遍历

我们从前序遍历入手,搞懂了一个,其它的也就容易了。

使用递归的方法遍历的话很简单,代码如下:

public void preorderTraversal(TreeNode root, List<Integer> result) {if (root == null) {return;}// 访问根节点result.add(root.val);// 递归遍历左子树preorderTraversal(root.left, result);// 递归遍历右子树preorderTraversal(root.right, result);
}

它利用了系统中线程的栈空间,先访问当前节点,再调用自身方法递归地去对左子节点进行前序遍历,这在线程栈空间中会新增加一个方法。线程也会优先去处理在线程栈上新增的方法。如下图所示:
在这里插入图片描述
这里发生的事情是:

  1. 系统为每次方法调用维护一个执行上下文,包含局部变量和返回地址
  2. 当执行到preorder(root.left)时,当前方法的执行被暂停,其状态被保存在系统栈中
  3. 系统转而执行左子树的遍历,完成后会自动返回到保存的执行点
  4. 继续执行preorder(root.right)

关键点是:系统栈自动保存了"接下来要做什么"的信息。每个节点被处理时,系统知道处理完左子树后还需要回来处理右子树。

而使用栈的话,我们只需要按照递归遍历的方式自己创建一个栈模拟着线程栈的方法去遍历就行。代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;public List<Integer> preorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null) return result;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();result.add(node.val);// 先右后左入栈,这样出栈时会先处理左子节点if (node.right != null) {stack.push(node.right);}if (node.left != null) {stack.push(node.left);}}return result;
}

这里的关键区别是:

  1. 我们只能用栈存储节点本身,而不能存储"执行到哪一步"的完整上下文
  2. 栈顶元素是下一个要处理的节点,而不是一个带有执行状态的方法调用
  3. 由于栈的后进先出特性,为了先处理左子节点,必须先将右子节点入栈,再将左子节点入栈

系统线程栈和手动实现栈的区别

  1. 系统线程栈的一个栈帧的运行(不包括递归调用产生的跳转)等同于手动实现栈的三件事:1.访问当前节点 2.入栈右子节点 3.入栈左子节点
  2. 系统线程栈新增了一个栈帧等同于手动实现栈的:出栈一个节点作为当前节点

中序遍历

递归代码如下:

public void inorderTraversal(TreeNode root, List<Integer> result) {if (root == null) {return;}// 递归遍历左子树inorderTraversal(root.left, result);// 访问根节点result.add(root.val);// 递归遍历右子树inorderTraversal(root.right, result);
}

系统线程栈中一个栈帧的执行过程(不包括递归调用产生的跳转)等同于手动实现栈中的三个操作:

  1. 递归访问左子树
  2. 访问当前节点
  3. 递归访问右子树

手动实现栈代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();TreeNode current = root;while (current != null || !stack.isEmpty()) {// 将所有左子节点入栈while (current != null) {stack.push(current);current = current.left;}// 处理栈顶节点current = stack.pop();result.add(current.val);// 转向右子树current = current.right;}return result;
}

核心思路是先将当前节点及其所有左子节点入栈,然后访问节点值,再处理右子树
无法用简单的"入栈-出栈-访问"模式表达,需要维护一个current指针跟踪当前处理节点
关键步骤:

  1. 将当前节点及其所有左子节点入栈
  2. 弹出栈顶节点并访问
  3. 将当前节点切换到右子节点,重复步骤1

后续遍历

递归代码如下:

public void postorderTraversal(TreeNode root, List<Integer> result) {if (root == null) {return;}// 递归遍历左子树postorderTraversal(root.left, result);// 递归遍历右子树postorderTraversal(root.right, result);// 访问根节点result.add(root.val);
}

系统线程栈中一个栈帧的执行过程等同于手动实现栈中的三个操作:

  1. 递归访问左子树
  2. 递归访问右子树
  3. 访问当前节点

手动实现栈代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null) return result;Stack<TreeNode> stack1 = new Stack<>();Stack<TreeNode> stack2 = new Stack<>();stack1.push(root);// 先将节点按 根-右-左 的顺序放入栈2while (!stack1.isEmpty()) {TreeNode node = stack1.pop();stack2.push(node);if (node.left != null) {stack1.push(node.left);}if (node.right != null) {stack1.push(node.right);}}// 从栈2中弹出的顺序就是 左-右-根while (!stack2.isEmpty()) {result.add(stack2.pop().val);}return result;
}

双栈法
系统线程栈中一个栈帧的执行等同于:

  1. 将当前节点压入第二个栈(结果栈)
  2. 将左子节点压入第一个栈(处理栈)
  3. 将右子节点压入第一个栈(处理栈)

注意:入栈顺序是"左-右",这样处理顺序变成"右-左",最终从结果栈弹出时顺序为"左-右-根"

单栈法

  1. 需要额外记录上次访问的节点
  2. 核心思路是判断右子树是否已访问,决定是访问节点还是处理右子树
  3. 由于逻辑较复杂,难以用简单的等价操作表述

总结

读者可以根据前序遍历的思路自行去理解中序遍历和后序遍历。重点就是理解系统的线程栈是怎么运作的,以及手动实现的栈是如何保存节点的。搞清楚了这两点,对二叉树的遍历的理解就会更上一层了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78369.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt Creator中自定义应用程序的可执行文件图标

要在Qt Creator中为你的应用程序设置自定义可执行文件图标&#xff0c;你需要按照以下步骤操作&#xff1a; Windows平台设置方法 准备图标文件&#xff1a; 创建一个.ico格式的图标文件&#xff08;推荐使用256x256像素&#xff0c;包含多种尺寸&#xff09; 可以使用在线工…

Windows11系统中GIT下载

Windows11系统中GIT下载 0、GIT背景介绍0.0 GIT概述0.1 GIT诞生背景0.2 Linus Torvalds 的设计目标0.3 Git 的诞生&#xff08;2005 年&#xff09;0.4 Git 的后续发展0.5 为什么 Git 能成功&#xff1f; 1、资源下载地址1.1 官网资源1.2 站内资源 2、安装指导3、验证是否下载完…

react的fiber 用法

在 React 里&#xff0c;Fiber 是 React 16.x 及后续版本采用的协调算法&#xff0c;它把渲染工作分割成多个小任务&#xff0c;让 React 可以在渲染过程中暂停、恢复和复用任务&#xff0c;以此提升渲染性能与响应能力。在实际开发中&#xff0c;你无需直接操作 Fiber 节点&am…

FPGA前瞻篇-数字电路基础-逻辑门电路设计

模拟信号&#xff1a; 一条随时间连续变化、平滑波动的曲线&#xff0c;比如正弦波。 数字信号&#xff1a; 一条只有高低两个状态&#xff08;0和1&#xff09;&#xff0c;跳变清晰的方波曲线。 在 IC 或 FPGA 的逻辑设计中&#xff0c;我们通常只能处理数字信号&#xff0…

RabbitMQ 基础概念(队列、交换机、路由键、绑定键、信道、连接、虚拟主机、多租户)介绍

本文是博主在梳理 RabbitMQ 知识的过程中&#xff0c;将所遇到和可能会遇到的基础知识记录下来&#xff0c;用作梳理 RabbitMQ 的整体架构和功能的线索文章&#xff0c;通过查找对应的知识能够快速的了解对应的知识而解决相应的问题。 文章目录 一、RabbitMQ 是什么&#xff1f…

机器学习第一篇 线性回归

数据集&#xff1a;公开的World Happiness Report | Kaggle中的happiness dataset2017. 目标&#xff1a;基于GDP值预测幸福指数。&#xff08;单特征预测&#xff09; 代码&#xff1a; 文件一&#xff1a;prepare_for_traning.py """用于科学计算的一个库…

Java面试高频问题(29-30)

二十九、全链路压测&#xff1a;数据隔离与流量 关键技术点 1. 流量染色&#xff1a;通过Header注入X-Test-TraceId标识压测流量 2. 影子库表&#xff1a;通过ShardingSphere实现数据隔离 3. 熔断降级&#xff1a;压测流量触发异常时自动切换回生产数据源 数据隔离方案对比 …

Python常用的第三方模块之数据分析【pdfplumber库、Numpy库、Pandas库、Matplotlib库】

【pdfplumber库】从PDF文件中读取内容 import pdfplumber #打开PDF文件 with pdfplumber.open(DeepSeek从入门到精通(20250204).pdf) as pdf:for i in pdf.pages: #遍历页print(i.extract_text()) #extract_text()方法提取内容print(f----------------第{i.page_number}页结束…

长短板理论——AI与思维模型【83】

一、定义 长短板理论思维模型&#xff0c;也被称为木桶原理&#xff0c;是指一只木桶能盛多少水&#xff0c;并不取决于最长的那块木板&#xff0c;而是取决于最短的那块木板。该理论将木桶视为一个整体系统&#xff0c;各个木板代表着系统的不同组成部分或要素&#xff0c;强…

2025蓝桥省赛c++B组第二场题解

前言 这场的题目非常的简单啊&#xff0c;至于为什么有第二场&#xff0c;因为当时河北正在刮大风被迫停止了QwQ&#xff0c;个人感觉是历年来最简单的一场&#xff0c;如果有什么不足之处&#xff0c;还望补充。 试题 A: 密密摆放 【问题描述】 小蓝有一个大箱子&#xff0…

【数据结构与算法】从完全二叉树到堆再到优先队列

完全二叉树 CBT 设二叉树的深度为 h , 若非最底层的其他各层的节点数都达到最大个数 , 最底层 h 的所有节点都连续集中在左侧的二叉树叫做 完全二叉树 . 特点 对任意节点 , 其右分支下的叶子节点的最底层为 L , 则其左分支下的叶子节点的最低层一定是 L 或 L 1 .完全二叉树…

Leetcode:1. 两数之和

题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。 示…

flume整合kafka

需求一&#xff1a; 启动flume 启动kafka消费者&#xff0c;验证数据写入成功 新增测试数据 需求二&#xff1a; 启动Kafka生产者 启动Flume 在生产者中写入数据

Hbase集群管理与实践

一、HBase集群搭建实战 1.1 环境规划建议 硬件配置基准(以10节点集群为例): 角色CPU内存磁盘网络HMaster4核16GBSSD 200GB(系统盘)10GbpsRegionServer16核64GB124TB HDD(JBOD)25GbpsZooKeeper4核8GBSSD 500GB10Gbps1.2 关键配置项示例(hbase-site.xml) <configu…

STM32 开发 - stm32f10x.h 头文件(内存映射、寄存器结构体与宏、寄存器位定义、实现点灯案例)

概述 STM32F10x.h 是 STM32F1 系列微控制器的核心头文件&#xff0c;提供了所有外设寄存器的定义和内存映射 一、内存映射 #define PERIPH_BASE ((uint32_t)0x40000000)#define APB1PERIPH_BASE PERIPH_BASE #define APB2PERIPH_BASE (PERIPH_BASE 0x…

QEMU源码全解析 —— 块设备虚拟化(23)

接前一篇文章:QEMU源码全解析 —— 块设备虚拟化(22) 本文内容参考: 《趣谈Linux操作系统》 —— 刘超,极客时间 《QEMU/KVM源码解析与应用》 —— 李强,机械工业出版社 特此致谢! QEMU启动过程中的块设备虚拟化 上一回解析了qcow2格式对应的qcow2_open函数,本回解…

【PCB工艺】推挽电路及交越失真

推挽电路(Push-Pull Circuit) 推挽电路(Push-Pull Circuit) 是一种常用于功率放大、电机驱动、音频放大等场合的电路结构,具有输出对称、效率高、失真小等优点。 什么是推挽电路? 推挽是指:由两种极性相反的器件(如 NPN 和 PNP、NMOS 和 PMOS)交替导通,一个“推”电…

RD电子实验记录本选用贴士A-B-C

传统的实验记录本&#xff0c;令人又爱又恨本 如何挑选电子实验室记录本&#xff08;ELN&#xff09;的品牌/服务商/供应商&#xff1f; 电子实验记录本&#xff0c;又名为ELN&#xff0c;Electronic lab notebook&#xff0c;enotebook&#xff0c;研发电子管理系统&#xf…

Qt实战之将自定义插件(minGW)显示到Qt Creator列表的方法

Qt以其强大的跨平台特性和丰富的功能&#xff0c;成为众多开发者构建图形用户界面&#xff08;GUI&#xff09;应用程序的首选框架。而在Qt开发的过程中&#xff0c;自定义插件能够极大地拓展应用程序的功能边界&#xff0c;让开发者实现各种独特的、个性化的交互效果。想象一下…

java基础之枚举和注解

枚举 简介 枚举&#xff1a;enumeration&#xff0c;jdk1.5中引入的新特性&#xff0c;用于管理和使用常量 入门案例 第一步&#xff1a;定义枚举&#xff0c;这里定义一个动物类&#xff0c;里面枚举了多种动物 public enum AnimalEnum {CAT, // 猫DOG, // 狗PIG // …