1. 程式人生 > >資料結構——二叉樹的遍歷

資料結構——二叉樹的遍歷

        “樹”是一種重要的資料結構,本文淺談二叉樹的遍歷問題,採用C語言描述。

一、二叉樹基礎

1)定義:有且僅有一個根結點,除根節點外,每個結點只有一個父結點,最多含有兩個子節點,子節點有左右之分。
2)儲存結構

        二叉樹的儲存結構可以採用順序儲存,也可以採用鏈式儲存,其中鏈式儲存更加靈活。

        在鏈式儲存結構中,與線性連結串列類似,二叉樹的每個結點採用結構體表示,結構體包含三個域:資料域、左指標、右指標。

        二叉樹在C語言中的定義如下:       

struct BiTreeNode{
 int c;
 struct BiTreeNode *left;
 struct BiTreeNode *right;
};

二、二叉樹的遍歷

        “遍歷”是二叉樹各種操作的基礎。二叉樹是一種非線性結構,其遍歷不像線性連結串列那樣容易,無法通過簡單的迴圈實現。

        二叉樹是一種樹形結構,遍歷就是要讓樹中的所有節點被且僅被訪問一次,即按一定規律排列成一個線性佇列。二叉(子)樹是一種遞迴定義的結構,包含三個部分:根結點(N)、左子樹(L)、右子樹(R)。根據這三個部分的訪問次序對二叉樹的遍歷進行分類,總共有6種遍歷方案:NLR、LNR、LRN、NRL、RNL和LNR。研究二叉樹的遍歷就是研究這6種具體的遍歷方案,顯然根據簡單的對稱性,左子樹和右子樹的遍歷可互換,即NLR與NRL、LNR與RNL、LRN與RLN,分別相類似,因而只需研究NLR、LNR和LRN三種即可,分別稱為“先序遍歷”、“中序遍歷”和“後序遍歷”。

        二叉樹遍歷通常借用“棧”這種資料結構實現,有兩種方式:遞迴方式及非遞迴方式。

        在遞迴方式中,棧是由作業系統維護的,使用者不必關心棧的細節操作,使用者只需關心“訪問順序”即可。因而,採用遞迴方式實現二叉樹的遍歷比較容易理解,演算法簡單,容易實現。

        遞迴方式實現二叉樹遍歷的C語言程式碼如下:

//先序遍歷--遞迴
int traverseBiTreePreOrder(BiTreeNode *ptree,int (*visit)(int))
{
	if(ptree)
	{
		if(visit(ptree->c))
			if(traverseBiTreePreOrder(ptree->left,visit))
				if(traverseBiTreePreOrder(ptree->right,visit))
					return 1;  //正常返回
		return 0;   //錯誤返回
	}else return 1;   //正常返回
}
//中序遍歷--遞迴
int traverseBiTreeInOrder(BiTreeNode *ptree,int (*visit)(int))
{
	if(ptree)
	{
		if(traverseBiTreeInOrder(ptree->left,visit))
			if(visit(ptree->c))
				if(traverseBiTreeInOrder(ptree->right,visit))
					return 1;
		return 0;
	}else return 1;
}
//後序遍歷--遞迴
int traverseBiTreePostOrder(BiTreeNode *ptree,int (*visit)(int))
{
	if(ptree)
	{
		if(traverseBiTreePostOrder(ptree->left,visit))
			if(traverseBiTreePostOrder(ptree->right,visit))
				if(visit(ptree->c))
					return 1;
		return 0;
	}else return 1;
}

        以上程式碼中,visit為一函式指標,用於傳遞二叉樹中對結點的操作方式,其原型為:int (*visit)(char)。

        大家知道,函式在呼叫時,會自動進行棧的push,呼叫返回時,則會自動進行棧的pop。函式遞迴呼叫無非是對一個棧進行返回的push與pop,既然遞迴方式可以實現二叉樹的遍歷,那麼借用“棧”採用非遞迴方式,也能實現遍歷。但是,這時的棧操作(push、pop等)是由使用者進行的,因而實現起來會複雜一些,而且也不容易理解,但有助於我們對樹結構的遍歷有一個深刻、清晰的理解。

        在討論非遞迴遍歷之前,我們先定義棧及各種需要用到的棧操作:

