數據結構與算法-二叉樹遍歷

樹的遍歷是當且僅當訪問樹中每一個節點一次的過程。遍歷能夠解釋爲把全部的節點放在一條線上,或者將樹線性化。
遍歷的定義只指定了一個條件:每一個節點僅訪問一次,沒有指定這些節點的訪問順序。所以,節點有多少種排列方式,就有多少種遍歷方法。對於有n個節點的樹,共有n!個不一樣的遍歷方式。然而大多數遍歷方式是混亂的,很難從中找到規律,所以實現這樣的遍歷缺少廣泛性。通過前人總結,這裏介紹兩種遍歷方式,即廣度優先遍歷與深度優先遍歷。
  • 廣度優先遍歷
廣度優先遍歷是從根節點開始,向下逐層訪問每一個節點。當使用隊列時,這種遍歷方式的實現至關直接。假設從上到下、從左到右進行廣度優先遍歷。在訪問了一個節點以後,它的子節點就放到隊列的末尾,而後訪問隊列頭部的節點。對於層次爲n的節點,它的子節點位於第n+1層,若是將該節點的全部子節點都放到隊列的末尾,那麼,這些節點將在第n層的全部節點都訪問以後再訪問。
代碼實現以下:
//廣度優先遍歷
void breadthFirst(Node* &bt){
	queue<Node*> que;
	Node* BT = bt;
	que.push(BT);
	while (!que.empty()){
		BT = que.front();
		que.pop();
		printf("%c", BT->data);
		if (BT->lchild)
			que.push(BT->lchild);
		if (BT->rchild)
			que.push(BT->rchild);
	}
}複製代碼
廣度優先遍歷的邏輯比較簡單,這裏就不過多介紹了。
  • 深度優先遍歷
樹的深度優先遍歷至關有趣。深度優先遍歷將盡量的向下進行,在遇到第一個轉折點時,向左或向右一步,而後再儘量的向下發展。這一過程一直重複,直至訪問了全部節點爲止。然而,這必定義並無清楚的指明何時訪問節點:在沿着樹向下進行以前仍是在折返以後呢?所以,深度優先遍歷有着幾種變種。
假如如今有個簡單的二叉樹:
咱們從左到右進行深度優先遍歷:
你會發現其實每一個節點遍歷到了3次:
紅色的是第一次遍歷到的次序,藍色的是第二次遍歷到的次序,黑色的是第三次遍歷到的次序。這其實就是二叉樹從左到右的前序、中序、後序遍歷,從右到左也有三種遍歷,和上面遍歷的相似。前輩們對這三種遍歷方式進一步總結,將深度遍歷分解成3個任務:
  • V-訪問節點
  • L-遍歷左子樹
  • R-遍歷右子樹
前序遍歷實際上是VLR,中序遍歷是LVR,後序遍歷是LRV
咱們來討論深度遍歷的實現方法,毫無疑問,遞歸是很是簡單而且容易理解的方式,代碼以下:
//從左向右前中後遞歸遍歷
void leftPreOrder(Node *bt) {
	if (bt){
		printf("%c", bt->data);
		leftPreOrder(bt->lchild);
		leftPreOrder(bt->rchild);
	}
}

void leftInOrder(Node *bt) {
	if (bt){
		leftInOrder(bt->lchild);
		printf("%c", bt->data);
		leftInOrder(bt->rchild);
	}
}

void leftPostOrder(Node *bt) {
	if (bt){
		leftPostOrder(bt->lchild);
		leftPostOrder(bt->rchild);
		printf("%c", bt->data);
	}
}複製代碼
能夠看到三種遍歷方式的實現代碼差異很小,其實差異就是V、L、R的順序不一樣,遞歸方式沒啥難點,這裏就很少講了。重點在於下面的非遞歸實現方式。
遞歸代碼是有缺陷的,毫無疑問會給運行時棧帶來沉重的負擔,由於每次遞歸(調用函數)都會在運行時棧上開闢新的函數棧空間,若是二叉樹的高度比較大,程序運行可能直接爆掉棧空間,這是不能容忍的。所以,咱們要實現非遞歸方式的二叉樹深度遍歷。
尋常的非遞歸遍歷方式實質上就是模擬上面動態圖中箭頭的轉移過程,在三種遍歷方式中前序遍歷是最簡單的,不管是哪種遍歷方式,都須要藉助棧來實現。由於在深度遍歷中會有回溯的過程,這就要求程序能夠記住以往遍歷過的節點,只有知足這一點才能回溯成功,這就須要藉助棧來保存遍歷節點的次序。下面是非遞歸遍歷方式的代碼:
節點定義:

