遞歸寫法,只要理解思想,幾行代碼。可是非遞歸寫法卻很不容易。這裏特地總結下,透徹解析它們的非遞歸寫法。其中,中序遍歷的非遞歸寫法最簡單,後序遍歷最難。我們的討論基礎是這樣的:
1
2
3
4
5
6
7
|
//Binary
Tree Node typedef
struct node { int data; struct
node* lchild; //左孩子 struct
node* rchild; //右孩子 }BTNode; |
首先,有一點是明確的:非遞歸寫法一定會用到棧,這個應該不用太多的解釋。我們先看中序遍歷:
中序遍歷
分析
中序遍歷的遞歸定義:先左子樹,後根節點,再右子樹。如何寫非遞歸代碼呢?一句話:讓代碼跟着思維走。我們的思維是什麼?思維就是中序遍歷的路徑。假設,你面前有一棵二叉樹,現要求你寫出它的中序遍歷序列。如果你對中序遍歷理解透徹的話,你肯定先找到左子樹的最下邊的節點。那麼下面的代碼就是理所當然的:
中序代碼段(i)
1
2
3
4
5
6
7
8
|
BTNode*
p = root; //p指向樹根 stack<btnode*>
s; //STL中的棧 //一直遍歷到左子樹最下邊,邊遍歷邊保存根節點到棧中 while (p) { s.push(p); p
= p->lchild; }</btnode*> |
保存一路走過的根節點的理由是:中序遍歷的需要,遍歷完左子樹後,需要藉助根節點進入右子樹。代碼走到這裏,指針p爲空,此時無非兩種情況:
說明:
上圖中只給出了必要的節點和邊,其它的邊和節點與討論無關,不必畫出。你可能認爲圖a中最近保存節點算不得是根節點。如果你看過樹、二叉樹基礎,使用擴充二叉樹的概念,就可以解釋。總之,不用糾結這個沒有意義問題。
整個二叉樹只有一個根節點的情況可以劃到圖a。 仔細想想,二叉樹的左子樹,最下邊是不是上圖兩種情況?不管怎樣,此時都要出棧,並訪問該節點。這個節點就是中序序列的第一個節點。根據我們的思維,代碼應該是這樣:
1
2
3
|
p
= s.top(); s.pop(); cout
<< p->data; |
我們的思維接着走,兩圖情形不同得區別對待: 1.圖a中訪問的是一個左孩子,按中序遍歷順序,接下來應訪問它的根節點。也就是圖a中的另一個節點,高興的是它已被保存在棧中。我們只需這樣的代碼和上一步一樣的代碼:
1
2
3
|
p
= s.top(); s.pop(); cout
<< p->data; |
2.再看圖b,由於沒有左孩子,根節點就是中序序列中第一個,然後直接是進入右子樹:p=p->rchild;在右子樹中,又會新一輪的代碼段(i)、代碼段(ii)……直到棧空且p空。 思維到這裏,似乎很不清晰,真的要區分嗎?根據圖a接下來的代碼段(ii)這樣的:
1
2
3
4
5
6
7
|
p
= s.top(); s.pop(); cout
<< p->data; p
= s.top(); s.pop(); cout
<< p->data; p
= p->rchild; |
根據圖b,代碼段(ii)又是這樣的:
1
2
3
4
|
p
= s.top(); s.pop(); cout
<< p->data; p
= p->rchild; |
我們可小結下:遍歷過程是個循環,並且按代碼段(i)、代碼段(ii)構成一次循環體,循環直到棧空且p空爲止。 不同的處理方法很讓人抓狂,可統一處理嗎?真的是可以的!回顧擴充二叉樹,是不是每個節點都可以看成是根節點呢?那麼,代碼只需統一寫成圖b的這種形式。也就是說代碼段(ii)統一是這樣的:
中序代碼段(ii)
1
2
3
4
|
p
= s.top(); s.pop(); cout
<< p->data; p
= p->rchild; |
口說無憑,得經的過理論檢驗。 圖a的代碼段(ii)也可寫成圖b的理由是:由於是葉子節點,p=-=p->rchild;之後p肯定爲空。爲空,還需經過新一輪的代碼段(i)嗎?顯然不需。(因爲不滿足循環條件)那就直接進入代碼段(ii)。看!最後還是一樣的吧。還是連續出棧兩次。看到這裏,要仔細想想哦!相信你一定會明白的。
這時寫出遍歷循環體就不難了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
BTNode*
p = root; stack<btnode*>
s; while (!s.empty()
|| p) { //代碼段(i)一直遍歷到左子樹最下邊,邊遍歷邊保存根節點到棧中 while (p) { s.push(p); p
= p->lchild; } //代碼段(ii)當p爲空時,說明已經到達左子樹最下邊,這時需要出棧了 if (!s.empty()) { p
= s.top(); s.pop(); cout
<< setw( 4 )
<< p->data; //進入右子樹,開始新的一輪左子樹遍歷(這是遞歸的自我實現) p
= p->rchild; } }</btnode*> |
仔細想想,上述代碼是不是根據我們的思維走向而寫出來的呢?再加上邊界條件的檢測,中序遍歷非遞歸形式的完整代碼是這樣的:
中序遍歷代碼一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
//中序遍歷 void InOrderWithoutRecursion1(BTNode*
root) { //空樹 if (root
== NULL) return ; //樹非空 BTNode*
p = root; stack<btnode*>
s; while (!s.empty()
|| p) { //一直遍歷到左子樹最下邊,邊遍歷邊保存根節點到棧中 while (p) { s.push(p); p
= p->lchild; } //當p爲空時,說明已經到達左子樹最下邊,這時需要出棧了 if (!s.empty()) { p
= s.top(); s.pop(); cout
<< setw( 4 )
<< p->data; //進入右子樹,開始新的一輪左子樹遍歷(這是遞歸的自我實現) p
= p->rchild; } } }</btnode*> |
恭喜你,你已經完成了中序遍歷非遞歸形式的代碼了。回顧一下難嗎? 接下來的這份代碼,本質上是一樣的,相信不用我解釋,你也能看懂的。
中序遍歷代碼二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//中序遍歷 void InOrderWithoutRecursion2(BTNode*
root) { //空樹 if (root
== NULL) return ; //樹非空 BTNode*
p = root; stack<btnode*>
s; while (!s.empty()
|| p) { if (p) { s.push(p); p
= p->lchild; } else { p
= s.top(); s.pop(); cout
<< setw( 4 )
<< p->data; p
= p->rchild; } } }</btnode*> |
前序遍歷
分析
前序遍歷的遞歸定義:先根節點,後左子樹,再右子樹。有了中序遍歷的基礎,不用我再像中序遍歷那樣引導了吧。 首先,我們遍歷左子樹,邊遍歷邊打印,並把根節點存入棧中,以後需藉助這些節點進入右子樹開啓新一輪的循環。還得重複一句:所有的節點都可看做是根節點。根據思維走向,寫出代碼段(i):前序代碼段(i)
1
2
3
4
5
6
7
|
//邊遍歷邊打印,並存入棧中,以後需要藉助這些根節點(不要懷疑這種說法哦)進入右子樹 while (p) { cout
<< setw( 4 )
<< p->data; s.push(p); p
= p->lchild; } |
接下來就是:出棧,根據棧頂節點進入右子樹。
前序代碼段(ii)
1
2
3
4
5
6
7
|
//當p爲空時,說明根和左子樹都遍歷完了,該進入右子樹了 if (!s.empty()) { p
= s.top(); s.pop(); p
= p->rchild; } |
同樣地,代碼段(i)(ii)構成了一次完整的循環體。至此,不難寫出完整的前序遍歷的非遞歸寫法。
前序遍歷代碼一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
void PreOrderWithoutRecursion1(BTNode*
root) { if (root
== NULL) return ; BTNode*
p = root; stack<btnode*>
s; while (!s.empty()
|| p) { //邊遍歷邊打印,並存入棧中,以後需要藉助這些根節點(不要懷疑這種說法哦)進入右子樹 while (p) { cout
<< setw( 4 )
<< p->data; s.push(p); p
= p->lchild; } //當p爲空時,說明根和左子樹都遍歷完了,該進入右子樹了 if (!s.empty()) { p
= s.top(); s.pop(); p
= p->rchild; } } cout
<< endl; }</btnode*> |
下面給出,本質是一樣的另一段代碼:
前序遍歷代碼二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//前序遍歷 void PreOrderWithoutRecursion2(BTNode*
root) { if (root
== NULL) return ; BTNode*
p = root; stack<btnode*>
s; while (!s.empty()
|| p) { if (p) { cout
<< setw( 4 )
<< p->data; s.push(p); p
= p->lchild; } else { p
= s.top(); s.pop(); p
= p->rchild; } } cout
<< endl; }</btnode*> |
在二叉樹中使用的是這樣的寫法,略有差別,本質上也是一樣的:
前序遍歷代碼三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
void PreOrderWithoutRecursion3(BTNode*
root) { if (root
== NULL) return ; stack<btnode*>
s; BTNode*
p = root; s.push(root); while (!s.empty())
//循環結束條件與前兩種不一樣 { //這句表明p在循環中總是非空的 cout
<< setw( 4 )
<< p->data; /* 棧的特點:先進後出 先被訪問的根節點的右子樹後被訪問 */ if (p->rchild) s.push(p->rchild); if (p->lchild) p
= p->lchild; else { //左子樹訪問完了,訪問右子樹 p
= s.top(); s.pop(); } } cout
<< endl; }</btnode*> |
最後進入最難的後序遍歷:
後序遍歷
分析
後序遍歷遞歸定義:先左子樹,後右子樹,再根節點。後序遍歷的難點在於:需要判斷上次訪問的節點是位於左子樹,還是右子樹。若是位於左子樹,則需跳過根節點,先進入右子樹,再回頭訪問根節點;若是位於右子樹,則直接訪問根節點。直接看代碼,代碼中有詳細的註釋。後序遍歷代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
//後序遍歷 void PostOrderWithoutRecursion(BTNode*
root) { if (root
== NULL) return ; stack<btnode*>
s; //pCur:當前訪問節點,pLastVisit:上次訪問節點 BTNode*
pCur, *pLastVisit; //pCur
= root; pCur
= root; pLastVisit
= NULL; //先把pCur移動到左子樹最下邊 while (pCur) { s.push(pCur); pCur
= pCur->lchild; } while (!s.empty()) { //走到這裏,pCur都是空,並已經遍歷到左子樹底端(看成擴充二叉樹,則空,亦是某棵樹的左孩子) pCur
= s.top(); s.pop(); //一個根節點被訪問的前提是:無右子樹或右子樹已被訪問過 if (pCur->rchild
== NULL || pCur->rchild == pLastVisit) { cout
<< setw( 4 )
<< pCur->data; //修改最近被訪問的節點 pLastVisit
= pCur; } /*這裏的else語句可換成帶條件的else
if: else
if (pCur->lchild == pLastVisit)//若左子樹剛被訪問過,則需先進入右子樹(根節點需再次入棧) 因爲:上面的條件沒通過就一定是下面的條件滿足。仔細想想! */ else { //根節點再次入棧 s.push(pCur); //進入右子樹,且可肯定右子樹一定不爲空 pCur
= pCur->rchild; while (pCur) { s.push(pCur); pCur
= pCur->lchild; } } } cout
<< endl; }</btnode*> |
總結
思維和代碼之間總是有巨大的鴻溝。通常是思維正確,清楚,但卻不易寫出正確的代碼。要想越過這鴻溝,只有多嘗試、多借鑑,別無它法。轉載請註明出處,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/37115355
專欄目錄:數據結構與算法目錄