二叉树迭代器算法

来源:互联网 发布:tb程序化交易编程服务 编辑:程序博客网 时间:2024/05/26 07:28

二叉树(Binary Tree)的前序、中序和后续遍历是算法和数据结构中的基本问题,基于递归的二叉树遍历算法更是递归的经典应用。

假设二叉树结点定义如下:

1
2
3
4
5
6
// C++
structNode {
    intvalue;
    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函数定义为:

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++
classIterator {
    public:
        virtualNode* 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++
classInorderIterator : publicIterator {
    public:
        InorderIterator(Node *node) {
            Node *current = node;
            while(NULL != current) {
                mStack.push(current);
                current = current->left;
            }
        }
        virtualNode* next() {
            if(mStack.empty()) {
                returnNULL;
            }
            Node *top = mStack.top();
            mStack.pop();
            if(NULL != top->right) {
                Node *current = top->right;
                while(NULL != current) {
                    mStack.push(current);
                    current = current->left;
                }
            }
            returntop;
         }
    private:
        std::stack<Node*> mStack;
};

下面我们再来考察一下这个迭代器实现的时间和空间复杂度。很显然,由于栈中最多需要保存所有的结点,所以其空间复杂度是O(n)的。那么时间复杂度呢?一次next()调用也最多会进行n次栈操作,而整个遍历过程需要调用n次next(),那么是不是整个迭代器的时间复杂度就是O(n^2)呢?答案是否定的!因为每个结点只会进栈和出栈一次,所以整个迭代过程的时间复杂度依然为O(n)。其实,这和递归遍历的时空复杂度完全一样。

PS:利用栈可以有效地将递归转化为迭代。另外在linux内核中也有红黑树实现,内核中遍历树不可能用递归的方法,于是内核中也有自己的转迭代的方法。详见我的另一篇文章linux内核红黑树构造

 

 

 

原创粉丝点击