二叉樹(Binary Tree)相關算法的實現


寫在前面:

二叉樹是比較簡單的一種數據結構,理解並熟練掌握其相關算法對於復雜數據結構的學習大有裨益

一.二叉樹的創建

[不喜歡理論的點我跳過>>]

所謂的創建二叉樹,其實就是讓計算機去存儲這個特殊的數據結構(特殊在哪里?特殊在它是我們自定義的)

首先,計算機內部存儲都是線性的,而我們的樹形結構是一種層級的,計算機顯然無法理解,計算機能夠接受的原始數據類型並不能滿足我們的需求

所以,只好自定義一種數據結構來表示層級關系

實際上是要定義結構 + 操作,結構是為操作服務的,舉個例子,我們要模擬買票的過程,現有的數據結構無法滿足我們的需求(不要提數組...),我們需要的操作可能是:

1.獲取站在買票隊伍最前面的人

2.把買好票的人踢出隊伍

3.第一個人買完票后,他后面的所有人都要“自覺”地向前移動

明確了這三個操作,再根據操作來定義結構,最后我們得到了隊列(數組/鏈表 + 對應的函數)

二叉樹也是這樣,計算機看到的只是結構 + 操作,結構是Node集合(二叉鏈表),操作是創建、遍歷、查找等等函數

結點:

struct bt
{
	char data;
	struct bt *left;
	struct bt *right;
};

結點就是一個桶,兩只手(桶里裝數據,兩只手伸出去抓左右兩個孩子)

操作:

//createBT();
//printBT();
//deleteNode(Node node);
//...

-------上面是對二叉樹的理解,下面是創建二叉樹具體實現-------

二叉樹的創建過程其實就是遍歷過程(此處指遞歸方式),我們知道二叉樹的任何一種遍歷方式都可以把樹形結構線性化(簡單的說就是一組遍歷結果可以唯一的表示一顆二叉樹),因此可以根據遍歷結果來還原一顆二叉樹

先序遍歷遞歸建樹的具體思路:

1.讀入當前根結點的數據

2.如果是空格,則將當前根置為空,否則申請一個新結點,存入數據

3.用當前根結點的左指針和右指針進行遞歸調用,創建左右子樹

語言描述可能不太好懂,代碼如下:

struct bt
{
	char data;
	struct bt *left;
	struct bt *right;
};

void createBT(struct bt ** root)
{
	char c;
	c=getchar();
	if(c == ' ')*root=NULL;//若為空格則置空
	else
	{
		*root=(struct bt *)malloc(sizeof(struct bt));//申請新結點
		(*root)->data=c;//存入數據
		createBT(&((*root)->left));//建立當前結點的左子樹
		createBT(&((*root)->right));//建立當前結點的右子樹
	}
}

例如,如果我們要建立一個二叉樹a(b, c),只要輸入它的先序遍歷結果ab××c××即可(×表示空格),其余兩種建樹方式於此類似,不再詳述,至於非遞歸的建樹方法參見下面的非遞歸遍歷,非常相似

二.遍歷

遍歷在實現方式上有遞歸與非遞歸兩種方式,所謂的非遞歸其實是由遞歸轉化而來的(手動維護一個棧),開銷(內存/時間)上可能非遞歸的更好一些,畢竟操作系統的棧中維護的信息更多,現場的保存與恢復開銷都要更大一些

在遍歷順序上有3種方式:

1.先序遍歷(根-左-右)

2.中序遍歷(左-根-右)

3.后序遍歷(左-右-根)

舉個例子,二叉樹a(b, c(d, e))的三種遍歷結果分別是:

1.abcde

2.badce

3.bdeca

-------下面看看后序遍歷的遞歸與非遞歸實現,其余的與之類似-------

后序遍歷遞歸:

void postOrder(struct bt * root)
{
	if(root == NULL)return;
	else
	{
		postOrder(root->left);
		postOrder(root->right);
		putchar(root->data);
	}
}

后序遍歷非遞歸:

void postOrder(struct st* root)
{
	struct st* stack[100];//聲明結點棧
	int top=-1;//棧頂索引
	struct bt* p=root;//當前結點(present)
	struct bt* q=NULL;//上一次處理的結點
	while(p!=NULL||top!=-1)
	{
		for(;p!=NULL;p=p->left)stack[++top]=p;//遍歷左子樹
		if(top!=-1)
		{
			p=stack[top];
			if(p->right==NULL||p->right==q)//無右孩子,或右孩子已經遍歷過
			{
				putchar(p->data);//輸出根結點
				q=p;
				p=stack[top];
				top--;
				p=NULL;
			}
			else p=p->right;//遍歷右子樹
		}
	}
}

為了描述地更清晰,上面直接實現了棧的操作,當然,更規范的做法是將棧作為一個獨立的數據結構封裝起來,在我們的函數中調用棧提供的操作函數來進行相關操作

三.輸出葉結點

檢索特定結點的一系列操作都是建立在遍歷的基礎上的,輸出葉結點就是一個例子,葉結點滿足的條件是左右孩子都為空,我們只要在遍歷中添加這樣的判斷條件就可以了

