二叉树深度优先遍历解题思路

article/2025/11/11 0:34:30

文章目录

    • 1.二叉树深度优先遍历解题思路
      • 1.1.三种深度优先遍历的方式
      • 1.2.深度优先遍历的启示
        • 1.2.1.递归形成条件
        • 1.2.2递归过程的实际工作顺序
          • 1.2.2.1.单路递归的实际工作顺序
          • 1.2.2.2. 双路递归的实际工作顺序
        • 1.2.3.三种深度优先遍历给我们的启示是什么?
      • 1.3.深度优先遍历二叉树解题模板
      • 1.4.二叉树深度优先遍历题目的一些补充

1.二叉树深度优先遍历解题思路

1.1.三种深度优先遍历的方式

  • 理解深度优先遍历的前提:
    二叉树实例
    任何一颗非空的二叉树,都有根结点,左子树和右子树。例如上面这颗二叉树,根结点是A,左子树的结点有B、C、D,右子树的结点有E、F。而任意一颗非空的二叉树的左子树和右子树也是一颗二叉树
    在这里插入图片描述
    上面这颗二叉树的根为B,左子树有C结点,右子树有D结点。 以B为根结点的左子树还是一颗二叉树。
    在这里插入图片描述
    上面这颗二叉树的根结点是C,左子树和右子树是null。

  • 先序遍历(先根遍历)
    先序遍历指的是,对于任意一颗二叉树,都先遍历其根结点,然后遍历其左子树,最后遍历其右子树。
    二叉树实例
    例如上面这颗二叉树,先序遍历的结果就是A-B-C-D-E-F;
//先序遍历public void preOrderTraversal(TreeNode root) {if (root == null) return;//对当前结点的实际工作 也可替换为其他语句System.out.print(root.val + " ");//实际工作结束this.preOrderTraversal(root.left);this.preOrderTraversal(root.right);}

  • 中序遍历(中根遍历)
    中序遍历指的是,对于任意一颗二叉树,都先遍历其左子树,然后遍历其根节点,最后遍历其右子树。
    二叉树实例
    例如上面这颗二叉树,中序遍历的结果就是C-B-D-A-E-F;
//中序遍历public void inOrderTraversal(TreeNode root) {if (root == null) return;this.inOrderTraversal(root.left);//对当前结点的实际工作 也可替换为其他语句System.out.print(root.val + " ");//实际工作结束this.inOrderTraversal(root.right);}

  • 后序遍历(后根遍历)
    后序遍历指的是,对于任意一颗二叉树,都先遍历其左子树,然后遍历其右子树,最后遍历其根结点。
    二叉树实例

    例如上面这颗二叉树,后序遍历的结果就是C-D-B-F-E-A;

//后序遍历public void postOrderTraversal(TreeNode root) {if (root == null) return;this.postOrderTraversal(root.left);this.postOrderTraversal(root.right);//对当前结点的实际工作 也可替换为其他语句System.out.print(root.val + " ");//实际工作结束}

1.2.深度优先遍历的启示

名词说明:由于本人表达能力有限,下面所提到的一些名词可能会让读者引起混淆。若在后面的文章阅读过程中对某些名词的含义产生了疑惑,可以翻到这里看一下对下面的出现的一些名词的说明。

  1. 结点实际操作:指本次递归中对当前的结点(或含参)进行实际操作。
  2. 结点实际操作顺序:对某个数据结构的结点实际操作的顺序。
  3. 结点遍历顺序:结点实际操作顺序。
  4. 结点调用顺序:指对某个数据结构中结点的实际访问顺序,或者说递归时的函数栈帧的开辟顺序。

1.2.1.递归形成条件

我们都理解,深度优先遍历的所使用的编码技巧是递归,而递归有形成的条件有3个:

  1. 函数调用自身
  2. 有一个终止条件
  3. 不断朝终止条件步进

所谓的递归,就是函数不断调用自身的过程,在这个过程中,必须由一个终止递归的条件,否则函数就会不断地调用自身引发程序错误。那么问题就来了,既然是函数自己调用自己,所调用的函数名都是相同的,那么系统是怎么得知某次函数调用是最后一次,某次函数调用不是呢。答案是参数,不同的函数调用最大的区别就是函数参数。 一旦某次递归调用的函数的参数达成了某个条件,系统就得知:“好了,我的函数参数已经达到了终止条件了,我不该再递归下去了”。


我们都清楚了递归的本质是对参数进行操作,一切都要围绕参数进行。那么在单次的函数递归中,想清楚自己究竟要做什么,应该考虑的点有哪些呢?

  1. 下一次递归的参数是什么?
  2. 我该返回什么?(为了返回我想要的,我要怎么利用下一次递归的返回值?)
  3. 我要对参数做什么?