//棧的定義,棧的資料是“樹結點的指標”
struct Stack{
	BiTreeNode **top;
	BiTreeNode **base;
	int size;
};
#define STACK_INIT_SIZE 100
#define STACK_INC_SIZE 10
//初始化空棧,預分配儲存空間
Stack* initStack()
{
	Stack *qs=NULL;
	qs=(Stack *)malloc(sizeof(Stack));
	qs->base=(BiTreeNode **)calloc(STACK_INIT_SIZE,sizeof(BiTreeNode *));
	qs->top=qs->base;
	qs->size=STACK_INIT_SIZE;
	return qs;
}
//取棧頂資料
BiTreeNode* getTop(Stack *qs)
{
	BiTreeNode *ptree=NULL;
	if(qs->top==qs->base)
		return NULL;
	ptree=*(qs->top-1);
	return ptree;
}
//入棧操作
int push(Stack *qs,BiTreeNode *ptree)
{
	if(qs->top-qs->base>=qs->size)
	{
		qs->base=(BiTreeNode **)realloc(qs->base,(qs->size+STACK_INC_SIZE)*sizeof(BiTreeNode *));
		qs->top=qs->base+qs->size;
		qs->size+=STACK_INC_SIZE;
	}
	*qs->top++=ptree;
	return 1;
}
//出棧操作
BiTreeNode* pop(Stack *qs)
{
	if(qs->top==qs->base)
		return NULL;
	return *--qs->top;
}
//判斷棧是否為空
int isEmpty(Stack *qs)
{
	return qs->top==qs->base;
}

        首先考慮非遞迴先序遍歷(NLR)。在遍歷某一個二叉(子)樹時,以一當前指標記錄當前要處理的二叉(左子)樹,以一個棧儲存當前樹之後處理的右子樹。首先訪問當前樹的根結點資料,接下來應該依次遍歷其左子樹和右子樹,然而程式的控制流只能處理其一,所以考慮將右子樹的根儲存在棧裡面,當前指標則指向需先處理的左子樹,為下次迴圈做準備;若當前指標指向的樹為空,說明當前樹為空樹,不需要做任何處理,直接彈出棧頂的子樹,為下次迴圈做準備。相應的C語言程式碼如下:

//先序遍歷--非遞迴
int traverseBiTreePreOrder2(BiTreeNode *ptree,int (*visit)(int))
{
	Stack *qs=NULL;
	BiTreeNode *pt=NULL;
	qs=initStack();
	pt=ptree;
	while(pt || !isEmpty(qs))
	{
		if(pt)
		{
			if(!visit(pt->c)) return 0;  //錯誤返回
			push(qs,pt->right);
			pt=pt->left;
		}
		else pt=pop(qs);
	}
	return 1;   //正常返回
}

        相對於非遞迴先序遍歷,非遞迴的中序/後序遍歷稍複雜一點。

        對於非遞迴中序遍歷,若當前樹不為空樹,則訪問其根結點之前應先訪問其左子樹,因而先將當前根節點入棧,然後考慮其左子樹,不斷將非空的根節點入棧,直到左子樹為一空樹;當左子樹為空時,不需要做任何處理,彈出並訪問棧頂結點,然後指向其右子樹,為下次迴圈做準備。

//中序遍歷--非遞迴
int traverseBiTreeInOrder2(BiTreeNode *ptree,int (*visit)(int))
{
	Stack *qs=NULL;
	BiTreeNode *pt=NULL;
	qs=initStack();
	pt=ptree;
	while(pt || !isEmpty(qs))
	{
		if(pt)
		{
			push(qs,pt);
			pt=pt->left;
		}
		else
		{
			pt=pop(qs);
			if(!visit(pt->c)) return 0;
			pt=pt->right;
		}
	}
	return 1;
}
//中序遍歷--非遞迴--另一種實現方式
int traverseBiTreeInOrder3(BiTreeNode *ptree,int (*visit)(int))
{
	Stack *qs=NULL;
	BiTreeNode *pt=NULL;
	qs=initStack();
	push(qs,ptree);
	while(!isEmpty(qs))
	{
		while(pt=getTop(qs)) push(qs,pt->left);
		pt=pop(qs);
		if(!isEmpty(qs))
		{
			pt=pop(qs);
			if(!visit(pt->c)) return 0;
			push(qs,pt->right);
		}
	}
	return 1;
}

        最後談談非遞迴後序遍歷。由於在訪問當前樹的根結點時,應先訪問其左、右子樹,因而先將根結點入棧,接著將右子樹也入棧,然後考慮左子樹,重複這一過程直到某一左子樹為空;如果當前考慮的子樹為空,若棧頂不為空,說明第二棧頂對應的樹的右子樹未處理,則彈出棧頂,下次迴圈處理,並將一空指標入棧以表示其另一子樹已做處理;若棧頂也為空樹,說明第二棧頂對應的樹的左右子樹或者為空,或者均已做處理,直接訪問第二棧頂的結點,訪問完結點後,若棧仍為非空,說明整棵樹尚未遍歷完,則彈出棧頂,併入棧一空指標表示第二棧頂的子樹之一已被處理。