//此處采用先序遍歷
void printLeaves(struct bt* root)
{
	if(root == NULL)return;
	else
	{
		if(root->left == NULL&&root->right == NULL)putchar(root->data);
		else
		{
			printLeaves(root->left);
			printLeaves(root->right);
		}
	}
}

於此類似的操作有,輸出二叉樹中滿足一定條件的結點,刪除指定結點,在指定位置添加結點(子樹)...都是在遍歷的基礎上做一些額外的操作

四.計算樹的深度

計算樹深有多種方式,例如:

1.分別計算根下左右子樹的高度,二者中的較大的為樹深

2.最大遞歸深度為樹深

...

我們采用第一種方式,更清晰一些

int btDepth(struct bt* root)
{
	int rd,ld;
	if(root==NULL)return 0;//空樹深度為0
	else
	{
		ld=1+btDepth(root->left);//遞歸進層,深度加1
		rd=1+btDepth(root->right);//遞歸進層,深度加1
		return ld > rd ? ld : rd;//返回最大值
	}
}

五.樹形輸出

所謂樹形輸出,即對自然表示的二叉樹逆時針旋轉90度,其實仍然是在遍歷的過程中記錄遞歸層數,以此確定輸出結果

//depth表示遞歸深度,初始值為0
void btOutput(struct bt* root,int depth)
{
	int k;
	if(root==NULL)return;
	else
	{
		btOutput(root->right,depth+1);//遍歷右子樹
		for(k=0;k<depth;k++)
			printf(" ");//遞歸層數為幾,就輸出幾個空格(縮進幾位)
		putchar(root->data);printf("\n");
		btOutput(root->left,depth+1);//遍歷左子樹
	}
}
//“右-中-左”的遍歷順序被稱為“逆中序”遍歷,采用這種順序是為了符合輸出規則(逆時針90度)

六.按層縮進輸出

按層縮進輸出就像代碼編輯器中的自動縮進,從根結點開始逐層縮進,只需要對上面的代碼稍作改動就可以實現

//k仍然表示遞歸深度,初始值為0
void indOutput(struct bt* root,int k)
{
	int i;
	if(root!=NULL)
	{
		for(i=1;i<=k;i++)
			putchar(' ');
		putchar(root->data);putchar('\n');
		indOutput(root->left,k+1);
		indOutput(root->right,k+1);
	}
	else return;
}
//按層縮進輸出與樹形輸出的唯一區別就是遍歷方式不同,前者是先序遍歷,后者是逆中序遍歷

七.按層順序輸出

按層順序輸出與前面提及的兩種輸出方式看似相似,實則有着很大不同,至少,我們無法簡單地套用任何一種遍歷過程來完成這個目標

所以,只能維護一個隊列來控制遍歷順序

void layerPrint(struct bt* root)
{
	struct bt* queue[100];//聲明結點隊列
	struct bt* p;//當前結點
	int amount=0,head,tail,j,k;//隊列相關屬性(元素總數,對頭、隊尾索引)
	queue[0]=root;
	head=0;
	tail=1;
	amount++;
	while(1)
	{
		j=0;
		for(k=0;k<amount;k++)
		{
			p=queue[head++];//取對頭元素
			if(p->left!=NULL)
			{
				queue[tail++]=p->left;//如果有則記錄左孩子
				j++;
			}
			if(p->right!=NULL)
			{
				queue[tail++]=p->right;//如果有則記錄右孩子
				j++;
			}
			putchar(p->data);//輸出當前結點值
		}
		amount=j;//更新計數器
		if(amount==0)break;
	}
}

八.計算從根到指定結點的路徑

要記錄路徑,當然不宜用遞歸的方式,這里采用后序遍歷的非遞歸實現

為什么選擇后序遍歷?

因為在這種遍歷方式中,某一時刻棧中現有的結點恰恰就是從根結點到當前結點的路徑(從棧底到棧頂)。嚴格地說,此時應該用隊列來保存路徑,因為棧不支持從棧底到棧頂的出棧操作(這樣的小細節就把它忽略好了...)

//參數c為指定結點值
void printPath(struct bt* root,char c)
{
	struct st* stack[100];//聲明結點棧
	int top=-1;//棧頂索引
	int i;
	struct bt* p=root;//當前結點
	struct bt* q=NULL;//上一次處理的結點
	while(p!=NULL||top!=-1)
	{
		for(;p!=NULL;p=p->left)stack[++top]=p;//遍歷左子樹
		if(top!=-1)
		{
			p=stack[top];//獲取棧頂元素
			if(p->right==NULL||p->right==q)//如果當前結點沒有右孩子或者右孩子剛被訪問過
			{
				if(p->data==c)//如果找到則輸出路徑
				{
					for(i=0;i<=top;i++)
					{
						p=stack[i];
						putchar(p->data);
					}
					printf("\n");
					//此處不跳出循環,因為可能存在不唯一的結點值,遍歷整個樹,找出所有路徑
				}
				q=p;
				p=stack[top];
				top--;
				p=NULL;
			}
			else p=p->right;//遍歷右子樹
		}
	}
}

