数据结构如何解遍历二叉树 树的遍历?

二叉树的遍历:是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。

访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计算,输出打印等。它算作是一个抽象操作。

二叉树的遍历次序不同于线性结构,最多也就是从头到尾、循环和双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。

二叉树的遍历方式可以有很多,如果我们限制从左到右的顺序,就主要分为四种:

若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如下图遍历顺序为:ABDGHCEIF。

若二叉树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点左子树,然后访问根结点,最后中序遍历右子树,如下图遍历顺序为:GDHBAEICF。

若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点,如下图遍历顺序为:GHDBIEFCA。

若二叉树为空,则空操作返回,否则从树的第一层开始,也就是从根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问,如下图遍历顺序为:ABCDEFGHI。

程序代码:这里的代码是C写的,换算成java一样,只是写法不一样,逻辑一样。所以接下来的代码,请看逻辑

实例:分析【前序遍历算法】代码执行逻辑:

实例:分析【中序遍历算法】代码执行逻辑:

实例:分析【后续遍历算法】代码执行逻辑:

我们提到的四种遍历方式,其实都是在把树种的结点编程某种意义上的线性序列,这样给程序执行带来了好处。

层序遍历很好理解,就是逐层遍历,每层从左到右逐个遍历;前序、中序、后序遍历最根本的区别就是双亲结点的访问时机:前序是先访问双亲结点,然后左孩子,最后右孩子;中序是左孩子,双亲,右孩子;后序是左孩子、右孩子最后双亲结点。

树的定义就使用了递归这一方式,当然,对树的遍历也是使用递归(注意递归算法一定要有结束递归的标志),关于二叉树遍历的算法,就不再详细描述了。

有一种题目是为了考察你对二叉树遍历的掌握程度,会这样出题:已知一棵二叉树的前序遍历顺序是ABCDEF,中序遍历顺序是CBAEDF,请问这棵树的后序遍历是什么?

对于这样的题目,如果真正了解各种遍历规则,其实是不难的。

三种遍历都是从根结点开始,前序遍历是先打印再递归左和右,所有前序遍历序列为ABCDEF,第一个字母A就是根结点;再由中序遍历序列CBAEDF,可以只带C和B是A的左子树上的结点,E、D、F是A的右子树上的结点,我们可以得到以下这样的图:

然后再看前序序列中的C和B,先B再C,所以B应该是A的左孩子,C就只能说B的孩子了,至于C是B的左还是右孩子,再看中序序列CBAEDF,C在B之前打印,说明C是B的左孩子,如图:

再看前序中的E、D、F,他们的顺序是ABCDEF,意味着D是A的右孩子,E和F是D的子孙(注意,他们中有一个不一定是孩子,还可能是孙子)。再看中序序列是CBAEDF,E在D的左侧,F在右侧,所以E是D的左孩子,F是D的右孩子,如图:

为了避免推导失误,我们最好自己再按此树推导一遍前序和中序遍历序列。根据二叉树结构图,轻松得到后序遍历为CBEFDA。

这里我们可以得到两个二叉树遍历的性质:

  1. 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;
  2. 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;

注意:已知前序和后序遍历,是不能确定一棵二叉树的。

当你第一次学习编码时,大部分人都是将数组作为主要数据结构来学习。

之后,你将会学习到哈希表。如果你是计算机专业的,你肯定需要选修一门数据结构的课程。上课时,你又会学习到链表,队列和栈等数据结构。这些都被统称为线性的数据结构,因为它们在逻辑上都有起点和终点。

当你开始学习树和图的数据结构时,你会觉得它是如此的混乱。因为它的存储方式不是线性的,它们都有自己特定的方式存储数据。

树是众所周知的非线性数据结构。它们不以线性方式存储数据。他们按层次组织数据。

树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。

(1)有且仅有一个特定的称为根(Root)的结点。

