1. 程式人生 > >二叉樹的非遞迴遍歷的實現

二叉樹的非遞迴遍歷的實現

1.前言:當然了,這也是複習。因為資料結構要考試了,把之前的演算法拿出來敲一遍。

2.二叉樹的遍歷是一個基於二叉樹很重要的操作,應用也很多。我們知道遞迴是樹的特性,所以樹上的所有演算法幾乎都是基於遞迴做的。但是在一些嵌入式裝置中,系統棧不允許很深的遞迴。這時候就要把遞迴的演算法轉換為非遞迴演算法,嗯,沒錯,應用場景看上去有點實際,但是真正促使我的還是考試,哈哈。

先給出一個遍歷的遞迴演算法把。

typedef struct BinaryTreeNode
{
	int data;
	struct BinaryTreeNode *Left;
	struct BinaryTreeNode *Right;
}Node,*node;
//先序遍歷
void preOrderTraverse(Node* root)
{
	if (root)
	{
		cout << root->data << ' ';
		preOrderTraverse(root->Left);
		preOrderTraverse(root->Right);
	}
}

//中序遍歷
void inOrderTraverse(Node* root)
{
	if (root)
	{
		inOrderTraverse(root->Left);
		cout << root->data << ' ';
		inOrderTraverse(root->Right);
	}
}

//後序遍歷
void lastOrderTraverse(Node* root)
{
	if (root)
	{
		lastOrderTraverse(root->Left);
		lastOrderTraverse(root->Right);
		cout << root->data << ' ';
	}
}

其實用遞迴寫很簡單了,就是看訪問的語句在什麼地方嘍,前邊就是前序,中間就是中序,後邊就是後序。程式碼十分的簡介而且易懂。但是,上邊我們也說了因為老師考試,哦不對,是有些裝置不允許這樣的程式出現在它內部。所以我們就要把遞迴轉成非遞迴演算法。那麼我們就來看一下怎麼轉換唄。

3.前序的非遞迴實現。我們知道前序遍歷的順序是根-左-右。根據這個特點我們知道,在訪問完根節點之後,我們就訪問他的左子樹,然後是右子樹。一種BFS的思想,其實也不難,就簡單寫一下OK。

void preOrder(Node*root)
{
	stack<node>st;
	st.push(root);
	Node*tem = NULL;
	while (!st.empty())
	{
		tem = st.top();
		st.pop();
		if (tem != NULL)
		{
			cout << tem->data << " ";
			st.push(tem->Left);
			st.push(tem->Right);
		}
	}
}

4.中序遍歷的非遞迴實現。中序的順序是左-根-右。我們的想法是首先要遞迴左子樹,把左子樹的節點全部都放在棧裡,想象一下這個過程,其實這個棧的內部存的就是二叉樹最左邊的那條路徑。訪問的時候,我們首先彈出棧頂,訪問,檢查這個節點有沒有右子樹,如果有,切換為右子樹做同樣的操作。那麼這個時候有人可能會有疑問:根節點哪去了?其實仔細想想,根節點相對於它的上一個元素不就是左子樹上的點嗎?所以這樣就會按照這個順序訪問完所有的節點。並且,需要注意的是,這個左右子樹是在交替的,看上去棧裡只有左子樹的根節點,其實整棵樹都在。或者我覺得,左右子樹是一開始我們就定義好的,其實他們是一樣的。下邊給出程式碼實現:

void inOrder(Node*root)
{
	stack<node>st;
	node p = root;
	do
	{
		while (p != NULL)
		{
			st.push(p);
			p = p->Left;
		}
		p = st.top();
		st.pop();
		cout << p->data << " ";
		if (p->Right != NULL)
		{
			p = p->Right;
		}
		else
		{
			p = NULL;
		}
	} while (p!=NULL||!st.empty());
}

5.最後是後續遍歷的實現:這個是有些困難的,我覺得是這三個中最不好寫的。我們知道後序的順序是左-右-根。這樣其實會出現一個問題,就是當你訪問根節點的時候不知帶左右子樹是否已經被訪問過了。解決辦法有很多,有的是給節點加一個屬性,但是我在這裡使用額外O(1)的空間來完成這件事。我們還是要左子樹入棧,但是需要有一個儲存最新被棧彈出的節點。我們跟著程式跑一遍來解釋這個問題。首先我們把左節點全部入棧,當前彈出來的節點初始化為root,當我們完成這些之後,開始訪問節點。這個時候已經被到了二叉樹最左邊的點,第一個if不成立了。檢查第二個依舊不成立。進入else彈出這個節點,更新彈出的節點,訪問它。好了,這個時候我們已經完成了最左邊的葉子節點的訪問(雖然不一定是葉子節點,但是不影響)。第二輪繼續進入迴圈,p被更新到父節點,雖然p-left不再是NULL,但是p-left=last,所以第一個條件不成立,這個時候我們發現第二個條件成立了,所以把p的右節點壓進去訪問。last被更新為右節點,繼續進入迴圈,這個時候我們發現兩個條件都是不成立的,所以只能訪問根節點。這樣就完成了訪問。是不是很神奇,程式碼之道,讓人驚歎!

//非遞迴後續遍歷二叉樹
void lastOrder(node root)
{
	assert(root);
	stack<node>st;
	st.push(root);
	node last = root;
	while (!st.empty())
	{
		node p = st.top();
		if (p->Left != NULL && p->Left != last && p->Right != last)
		{
			st.push(p->Left);
		}
		else if (p->Right != NULL && p->Right != last && (p->Left == NULL || p->Left == last))
		{
			st.push(p->Right);
		}
		else
		{
			st.pop();
			last = p;
			cout << p->data << " ";
		}
	}
}