数据结构之树(二)

绪论

这一小节主要讲述的是基本二叉树的遍历方法和线索二叉树的一些遍历方法等等。

3. 二叉树的遍历和线索二叉树

3.1 二叉树的遍历

二叉树的遍历指的是按照某条搜索路径访问树中的每个结点,使得每个结点均被访问一次,而且仅仅被访问一次.
由二叉树的定义可以知道,遍历一棵二叉树便要决定对根结点 N N N、左子树 L L L和右子树 R R R的访问顺序.按照先遍历左子树再遍历右子树的方法,常见的遍历顺序有先序遍历(NLR)、中序遍历(LNR)和后序遍历(LRN)三种遍历的算法,这种名称的叫法指的是根结点何时被访问.

  1. 先序遍历
    先序遍历的基本过程是:若根结点为空,那么什么也不做,否则:① 先序遍历根节点;② 然后访问左子树;③ 先序遍历右子树.所以基本的代码可以写作:
void PreOrderVisit(BiNode<ElementType>* root){
        if(root==NULL) return;
        else{
            std::cout<<root->GetData()<<" ";
            PreOrderVisit(root->GetLeft());
            PreOrderVisit(root->GetRight());
        }
    }

这样是一种递归的方式先序遍历访问访问二叉树中的每一个结点的方法.当然也有一种非递归方式访问二叉树的方法,这里需要用到栈来保存一些结点的基本数据.算法的基本思想如下所示
① 初始化遍历指针p=root,结点栈st;
② 当p不为空指针或者栈st不为空的时候,进行以下的步骤;
③ 若p不为空指针的时候,先访问p结点的元素值,然后将当前的p结点指针压入结点栈st,p的指针指向p的左子树,直到p为空指针的时候停止;
④ 判断st是否为空,若st不为空,则将st栈顶元素弹出并保存到指针p当中,然后p的指针指向p的右子树;
⑤ 判断此时是否满足条件② ,若不满足条件②则退出;若满足条件②则进行条件③.
综上所叙,这样非递归方式先序遍历的二叉树如下所示

void PreOrderWithSt(BiNode<ElementType>* root){
        if(root==NULL) return;
        BiNode<ElementType>* ptr = root;
        Stack<BiNode<ElementType>*> st;
        while(ptr!=NULL|!st.IsEmpty()){
            while(ptr!=NULL){
                std::cout<<ptr->GetData()<<" ";
                st.Push(ptr);
                ptr = ptr->GetLeft();
            }
            if(!st.IsEmpty()){
                ptr = st.Pop();
                ptr = ptr->GetRight();
            }
        }
        std::cout<<std::endl;
    }
  1. 中序遍历
    中序遍历的操作过程为:如果根结点为空,那么什么也不做,否则:① 中序遍历左子树;② 访问根节点;③ 中序遍历右子树.这样中序遍历的递归方法如下所示:
void InOrderVisit(BiNode<ElementType>* root){
        if(root==NULL) return;
        else{
            InOrderVisit(root->GetLeft());
            std::cout<<root->GetData()<<" ";
            InOrderVisit(root->GetRight());
        }
    }

中序遍历的非递归方法和先序遍历的非递归方法类似,都是基于结点栈的思想来存储上一次经过的结点然后再进行访问的操作.具体算法的思想如下所示:
① 初始化遍历指针p=root,结点栈st;
② 当p不为空指针或者栈st不为空的时候,进行以下的步骤;
③ 若p不为空指针的时候,将当前的p结点指针压入结点栈st,p的指针指向p的左子树,直到p为空指针的时候停止;
④ 判断st是否为空,若st不为空,则将st栈顶元素弹出并保存到指针p当中,然后访问结点p的元素,p的指针指向p的右子树;
⑤ 判断此时是否满足条件② ,若不满足条件②则退出;若满足条件②则进行条件③.
综上所叙,这样非递归方式中序遍历的二叉树如下所示

void InOrderWithSt(BiNode<ElementType>* root){
        if(root==NULL) return;
        BiNode<ElementType>* ptr = root;
        Stack<BiNode<ElementType>*> st;
        while(ptr!=NULL|!st.IsEmpty()){
            while(ptr!=NULL){
                st.Push(ptr);
                ptr = ptr->GetLeft();
            }
            if(!st.IsEmpty()){
                ptr = st.Pop();
                std::cout<<ptr->GetData()<<" ";
                ptr = ptr->GetRight();
            }
        }
        std::cout<<std::endl;
    }
  1. 后序遍历
    和上述的两种遍历方法类似,后序遍历的操作为:如果二叉树为空,则什么也不做,否则:① 后序遍历左子树;② 后续遍历右子树;③ 访问根节点;所以递归方式后序遍历如下所示:
