所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。
访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。
一、遍历二叉树
数据结构-二叉树遍历:https://www.cnblogs.com/polly333/p/4740355.html#7
二叉树遍历算法:https://blog.51cto.com/4837471/2327322
二叉树是由三个基本单元组成的:根(D)、左子树(L)、右子树(R);
若能依次遍历这三个部分,便是遍历了整个二叉树。
若限定先左后右,则有三种遍历方案:
先根(序)遍历:DLR
中根(序)遍历:LDR
后根(序)遍历:LRD
二叉树的定义是递归的;
遍历二叉树分为递归算法和非递归算法;
非递归算法是通过栈的方式实现的;
直接上代码:
//前序递归遍历 void PreOrderTraverse(BiTree t) { //注意跳出条件 if(t != NULL) { //注意访问语句顺序 printf("%c ", t->data); PreOrderTraverse(t->lchild); PreOrderTraverse(t->rchild); } }
前序非递归遍历:
对于任一结点p:
a. 访问结点p,并将结点p入栈;
b. 判断结点p的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点p,循环置a;若不为空,则将p的左孩子置为当前结点p;
c. 直到p为空,并且栈为空,则遍历结束。
//前序非递归遍历 int NoPreOrderTraverse(BiTree t) { SqStack s; InitStack(&s); BiTree tmp = t; if(tmp == NULL) { fprintf(stdout, "the tree is null.\n"); return ERROR; } //现将左子树压入栈,当到叶子结点后,出栈,获取右子树,然后在压入右子树的左子树。 //顺序不能变 while((tmp != NULL) || (IsEmpty(&s) != 1)) { while(tmp != NULL) { Push(&s, tmp); printf("%c ", tmp->data); tmp = tmp->lchild; } if(IsEmpty(&s) != 1) { Pop(&s, &tmp); tmp = tmp->rchild; } } return OK; }
//中序递归遍历 void InOrderTraverse(BiTree t) { if(t != NULL) { InOrderTraverse(t->lchild); printf("%c ", t->data); InOrderTraverse(t->rchild); } }
中序非递归遍历
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一个根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才停止访问,然后按相同的规则访问其右子树。其处理过程如下:
对于任一结点:
a. 若其左孩子不为空,则将p入栈,并将p的左孩子设置为当前的p,然后对当前结点再进行相同的操作;
b. 若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的p置为栈顶结点的右孩子;
c. 直到p为空并且栈为空,则遍历结束。
//中序非递归遍历二叉树 int NoInOrderTraverse(BiTree t) { SqStack s; InitStack(&s); BiTree tmp = t; if(tmp == NULL) { fprintf(stderr, "the tree is null.\n"); return ERROR; } while(tmp != NULL || (IsEmpty(&s) != 1)) { while(tmp != NULL) { Push(&s, tmp); tmp = tmp->lchild; } if(IsEmpty(&s) != 1) { Pop(&s, &tmp); printf("%c ", tmp->data); tmp = tmp->rchild; } } return OK; }
//后序递归遍历 void PostOrderTraverse(BiTree t) { if(t != NULL) { PostOrderTraverse(t->lchild); PostOrderTraverse(t->rchild); printf("%c ", t->data); } }
后序遍历的非递归实现是三种遍历方式中最难的一种。因为在后序遍历中,要保证左孩子和右孩子都已被访问,并且左孩子在右孩子之前访问才能访问根结点,这就为流程控制带来了难题。下面介绍一种思路。
要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点p,先将其入栈。若p不存在左孩子和右孩子,则可以直接访问它,或者p存在左孩子或右孩子,但是其左孩子和右孩子都已经被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将p的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子之前别访问,左孩子和右孩子都在根结点前面被访问。
//后序非递归遍历二叉树 int NoPostOrderTraverse(BiTree t) { SqStack s; InitStack(&s); BiTree cur; //当前结点 BiTree pre = NULL; //前一次访问的结点 BiTree tmp; if(t == NULL) { fprintf(stderr, "the tree is null.\n"); return ERROR; } Push(&s, t); while(IsEmpty(&s) != 1) { GetTop(&s, &cur);// if((cur->lchild == NULL && cur->rchild == NULL) || (pre != NULL && (pre == cur->lchild || pre == cur->rchild))) { printf("%c ", cur->data); //如果当前结点没有孩子结点或者孩子结点都已被访问过 Pop(&s, &tmp); pre = cur; } else { if(cur->rchild != NULL) { Push(&s, cur->rchild); } if(cur->lchild != NULL) { Push(&s, cur->lchild); } } } return OK; }
对二叉树进行遍历的方法除了上面三种之外,还可以按照从上到下,从左到右的层次遍历进行。
显然,遍历二叉树的算法中的基本操作时访问结点,则不论按哪一种次序进行遍历,对含n个结点的二叉树,其时间复杂度均为O(n)。
所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n。则空间复杂度也为O(n)。
遍历时也可以采用二叉树的其他存储结构,例如带标志域的三叉链表,此时存储结构已存有遍历所需足够信息,则遍历过程中不需另设栈。
采用带标志域的二叉链表作为存储结构,并在遍历过程中利用指针域暂存遍历路径,也可省略栈的空间,但这样做将使时间上有很大损失。
二、线索二叉树
深入学习二叉树-线索二叉树:https://www.jianshu.com/p/3965a6e424f5 //这个只讲了中序遍历
线索二叉树详解:https://blog.csdn.net/qq_29542611/article/details/79331315
遍历二叉树是以一定规则将二叉树中结点排列成一个线性序列,得到二叉树中结点的先序序列或中序序列或后序序列。
这实质上是对一个非线性结构进行线性化操作,使每个结点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。
简单说就是“前驱”和“后继”。
但是,当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点在任一序列中的前驱和后继信息。
这种信息只有在遍历动态过程中才能得到。
如何保存这种在遍历过程中得到的信息呢?一种最简单的办法是在每个结点上增加两个指针域fwd和bkwd,分别指示结点在依任一次序遍历时得到的前驱和后继信息。
显然,这样做使得结构的存储密度大大降低。
而且在另一方面:在n个结点的二叉链表中必定存在n+1个空链域。
现有一棵结点数目为n的二叉树,采用二叉链表的形式存储。对于每个结点均有指向左右孩子的两个指针域,而结点为n的二叉树一共有n-1条有效分支路径。那么,则二叉链表中存在2n-(n-1)=n+1个空指针域。那么,这些空指针造成了空间浪费。
因此可以设想利用这些空链域来存放结点的前驱和后继信息。
新的结点结构:
ltag为0时,指向左孩子,为1时指向前驱
rtag为0时,指向右孩子,为1时指向后继
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫作线索链表。
其中指向结点前驱和后继的指针,叫作线索。
加上线索的二叉树叫作线索二叉树。
对二叉树以某种次序遍历使其变为线索二叉树的过程叫作线索化。
线索二叉树也分三种:前序线索二叉树、中序线索二叉树、后序线索二叉树;
在后序线索树中找结点的后继较为复杂些。
二叉树的二叉线索存储表示
1 typedef enum PointerTag {Link, Thread}; //Link ==0:指针,Thread ==1:线索 2 3 typedef struct BiThrNode{ 4 TElemType data; 5 struct BiThrNode *lchild *rchild; 6 PointerTag LTag, RTag; 7 }BiThrNode, * BiThrTree;
如何进行二叉树的线索化?
由于线索化的实质是将二叉链表中的空指针改为指向前驱或后继的线索。
而前驱和后继的信息只有在遍历时才能得到,因此线索化的过程即为在遍历的过程中修改空指针的过程。