二叉树迭代器算法

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

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

// C++
struct Node {
    int value;
    Node *left;
    Node *right;
}

中序递归遍历算法:

// 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函数定义为:

int sum(Iterator it)

链表作为一种线性结构,它的迭代器实现非常简单和直观,而二叉树的迭代器实现则不那么容易,我们不能直接将递归遍历转换为迭代器。究其原因,这是因为二叉树递归遍历过程是编译器在调用栈上自动进行的,程序员对这个过程缺乏足够的控制。既然如此,那么我们如果可以自己来控制整个调用栈的进栈和出栈不是就达到控制的目的了吗?我们先来看看二叉树遍历的非递归算法:

// 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());
}

通过基于栈的非递归算法我们获得了对于遍历过程的控制,下面我们考虑如何将其封装为迭代器呢? 这里关键在于理解遍历的过程是由栈的状态来表示的,所以显然迭代器内部应该包含一个栈结构,每次迭代的过程就是对栈的操作。假设迭代器的接口为:

// C++
class Iterator {
    public:
        virtual Node* next() = 0;
};

下面是一个二叉树中序遍历迭代器的实现:

//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)。其实,这和递归遍历的时空复杂度完全一样。

除了上面显式利用栈控制代码执行顺序外,在支持yield语义的语言(C#, Python等)中,还有更为直接的做法。下面基于yield的二叉树中序遍历的Python实现:

// 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代码。

转自:http://coolshell.cn/articles/9886.html

原创文章,作者:s19930811,如若转载,请注明出处:http://www.178linux.com/2133

(0)
s19930811s19930811
上一篇 2016-08-15
下一篇 2016-08-15

相关推荐

  • 马哥教育网络20期+第五周课程练习

    1、显示/boot/grub/grub.conf中以至少一个空白字符开头的行; [root@localhost ~]# grep "^[[:space:]]\+" /boot/grub/grub.conf 2、显示/etc/rc.d/rc.sysinit文件中以#开头,后面跟至少一个空白字符,而后又…

    Linux干货 2016-07-07
  • AIX下NFS共享导致命令行输入卡住

     1、事件描述:之前公司测试某厂商存储一体机的NFS共享功能,将该NFS目录共享给AIX主机使用(用于备份对应对应的数据库,因为数据库版本太老,Oracle8i的,不支持存储厂商的备份代理),测试完成后,厂商将设备直接关机(这时候在AIX上挂载的NFS目录没有进行卸载)。然后,当我们登陆到AIX服务器上的时候,发现每次敲入“df -g”或者“ls”…

    Linux干货 2016-07-07
  • Linux逻辑卷LVM实现

    Linux逻辑卷LVM实现 背景: 在学习Linux中,学习到了逻辑卷LVM,发现LVM是个十分好用的一个技术,可以多个硬盘合并在一起使用,同时还可以动态的增加和减少。在这里将Linux逻辑卷的具体实现记录下来。 介绍: 逻辑卷LVM:Logica Volum Manager,它是Linux环境下对磁盘分区进行管理的一种机制,普通的磁盘分区管理方式在逻辑分区…

    2017-08-12
  • 马哥linux运维第一天感受

    101个人的大家庭

    Linux干货 2018-03-26
  • Linux脚本基础练习

    马哥教育网络班+ 第7周课堂练习 Linux脚本基础练习 练习: 1、创建一个10G分区,并格式为ext4文件系统; (1)要求其block大小为2048,预留空间百分比为2,卷标为MYDATA,默认挂载属性包含acl (2)挂载至/data/mydata目录 要求挂载时禁止程序自动运行,且不更新文件的访问时间戳 第一步,在虚拟机中挂载一块20G的新硬盘 &…

    Linux干货 2016-11-09
  • Linux系统命令使用格式已经如何查看帮助

    在Linux 系统中有非常多的命令一般的命令格式:“COMMAND OPTIONS ARGUMENTS” 下面有一些基础命令可以体验下命令的基本格式用法 ifconfig命令    作用: 配置网络接口,查看网络接口信息    基本语法:     ifconfig  […

    Linux干货 2016-08-15