也就是单次递归,只要搞清楚递什么(类似一个回旋镖,递什么参数,它返回什么),归(自己返回什么)什么就行了。

1.2.2递归过程的实际工作顺序

我们已经知道单次递归要做什么了,但整个递归的过程的考虑不应该只有这些,还有一个很重要的问题。在一次递归调用中,我既要把参数传给下一次递归并获得下一次递归的返回值(也可能没返回值),又要对参数操作,那操作当前参数和进行下一次递归,谁先谁后呢?

1.2.2.1.单路递归的实际工作顺序

所谓的单路递归,即在某次递归调用中,最多递归调用自己一次。典型的有对单链表进行递归,下次进行递归的参数就是自己的后继结点。

在这里插入图片描述

就单路递归,某一次函数调用,根据下一次函数调用和本次函数调用的工作的先后顺序,可以分为:

  1. 我先把参数B(当前结点的后继)递给下一次递归, 等下一次递归返回的时候,我再操作参数A(当前结点),然后我再返回。
  2. 我先操作参数A(当前结点),然后把参数B(当前结点的后继)递给下一次递归,等这个递归返回后,我再接受它的返回值,最后再返回我想要返回的值。

发现没有,就单路递归而言,操作参数和把递给下一次递归的先后顺序不同,你对单链表的实际操作顺序就不同了。第一种递归你从后往前操作单链表,第二个递归则是从前往后操作单链表。 换句话来说,第一种是逆向遍历单链表,第二种是正向遍历单链表。

当然还有不同的先后顺序安排,这里不再列举,只是想通过这个例子让你知道在单路递归中,函数调用中的下一次递归的和操作当前参数的执行先后顺序不同,实际的操作顺序截然不同。

这里也就得出了一个重要结论,遍历指的就是实际的业务操作顺序,而不是调用顺序

1.2.2.2. 双路递归的实际工作顺序

二叉树实例
双路递归顾名思义,每次函数调用都会递归调用两次,典型的有对二叉树进行深度优先遍历,对某个结点递归后,该节点的左右孩子也会参与递归。想一下对二叉树双路递归的不同的业务操作时机对实际二叉树结点的操作顺序的影响,你可能就已经开始头疼了。

列举三个在递归二叉树时,操作当前结点和进行下一次递归的不同先后顺序组合:

1在单次递归调用中,我先操作参数(当前根结点),然后递归调用左孩子结点,再递归调用右孩子结点,最后返回。
2在单次递归调用中,我先递归调用左孩子结点,然后操作参数(当前根结点),再递归调用右孩子结点。
1在单次递归调用中,我先递归调用左孩子结点,然后递归调用右孩子结点,再操作当前参数(当前根结点)。

:这三种情况,二叉树结点的实际操作顺序是什么样的?

实际上,这三种递归方式说的的就是先序遍历,中序遍历和后序遍历,可以多多体会一下。

1.2.3.三种深度优先遍历给我们的启示是什么?

二叉树实例
实际上,三种优先遍历告诉了我们递归过程中对二叉树结点的实际操作顺序,所谓的不同深度优先遍历方式等价于不同的二叉树结点实际操作顺序。 你会发现,三种深度优先遍历方式对二叉树结点的函数调用顺序都是相同的,但实际操作结点的顺序却是不同的。

例如后序遍历,从函数调用来看,你的函数调用的结点也是A-B-C,但实际操作的第一个结点却是C,演示如下:

  1. 你在调用了A这个结点后,还没来得及操作,就马上递归调用B结点。

  2. 同理B结点调用之后,你也还没操作,你就递归调用了C结点。

  3. 还没来得及操作C,你又递归调用了C的左子树(空节点),检测到 参数不合法,递归返回到C结点中。

  4. C左子树递归调用返回之后,才对C结点进行了实际的操作。


在这里插入图片描述

  • 不管是哪一种深度优先遍历方式(先序,中序和后序),结点调用的顺序都是相同的,对结点的实际操作顺序却是不同的。

1.3.深度优先遍历二叉树解题模板

根据1.2的推论,我们可以对二叉树深度优先遍历的题目的解法得出一个结论:只要知道某道题的合适的深度优先遍历方式(实际工作顺序),递归的参数,返回的参数,就可以解答了。

  1. 确定某道二叉树题目适合深度优先遍历,且知道最佳的深度遍历方式。
  2. 根据深度遍历方式设计单次递归模板
    先序遍历:操作当前结点-左路递归-右路递归-返回
    中序遍历:左路递归-操作当前结点-右路递归-返回
    后序遍历:左路递归-右路递归-操作当前结点-返回
  3. 确定左路递归和右路递归的参数和返回值
  4. 编码

