二叉樹迭代器算法
二叉樹(Binary Tree)的前序、中序和后續(xù)遍歷是算法和數(shù)據(jù)結(jié)構(gòu)中的基本問題,基于遞歸的二叉樹遍歷算法更是遞歸的經(jīng)典應用。
假設(shè)二叉樹結(jié)點定義如下:
- struct Node {
- int value;
- Node *left;
- Node *right;
- }
- void inorder_traverse(Node *node) {
- if (NULL != node->left) {
- inorder_traverse(node->left);
- }
- do_something(node);
- if (NULL != node->right) {
- inorder_traverse(node->right);
- }
- }
前序和后序遍歷算法類似。
但是,僅有遍歷算法是不夠的,在許多應用中,我們還需要對遍歷本身進行抽象。假如有一個求和的函數(shù)sum,我們希望它能應用于鏈表,數(shù)組,二叉樹等等不同的數(shù)據(jù)結(jié)構(gòu)。這時,我們可以抽象出迭代器(Iterator)的概念,通過迭代器把算法和數(shù)據(jù)結(jié)構(gòu)解耦了,使得通用算法能應用于不同類型的數(shù)據(jù)結(jié)構(gòu)。我們可以把sum函數(shù)定義為:
- int sum(Iterator it)
鏈表作為一種線性結(jié)構(gòu),它的迭代器實現(xiàn)非常簡單和直觀,而二叉樹的迭代器實現(xiàn)則不那么容易,我們不能直接將遞歸遍歷轉(zhuǎn)換為迭代器。究其原因,這是因為二叉 樹遞歸遍歷過程是編譯器在調(diào)用棧上自動進行的,程序員對這個過程缺乏足夠的控制。既然如此,那么我們?nèi)绻梢宰约簛砜刂普麄€調(diào)用棧的進棧和出棧不是就達到 控制的目的了嗎?我們先來看看二叉樹遍歷的非遞歸算法:
- 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(); //彈出棧頂,對應遞歸算法的函數(shù)返回
- do_something(top);
- if (NULL != top->right) {
- node = top->right; //將當前子樹置為剛剛遍歷過的結(jié)點的右孩子,對應遞歸算法的右子樹遞歸
- break;
- }
- }
- while (!stack.empty());
- }
- while (!stack.empty());
- }
通過基于棧的非遞歸算法我們獲得了對于遍歷過程的控制,下面我們考慮如何將其封裝為迭代器呢? 這里關(guān)鍵在于理解遍歷的過程是由棧的狀態(tài)來表示的,所以顯然迭代器內(nèi)部應該包含一個棧結(jié)構(gòu),每次迭代的過程就是對棧的操作。假設(shè)迭代器的接口為:
- class Iterator {
- public:
- virtual Node* next() = 0;
- };
下面是一個二叉樹中序遍歷迭代器的實現(xiàn):
- 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;
- };
下面我們再來考察一下這個迭代器實現(xiàn)的時間和空間復雜度。很顯然,由于棧中最多需要保存所有的結(jié)點,所以其空間復雜度是O(n)的。那么時間復雜度 呢?一次next()調(diào)用也最多會進行n次棧操作,而整個遍歷過程需要調(diào)用n次next(),那么是不是整個迭代器的時間復雜度就是O(n^2)呢?答案 是否定的!因為每個結(jié)點只會進棧和出棧一次,所以整個迭代過程的時間復雜度依然為O(n)。其實,這和遞歸遍歷的時空復雜度完全一樣。
除了上面顯式利用??刂拼a執(zhí)行順序外,在支持yield語義的語言(C#, Python等)中,還有更為直接的做法。下面基于yield的二叉樹中序遍歷的Python實現(xiàn):
- // 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區(qū)別的一種通俗解釋是yield返回時系統(tǒng)會保留函數(shù)調(diào)用的狀態(tài),下次該函數(shù)被調(diào)用時會接著從上次的執(zhí)行點繼續(xù)執(zhí)行,這是一種與 棧語義所完全不同的流程控制語義。我們知道Python的解釋器是C寫的,但是C并不支持yield語義,那么解釋器是如何做到對yield的支持的呢? 有了上面把遞歸遍歷變換為迭代遍歷的經(jīng)驗,相信你已經(jīng)猜到Python解釋器一定是對yield代碼進行了某種變換。如果你已經(jīng)能夠?qū)崿F(xiàn)遞歸變非遞歸,不 妨嘗試一下能否寫一段編譯程序?qū)ield代碼變換為非yield代碼。