九.完整源碼與截圖示例

源碼:

#include<stdio.h>

struct bt
{
	char data;
	struct bt *left;
	struct bt *right;
};

void createBT(struct bt ** root)
{
	char c;
	c=getchar();
	if(c == ' ')*root=NULL;
	else
	{
		*root=(struct bt *)malloc(sizeof(struct bt));
		(*root)->data=c;
		createBT(&((*root)->left));
		createBT(&((*root)->right));
	}
}

void preOrder(struct bt * root)
{
	if(root == NULL)return;
	else
	{
		putchar(root->data);
		preOrder(root->left);
		preOrder(root->right);
	}
}

void inOrder(struct bt * root)
{
	if(root == NULL)return;
	else
	{
		inOrder(root->left);
		putchar(root->data);
		inOrder(root->right);
	}
}

void printLeaves(struct bt* root)
{
	if(root == NULL)return;
	else
	{
		if(root->left == NULL&&root->right == NULL)putchar(root->data);
		else
		{
			printLeaves(root->left);
			printLeaves(root->right);
		}
	}
}

int btDepth(struct bt* root)
{
	int rd,ld;
	if(root==NULL)return 0;
	else
	{
		ld=1+btDepth(root->left);
		rd=1+btDepth(root->right);
		return ld > rd ? ld : rd;
	}
}

void btOutput(struct bt* root,int depth)
{
	int k;
	if(root==NULL)return;
	else
	{
		btOutput(root->right,depth+1);
		for(k=0;k<depth;k++)
			printf(" ");
		putchar(root->data);printf("\n");
		btOutput(root->left,depth+1);
	}
}

void postOrder(struct st* root)
{
	struct st* stack[100];
	int top=-1;
	struct bt* p=root;
	struct bt* q=NULL;
	while(p!=NULL||top!=-1)
	{
		for(;p!=NULL;p=p->left)stack[++top]=p;
		if(top!=-1)
		{
			p=stack[top];
			if(p->right==NULL||p->right==q)
			{
				putchar(p->data);
				q=p;
				p=stack[top];
				top--;
				p=NULL;
			}
			else p=p->right;
		}
	}
}

void printPath(struct bt* root,char c)
{
	struct st* stack[100];
	int top=-1;
	int i;
	struct bt* p=root;
	struct bt* q=NULL;
	while(p!=NULL||top!=-1)
	{
		for(;p!=NULL;p=p->left)stack[++top]=p;
		if(top!=-1)
		{
			p=stack[top];
			if(p->right==NULL||p->right==q)
			{
				if(p->data==c)
				{
					for(i=0;i<=top;i++)
					{
						p=stack[i];
						putchar(p->data);
					}
					printf("\n");
				}
				q=p;
				p=stack[top];
				top--;
				p=NULL;
			}
			else p=p->right;
		}
	}
}

void layerPrint(struct bt* root)
{
	struct bt* queue[100];
	struct bt* p;
	int amount=0,head,tail,j,k;
	queue[0]=root;
	head=0;
	tail=1;
	amount++;
	while(1)
	{
		j=0;
		for(k=0;k<amount;k++)
		{
			p=queue[head++];
			if(p->left!=NULL)
			{
				queue[tail++]=p->left;
				j++;
			}
			if(p->right!=NULL)
			{
				queue[tail++]=p->right;
				j++;
			}
			putchar(p->data);
		}
		amount=j;
		if(amount==0)break;
	}
}

void indOutput(struct bt* root,int k)
{
	int i;
	if(root!=NULL)
	{
		for(i=1;i<=k;i++)
			putchar(' ');
		putchar(root->data);putchar('\n');
		indOutput(root->left,k+1);
		indOutput(root->right,k+1);
	}
	else return;
}

void main()
{
	char c;
	struct bt * root;
	printf("請輸入先序遍歷結果: ");
	createBT(&root);
	printf("先序遍歷(preOrder)[遞歸]: \n");
	preOrder(root);
	printf("\n中序遍歷(inOrder)[遞歸]: \n");
	inOrder(root);
	printf("\n后序遍歷(postOrder)[非遞歸]: \n");
	postOrder(root);
	printf("\n葉結點(leaves): \n");
	printLeaves(root);
	printf("\n深度(depth): \n");
	printf("%d\n",btDepth(root));
	printf("樹形輸出(tree output): \n");
	btOutput(root,0);
	printf("縮進輸出(indentation output): \n");
	indOutput(root,0);
	printf("請輸入目標結點(target node): ");
	getchar();
	c=getchar();
	printf("路徑(path): \n");
	printPath(root,c);
	printf("按層輸出(layerPrint): \n");
	layerPrint(root);
	printf("\n");
}

截圖示例:

一顆較為復雜的二叉樹:

其先序遍歷結果為:ABD××EG×××C×FH××I××(×表示空,輸入的時候要把×換成空格)

把D改成H再試一次(存在重復元素了,因該有兩條到H的路徑)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM