二叉樹(Binary Tree)的前序、中序和後續遍歷是算法和數據結構中的基本問題,基於遞歸的二叉樹遍歷算法更是遞歸的經典應用。node
假設二叉樹結點定義以下:python
1
2
3
4
5
6
|
// C++
struct
Node {
int
value;
Node *left;
Node *right;
}
|
中序遞歸遍歷算法:程序員
1
2
3
4
5
6
7
8
9
10
|
// C++
void
inorder_traverse(Node *node) {
if
(NULL != node->left) {
inorder_traverse(node->left);
}
do_something(node);
if
(NULL != node->right) {
inorder_traverse(node->right);
}
}
|
前序和後序遍歷算法相似。算法
可是,僅有遍歷算法是不夠的,在許多應用中,咱們還須要對遍歷自己進行抽象。假若有一個求和的函數sum,咱們但願它能應用於鏈表,數組,二叉樹等等不一樣的數據結構。這時,咱們能夠抽象出迭代器(Iterator)的概念,經過迭代器把算法和數據結構解耦了,使得通用算法能應用於不一樣類型的數據結構。咱們能夠把sum函數定義爲:shell
1
|
int
sum(Iterator it)
|
鏈表做爲一種線性結構,它的迭代器實現很是簡單和直觀,而二叉樹的迭代器實現則不那麼容易,咱們不能直接將遞歸遍歷轉換爲迭代器。究其緣由,這是由於二叉樹遞歸遍歷過程是編譯器在調用棧上自動進行的,程序員對這個過程缺少足夠的控制。既然如此,那麼咱們若是能夠本身來控制整個調用棧的進棧和出棧不是就達到控制的目的了嗎?咱們先來看看二叉樹遍歷的非遞歸算法:數組
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// C++
void
inorder_traverse_nonrecursive(Node *node) {
Stack stack;
do
{
// node表明當前準備處理的子樹,層層向下把左孩子壓棧,對應遞歸算法的左子樹遞歸
while
(NULL != node) {
stack.push(node);
node = node->left;
}
do
{
Node *top = stack.top();
stack.pop();
//彈出棧頂,對應遞歸算法的函數返回
do_something(top);
if
(NULL != top->right) {
node = top->right;
//將當前子樹置爲剛剛遍歷過的結點的右孩子,對應遞歸算法的右子樹遞歸
break
;
}
}
while
(!stack.empty());
}
while
(!stack.empty());
}
|
經過基於棧的非遞歸算法咱們得到了對於遍歷過程的控制,下面咱們考慮如何將其封裝爲迭代器呢? 這裏關鍵在於理解遍歷的過程是由棧的狀態來表示的,因此顯然迭代器內部應該包含一個棧結構,每次迭代的過程就是對棧的操做。假設迭代器的接口爲:數據結構
1
2
3
4
5
|
// C++
class
Iterator {
public
:
virtual
Node* next() = 0;
};
|
下面是一個二叉樹中序遍歷迭代器的實現:函數
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
|
//C++
class
InorderIterator :
public
Iterator {
public
:
InorderIterator(Node *node) {
Node *current = node;
while
(NULL != current) {
mStack.push(current);
current = current->left;
}
}
virtual
Node* next() {
if
(mStack.empty()) {
return
NULL;
}
Node *top = mStack.top();
mStack.pop();
if
(NULL != top->right) {
Node *current = top->right;
while
(NULL != current) {
mStack.push(current);
current = current->left;
}
}
return
top;
}
private
:
std::stack<Node*> mStack;
};
|
下面咱們再來考察一下這個迭代器實現的時間和空間複雜度。很顯然,因爲棧中最多須要保存全部的結點,因此其空間複雜度是O(n)的。那麼時間複雜度呢?一次next()調用也最多會進行n次棧操做,而整個遍歷過程須要調用n次next(),那麼是否是整個迭代器的時間複雜度就是O(n^2)呢?答案是否認的!由於每一個結點只會進棧和出棧一次,因此整個迭代過程的時間複雜度依然爲O(n)。其實,這和遞歸遍歷的時空複雜度徹底同樣。spa
除了上面顯式利用棧控制代碼執行順序外,在支持yield語義的語言(C#, Python等)中,還有更爲直接的作法。下面基於yield的二叉樹中序遍歷的Python實現:code
1
2
3
4
5
6
7
8
|
/
/
Python
def
inorder(t):
if
t:
for
x
in
inorder(t.left):
yield
x
yield
t.label
for
x
in
inorder(t.right):
yield
x
|
yield與return區別的一種通俗解釋是yield返回時系統會保留函數調用的狀態,下次該函數被調用時會接着從上次的執行點繼續執行,這是一種與棧語義所徹底不一樣的流程控制語義。咱們知道Python的解釋器是C寫的,可是C並不支持yield語義,那麼解釋器是如何作到對yield的支持的呢? 有了上面把遞歸遍歷變換爲迭代遍歷的經驗,相信你已經猜到Python解釋器必定是對yield代碼進行了某種變換。若是你已經可以實現遞歸變非遞歸,不妨嘗試一下可否寫一段編譯程序將yield代碼變換爲非yield代碼。