树的遍历及实现

    最近打算总结一下数据结构的知识,所以就决定写几篇文章记录一下好了。

    树的遍历分为三种,先序遍历,中序遍历,后序遍历。而就我而言,了解以下三种实现遍历的方式:递归,利用栈,Morris遍历(比较酷炫的方法)。(本文使用java语言来实现)

    那么就从最简单的递归实现开始吧。

//先序遍历
public void preorderRecursion(TreeNode root){
        doSomething(root);
        if(root.left!=null)
            preorderRecursion(root.left);
        if(root.right!=null)
            preorderRecursion(root.right);
    }
//中序遍历
public void inorderRecursion(TreeNode root){
        if(root.left!=null)
            inorderRecursion(root.left);
        doSomething(root);
        if(root.right!=null)
            inorderRecursion(root.right);
    }
//后序遍历
public void postorderRecursion(TreeNode root){
        if(root.left!=null)
            postorderRecursion(root.left);
        if(root.right!=null)
            postorderRecursion(root.right);
        doSomething(root);
    }

递归的实现是非常简单的,只需要修改一下doSomething()的顺序即可。

递归实现先序遍历的思想是首先将preorderRecursion看作实现树的先序遍历的函数,那么我们只需要

1. 对root节点doSomething

2. 对root的左子树进行先序遍历

3. 对root的右子树进行先序遍历


接下来是利用栈来实现遍历。

//先序遍历
public void preorderStack(TreeNode root){
        Stack s = new Stack();
        preorderStackSupport(root,s);
        TreeNode temp;
        while(!s.empty())
        {
            temp = (TreeNode)s.pop();
            if(temp.right!=null)
                preorderStackSupport(temp.right,s);
        }
    }
    public void preorderStackSupport(TreeNode root,Stack s){
        while(root.left!=null)
        {
            doSomething(root);
            s.push(root);
            root = root.left;
        }
        doSomething(root);
        s.push(root);
    }

辅助函数的作用是:将传入结点沿左儿子方向上所有的结点进行相应处理并入栈,这个过程保证了父结点->左儿子的顺序。

在调用了一次辅助函数之后,栈已经有了第一批按父节点->左儿子顺序处理过的元素。此时栈顶就是根结点的最左儿子。由于栈顶的左儿子为空,此时已经无法按父节点->左儿子的顺序进行下去,因此我们对栈顶的右儿子调用辅助函数。

//中序遍历
public void inorderStack(TreeNode root) {
        Stack s = new Stack();
        inorderStackSupport(root,s);
        TreeNode temp;
        while(!s.empty())
        {
            temp = (TreeNode)s.pop();
            doSomething(temp);
            if(temp.right!=null)
                inorderStackSupport(temp.right,s);
        }
    }
    public void inorderStackSupport(TreeNode root,Stack s){
        while(root.left!=null)
        {
            s.push(root);
            root = root.left;
        }
        s.push(root);
    }

辅助函数的作用是:将传入结点沿左儿子方向上所有的结点入栈,根据栈的特性,我们知道入栈之后父节点会在儿子节点的下面。

在调用了一次辅助函数之后,栈已经有了第一批按父节点->左儿子顺序进入栈的元素,那么出栈时就是左儿子->父节点。此时栈顶就是根结点的最左儿子。与先序遍历不同,栈中的元素还未作相应处理。因此我们先对栈顶元素作相应处理,再对栈顶的右儿子调用辅助函数。

//后序遍历
public void postorderStack(TreeNode root){
        Stack s = new Stack();
        postorderStackSupport(root,s);
        TreeNode temp,tempL;
        while(!s.empty())
        {
            temp = (TreeNode)s.peek();
            if(temp.right!=null)
                postorderStackSupport(temp.right,s);
            temp = (TreeNode)s.pop();
            doSomething(temp);
            tempL = (TreeNode)s.peek();
            if(temp == tempL.right)
            {
                doSomething(tempL);
                s.pop();
            }
        }
    }
    public void postorderStackSupport(TreeNode root,Stack s) {
        while(root.left!=null)
        {
            s.push(root);
            root = root.left;
        }
        s.push(root);
        while (root.right!= null)
        {
            root = root.right;
            while(root.left!=null)
            {
                s.push(root);
                root = root.left;
            }
            s.push(root);
        }
    }

后序遍历的顺序是左儿子->右儿子->父结点,因此辅助函数需要作一些修改。对于前两种我们只需要以根节点为起点,沿父节点->左儿子的顺序知道某个节点没有左儿子即可。但现在我们不仅要求该节点没有左儿子,还要求该节点没有右儿子,否则该节点就不是第一个应该被处理的节点(因为这样的话就是父节点了),因此有了如图的实现。