(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、.....、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

其中根结点A有两个子树:

我们硬盘的文件系统就是很经典的树形结构。

“树”它具有以下的特点:

    ④除了根节点外,每个子节点可以分为多个不相交的子树;

树的首结点叫根结点(即 root结点)。如果这个根结点和其他结点所连接,那么根结点是父结点与根结点连接的是子结点。

所有的结点都通过边连接。它是树中很重要得一个概念,因为它负责管理节点之间的关系。

叶子结点是树末端,它们没有子结点。像真正的大树一样,我们可以看到树上有根、枝干和树叶。

  • 边是两个结点之间的连接

  • 子结点是具有父结点的结点

  • 父结点是与子结点有连接的结点

  • 叶子结点是树中没有子结点的结点(树得末端)

  • 高度是树到叶子结点(树得末端)的长度

  • 深度是结点到根结点的长度

树的结点包含一个数据元素及若干指向其子树的分支

结点拥有的子树数称为结点的度(Degree)

树的度是树内各结点度的最大值。

 结点的层次从根开始定义起,根为第一层,根的孩子为第二层,以此类推,若某结点在第 i 层,则其子树的根就在第 i+1 层。

其双亲在同一层的结点互为堂兄弟。显然下图中的D、E、F是堂兄弟,而G、H、l、J也是。

树的深度(Depth)或高度是树中结点的最大层次。 

  • 树的高度是到叶子结点(树末端)的长度,也就是根结点到叶子结点的最大边长度

  • 结点的深度是它到根结点的长度,也就是层次

在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。

优点:parent指针域指向数组下标,所以找双亲结点的时间复杂度为O(1),向上一直找到根节点也快
缺点:由上向下找就十分慢,若要找结点的孩子或者兄弟,要遍历整个树

缺点:占用了大量不必要的孩子域空指针。 若要找结点的父亲,要遍历整个树。

改进一:为每个结点添加一个结点度域,方便控制指针域的个数

改进二:结合顺序结构和链式结构

把所有结点先放在数组里面,每个结点都会有自己的子结点,第一个孩子就用一个指针表示,每个孩子的next指针指向它的兄弟

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针 ,分别指向该结点的第一个孩子和此结点的右兄弟

二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成(子树也为二叉树)。

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

  2、只有一个根结点

  3、根结点只有左子树

  4、根结点只有右子树

  5、根结点既有左子树又有右子树

左斜树:  右斜树:

性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)

性质2:深度为k的二叉树至多有2k-1个结点(k>=1)

性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1。

一棵二叉树,除了终端结点(叶子结点),就是度为1或2的结点。假设n1度为1的结点数,则数T 的结点总数n=n0+n1+n2。我们再换个角度,看一下树T的连接线数,由于根结点只有分支出去,没有分支进入,所以连接线数为结点总数减去1。也就是n-1=n1+2n2,可推导出n0+n1+n2-1 =

性质4:具有n个结点的完全二叉树的深度为[log2n +1] ([X]表示不大于X的最大整数)。

由性质2可知,满二叉树的结点个数为2k-1,可以推导出满二叉树的深度为k=log2(n + 1)。对于完全二叉树,它的叶子结点只会出现在最下面的两层,所以它的结点数一定少于等于同样深度的满二叉树的结点数2k-1,但是一定多于2k-1 -1。因为n是整数,所以2k-1 <= n < 2k,不等式两边取对数得到:k-1 <= log2n

性质5:如果对一颗有n个结点的完全二叉树(其深度为 [ log2n+1 ] )的结点按层序编号(从第1层到第 [log2n+1] 层,每层从左到右),对任一结点 i (1<=i<=n) 有:

  1. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
  2. 如果2i+1>n,则结点 i 无右孩子;否则其右孩子是结点2i+1

^ 代表不存在的结点。

链表每个结点包含一个数据域和两个指针域:

其中data是数据域,lchild和rchild都是指针域,分别指向左孩子和右孩子。

DFS 在回溯和搜索其他路径之前找到一条到叶节点的路径。让我们看看这种类型的遍历的示例。

输出结果为: 1–2–3–4–5–6–7

  1. 从根结点(1)开始。输出

  2. 进入左结点(2)。输出

  3. 然后进入左孩子(3)。输出

  4. 回溯,并进入右孩子(4)。输出

  5. 回溯到根结点,然后进入其右孩子(5)。输出

  6. 进入左孩子(6)。输出

  7. 回溯,并进入右孩子(7)。输出

当我们深入到叶结点时回溯,这就被称为 DFS 算法。

既然我们对这种遍历算法已经熟悉了,我们将讨论下 DFS 的类型:前序、中序和后序。

这和我们在上述示例中的作法基本类似。

  1. 进入其左结点并输出。当且仅当它拥有左结点。

  2. 进入右结点并输出之。当且仅当它拥有右结点

前序遍历 - -栈实现

//避免叶子结点为空,出现空指针异常

示例中此树的中序算法的结果是3–2–4–1–6–5–7。

左结点优先,之后是中间,最后是右结点。

以此树为例的后序算法的结果为 3–4–2–6–7–5–1 。

左结点优先,之后是右结点,根结点的最后。

自创遍历小技巧(附链接)

 “树”是一种重要的数据结构,本文浅谈二叉树的遍历问题,采用C语言描述。

1)定义:有且仅有一个根结点,除根节点外,每个结点只有一个父结点,最多含有两个子节点,子节点有左右之分。

        在链式存储结构中,与线性链表类似,二叉树的每个结点采用结构体表示,结构体包含三个域:数据域、左指针、右指针。

        “遍历”是二叉树各种操作的基础。二叉树是一种非线性结构,其遍历不像线性链表那样容易,无法通过简单的循环实现。

