6.3二叉树的遍历
二叉树的遍历是指按一定规律对二叉树中的每个结点进行访问且仅访问一次。二叉树 是非线性数据结构,遍历操作就是将二叉树中结点按一定规律线性化的操作,目的在于将非 线性化结构变成线性化的访问序列。二叉树的遍历操作是二叉树中最基本的运算。
一、二叉树遍历的定义
二叉树的遍历是指按一定规律对二叉树中的每个结点进行访问且仅访问一次。其中的访问可指计算二叉树中结点的数据信息,打印该结点的信息,也包括对结点进行任何其他操作。
为什么需要遍历二叉树?二叉树是非线性数据结构,通过遍历可以将二叉树中的结点访问一次且仅一次,从而得到访问结点的顺序序列。从这个意义上说,遍历操作就是将二叉树 中结点按一定规律线性化的操作,目的在于将非线性化结构变成线性化的访问序列。二叉树的遍历操作是二叉树中最基本的运算。
二、二叉树遍历的规律及相关递归遍历算法
二叉树的基本结构是由根结点、左子树和右子树三个基本单元组成的,因此只要依次遍历这三部分,就遍历了整个二叉树。
如果用 L、D、R 分别表示遍历左子树、访问根结点、遍历右子树,那么对二叉树的遍历顺 序就可以有 6 种方式:
⑴ 访问根,遍历左子树,遍历右子树(记做 DLR)。
⑵ 访问根,遍历右子树,遍历左子树(记做 DRL)。
⑶ 遍历左子树,访问根,遍历右子树(记做 LDR)。
⑷ 遍历左子树,遍历右子树,访问根(记做 LRD)。
⑸ 遍历右子树,访问根,遍历左子树(记做 RDL)。
⑹ 遍历右子树,遍历左子树,访问根(记做 RLD)。
在以上 6 种遍历方式中,如果规定按先左后右的顺序,那么就只剩有 DLR 、LDR 和 LRD 三种。根据对根的访问先后顺序不同,分别称 DLR 为先序遍历或先根遍历;LDR 为中序遍 历(对称遍历);LRD 为后序遍历。
注意:先序、中序、后序遍历是递归定义的,即在其子树中亦按上述规律进行遍历。
下面就分别介绍三种遍历方法的递归定义。
- (1)先序遍历(DLR)操作过程
若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 访问根结点;
② 按先序遍历左子树;
③ 按先序遍历右子树 - (2)中序遍历(LDR)操作过程
若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 按中序遍历左子树;
② 访问根结点;
③ 按中序遍历右子树。 - (3)后序遍历(LRD)操作过程
若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 按后序遍历左子树;
② 按后序遍历右子树;
③ 访问根结点。
显然,遍历操作是一个递归过程。
对于如下图所示的二叉树,其先序、中序、后序遍历的序列如下:
先序遍历: A、B、D、F、G、C、E、H 。
中序遍历: B、F、D、G、A、C、E、H 。
后序遍历: F、G、D、B、H、E、C、A 。
最早提出遍历问题是对存储在计算机中的表达式求值。例如:(a+bc)-d/e。
该表达式用二叉树表示如上右图所示。当对此二叉树进行先序、中序、后序遍历时,便可获得表达式的前缀、中缀、后缀书写形式:
前缀:-+abc/de
中缀:a+bc-d/e
后缀:abc+de/-
其中中缀形式是算术表达式的通常形式,只是没有括号。前缀表达式称为波兰表达式。算术 表达式的后缀表达式被称作逆波兰表达式。在计算机内,使用后缀表达式易于求值。 下面以二叉链表作为存储结构来具体讨论二叉树的遍历算法。
(1) 先序遍历
【算法描述】
void PreOrder(BiTree root) /*先序遍历二叉树, root 为指向二叉树(或某一子树)根结点的指针*/
{
if (root!=NULL)
{
Visit(root ->data); /*访问根结点*/
PreOrder(root ->LChild); /*先序遍历左子树*/
PreOrder(root ->RChild); /*先序遍历右子树*/
}
}
(2)中序遍历
【算法描述】
void InOrder(BiTree root) /*中序遍历二叉树, root 为指向二叉树(或某一子树)根结点的指针*/
{
if (root!=NULL)
{
InOrder(root ->LChild); /*中序遍历左子树*/
Visit(root ->data); /*访问根结点*/
InOrder(root ->RChild); /*中序遍历右子树*/
}
}
(3) 后序遍历
【算法描述】
void PostOrder(BiTree root) /* 后序遍历二叉树,root为指向二叉树(或某一子树)根结点的指针*/
{
if(root!=NULL)
{
PostOrder(root ->LChild); /*后序遍历左子树*/
PostOrder(root ->RChild); /*后序遍历右子树*/
Visit(root ->data); /*访问根结点*/
}
}
显然这三种遍历算法的区别表现在 Visit 语句的位置不同,但都采用了递归的方法。 为了便于理解递归算法,以中序遍历为例,说明中序遍历二叉树的递归过程,如下图所示
当中序遍历图 6.14(a)时,p 指针首先指向 A 结点。
按照中序遍历的规则,先要遍历 A 的左子树。此时递归进层,p 指针指向 B 结点,进一步递归进层到 B 的左子树根。此时由于 p 指针等于NULL,对 B 的左子树的遍历结束,递归退层到 B 结点。访问 B 结点后递归进层到 B 的右子树。此时 p 指针指向 D 结点。进一步进层到 D 的左子树,由于 D 没有左子树, 退层到 D 结点。访问 D 后进层到 D 的右子树,由于 D 没有右子树,又退层到 D 结点,此时完成了对 D 结点的遍历,退层到 B 结点。此时对 B 结点的遍历完成,递归退层到 A 结点, 访问 A 结点后又进层到 A 的右子树。此时 p 指针指向 C 结点。同样,按照中序的规则,应该递归进层到 C 的左子树,此时 p 指针为 NULL,退层到 C 结点,访问 C 结点后又递归到 C 的右子树。此时 p 指针指向 E 结点,进一步进层到 E 的左子树。因为 p 等于 NULL,所 以退层到 E,访问 E 结点后,进层到 E 的右子树。由于 p 等于 NULL,又退层到 E,完成对 E 结点的遍历,进一步退层到 C 结点,完成对 C 的遍历。最后退层到 A。至此完成了对整 个二叉树的遍历。
递归算法的时间复杂度分析:设二叉树有 n 个结点,对每个结点都要进行一次入栈和出栈的 操作,即入栈和出栈各执行 n 次,对结点的访问也是 n 次。这些二叉树递归遍历算法的时间 复杂度为 O(n)。