void PostOrderVisit(BiNode<ElementType>* root){
        if(root==NULL) return;
        else{
            PostOrderVisit(root->GetLeft());
            PostOrderVisit(root->GetRight());
            std::cout<<root->GetData()<<" ";
        }
    }

后续遍历的方法与上述的非递归算法有些许不同,但还是用到了栈存储结点的思想来解决这样的一个问题.非递归算法思想如下所示:
① 初始化当前访问结点的指针ptr=root,上一次访问过的结点prev=NULL,以及存储结点指针的栈st;
② 当结点指针ptr不为空或者是st栈不为空的时候,进行下一步操作;
② 当结点指针ptr部位空指针的时候,将ptr压入栈st,ptr指向ptr的左子树,直到ptr指针为空指针;
③ 将st的栈顶元素保存到ptr中;
④ 若ptr的右子树为空或者是ptr的右子树被访问过的时候(上一次访问过),那么访问结点元素,并将当前ptr指针记录为prev,ptr设置为NULL,st栈弹出指针;否则ptr指向ptr的右子树;
⑤ 判断此时是否满足条件② ,若不满足条件②则退出;若满足条件②则进行条件③.
后序遍历的方法关键在于记录上一次遍历的指针.这个条件是最为基本的条件.

void PostOrderWithSt(BiNode<ElementType>* root){
        if(root==NULL) return ;
        Stack<BiNode<ElementType>*>st;
        BiNode<ElementType>*prev=NULL;
        BiNode<ElementType>*ptr = root;
        while(!st.IsEmpty()||ptr!=NULL){
            while(ptr!=NULL){
                st.Push(ptr);
                ptr=ptr->GetLeft();
            }
            ptr = st.Top();
            if(ptr->GetRight()==NULL||ptr->GetRight()==prev){
                std::cout<<ptr->GetData()<<" ";
                prev = ptr;
                ptr = NULL;
                st.Pop();
            }else ptr= ptr->GetRight();
        }
        std::cout<<std::endl;
    }

三种遍历中递归遍历左、右子树的顺序都是固定的,只是访问根节点的顺序并不相同.另外还有三种不常见的遍历方法,即NRL、RNL、RLN,这三种遍历方法分别是上述三种遍历方法结果的逆序.不管是采用哪一种方法,每一个结点都是一次且仅访问一次,所以算法的复杂度为 o ( n ) o(n) o(n).在递归遍历中,递归算法中的工作栈的深度正好是树的深度,当然计算深度的时候利用栈的方法也是一种比较简单的一种方法.从算法的角度考虑的话,在最坏的情况下,二叉树是有n个结点并且深度为n的单支树,遍历算法的空间复杂度为 O ( n ) O(n) O(n).从上述算法的角度来看,明显非递归算法要比递归算法更好一些,执行效率更为高效.
4. 层次遍历
二叉树的层次遍历,顾名思义就是指从二叉树的第一层(根节点)开始,从上至下逐层遍历,在同一层中,则按照从左到右的顺序对节点逐个访问.在逐层遍历过程中,按从顶层到底层的次序访问树中元素,在同一层中,从左到右进行访问.
进行层次遍历二叉树需要借助一个队列来进行层次遍历的访问.算法的过程如下所示:
① 初始化ptr=root,初始化队列qe,并将ptr指针压入队列qe;
② 当队列qe不为空的时候,进行下面的步骤;
③ 将qe中元素出队到ptr变量中,并访问ptr元素;
④ 如果ptr的左子树不为空的话,则将ptr左子树入队;如果ptr的右子树不为空的话,则将ptr右子树入队;
⑤ 判断此时是否满足条件② ,若不满足条件②则退出;若满足条件②则进行条件③.
所以层次遍历的方法可以由如下描述:

void LevelOrder(BiNode<ElementType>* root){
        if(root==NULL) return ;
        BiNode<ElementType>* ptr = root;
        Queue<BiNode<ElementType>*> qe;
        qe.Push(ptr);
        while(!qe.IsEmpty()){
            ptr = qe.Pop();
            std::cout<<ptr->GetData()<<" ";
            if(ptr->GetLeft()!=NULL) qe.Push(ptr->GetLeft());
            if(ptr->GetRight()!=NULL) qe.Push(ptr->GetRight());
        }
        std::cout<<ptr->GetData()<<" ";
    }

层次遍历过程中,每次出队的元素其实就是当前访问的根节点,并且此时队列qe的队长就是当前层次的结点的个数.
遍历操作是二叉树以及树的最基本的访问操作,例如最为常见的计算二叉树叶子结点的个数、求二叉树的深度、求结点的孩子结点、求结点的双亲、判断两个二叉树是否相等等等这些问题都是基于二叉树的遍历操作.

3.2 二叉树中的一些常见的基本问题

  1. 求二叉树的高度(深度)、二叉树中某个结点所处的层数.
    根据二叉树的定义可以得到,二叉树的高度指的是树中结点最大的层数.当然有递归的算法和非递归算法两种.
    递归算法很简单,最为基本的思想就是:已知根结点,若根结点为空,则二叉树的高度为0;若根结点不为空,则计算左子树高度 h l h_{l} hl,然后计算右子树的高度 h r h_{r} hr,那么这棵二叉树的高度就是 h = 1 + max ⁡ ( h l , h r ) h=1+\max{(h_{l},h_{r})} h=1+max(hl,hr).所以算法可以写作
unsigned int GetTreeHeight(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        else{
            if (root->GetLeft()==NULL && root->GetRight()==NULL) return 1;
            else {
                unsigned int left =GetTreeHeight(root->GetLeft());
                unsigned int right = GetTreeHeight(root->GetRight());
                if(left>right) return 1+left;
                else return right+1;
            }
        }
    }

递归算法会遇到一种非常棘手的问题,二叉树的结构若趋近于线性结构的时候会开销很大一部分的工作栈的空间,这是非常不合理的.这里当然是用到队列的方法求解二叉树中的高度.算法思想如下所示:
① 初始化队列qe,高度depth设置为0,以及遍历指针ptr=root,并将根结点入队;
② 当队列qe不为空的时候,进行下面的步骤;
③ 计算出当前队列的长度nodes(这里队列的长度引申含义就是当前结点所在层数的总结点个数);
④ 当队nodes大于0的时候,执行以下的步骤;
⑤ 将qe中元素出队到ptr变量中,如果ptr的左子树不为空的话,则将ptr左子树入队;如果ptr的右子树不为空的话,则将ptr右子树入队.然后将leaves数量减1;
⑥ 判断此时是否满足条件④ ,若不满足条件④则进行下一步;若满足条件④则进行条件⑤.
⑦ 深度depth加1;
⑧ 判断此时是否满足条件② ,若不满足条件②则进行下一步;若满足条件②则进行条件③.
所以层次遍历的方法可以由如下描述:

unsigned int GetHeightWithSt(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        Queue<BiNode<ElementType>*> qe;
        BiNode<ElementType>* ptr = root;
        qe.Push(ptr);
        unsigned int depth = 0;
        while(!qe.IsEmpty()){
            unsigned int nodes = qe.Size();
            while(nodes>0){
                ptr = qe.Pop();
                if(ptr->GetLeft()!=NULL) qe.Push(ptr->GetLeft());
                if(ptr->GetRight()!=NULL) qe.Push(ptr->GetRight());
                nodes--;
            }
            depth++;
        }
        return depth;
    }
  1. 求二叉树中叶结点的个数
    二叉树中叶子结点个数的求法也可以是两种方法,即递归的方法和非递归的方法.非递归的方法可以使用栈也可以使用队列进行求解.
    递归求解叶结点个数思想比较简单,即若根结点为空,则叶子结点为0;若根结点的左子树为空,右子树结点为空,那么叶子结点为1;若两个子树结点均不为空,那么整个树的叶子结点个数是根结点的左子树叶子结点个数与右子树叶子结点个数之和.算法的实现方式如下所示:
unsigned int GetTreeLeaves(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        else{
            if (root->GetLeft()==NULL && root->GetRight()==NULL) return 1;
            else return GetTreeLeaves(root->GetLeft())+GetTreeLeaves(root->GetRight());
        }
    }

