【面试题之算法部分】二叉树的遍历

来源:互联网 发布:java中的compare 编辑:程序博客网 时间:2024/06/07 02:06

本篇文章主要目的是详细讨论二叉树的前序、中序和后序遍历算法,包括递归版和迭代版。首先给出递归版的一般思想:

  • 前序遍历:先访问根节点,前序遍历左子树,前序遍历右子树。
  • 中序遍历:中序遍历左子树,访问根节点,中序遍历右子树。
  • 后序遍历:后序遍历左子树,后序遍历右子树,访问根结点。

我们首先定义节点的结构

typedef struct node{    int val;    node *lChild;    node *rChild;}BiTree;

一. 前序遍历

递归版本:

void PreOrder(BiTree* T){    if(!T) return;    visit(T); //访问根节点的值    PreOrder(T->lChild); //递归访问左子树    PreOrder(T->rChild); //递归访问右子树}visit(Bitree *T){    cout << T->val << " " << endl;}

递归版本的时间复杂度为O(n),但是由于空间复杂度的常系数较迭代版本更高,我们可以改用等效迭代版本。

迭代版本1:

#define HasRChild(x) ((x).rChild)#define HasLChild(x) ((x).lChild)void PreOrder(BiTree *T){    stack<BiTree *> s;    if(T) s.push(T);    while(!s.empty())    {        T = s.top();        s.pop();        visit(T); //每次先访问根节点        if(HasRChild(*T)) s.push(T->rChild); //将右孩子入栈        if(HasLChild(*T)) s.push(T->lChild); //将左孩子入栈    }}

迭代版本2:

我们先沿最左侧通路自顶向下访问访问沿途节点,再自底而上一次遍历这些节点的右子树。其实遍历过程和迭代版本1大同小异。

void visitAlongLeftBranch(BiTree *x, stack<BiTree *> &s){    while(x)    {        visit(x);        if(x->rChild) s.push(x->rChild);        x = x->lChild;    }}void PreOrder(BiTree *x){    stack<BiTree *> s;    while(true)    {        visitAlongLeftBranch(x, s);        if(s.empty()) break;        x = s.top();        s.pop();    }}

二. 中序遍历

注意二叉查找树的中序遍历是数据的升序过程。

递归版本:

void InOrder(BiTree *T){    if(!T) return;    Inorder(T->lChild);    visit(T);    InOrder(T->rChild);}

将中序遍历递归版本改成迭代版本的难度在于:尽管右子树的递归是属于严格尾递归的,但是右子树的递归并不是,我们可以参看前序遍历迭代版本2的思想。

迭代版本1:

void visitAlongLeftBranch(BiTree *x, stack<BiTree *> s){    while(x)    {        s.push(x);        x = x->lChild;    }}void InOrder(BiTree *x){    stack<BiTree *> s;    while(true)    {        visitLongLeftBranch(x, s); //不断将左侧节点入栈        if(s.empty()) break;        x = s.top();        s.pop();        visit(x); //自底向上访问左侧节点        x = x->rChild; //进入右子树    }}

进一步优化,可将上面的两个while循环改成一个循环
迭代版本2

void InOrder(Bitree *x){    stack<Bitree *> s;    while(true)    {        if(x)        {            s.push(x);            x = x->lChild;        }        else        {            if(!empty())            {                x = s.top();                s.pop();                visit(x);                x = x->rChild;            }            else break;        }    }}

以上版本都需要辅助栈,尽管时间复杂度没有什么实质影响,但所需的空间复杂度正比于二叉树的高度,最坏情况下将达到O(n)(退化成单链)。为此,我们继续优化,如果node节点内有parent指针,我们借助parent指针来实现。基于一个事实:中序遍历中前后元素关系满足调用succ()函数(寻找其直接后继的函数)前后两元素的关系。
简单来讲,假设我现在遍历到节点A,按中序遍历下一个节点是B。那同样,我对A调用succ()后返回的节点一定也是B。

void Inorder(BiTree *x){    bool backtrace = false;    while(true)    {        if(!backtrace && x->lchild) x = x->lChild;        else        {            visit(x);            if(x->rChild)            {                x = x->rChild;                backtrace = false;            }            else            {                if(!(x = succ(x))) break;                backtrace = true;            }        }    }}//直接后继函数BiTree* succ(BiTree* x){    if(x->rChild) //如果存在右子树,则直接后继是右子树中的最左子孙    {        x = x->rChild->lChild;        while(x) x = x->lChild;    }    else //如果不存在右子树,则直接后继是“包含x节点于左子树中的最低祖先”    {        while(x == x->parent->rChild) x =  x->parent;        x = x->parent;    }}

三. 后序遍历

递归版本:

void PostOrder(Bitree *T){    if(!T) return;    PostOrder(T->lChild);    PostOrder(T->rChild);    visit(T);}

迭代版本:

在后序遍历算法的递归版中,由于左、右子树递归遍历均严格的不属于尾递归,因此实现对应的迭代式算法难度更大,不过,仍可套用之前的思路。
我们思考的起点依然是,此时首先访问哪个节点?如下图所示,从左侧水平向右看去,未被遮挡的最高叶节点v——称作最高左侧可见节点(HLVFL)——即为后序遍历首先访问的节点。注意,该节点有可能是左孩子,也有可能是右孩子。故图中以垂直边示意它与其父节点的联边。
这里写图片描述

考察连接于v与树根之间的唯一通路(粗线所示),与先序和中序遍历类似,自底而上沿着该通路,整个后序遍历也可以分解为若干个片段。每个片段,分别起始于通路的一个节点,并包括三步: 访问当前节点;遍历其右兄弟(若存在)为根的子树;以及向上回溯至父节点(若存在)并转入下一片段。
基于以上理解,导出迭代式后序遍历算法:

void gotoHLVFL(stack<BiTree *> &s) //以s栈节点为根的子树中,寻找最高左侧可见节点(HLVFL){    while(BiTree *x = s.top()) //自顶向下,反复检查当前节点(即栈顶)    {        if(HasLChild(*x)) //尽可能向左        {            if(HasRChild(*x)) s.push(x->rChild); //若有右孩子,优先入栈            s.push(x->lChild); //然后才转至左孩子        }        else s.push(x->rChild); //实不得已才向右    }    s.pop(); //返回前弹出在栈顶的空节点}PostOrder(Bitree *x){    stack<Bitree *> s;    if(x) s.push(x);    while(!s.empty())    {        //如果栈顶元素非当前元素之父,则必为当前元素的右兄弟,所以需要递归以右兄弟为根的树,继续寻找最高左侧可见节点(HLVFL)        if(s.top() != x->parent) gotoHLVFL(s);         x = s.top();        s.pop();        visit(x);    }}

参考文献:
《数据结构c++语言版》,邓俊辉

0 0