typedef struct BiNode{
  int data;
  struct BiNode * lchild;
  struct BiNode * rchild;
}BiNode,*BiTree;

前序遍歷:

void preOrderBiTree(BiNode * root){
	if(root == NULL)
		return;
	BiNode * node = root;
	stack<BiNode*> nodes;
	while(node || !nodes.empty()){
		while(node != NULL){
			nodes.push(node);
			printf("%d",node->data);
			node = node -> lchild;
		}
		node = nodes.top();//回溯到父節點
		nodes.pop();
		node = node -> rchild;
	}
}複製代碼
能夠看到,在將節點入棧以前已經訪問過節點,棧在這裏的做用只有回溯。
中序遍歷代碼以下:
void inOrderBinaryTree(BiNode * root){
        if(root == NULL)
                return;
        BiNode * node = root;
        stack<BiNode*> nodes;
        while(node || !nodes.empty()){
                while(node != NULL){
                        nodes.push(node);
                        node = node ->lchild;
                }
                node = nodes.top();
                printf("%d ",node ->data);
                nodes.pop();
                node = node ->rchild;
        }
}複製代碼
中序遍歷中訪問節點發生在出棧以前,棧在這裏的做用不只僅是回溯,出棧還表明着第二次遍歷到該節點。後序遍歷更是須要程序實現V以前L和R操做已經完成,這就須要程序具備記憶功能。
後序遍歷代碼以下:
void PostOrderS(Node *root) {
    Node *p = root, *r = NULL;
    stack<Node*> s;
    while (p!=NULL || !s.empty()) {
        if (p!=NULL) {//走到最左邊
            s.push(p);
            p = p->left;
        }
        else {
            p = s.top();
            if (p->right!=NULL && p->right != r)//右子樹存在,未被訪問
                p = p->right;
            else {
                s.pop();
                visit(p->val);
                r = p;//記錄最近訪問過的節點
                p = NULL;//節點訪問完後,重置p指針
            }
        }//else
    }//while
}複製代碼
後序遍歷中最重要的邏輯是保證節點的左右子樹訪問以後再訪問該節點,程序中使用變量保存了上次訪問的節點。若是節點i存在右子樹,而且上個訪問的節點是右子樹的根節點,才能訪問i節點。
毫無疑問,以上經過程序模擬前中後三種遍歷過程,就是咱們常說的過程式代碼,若是你在面試中遇到這類問題,情急之下不必定能寫的出來。究其緣由在於思想不統一,前中後遍歷的三種代碼實現方式是割裂開的。怎麼辦?筆者探究了一種實現方式,從思想上統一前中後三種遍歷過程。
首先用天然語言描述實現邏輯:
假若有如下二叉樹:
咱們來實現後序遍歷,後序遍歷的邏輯是LRV,也就是說,對於一顆二叉樹,咱們但願首先遍歷左子樹,而後是右子樹,最後是根節點,放到棧裏就是這樣的:
注意這裏,這裏看似只是壓入了三個節點,實質上已經將整棵樹壓入到棧裏了。由於13在這裏表明的不是一個節點,而是整棵左子樹,23也是表明着整棵右子樹。這裏就有疑問了,以這樣的方式入棧有個錘子用處,你告訴我怎麼訪問13這顆左子樹?如何解決這個矛盾點纔是核心邏輯。咱們能夠繼續分解左子樹,就像這樣:
能夠看到,13表明的左子樹以LRV的方式分解成3部分。這時棧中存在兩個節點三顆樹,程序的下一步繼續彈出棧頂節點,若是是樹就繼續分解,若是是節點就表明着該節點是第一個能夠訪問到的節點。
使用這種方式來遍歷二叉樹,前中後三種遍歷的差異只有一點,就是如何分解樹。前序是VLR,中序是LVR,後序是LRV,程序的其餘部分沒有差異。
完整邏輯以下:
首先將根節點壓入棧,隨後彈出棧頂節點,發現是棵樹,分解該樹,按照指定的方式將左子樹、右子樹、根節點壓入棧中。隨後彈出棧頂節點,若是是棵樹就繼續分解,若是是節點就訪問該節點。程序的結束標誌是棧爲空,表明整棵樹的節點都已經訪問完畢。
這種遍歷方式筆者稱之爲分解遍歷,由於核心邏輯是如何分解棧中表明樹的節點。分解遍歷使得前中後非遞歸遍歷以統一的邏輯來處理。
分解遍歷要求棧中節點保存額外的屬性,即該節點是樹仍是結點,就是說咱們須要一種方式知道彈出節點是樹仍是結點。方法有不少,能夠修改節點定義,添加一個額外的成員變量,或者棧中存儲字典,key是節點,value表明該節點是樹仍是結點,或者修改棧,爲每一個節點保存額外的屬性。筆者使用的方法是修改棧,爲棧中每一個節點保存額外屬性。
參考代碼以下:
/*二叉樹節點*/
typedef struct node {
	char data;
	struct node *lchild, *rchild;
}Node;

