深刻理解二叉树遍历

二叉树:二叉树是每个节点最多有两个子树的树结构。

 

本文介绍二叉树的遍历相关知识。

我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

首先我们定义一颗二叉树

typedef char ElementType;
typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};

前序

首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

思路:

就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

void PreorderTraversal( BinTree BT )
{
    if(BT==NULL)return ;
    printf(" %c", BT->Data);
    PreorderTraversal(BT->Left);
    PreorderTraversal(BT->Right);
}

 

中序

首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

思路:

还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

void InorderTraversal( BinTree BT )
{
    if(BT==NULL)return ;
    InorderTraversal(BT->Left);
    printf(" %c", BT->Data);
    InorderTraversal(BT->Right);
}

后序

首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

思路:

先分别对左右子树重复此过程,然后打印根

void PostorderTraversal(BinTree BT)
{
    if(BT==NULL)return ;
    PostorderTraversal(BT->Left);
    PostorderTraversal(BT->Right);
    printf(" %c", BT->Data);
}

进一步思考

看似好像很容易地写出了三种遍历。。。。。

 

但是你真的理解为什么这么写吗?

比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

仔细想想,其实有一丝不对劲的。。。

再看代码:

void Traversal(BinTree BT)//遍历
{
//1111111111111
    Traversal(BT->Left);
//22222222222222
    Traversal(BT->Right);
//33333333333333
}

为了叙述清楚,我给三个位置编了号 1,2,3

我们凭什么能前序遍历,或者中序遍历,后序遍历?

我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

其实,遍历是利用了一种数据结构:栈

而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

想到这里,可能就有更深刻的理解了。

我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

核心思路代码实现:

*p=root;
while(p || !st.empty())
{
    if(p)//非空
    {
        //visit(p);进行操作
        st.push(p);//入栈
        p = p->lchild;左
    } 
    else//空
    {
        p = st.top();//取出
        st.pop();
        p = p->rchild;//右
    }
}

中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

(对应递归是第二次遇到)

核心代码实现:

*p=root;
while(p || !st.empty())
{
    if(p)//非空
    {
        st.push(p);//压入
        p = p->lchild;
    }
    else//空
    {
        p = st.top();//取出
        //visit(p);操作
        st.pop();
        p = p->rchild;
    }
}

后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

s.push(root);
while(!s.empty())
{
    cur=s.top();
    if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
    {
        //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
        s.pop();//弹出
        pre=cur; //记录
    }
    else//分别放入右左孩子
    {
        if(cur->rchild!=NULL)
            s.push(cur->rchild);
        if(cur->lchild!=NULL)    
            s.push(cur->lchild);
    }
}

这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

 

 

 

猜你喜欢

转载自blog.csdn.net/hebtu666/article/details/82853988