//後序遍歷--非遞迴
int traverseBiTreePostOrder2(BiTreeNode *ptree,int (*visit)(int))
{
	Stack *qs=NULL;
	BiTreeNode *pt=NULL;
	qs=initStack();
	pt=ptree;
	while(1)  //迴圈條件恆“真”
	{
		if(pt)
		{
			push(qs,pt);
			push(qs,pt->right);
			pt=pt->left;
		}
		else if(!pt)
		{
			pt=pop(qs);
			if(!pt)
			{
				pt=pop(qs);
				if(!visit(pt->c)) return 0;
				if(isEmpty(qs)) return 1;
				pt=pop(qs);
			}
			push(qs,NULL);
		}
	}
	return 1;
}


三、二叉樹的建立

        談完二叉樹的遍歷之後,再來談談二叉樹的建立,這裡所說的建立是指從控制檯依次(先/中/後序)輸入二叉樹的各個結點元素(此處為字元),用“空格”表示空樹。

        由於控制檯輸入是儲存在輸入緩衝區內,因此遍歷的“順序”就反映在讀取輸入字元的次序上。

        以下是遞迴方式實現的先序建立二叉樹的C程式碼。

//建立二叉樹--先序輸入--遞迴
BiTreeNode* createBiTreePreOrder()
{
	BiTreeNode *ptree=NULL;
	char ch;
	ch=getchar();
	if(ch==' ')
		ptree=NULL;
	else
	{
		ptree=(struct BiTreeNode *)malloc(sizeof(BiTreeNode));
		ptree->c=ch;
		ptree->left=createBiTreePreOrder();
		ptree->right=createBiTreePreOrder();
	}
	return ptree;
}

        對於空樹,函式直接返回即可;對於非空樹,先讀取字元並賦值給當前根結點,然後建立左子樹,最後建立右子樹。因此,要先知道當前要建立的樹是否為空,才能做相應處理,“先序”遍歷方式很好地符合了這一點。但是中序或後序就不一樣了,更重要的是,中序或後序方式輸入的字元序列無法唯一確定一個二叉樹。我還沒有找到中序/後序實現二叉樹的建立(控制檯輸入)的類似簡單的方法,希望各位同仁網友不吝賜教哈!

四、執行及結果

        採用如下的二叉樹進行測試,首先先序輸入建立二叉樹,然後依次呼叫各個遍歷函式。

        先序輸入的格式:ABC ^ ^ D E ^ G ^ ^ F ^ ^ ^     (其中, ^  表示空格字元)

        遍歷操作採用標準I/O庫中的putchar函式,其原型為:int putchar(int);

        各種形式遍歷輸出的結果為:

                先序:ABCDEGF

                中序:CBEGDFA

                後序:CGEFDBA

        測試程式的主函式如下:

int main(int argc, char* argv[])
{
	BiTreeNode *proot=NULL;
	printf("InOrder input chars to create a BiTree: ");
	proot=createBiTreePreOrder();  //輸入(ABC  DE G  F   )
	printf("PreOrder Output the BiTree recursively: ");
	traverseBiTreePreOrder(proot,putchar);
	printf("\n");
	printf("PreOrder Output the BiTree non-recursively: ");
	traverseBiTreePreOrder2(proot,putchar);
	printf("\n");
	printf("InOrder Output the BiTree recursively: ");
	traverseBiTreeInOrder(proot,putchar);
	printf("\n");
	printf("InOrder Output the BiTree non-recursively(1): ");
	traverseBiTreeInOrder2(proot,putchar);
	printf("\n");
	printf("InOrder Output the BiTree non-recursively(2): ");
	traverseBiTreeInOrder3(proot,putchar);
	printf("\n");
	printf("PostOrder Output the BiTree non-recursively: ");
	traverseBiTreePostOrder(proot,putchar);
	printf("\n");
	printf("PostOrder Output the BiTree recursively: ");
	traverseBiTreePostOrder2(proot,putchar);
	printf("\n");
	return 0;
}