非递归算法可以使用栈也可以使用队列的方法.首先我们使用栈访问的思想是,每次访问结点元素的时候判断它的左子树和右子树是否都为空,若是的话那么就是叶子结点,否则不是叶子结点.例如使用逆中序遍历(RNL)中非递归算法进行求解:

unsigned int GetLeavesWithSt(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        BiNode<ElementType>* ptr = root;
        Stack<BiNode<ElementType>*> st;
        unsigned int leaves = 0;
        while(ptr!=NULL|!st.IsEmpty()){
            while(ptr!=NULL){
                st.Push(ptr);
                ptr = ptr->GetRight();
            }
            if(!st.IsEmpty()){
                ptr = st.Pop();
                if(ptr->GetLeft()=NULL && ptr->GetRight()==NULL) leaves++;
                ptr = ptr->GetLeft();
            }
        }
        return leaves;
    }

当然,栈的方式页可以使用先序遍历、后序遍历或者是其他含有栈的非递归遍历的方式进行求解.当然也可以使用队列的方式进行求解,算法的思想和求解高度时候的思想类似:

unsigned int GetLeavesWithQe(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        Queue<BiNode<ElementType>*> qe;
        BiNode<ElementType>* ptr = this->root;
        qe.Push(ptr);
        unsigned int leaves = 0;
        while(!qe.IsEmpty()){
            unsigned int tmpnodes = qe.Size();
            while(tmpnodes>0){
                ptr = qe.Pop();
                if(ptr->GetLeft()==NULL && ptr->GetRight()==NULL) leaves++;
                if(ptr->GetLeft()!=NULL) qe.Push(ptr->GetLeft());
                if(ptr->GetRight()!=NULL) qe.Push(ptr->GetRight());
                tmpnodes--;
            }
        }
        return leaves;
    }
  1. 求解二叉树中结点的个数
    二叉树中叶子结点个数的求法也可以是两种方法,即递归的方法和非递归的方法(队列或者是栈的方法).递归的方法即为:若二叉树为空,则结点的个数为 n = 0 n=0 n=0;二叉树不为空,计算左子树结点个数 n l n_{l} nl,右子树结点个数 n r n_{r} nr,则结点的个数就是 n = 1 + n l + n r n=1+n_{l}+n_{r} n=1+nl+nr.
    所以递归算法表示如下所示
unsigned int GetTreeNodes(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        else{
            if (root->GetLeft()==NULL && root->GetRight()==NULL) return 1;
            else return 1+GetTreeNodes(root->GetLeft())+GetTreeNodes(root->GetRight());
        }
    }

使用栈的非递归的算法也是使用六中遍历算法中的其中一种,队列的非递归算法与上述队列的算法类似.
这里采用逆后续遍历(RLN)的非递归算法,如下所示:

unsigned int GetNodes(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        Stack<BiNode<ElementType>*>st;
        BiNode<ElementType>*prev=NULL;
        BiNode<ElementType>*ptr = root;
        unsigned int numbers = 0;
        while(!st.IsEmpty()||ptr!=NULL){
            while(ptr!=NULL){
                st.Push(ptr);
                ptr=ptr->GetRight();
            }
            ptr = st.Top();
            if(ptr->GetLeft()==NULL||ptr->GetLeft()==prev){
                numbers++;
                prev = ptr;
                ptr = NULL;
                st.Pop();
            }else ptr= ptr->GetLeft();
        }
        return numbers;
    }

队列的算法如下所示:

unsigned int GetNodesWithQe(BiNode<ElementType>* root){
        if(root==NULL) return 0;
        Queue<BiNode<ElementType>*> qe;
        BiNode<ElementType>* ptr = this->root;
        qe.Push(ptr);
        unsigned int nodes = 0;
        while(!qe.IsEmpty()){
            unsigned int tmpnodes = qe.Size();
            nodes = nodes + tmpnodes;
            while(tmpnodes>0){
                ptr = qe.Pop();
                if(ptr->GetLeft()!=NULL) qe.Push(ptr->GetLeft());
                if(ptr->GetRight()!=NULL) qe.Push(ptr->GetRight());
                tmpnodes--;
            }
        }
        return nodes;
    }
  1. 由遍历序列构造二叉树
    由二叉树的先序遍历序列和中序遍历序列可以唯一地确定一棵二叉树,在先序遍历序列中,第一个结点一定是二叉树的根结点,而在中序遍历中,根结点必然是将中序序列分割成两个子序列,前一个子序列就是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列.当然,由二叉树的后序序列和中序序列也可以唯一确定一棵二叉树,层序序列和中序序列也可以唯一确定一棵二叉树.例如下面的这个问题:
    已知含有若干个字母的二叉树的先序序列、后序序列或层序序列和中序序列的两个字符串(或者是列表),求此二叉树.用递归的方法很容易做出这样的结果.
    使用先序序列和中序序列求解二叉树:
//已知中序序列和先序序列求解二叉树
BiNode<char>* CreateBiTreePreOrder(std::string preorder,std::string inorder){
    assert(preorder.length()==inorder.length());
    if(preorder.length()==0 && inorder.length()==0) return NULL;
    char data = preorder[0];
    int mid = inorder.find(data);
    unsigned int length = inorder.length();
    std::string leftinorder = inorder.substr(0,mid);
    std::string rightinorder = inorder.substr(mid+1,length);

    std::string leftpreorder = preorder.substr(1,leftinorder.length());
    std::string rightpreorder = preorder.substr(1+leftinorder.length(),length);

    BiNode<char>* root = new BiNode<char>(data);
    root->SetLeft(CreateBiTreePreOrder(leftpreorder,leftinorder));
    root->SetRight(CreateBiTreePreOrder(rightpreorder,rightinorder));
    return root;
}

使用后序序列和中序序列求解二叉树

//已知中序序列和后序序列求解二叉树
BiNode<char>* CreateBiTreePostOrder(std::string postorder,std::string inorder){
    assert(postorder.length()==inorder.length());
    if(postorder.length()==0 && inorder.length()==0) return NULL;
    unsigned int length = inorder.length();
    char data = postorder[length-1];
    int mid = inorder.find(data);
    std::string leftinorder = inorder.substr(0,mid);
    std::string rightinorder = inorder.substr(mid+1,length);

    std::string leftpostorder = postorder.substr(0,leftinorder.length());
    std::string rightpostorder = postorder.substr(leftinorder.length(),rightinorder.length());

    BiNode<char>* root = new BiNode<char>(data);
    root->SetLeft(CreateBiTreePostOrder(leftpostorder,leftinorder));
    root->SetRight(CreateBiTreePostOrder(rightpostorder,rightinorder));
    return root;
}

使用层次序列和中序序列求解二叉树

//已知中序序列和层序序列求解二叉树
BiNode<char>* CreateBiTreeLevelOrder(std::string levelorder,std::string inorder){
    assert(levelorder.length()==inorder.length());
    if(levelorder.length()==0 && inorder.length()==0) return NULL;
    unsigned int length = inorder.length();
    char data = levelorder[0];
    int mid = inorder.find(data);
    std::string leftinorder = inorder.substr(0,mid);
    std::string rightinorder = inorder.substr(mid+1,length);

    std::string leftlevelorder = "";
    std::string rightlevelorder = "";
    for(unsigned int k=1;k<levelorder.size();k++){
        for(unsigned int j=0;j<leftinorder.size();j++){
            if(leftinorder[j]==levelorder[k]){
                leftlevelorder = leftlevelorder + levelorder[k];
            }
        }
        for(unsigned int j=0;j<rightinorder.size();j++){
            if(rightinorder[j]==levelorder[k]){
                rightlevelorder = rightlevelorder + levelorder[k];
            }
        }
    }
    BiNode<char>* root = new BiNode<char>(data);
    root->SetLeft(CreateBiTreeLevelOrder(leftlevelorder,leftinorder));
    root->SetRight(CreateBiTreeLevelOrder(rightlevelorder,rightinorder));
    return root;
}

这三种构造方法都遵循一个基本的原则,即对子串进行划分处理,然后递归调用和处理各个子串.实际上上述的构造的三种方式也是基于先序序列、中序序列和后序序列的递归框架基础上进行构造,所以上述的构造方式也可以使用非递归方式进行构造.此处就不再过多叙述了.

小结

四种遍历方法是学习树结构最为常见的树的数据结构处理方法,这些方法贯穿数据结构始终,很多时候都是依赖于这种树结构的方式进行计算构造。在下一小节会更多讲述这些遍历方法的变形以及很多应用等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值