二叉树是一种树形结构,遍历就是要让树中的所有节点被且仅被访问一次,即按一定规律排列成一个线性队列。二叉(子)树是一种递归定义的结构,包含三个部分:根结点(N)、左子树(L)、右子树(R)。根据这三个部分的访问次序对二叉树的遍历进行分类,总共有6种遍历方案:NLR、LNR、LRN、NRL、RNL和LNR。研究二叉树的遍历就是研究这6种具体的遍历方案,显然根据简单的对称性,左子树和右子树的遍历可互换,即NLR与NRL、LNR与RNL、LRN与RLN,分别相类似,因而只需研究NLR、LNR和LRN三种即可,分别称为“先序遍历”、“中序遍历”和“后序遍历”。

        在递归方式中,栈是由操作系统维护的,用户不必关心栈的细节操作,用户只需关心“访问顺序”即可。因而,采用递归方式实现二叉树的遍历比较容易理解,算法简单,容易实现。

大家知道,函数在调用时,会自动进行栈的push,调用返回时,则会自动进行栈的pop。函数递归调用无非是对一个栈进行返回的push与pop,既然递归方式可以实现二叉树的遍历,那么借用“栈”采用非递归方式,也能实现遍历。但是,这时的栈操作(push、pop等)是由用户进行的,因而实现起来会复杂一些,而且也不容易理解,但有助于我们对树结构的遍历有一个深刻、清晰的理解。

        首先考虑非递归先序遍历(NLR)。在遍历某一个二叉(子)树时,以一当前指针记录当前要处理的二叉(左子)树,以一个栈保存当前树之后处理的右子树。首先访问当前树的根结点数据,接下来应该依次遍历其左子树和右子树,然而程序的控制流只能处理其一,所以考虑将右子树的根保存在栈里面,当前指针则指向需先处理的左子树,为下次循环做准备;若当前指针指向的树为空,说明当前树为空树,不需要做任何处理,直接弹出栈顶的子树,为下次循环做准备。相应的C语言代码如下:

        对于非递归中序遍历,若当前树不为空树,则访问其根结点之前应先访问其左子树,因而先将当前根节点入栈,然后考虑其左子树,不断将非空的根节点入栈,直到左子树为一空树;当左子树为空时,不需要做任何处理,弹出并访问栈顶结点,然后指向其右子树,为下次循环做准备。

最后谈谈非递归后序遍历。由于在访问当前树的根结点时,应先访问其左、右子树,因而先将根结点入栈,接着将右子树也入栈,然后考虑左子树,重复这一过程直到某一左子树为空;如果当前考虑的子树为空,若栈顶不为空,说明第二栈顶对应的树的右子树未处理,则弹出栈顶,下次循环处理,并将一空指针入栈以表示其另一子树已做处理;若栈顶也为空树,说明第二栈顶对应的树的左右子树或者为空,或者均已做处理,直接访问第二栈顶的结点,访问完结点后,若栈仍为非空,说明整棵树尚未遍历完,则弹出栈顶,并入栈一空指针表示第二栈顶的子树之一已被处理。

        谈完二叉树的遍历之后,再来谈谈二叉树的创建,这里所说的创建是指从控制台依次(先/中/后序)输入二叉树的各个结点元素(此处为字符),用“空格”表示空树。

对于空树,函数直接返回即可;对于非空树,先读取字符并赋值给当前根结点,然后创建左子树,最后创建右子树。因此,要先知道当前要创建的树是否为空,才能做相应处理,“先序”遍历方式很好地符合了这一点。但是中序或后序就不一样了,更重要的是,中序或后序方式输入的字符序列无法唯一确定一个二叉树。我还没有找到中序/后序实现二叉树的创建(控制台输入)的类似简单的方法,希望各位同仁网友不吝赐教哈!

对于visit函数指针,可以这样简单的理解。visit是一个指针变量,它指向一个函数,这个函数的返回类型是int,这个函数的形参只有一个,也是int。那么怎么样表示这样一个指针才能让编译器知道visit是一个函数指针呢(即指向一个函数)?C标准给我们定了一个形式,就是 int (*visit) (int); 前一个int 表示返回类型,后一个int表示函数形参类型。 说到这里,我想起了之前我对此的一个误解。我之前以为这个被指向的函数肯定是这样的:int visit(int);visit就是函数名。其实不是这样的,这个被指向的函数可以是这样的,int hanshu(int)。 另外,这个函数指针也不一定非得叫visit,也可以定义为 int (*v)(int),v照样指向返回类型是int,形参类型是int的一个函数。 这是我当初看到visit的一些误区,我发现把这些误区弄清楚了,我就搞清楚了visit到底是什么了,希望对你有帮助。

我要回帖

更多关于 数据结构如何解遍历二叉树 的文章

 

随机推荐