二叉树迭代器算法

二叉树(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代码。

相关文章
相关标签/搜索