/*二叉樹棧*/
typedef struct {
	int top;//指向二叉樹節點指針的指針
	struct node* data[MAX];
	bool   isNode[MAX];
}SqStack;

void push(SqStack *&s, Node *bt, bool isNode = true) {
	if (s == NULL)
               return;
	if (s->top == MAX - 1)
	{
		printf("棧滿,不能再入棧");
	}
	else
	{
		s->top++;
		s->data[s->top] = bt;
		s->isNode[s->top] = isNode;
	}
}

Node* pop(SqStack *&s) {
	if (s->top == -1)
	{
		printf("棧空,不能出棧");
	}
	else
	{
		Node* temp;
		temp = s->data[s->top];
		s->top--;
		return temp;
	}
}

bool topIsNode(SqStack *&s){
	if (s->top != -1)
		return s->isNode[s->top];
	return false;
}

//從左向右前中後非遞歸遍歷
void leftPreOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
			push(s, BT, true);
		}		
	}
}

void leftSimPreOrder(Node *bt) {
	Node *BT = bt;
	push(s, BT);
	while (!EmptyStack(s)){
		BT = pop(s);
		printf("%c", BT->data);
		if (BT->rchild != NULL)
			push(s, BT->rchild);
		if (BT->lchild != NULL)
			push(s, BT->lchild);
	}
}

void leftInOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			push(s, BT, true);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
		}
	}
}

void leftPostOrder(Node *bt) {
	Node *BT = bt;
	bool isNode = false;
	push(s, BT, isNode);
	while (!EmptyStack(s)){
		isNode = topIsNode(s);
		BT = pop(s);
		if (isNode){
			printf("%c", BT->data);
		}
		else{
			push(s, BT, true);
			if (BT->rchild != NULL)
				push(s, BT->rchild, false);
			if (BT->lchild != NULL)
				push(s, BT->lchild, false);
		}
	}
}複製代碼
參考代碼中前中後非遞歸遍歷的邏輯幾乎一致,差異在於分解樹以後,以怎樣的方式入棧。並且對於前序遍歷,筆者進行了簡化。由於前序遍歷中,樹以VLR的方式分解,入棧以後,棧頂老是結點,所以能夠省去判斷棧頂是樹仍是結點的邏輯。
使用棧的二叉樹遍歷到此已經探究完畢,更多內容請看下一章節。

數據結構與算法-二叉查找樹
node

相關文章
相關標籤/搜索