当然,并非所有的二叉树深度遍历题都适用这个模板,因为除了这三种还有其他的深度优先遍历方式,例如你可以——操作当前结点-右路递归-左路递归-返回。但在理解了最常见的三种深度遍历方式,其他的深度遍历方式可以类推了。

1.4.二叉树深度优先遍历题目的一些补充

对于一些特殊的深度优先遍历的题目,特别是一次递归的实际工作中需要操作一个以上二叉树结点的,由于一般来说一次函数调用我们只有一个结点的参数,不太好定位到其他的结点,所以我们可以考虑使用成员变量作为我们的辅助变量。


例题:
在这里插入图片描述

描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示

数据范围:输入二叉树的节点数 0≤n≤10000 \le n \le 10000≤n≤1000,二叉树中每个节点的值 0≤val≤10000\le val \le 10000≤val≤1000
要求:空间复杂度O(1)O(1)O(1)(即在原树上操作),时间复杂度 O(n)O(n)O(n)

注意:
1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
2.返回链表中的第一个节点的指针
3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构
4.你不用输出双向链表,程序会根据你的返回值自动打印输出
输入描述:
二叉树的根节点
返回值描述:
双向链表的其中一个头节点。
示例1
输入:

{10,6,14,4,8,12,16}

返回值:

From left to right are:4,6,8,10,12,14,16;From right to left are:16,14,12,10,8,6,4;

说明:

输入题面图中二叉树,输出的时候将双向链表的头节点返回即可。

示例2
输入:

{5,4,#,3,#,2,#,1}

返回值:

From left to right are:1,2,3,4,5;From right to left are:5,4,3,2,1;

说明:

                5/4/3/2/
1

树的形状如上图

题解

/**
public class TreeNode {int val = 0;TreeNode left = null;TreeNode right = null;public TreeNode(int val) {this.val = val;}}
*/
//中序遍历二叉树 并改变当前二叉树的结点的左子树指向和前一个操作的二叉树的右子树指向(用成员变量存储)
public class Solution {//用成员变量记录上一次遍历的结点public TreeNode prev;public void inOrder(TreeNode root) {//递归结束条件判定if (root == null) return;//先递归左子树inOrder(root.left);//具体的工作root.left = prev;if (prev != null) prev.right = root;prev = root;//工作结束//递归右子树inOrder(root.right);}public TreeNode Convert(TreeNode root) {if (root == null) return null;inOrder(root);while (root.left != null) root = root.left;return root;}
}

http://chatgpt.dhexx.cn/article/ch3rgXmb.shtml

相关文章

[剑指Offer]-二叉树的深度

题目描述(一) 输入一棵二叉树的根结点,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。例如下图中的二叉树的深度为4,因为它从根结点…

二叉树的高度和深度定义、回溯(个人学习记录)

1.二叉树的高度和深度定义 (对某个节点来说)深度是指从根节点到该节点的最长简单路径边的条数;高度是指从最下面的叶子节点到该节点的最长简单路径边的条数; (对二叉树)深度是从根节点数到它的叶节点&…

二叉树之二叉树的深度

1.二叉树的max deep 1. 高度与深度 二叉树的高度: 任意一个节点到叶节点的max距离 深度: 任意一个节点到根节点的max距离 求深度: 后续 left right mid 先求子节点的深度,1即为父节点深度 求高度 lef mid right 1 1 1逼近高度 2.求高度 1.递归思路 1max(left,right) 递归函…

二叉树的深度

二叉树的深度计算 1、一颗树只有一个节点,它的深度是1; 2、二叉树的根节点只有左子树而没有右子树,那么可以判断,二叉树的深度应该是其左子树的深度加1; 3、二叉树的根节点只有右子树而没有左子树,那么可…

计算二叉树深度算法(递归、非递归)入门详解

一、引言 二叉树在应用时,经常需要知道二叉树的深度。二叉树的深度就是二叉树的层数,即从树根算起,到最底下一层的层数是多少,即二叉树中结点的最大层次值。 本文给出了计算二叉树深度的算法,包括递归算法和非递归算法…

计算二叉树的深度

[算法步骤] 如果是空树,递归结束,深度为0;否则执行一下操作 递归计算左子树的深度计为m;递归计算右子树的深度计为n;如果m大于n,二叉树的深度为m1,否则为n1; [算法描述] int Depth(BiTree T) {int m, n…

CreateDialog

使用对话框模版资源创建一个非模态对话框。 CreateDialog调用 CreateDialogParam 函数。 调用语序: HWND CreateDialog(HINSTANCE hInstance,LPCTSTR lpTemplate,HWND hWndParent,DLGPROC lpDialogFunc); 参数 hInstance类型:HINSTANCE 对话框模版…

android 使用showDialog调用相应的对话框

在Activity下调用此函数 showDialog(10); 然后重写以下函数 protected Dialog onCreateDialog(int id) {// TODO Auto-generated method stubswitch(id){case 10:return new AlertDialog.Builder(Activity13.this).setTitle(getString(R.string.title)).setMessage(getString(…

Show()跟ShowDialog()的区别

Show和ShowDialog有什么不同呢,什么时候用Show,什么时候用ShowDialog呢?相信看完这篇博客,你会有一个比较明确的答案。 说到show跟ShowDialog的区别很多人会想到的是,他们一个是非模态一个是模态,模态窗体就…

WPF Tips: Window.ShowDialog()方法:Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelp

关于Window.ShowDialog()方法&#xff0c;有一个常见的容易犯的错误。下面给出这个错误的例子&#xff1a; DemoA&#xff1a;错误的例子 1. 在WPF项目中&#xff0c;创建一个Windows&#xff1a;DialogWindow DialogWindow.xaml <Window x:Class"DemoA.DialogWindow&…

showdialog

在C#中窗口的显示有两种方式&#xff1a;模态显示&#xff08;showdialog&#xff09;和非模态显示&#xff08;show&#xff09;。 区别&#xff1a; 模态与非模态窗体的主要区别是窗体显示的时候是否可以操作其他窗体。模态窗体不允许操作其他窗体&#xff0c;非模态窗体可以…

Flutter dialog (1) - showDialog的讲解

在应用开发中,或多或少都会遇到需要弹框的问题, 比如:需要用户确认,需要输入一些信息等等的问题,这就要用到 dialog 相关的概念了 而在 flutter 中,所有可以看见的都是 Widget,dialog 也不例外 不过和 android 或 iOS 中不同的一点是,Flutter 中 dialog 不是一个单独的类,而是…

C# 按Button弹出新的窗体 Show()方法 和 ShowDialog()方法

在做串口通信程序时&#xff0c;有个想法&#xff0c;当点击串口设置按钮时&#xff0c;弹出一个新的窗口&#xff0c;可以设置串口相关信息&#xff0c;如何实现这一操作呢&#xff1f; 1 新建一个项目&#xff0c;窗体为form1 2 选择项目名称&#xff0c;单击右键&#xff0…

C# 弹出窗口 show()和showdialog()

目录 一、构建工程和界面介绍二 、添加代码三、验证效果和小结 我们在构建C# Form窗口的时候经常需要到弹出新的窗口&#xff0c;那么接着就会如何弹出窗口的疑问。这里介绍最常见的两种弹窗方法show()和showdialog()。我在VS2019中构建一个简单的工程来讲解让他们之间的区别。…

fwrite函数,fread函数和fgets函数详解以及使用方法

c/c文件处理函数 1. fgets函数 函数原型 char *fgets(char *s, int size, FILE *stream);参数解释&#xff1a; s 代表要保存到的内存空间的首地址&#xff0c;可以是字符数组名&#xff0c;也可以是指向字符数组的字符指针变量名。size 代表的是读取字符串的长度。stream …

c语言 fread读指定字节,fread函数 c语言中fread函数怎么用

fread是一个函数,它从文件流中读数据,最多读取count个项,每个项size个字节,如果调用成功返回实际读取到的项个数(小于或等于count),如果不成功或读到文件末尾返回0。返回真实读取的项数,若大于count则意味着产生了错误。另外,产生错误后,文件位置指示器是无法确定的。若…

c语言fread函数,C语言“fread”函数的用法?

C语言“fread”函数的用法? C语言“fread”函数的用法为“size_tf read(void *buffer,size_t size,size_t count,FILE *stream)”,其作用是从一个文件流中读数据,读取count个元素,每个元素size字节。 示例1#include #include #include int main() {FILE *stream; char m…

【C 语言】文件操作 ( fread 函数 )

文章目录 一、fread 函数二、缓冲区受限的情况 ( 循环读取文件 | feof 函数判定文件读取完毕 )三、处理乱码问题四、记录读取的字节个数五、读取到 0 字节的情况六、读取完毕的情况七、读取文本文件 " " 与 读取二进制文件 " " 区别 二进制文件读写两个重…

Scala对象 转Json字符串

2019独角兽企业重金招聘Python工程师标准>>> import org.json4s.{Formats, NoTypeHints} import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization.writeobject Json4sDemo {// 需要添加隐式转换implicit val formats: AnyRef with Forma…

【系统学习SpringBoot】SpringBoot 对象转JSON输出

SpringBoot输出JSON 以往使用SpringMVC中开发时&#xff0c;对象转JSON需要配置很多东西 【1】添加FastJson/jackjson等第三方jar 【2】在配置文件中配置Controller扫描 【3】给方法添加ResponseBody 配置FastJson还需要给配置文件中添加(很麻烦( ▼-▼ )) <mvc:annot…