二叉樹深度優先遍歷算法的非遞歸實現
非遞歸實現就是自己定義棧來實現,而遞歸實現就是調用系統定義的棧。
示例二叉樹如下:
(1)先序遍歷非遞歸算法
先序遍歷非遞歸算法的思路:
- 在二叉樹先序遍歷非遞歸算法中,先將根結點壓棧,在棧不爲空的時候執行循環:
- 讓棧頂元素p出棧,訪問棧頂元素p,如果p的右孩子不爲空,則讓其右孩子先進棧,
- 如果p的左孩子不爲空,則再讓其左孩子進棧(注意:進棧順序一定是先右孩子,再左孩子)。
圖解:
核心代碼:
/* 先序遍歷非遞歸算法 */
void preOrderNonrecursion(BTNode *bt){
if(bt!=NULL){
BTNode *stack[maxSize];// 定義一個棧
int top=-1;// 初始化一個棧
BTNode *p;
stack[++top]=bt;// 根節點入棧
while(top!=-1){// 當棧不空的時候執行循環,棧空循環退出
p=stack[top--];// 出棧並輸出棧頂結點
printf("%c",p->data);// 訪問p結點的數據
if(p->rchild!=NULL){// 棧頂結點的右孩子存在,則右孩子入棧
stack[++top]=p->rchild;
}
if(p->lchild!=NULL){// 棧頂結點的左孩子存在,則左孩子入棧
stack[++top]=p->lchild;
}
}
}
}
(2)中序遍歷非遞歸算法
類似於先序遍歷。
先序遍歷非遞歸算法的思路:
- 開始根節點入棧。
- 循環如下操作:如果棧頂結點的左孩子存在,則左孩子入棧;如果棧頂結點左孩子不存在,則出棧並輸出棧頂結點,然後檢查其右孩子是否存在,如果存在,則右孩子入棧。
- 當棧空時算法結束。
圖解:
核心代碼:
/* 中序遍歷非遞歸算法 */
void inOrderNonrecursion(BTNode *bt) {
if(bt!=NULL) {
BTNode *stack[maxSize];// 定義棧
int top=-1;// 初始化棧
BTNode *p=bt;
/* 下邊這個循環完成中序遍歷。注意:在中序遍歷的過程中的進棧與出棧過程中會出現棧空狀態,但這時遍歷沒有結束,因爲根節點的右子樹還沒有遍歷,此時p非空,根據這一點來維持循環的進行 */
while(top!=-1||p!=NULL) {
while(p!=NULL) { // 左孩子存在,則左孩子入棧
stack[++top]=p;
p=p->lchild;
}
if(top!=-1) { // 在棧不空的情況下輸出棧頂結點
p=stack[top--];
printf("%c",p->data);// 打印出棧結點的數據
p=p->rchild;
}
}
}
}
(3)後序遍歷非遞歸算法
首先手工寫出對上圖中二叉樹進行先序和後序遍歷的序列。
先序遍歷序列: 1、 2、3、5、4
後序遍歷序列: 3、5、2、4. 1
把後序遍歷序列逆序得:
逆後序遍歷序列: 1、4、2、5、3
觀察發現,逆後序遍歷序列和先序遍歷序列有一定的聯繫,遵後序遍歷序列只不過是先序遍歷過程中對左右子樹遍歷順序交換所得到的結果。
因此,只需要將前邊講到的非遞歸先序遍歷算法中對左右子樹的遍歷顧序交換就可以得到逆後序遍歷序列,然後將逆後序遍歷序列逆序就得到了後字海歷字列。因此我們需要兩個棧,個棧 stackl 用來輔助做逆後序遍歷(將先序遍歷的左、右子樹遍歷順序交換的遍歷方式稱爲逆後序遍歷)並將遍歷結果序列壓入另一個棧stack2, 然後將stack2 中的元素全部出棧,所得到的序列即爲後序遍歷序列。具體的過程如下:
核心代碼如下:
/* 後序遍歷非遞歸算法 */
void postOrderNonrecursion(BTNode *bt) {
if(bt!=NULL) {
/* 定義兩個棧 */
BTNode *stack1[maxSize];
int top1=-1;// 初始化棧
BTNode *stack2[maxSize];
int top2=-1;
BTNode *p=NULL;
stack1[++top1]=bt;// 將根節點入棧1
while(top1!=-1) {
p=stack1[top1--];// 出棧棧頂結點
stack2[++top2]=p;// 注意這裏和先序遍歷的區別,輸出改爲如stack2
/* 注意:下邊這兩個if語句和先序遍歷的區別,左、右孩子的入棧順序相反 */
if(p->lchild!=NULL) {
stack1[++top1]=p->lchild;
}
if(p->rchild!=NULL) {
stack1[++top1]=p->rchild;
}
}
while(top2!=-1) {
/* 出棧序列即爲後序遍歷序列 */
p=stack2[top2--];
printf("%c",p->data);// 打印結點的值
}
}
}
完整代碼如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#define maxSize 20
/* 數結構體類型定義*/
typedef struct BTNode {
char data;// 這裏默認結點data域爲char類型
struct BTNode *lchild;// 左孩子指針域
struct BTNode *rchild;// 右孩子指針域
} BTNode,*BiTree;
/* 根據輸入創建二叉樹 */
/* 例:123##5##4## */
void CreatBiNode(BTNode **Node) { //此處應注意傳遞的參數(二重指針)
char data;
scanf("%c", &data);
*Node = (BiTree)malloc(sizeof(BTNode));
if (data == '#') {
*Node = NULL;
} else if ((data != '#') && (*Node)) {
(*Node)->data = data;
(*Node)->lchild = NULL;
(*Node)->rchild = NULL;
CreatBiNode(&(*Node)->lchild);
CreatBiNode(&(*Node)->rchild);
}
}
/* 先序遍歷非遞歸算法 */
void preOrderNonrecursion(BTNode *bt) {
if(bt!=NULL) {
BTNode *stack[maxSize];// 定義一個棧
int top=-1;// 初始化一個棧
BTNode *p;
stack[++top]=bt;// 根節點入棧
while(top!=-1) { // 當棧不空的時候執行循環,棧空循環退出
p=stack[top--];// 出棧並輸出棧頂結點
printf("%c",p->data);// 訪問p結點的數據
if(p->rchild!=NULL) { // 棧頂結點的右孩子存在,則右孩子入棧
stack[++top]=p->rchild;
}
if(p->lchild!=NULL) { // 棧頂結點的左孩子存在,則左孩子入棧
stack[++top]=p->lchild;
}
}
}
}
/* 中序遍歷非遞歸算法 */
void inOrderNonrecursion(BTNode *bt) {
if(bt!=NULL) {
BTNode *stack[maxSize];// 定義棧
int top=-1;// 初始化棧
BTNode *p=bt;
/* 下邊這個循環完成中序遍歷。注意:在中序遍歷的過程中的進棧與出棧過程中會出現棧空狀態,但這時遍歷沒有結束,因爲根節點的右子樹還沒有遍歷,此時p非空,根據這一點來維持循環的進行 */
while(top!=-1||p!=NULL) {
while(p!=NULL) { // 左孩子存在,則左孩子入棧
stack[++top]=p;
p=p->lchild;
}
if(top!=-1) { // 在棧不空的情況下輸出棧頂結點
p=stack[top--];
printf("%c",p->data);// 打印出棧結點的數據
p=p->rchild;
}
}
}
}
/* 後序遍歷非遞歸算法 */
void postOrderNonrecursion(BTNode *bt) {
if(bt!=NULL) {
/* 定義兩個棧 */
BTNode *stack1[maxSize];
int top1=-1;// 初始化棧
BTNode *stack2[maxSize];
int top2=-1;
BTNode *p=NULL;
stack1[++top1]=bt;// 將根節點入棧1
while(top1!=-1) {
p=stack1[top1--];// 出棧棧頂結點
stack2[++top2]=p;// 注意這裏和先序遍歷的區別,輸出改爲如stack2
/* 注意:下邊這兩個if語句和先序遍歷的區別,左、右孩子的入棧順序相反 */
if(p->lchild!=NULL) {
stack1[++top1]=p->lchild;
}
if(p->rchild!=NULL) {
stack1[++top1]=p->rchild;
}
}
while(top2!=-1) {
/* 出棧序列即爲後序遍歷序列 */
p=stack2[top2--];
printf("%c",p->data);// 打印結點的值
}
}
}
int main() {
printf("先序輸入二叉樹(空結點用'#'表示):");
BiTree T=NULL;
CreatBiNode(&T);// 創建二叉樹
/* 先序遍歷 */
printf("先序遍歷:");
preOrderNonrecursion(T);
/* 中序遍歷 */
printf("\n中序遍歷:");
inOrderNonrecursion(T);
/* 後序遍歷 */
printf("\n後序遍歷:");
postOrderNonrecursion(T);
return 0;
}
運行結果如下:
線索樹
線索二叉樹可以將用戶棧也省掉,把二叉樹的遍歷過程線性化,進一步提高效率。
線索二叉樹是利用二叉鏈表的空鏈域。
(1)中序線索二叉樹的構造
線索二叉樹的結點構造如下:
lchild | ltag | data | rtag | rchild |
在二叉樹線索化的過程中會把樹中的空指針利用起來作爲尋找當前結點前驅或後繼的線索,這樣就出現了一個問題,即線索和樹中原有指向孩子結點的指針無法區分。上邊的結點設計就是爲了區分這兩類指針,其中,Itag 和rtag爲標識域,它們的具體意義如下:
1)如果Itag=0, 則表示lchild爲指針,指向結點的左孩子;如果ltag=1, 則表示lchild 爲線索,指向結點的直接前驅。
2)如果rtag=0,則表示rchild爲指針,指向結點的石孩子;如果rtag=1,則表示rchild爲線索,指向結點的直接後繼。
對應的線索二叉樹的結點定義如下:
typedef struct TBTNode
{
char data;// 結點信息
int ltag;// 如果ltag爲0表示lchild爲指針,指向結點的左孩子;如果ltag爲1,則表示lchild爲線索,指向結點的直接前驅
int rtag;// 如果rtag爲0表示rchild爲指針,指向結點的右孩子;如果rtag爲0,則表示rchild爲線索,指向結點的直接後繼
struct TBTNode *lchild;// 左孩子
struct TBTNode *rchild;// 右孩子
}TBTNode;
線索二叉樹可以分爲前序線索二叉樹、中序線索二叉樹和後序線索二叉樹。對一棵二叉樹中所有結點的空指針域按照某種遍歷方式加線索的過程叫作線索化,被線索化了的二叉樹稱爲線索二叉樹。
二叉樹線索化的圖解如下:
二叉樹中序線索化分析:
1) 既然要對二又樹進行中序線索化,首先要有個中序遍歷的框架,這裏採用二叉樹中序遞歸遍歷算法,在遍歷過程中連接上合適的線索即可。
2) 線索化的規則是,左線索指針指向當前結點在中序遍歷序列中的前驅結點,右線索指針指向後繼結點。因此我們需要一個指針P指向當前正在訪問的結點,pre指向p的前驅結點,p的左線索如果存在則讓其指向pre, pre 的右線索如果存在則讓其指向p,因爲p是pre的後繼結點,這樣就完成了一對線索的連接。如上圖所示,某一時刻p指向A, pre 指向了中序遍歷過程中A的前驅D,A是D的後繼,D的右線索存在則指向A。按照這樣的規則一直進行下去,當整棵二叉樹遍歷完成的時候,線索化也就完成了。
3)上一步中保持pre始終指向P前驅的具體過程是,當p將要離開一個訪問過的結點時,pre指向p;當p來到一個新結點時,pre顯然指向的是此時P所指結點的前驅結點。
故通過中序遍歷二叉樹線索化的遞歸算法代碼如下:
/* 通過中序遍歷對二叉樹線索化的遞歸算法 */
void inThread(TBTNode *p,TBTNode *&pre) {
if(p!=NULL) {
inThread(p->lchild,pre); // 遞歸,左子樹線索化
if(p->lchild==NULL) { // 建立當前結點前驅線索
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL) { // 建立前驅結點的後繼線索
pre->rchild=p;
pre->rtag=1;
}
pre=p;// pre指向當前的p,作爲p將要指向的下一個結點的前驅結點指示指針
inThread(p->rchild,pre);// p指向一個新的結點,此時pre和p分別指向的結點形成了一個前驅後繼對爲下一次線索的連接做準備。遞歸,右子樹線索化
}
}
通過中序遍歷建立中序線索二叉樹的主程序如下:
/* 通過中序遍歷建立中序線索二叉樹 */
void createThread(TBTNode *root){
TBTNode *pre=NULL;// 前驅結點指針
if(root!=NULL){
inThread(root,pre);
pre->rchild=NULL;// 非空二叉樹,線索化
pre->rtag=1;
}
}
(2)遍歷中序線索二叉樹
訪問運算主要是爲遍歷中序線索二叉樹服務的。這種遍歷不再需要棧,因爲它利用了隱含在線索二叉樹的前驅和後繼信息。
求以p爲根的中序線索二叉樹中,中序序列下的第一個結點的算法如下:
TBTNode *First(TBTNode *p){
while(p->ltag==0){
p=p->lchild;// 最左下結點(不一定是葉結點)
}
return p;
}
求在中序線索二叉樹中,結點p在中序下的後繼結點的算法如下:
TBTNode *Next(TBTNode *p){
if(p->rtag==0){
return First(p->rchild);
}else{
return p->rchild;// rtag==1,直接返回後繼線索
}
}
如果把程序中First 的ltag和lchild換成rtag和rchild,同時把函數名First 換成Last,則可得到求中序序列下最後一個結點的函數Last(;如果把程序中Next的rtag和rchild換成ltag和lchild.並同時把函數First(換成函數Last(,再把函數名Next改爲Prior則可得到求中序序列下前驅結點的函數Prior0)。
在中序線索二叉樹上執行中序遍歷的算法:
void Inorder(TBTNode *root){
for(TBTNode *p=First(root);p!=NULL;p=Next(p)){
printf("%c",p->data);
}
}
(3)前序線索二叉樹
前序線索二叉樹和中序線索二叉樹代碼相似,最大的區別就是把連接線索的代碼提到了兩遞歸入口前邊。
void preThread(TBTNode *p,TBTNode *&pre){
if(p!=NULL){
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(p!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
/* 注意:這裏在遞歸入口處有限制條件,左、右指針不是線索才繼續遞歸 */
if(p->ltag==0){
preThread(p->lchild,pre);
}
if(p->rtag==0){
preThread(p->rchild,pre);
}
}
}
在前序線索二叉樹上執行前序遍歷的算法如下:
void preOrder(TBTNode *root){
if(root!=NULL){
TBTNode *p=root;
while(p!=NULL){
while(p->ltag==0){// 左指針不是線索,則邊訪問邊左移
printf("%c",p->data);
p=p->lchild;
}
printf("%c",p->data);// 此時p左指針必爲線索,但還沒有被訪問,則訪問
p=p->rchild;// 此時p左孩子不存在,則右指針若非空,則不論是否爲線索都指向其後繼
}
}
}
(4)後序線索二叉樹
後序線索二叉樹代碼和中序線索化代碼極爲相似,最大的區別就是把連接線索的代碼放到了兩遞歸入口後邊。
void postThread(TBTNode *p,TBTNode *&pre){
if(p!=NULL){
postThread(p->lchild,pre);// 遞歸,左子樹線索化
postThread(p->rchild,pre);// 遞歸,右子樹線索化
if(p->lchild==NULL){// 建立當前結點的前驅線索
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){// 建立前驅結點的後繼線索
pre->rchild=p;
pre->rtag=1;
}
pre=p;
}
}