借助辅助函数,我们成功按后序遍历的顺序将元素入栈,但为了防止我们无限对某一节点调用辅助函数,因此需要判断出栈的节点是否是当前栈顶的右儿子,如果是的话,说明已经对栈顶调用过辅助函数,我们再次出栈,然后处理。

Morris Traversal实现

前面两种遍历方式都使用了O(n)的空间,而这种遍历方式只需要O(1)的空间。(或许这就是大佬的思维方式吧)

算法的思路是:通过将结点的前驱结点指向自身来完成树的遍历。其实为什么要利用前驱结点呢?是因为树实际上是由许多单向链表组成,因此我们无法直接从儿子结点跳到父结点。而利用前驱结点正是一种解决这个问题的技巧。

//先序遍历
public void preorderMorris(TreeNode root){
        TreeNode cur = root;
        TreeNode pre = null;
        while(cur!=null)
        {
            doSomething(cur);
            if(cur.left==null)
                cur = cur.right;
            else
            {
                pre = cur.left;
                while(pre.right!=null&&pre.right!=cur)
                    pre = pre.right;
                if(pre.right==null)
                {
                    pre.right = cur.right;
                    cur = cur.left;
                }
                else{
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
    }

在先序遍历中,前驱结点是它左儿子沿node->node.right路径上的最后一个结点。

1.先输出再说。然后判断左儿子是否为空,为空的话也就没有前驱节点了。

2.如果有左儿子,那么找到自己的前驱结点或找到当前节点(这说明前驱结点已经连接了当前结点)。

    a.若找到前驱结点,则将前驱结点和当前节点连接,然后可以放心的将当前结点置为当前结点的左儿子。(后面可以通过连接回到这个结点)

    b.若找到当前节点,则解除连接,然后将当前结点置为当前结点的右儿子(因为已经连接过了,说明当前结点的左子树已遍历完)


//中序遍历
public void inorderMorris(TreeNode root){
        TreeNode cur = root;
        TreeNode pre = null;
        while(cur!=null)
        {
            if(cur.left==null)
            {
                doSomething(cur);
                cur = cur.right;
            }
            else
            {
                pre = cur.left;
                while(pre.right!=null&&pre.right!=cur)
                    pre = pre.right;
                if(pre.right==null)
                {
                    pre.right = cur;
                    cur = cur.left;
                }
                else{
                    pre.right = null;
                    doSomething(cur);
                    cur = cur.right;
                }
            }
        }
    }
中序遍历和先序遍历比较类似,就不赘述了。


//后序遍历
public void postorderMorris(TreeNode root){
        TreeNode htemp = new TreeNode(0);
        htemp.left = root;
        TreeNode cur = htemp;
        TreeNode pre = null;
        while(cur!=null)
        {
            if(cur.left==null)
                cur = cur.right;
            else
            {
                pre = cur.left;
                while(pre.right!=null&&pre.right!=cur)
                    pre = pre.right;
                if(pre.right==null)
                {
                    pre.right = cur;
                    cur = cur.left;
                }
                else{
                    printReverse(cur.left,pre);
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
    }
//输出翻转链表
public void printReverse(TreeNode from,TreeNode to) {
        reverse(from,to);
        TreeNode temp = to;
        while(temp!=from)
        {
            doSomething(temp);
            temp = temp.right;
        }
        doSomething(temp);
        reverse(from,to);
    }
//翻转链表
public void reverse(TreeNode from,TreeNode to){
        TreeNode x = from;
        TreeNode y = to;
        TreeNode z;
        while(x!=to)
        {
            z = y.right;
            y.right = x;
            x = y;
            y = z;
        }
    }

后续遍历的实现同样比较复杂,需要借助一个额外结点htemp和辅助函数reversePrint(),reverse()来倒序输出链表。

后序遍历的前驱结点依旧和前两种遍历方式一样。

1.判断当前节点的左儿子是否为空,若为空说明没有前驱结点。

2.若左儿子不为空,那么找到自己的前驱结点或找到当前节点(这说明前驱结点已经连接了当前结点)。

    a.若找到前驱结点,则将前驱结点和当前节点连接,将当前结点置为当前结点的左儿子。(保证了左儿子最优先处理的顺序)

    b.若找到当前结点自己,则倒序输出当前结点的左儿子与前驱结点之间的路径上的所有结点。(即保证了右儿子->父结点的顺序)再将当前结点置为当前结点的右儿子。


至此终于算是总结完了,总觉得总结的没到点上,可能还是因为理解的不够透彻吧。

你可能感兴趣的:(数据结构)