数据结构之树(三)

绪论

本小结主要讲述的是线索二叉树的建立与使用方法,还有一些树、森林的存储结构、遍历方法以及并查集的应用。

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

3.4 线索二叉树

3.4.1 线索二叉树的基本概念和定义

实际上根据几种遍历算法的描述,其实遍历算法的实质就是将一个非线性的数据结构转化为一个线性的数据结构的方式,使得在这个访问序列中每一个结点(除了第一个结点和最后一个结点之外)都有一个直接前驱和直接后继.
基本的二叉树链式存储方式仅仅能体现一种父子关系,不能直接得到结点在遍历中的前驱或者后继结点.通过观察,可以发现二叉链表中表示的二叉树存在着大量的空指针,若利用这些空链域存放指向其直接前驱或者直接后继的指针,则可以非常方便地运用某些二叉树操作算法.引入线索二叉树是为了加快查找结点前驱和后继的速度.
我们可以得到,在包含有 N N N个结点的二叉树中有 N + 1 N+1 N+1个空指针.这是因为每一个叶结点有2个空指针,而每一个度为1的结点有一个空指针,总的空指针数量为 2 N 0 + N 1 2N_{0}+N_{1} 2N0+N1,所以总的空指针数量为 N 0 + N 1 + N 2 + 1 = N + 1 N_{0}+N_{1}+N_{2}+1=N+1 N0+N1+N2+1=N+1.
在二叉树线索化的时候,通常规定:若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点.还需要增加两个标志域表明当前指针域所指对象是指向左(右)子结点还是直接前驱(后继).
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1LJkPcpK-1609381031351)(线索二叉树.jpg)]
其中ltag=0表示lchild域指示结点的左孩子,ltag=1表示lchild域指示结点的前驱;rtag=0表示rchild域指示结点的右孩子,rtag=1表示rchild域指示结点的后继.
线索二叉树的存储结构描述如下所示

typedef struct ThreadNode{
    ElementType data;
    struct ThreadNode* lchild,rchild;
    int ltag,rtag;
}ThreadNode,*ThreadTree;

为了方便表示,这里我们用cpp的类的结构描述这一个存储结构表示

# ifndef THREADBITREENODE_H
# define THREADBITREENODE_H
template<typename ElementType>
class ThreadBiTreeNode{
private:
    bool lefttag;
    bool righttag;
    ThreadBiTreeNode<ElementType>* left;
    ThreadBiTreeNode<ElementType>* right;
    ElementType data;
public:
    ThreadBiTreeNode(){
        this->left=NULL;
        this->right=NULL;
        this->lefttag = false;
        this->righttag = false;
    }
    ThreadBiTreeNode(ElementType data,ThreadBiTreeNode<ElementType>*left,ThreadBiTreeNode<ElementType>*right,bool lefttag,bool righttag){
        this->left= left;
        this->right=right;
        this->lefttag=lefttag;
        this->righttag=righttag;
        this->data = data;
    }
    ThreadBiTreeNode(ElementType data,ThreadBiTreeNode<ElementType>*left,ThreadBiTreeNode<ElementType>*right){
        this->left= left;
        this->right=right;
        this->data = data;
        this->lefttag = false;
        this->righttag = false;
    }
    ThreadBiTreeNode(ThreadBiTreeNode<ElementType>*left,ThreadBiTreeNode<ElementType>*right){
        this->left= left;
        this->right=right;
        this->lefttag = false;
        this->righttag = false;
    }
    ThreadBiTreeNode(ElementType data){
        this->data = data;
        this->left= NULL;
        this->right=NULL;
        this->lefttag = false;
        this->righttag = false;
    }
    void SetLeft(ThreadBiTreeNode<ElementType>*left){this->left=left;}
    void SetRight(ThreadBiTreeNode<ElementType>*right){this->right=right;}
    void SetData(ElementType data){this->data=data;}
    void SetLeftTag(bool lefttag){this->lefttag=lefttag;}
    void SetRightTag(bool righttag){this->righttag=righttag;}
    ThreadBiTreeNode<ElementType>* GetLeft(){return this->left;}
    ThreadBiTreeNode<ElementType>* GetRight(){return this->right;}
    ElementType GetData(){return this->data;}
    bool GetLeftTag(){return this->lefttag;}
    bool GetRightTag(){return this->righttag;}
    ~ThreadBiTreeNode(){
        
    }
};
# endif //THREADBITREENODE_H

以这种结点结构构成的二叉链表作为二叉树的存储结构叫做线索链表,其中指向结点前驱和后继指针叫做线索.加上线索的二叉树称为线索二叉树.对二叉树以某种次序遍历使得其变为线索二叉树的过程称为线索化.

3.4.2 线索二叉树的构造

对二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左、右指针是否为空,若为空,则将它们改为指向前驱结点或者后继结点的线索.

  1. 先序遍历的线索二叉树的构造
    这里使用递归的方法进行先序线索二叉树的构造,构造的基本原则和先序遍历的方式很类似.算法如下所示
void PreThreadBiTree(ThreadBiTreeNode<ElementType>*root,ThreadBiTreeNode<ElementType>** prev){
        if(root!=NULL){
            if(root->GetLeft()==NULL){
                root->SetLeft(*prev);
                root->SetLeftTag(true);
            }
            if(*prev!=NULL && (*prev)->GetRight()==NULL){
                (*prev)->SetRight(root);
                (*prev)->SetRightTag(true);
            }
            *prev = root;
            if(!root->GetLeftTag()) PreThreadBiTree(root->GetLeft(),prev);
            if(!root->GetRightTag()) PreThreadBiTree(root->GetRight(),prev);
        }
    }

先序遍历算法的主程序如下所示

void CreatePreOrderThreading(ThreadBiTreeNode<ElementType>*root){
    ThreadBiTreeNode<ElementType>** prev = new ThreadBiTreeNode<ElementType>* ;
    *prev=NULL;
    ThreadBiTree<ElementType>::PreThreadBiTree(root,prev);
    (*prev)->SetRight(NULL);
    (*prev)->SetRightTag(true);
}

这样形成的先序线索二叉树的形式如下所示
先序线索二叉树

当然,有时候为方便,仿照线性表的存储结构,在二叉树的线索链表上也添加一个头结点,并且令其lchild域的指针指向二叉树的根节点,其rchild域的指针指向中序遍历时候访问的最后一个结点;反之,令二叉树中序序列的第一个结点的lchild域的指针和最后一个结点rchild域的指针均指向头结点.如下图所示
带头结点的先序线索二叉树

先序线索二叉树遍历的方法如下所示

void PreOrderVisit(ThreadBiTreeNode<ElementType>*root){
        if(root==NULL) return ; 
        ThreadBiTreeNode<ElementType>* pCur = root;  
        while (pCur != NULL){  
            while (pCur->GetLeft() != NULL && !pCur->GetLeftTag()){  
                std::cout<< pCur->GetData()<< ' ';  
                pCur = pCur->GetLeft();  
            }  
            std::cout<< pCur->GetData()<< ' ';
            if (pCur->GetLeftTag()) pCur = pCur->GetRight();
            while (pCur != NULL){  
                if (pCur->GetLeft()!= NULL && !pCur->GetLeftTag()) break;
                std::cout << pCur->GetData()<< ' ';  
                pCur = pCur->GetRight();  
            }  
        } 
    }
  1. 中序遍历的线索二叉树的构造
    这里使用递归的方式构造中序遍历的线索二叉树,稍微和先序遍历线索二叉树有些不同,递归算法如下所示:
void InThreadBiTree(ThreadBiTreeNode<ElementType>*root,ThreadBiTreeNode<ElementType>** prev){
        if(root!=NULL){
            InThreadBiTree(root->GetLeft(),prev);
            if(root->GetLeft()==NULL){
                root->SetLeft(*prev);
                root->SetLeftTag(true);
            }
            if(*prev!=NULL && (*prev)->GetRight()==NULL){
                (*prev)->SetRight(root);
                (*prev)->SetRightTag(true);
            }
            *prev = root;
            InThreadBiTree(root->GetRight(),prev);
        }
}

形成的线索二叉树如下所示
中序线索二叉树

这样,中序遍历建立线索二叉树的主过程算法如下所示:

void CreateInOrderThreading(ThreadBiTreeNode<ElementType>*root){
    ThreadBiTreeNode<ElementType>** prev = new ThreadBiTreeNode<ElementType>* ;
    *prev=NULL;
    ThreadBiTree<ElementType>::InThreadBiTree(root,prev);
    (*prev)->SetRight(NULL);
    (*prev)->SetRightTag(true);
    delete prev;
}

中序线索二叉树遍历的过程算法是这样的:
① 初始化指针ptr=root;
② 用ptr指针遍历树并找到树的最左下角的元素,然后访问;
③ 当ptr指针不为空的时候,进行以下的步骤;
④ 若ptr右指针不是后继线索的时候,ptr赋值为ptr的右孩子指针,用ptr遍历找到右孩子的最左下角指针并访问;否则ptr赋值为ptr的后继指针,若ptr不为空的时候,访问该指针的值;
⑤ 判断此时的指针ptr是否为空,若为空指针的话,退出循环;若不为空,继续执行第④步骤.
所以中序线索二叉树的遍历过程如下所示:

void InOrderVisit(ThreadBiTreeNode<ElementType>*root){
        if(root==NULL) return ;
        ThreadBiTreeNode<ElementType>*ptr = root;
        while(!ptr->GetLeftTag()) ptr= ptr->GetLeft();std::cout<<ptr->GetData()<<" ";
        while(ptr!=NULL){
            if(!ptr->GetRightTag()){
                ptr=ptr->GetRight();
                while(!ptr->GetLeftTag()) ptr= ptr->GetLeft();
                std::cout<<ptr->GetData()<<" ";
            }
            else {
                ptr = ptr->GetRight();
                if(ptr!=NULL) std::cout<<ptr->GetData()<<" ";
            }
        }   
        std::cout<<std::endl;
    }

带头结点的中序线索二叉树如图所示
带头结点的中序线索二叉树

  1. 后序遍历的线索二叉树的构造
    后序线索二叉树和上述两种线索二叉树又有一些不同的特点.这里构造后序线索二叉树的算法与后序遍历二叉树是一致的,构造方式如下所示
void PostThreadBiTree(ThreadBiTreeNode<ElementType>*root,ThreadBiTreeNode<ElementType>** prev){
        //建立后序线索二叉树
        if(root!=NULL){
            PostThreadBiTree(root->GetLeft(),prev);
            PostThreadBiTree(root->GetRight(),prev);
            if(root->GetLeft()==NULL){
                root->SetLeft(*prev);
                root->SetLeftTag(true);
            }
            if(*prev!=NULL && (*prev)->GetRight()==NULL){
                (*prev)->SetRight(root);
                (*prev)->SetRightTag(true);
            }
            *prev = root;
        }
    }

这样后序线索二叉树的主程序如下所示

void CreatePostOrderThreading(ThreadBiTreeNode<ElementType>*root){
    ThreadBiTreeNode<ElementType>** prev = new ThreadBiTreeNode<ElementType>* ;
    *prev=NULL;
    ThreadBiTree<ElementType>::PostThreadBiTree(root,prev);
}

这样构造出来的后序线索二叉树如下所示
后序线索二叉树

同样也可以设计成为带有头结点的后序线索二叉树,如下图所示
带头结点的后序线索二叉树

后序线索二叉树的遍历稍微有些复杂,因为在递归返回父结点的时候需要用到父结点的指针,所以这样的一个过程仍然需要栈的支持,这里可以设计成为栈的方式也可以设计成为递归构造的方式.我们这里使用到了栈来求结点的父结点的方法.
求父结点有以下的几种情形:
(1) 若所求的结点为根结点时候,那么父结点为NULL;
(2) 找到最左端的结点,在遍历的过程中注意判断子结点是否为父结点的孩子,将当前指针压入栈,并用prev记录上一次遍历的结点(注意左、右子树);
(3) 判断子结点是否为父结点的孩子;
(3) 若右孩子是线索,那么访问上一次结点的右子树,并退栈,然后判断子结点是否为父结点的孩子;
(4) 若右孩子不是线索,那么就访问上一次的结点.
所以求父结点的非递归算法如下所示

template<typename ElementType>
ThreadBiTreeNode<ElementType>* GetParent(ThreadBiTreeNode<ElementType>* tree,ThreadBiTreeNode<ElementType>* node){
        if(node == NULL) return NULL;
        else{
            ThreadBiTreeNode<ElementType>* ptr = tree;
            ThreadBiTreeNode<ElementType>* prev = NULL;
            Stack<ThreadBiTreeNode<ElementType>*> st;
            while(!ptr->GetRightTag()||!ptr->GetLeftTag()||!st.IsEmpty()){
                while(!ptr->GetLeftTag()){
                    if(ptr == node) return prev;
                    st.Push(ptr);
                    prev = ptr;
                    ptr = ptr->GetLeft();
                }
                if(ptr == node) return prev;
                if(!st.IsEmpty()){
                    if(!ptr->GetRightTag()){
                        prev = ptr;
                        ptr =ptr->GetRight();
                    }else{
                        ptr = st.Top();
                        ptr = ptr->GetRight();
                        prev =st.Pop();
                        if(ptr == node) return prev;
                    }
                }
            }
            return NULL;
        }
}

所以后序线索二叉树的遍历方式就有以下的思路:
(1) 找到最左边的结点(存在右子树,不存在右子树的情形);
(2) while 循环已知遍历结点的后继(prev保存上一次访问的结点),注意左单支*,跳出循环的条件是当前结点有右子树或者可能是根结点;
(3) 跳出循环:判断是否为根结点:如果根结点没有右子树,则直接返回函数退出;
(4) 当前结点不是根结点的时候,循环访问当前结点的双亲结点,注意右单支
(5) 最后判断是否为有右子树,当前结点指向其右子树.
所以后序线索二叉树的遍历如下所示

template<typename ElementType>
void ThreadBiTree<ElementType>::PostOrderWithSt(){
        assert(this->type=="postorder");
        if(this->root==NULL) return ;
        ThreadBiTreeNode<ElementType>*ptr = this->root;
        ThreadBiTreeNode<ElementType>*prev = NULL;
        while(ptr!=NULL){
            //找到最左边的结点
            while(ptr!=NULL &&!ptr->GetLeftTag() && ptr->GetLeft()!=prev) ptr= ptr->GetLeft();
            //跳出循环的条件,ptr现在是最左边的结点
            if(ptr!=NULL && ptr->GetLeftTag()){
                std::cout<<ptr->GetData()<<" ";
                prev = ptr;
            }
            //判断是否为根节点
            if(ptr == this->root && ptr->GetRight()==NULL){
                std::cout<<ptr->GetData()<<" ";
                break;
            }
            ptr= ptr->GetRight();
            while(ptr!=NULL && ptr->GetRight()==prev){
                std::cout<<ptr->GetData()<<" ";
                prev = ptr;
                ptr =  GetParent(this->root,ptr);
                if(ptr == prev) break;
            }
        }
        std::cout<<std::endl;
}
  1. 层次遍历的线索二叉树的构造
    层次遍历是一个非常可观的遍历方法,结点线索的设置比起上述的三种就会简单很多.层次遍历的线索二叉树构造方法如下图所示
    层序线索二叉树

带头结点的层序线索二叉树如下所示
带头结点的层序线索二叉树

层序线索二叉树的构造和遍历比较简单,此处省略.
大总结:

  • 中序遍历开始于最左下结点(从根出发,一路沿着left指针到达最远结点,未必是叶子结点),结束语最右下结点.每个结点在遍历中的下一个结点是其右子树最左下的结点,若右子树为空,则不计算;其上一结点是左子树的最右下结点,若左子树为空,同样不便计算.
  • 先序遍历开始于根结点,结束语一个最右下的叶子结点,若最右下结点不是叶子结点,则搜寻找其左子树的最右下的结点,以此类推,每个结点在遍历中的下一个结点是其第一个子结点,当子结点不存在的时候,则不便计算;其上一结点是左兄弟树的先序遍历终点,当左兄弟不存在的时候(自己是父结点的左子结点,或者是左兄弟为空),上一个结点为父结点.
  • 后序遍历结束于根结点,开始于一个最左下的叶子结点,若最左下结点不是叶子结点,则寻找其右子树的最左下结点,以此类推,每个结点在遍历中的上一个结点是其最后一个子结点,当子结点不存在的时候,则不便计算;其下一个结点是右兄弟树的后序遍历起点,当右兄弟不存在的时候(自己是父结点的右子树结点,或者是右兄弟为空),下一个结点是父结点.

4. 树、森林

4.1 树的存储结构

树的存储结构有很多种方式,既可以顺序存储结构也可以采用链式存储结构,但是无论是哪一种存储结构都要求能够唯一地反映出树中各个结点之间的逻辑关系.我们这里介绍三种最为常见基本的存储结构.

  1. 双亲表示方法:这种存储方式是采用一组连续空间来存储每一个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置.根结点下标为0,其中伪指针为-1.举个例子,如下图所示
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HXFOU8fA-1609381031364)(双亲表示法.jpg)]
    这种方法的存储结构表示如下所示
# define MAX_SIZE 100
typedef struct{
    ElementType data;
    int parent; //结点指向的父结点位置域
}PTNode;
typedef struct{
    PTNode nodes[MAX_SIZE]; // 双亲表示区域
    int num; // 结点的个数.
}PTree;

这种表示结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但是求结点的孩子结点却需要遍历整个结构,这也是这种方法表示的缺点.
2. 孩子表示法
孩子表示法最为直接,它是将每个结点的孩子结点都用单链表的方式链接起来形成一个线性的结构,则 N N N个结点就有 N N N个孩子链表(叶子结点的孩子链表为空表).
对于这种存储方式寻找子女的操作非常直接,但是寻找双亲的操作需要遍历 N N N个结点中孩子链表指针域所指向的 N N N个孩子链表,这是这种结构不太合理的地方,也是一个缺点.存储结构表示如下所示

# ifndef CHILDRENTREENODE_H
# define CHILDRENTREENODE_H
template<typename ElementType>
class ChildrenNode{
private:
    ElementType data;
    ChildrenNode<ElementType>*  childrenlist[];
    unsigned int number;
public:
    ChildrenNode();
    ChildrenNode(ElementType data);
    ChildrenNode(ElementType data,ChildrenNode<ElementType>* childrenlist[],unsigned int number);
    ~ChildrenNode();
};
template<typename ElementType>
ChildrenNode<ElementType>::ChildrenNode(){
    this->childrenlist = NULL;
}
template<typename ElementType>
ChildrenNode<ElementType>::ChildrenNode(ElementType data){
    this->data = data;
    this->childrenlist = NULL;
}
template<typename ElementType>
ChildrenNode<ElementType>::ChildrenNode(ElementType data,ChildrenNode<ElementType>* childrenlist[],unsigned int number){
    this->data = data;
    this->number = number;
    this->childrenlist = childrenlist;
}
template<typename ElementType>
ChildrenNode<ElementType>::~ChildrenNode(){
    
}

# endif //CHILDRENTREENODE_H
  1. 孩子兄弟表示法
    孩子兄弟表示法又称为树的二叉树表示法,即以二叉链表作为树的存储结构.孩子兄弟表示法是每个结点包含有三部分内容:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(所以沿着此指针一直可以找打所有兄弟结点).
    所以说这种表示方法和二叉树表示方法一致,存储结构可以表示为
typedef struct CSNode{
    ElementType data;
    struct CSNode *firstchild,*nextsibling; //第一个孩子和右兄弟指针
}CSNode,*CSTree; 

这种存储表示法比较灵活,其最大的优点就是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等等,但缺点是从当前结点查找其双亲结点比较麻烦.当然,可以在结点中设置一个结点的parent指针则会方便许多.

4.2 树、森林与二叉树的转换

由于二叉树和树都可以用二叉链表作为存储结构,则以二叉链表作为媒介可以导出树与二叉树的一个对应关系,即给定一棵树可以直接找到唯一的一棵二叉树与之对应.
树转换为二叉树的规则: 每个结点的左指针指向它的第一个孩子结点,右指针指向它在树中相邻兄弟结点,可以表示为"左孩子右兄弟".由于根节点没有兄弟,所以树转换得到的二叉树没有右子树.
森林转换为二叉树的规则: 先将森林中的每一棵树转换为二叉树,再将第一棵树的根作为转换后二叉树的根,第一棵树的左子树作为作为转换后二叉树根的左子树,第二棵树作为转换后二叉树的右子树,第三棵树作为转换后二叉树的右子树的右子树,以此类推,就可以将森林转换成为二叉树.
二叉树转换成为森林的规则: 若二叉树非空,则二叉树根及其左子树为第一棵树的二叉树形式,二叉树根的右子树又可以看做是一个除第一棵树以外的森林转换后的二叉树,应用同样的方法,直到最后产生一棵没有右子树的二叉树为止,这样就得到了原森林.
二叉树转换为树的规则与二叉树转换为森林的规则类似,此处不做过多的叙述.

4.3 树和森林的遍历

树的遍历操作是以某种方式访问树中每一个结点,并且仅访问一次.树的遍历操作主要有先根遍历和后根遍历两种操作.

  1. 先根遍历: 若树是非空的,则先访问根结点,再按照从左往右的顺序遍历根结点的每一棵子树,其访问顺序与这棵树相应的二叉树的先序遍历相同(即先访问根结点然后访问子树的结点).
  2. 后根遍历: 若树是非空的,则按照从左到右的顺序遍历根结点的每一棵子树,之后再访问根结点.其访问顺序和这棵树相应的二叉树的中序遍历顺序相同(即先访问子树结点然后访问根结点).
    同样,树也有对应的层次遍历,这与二叉树的层次遍历的思想基本相同,也是按层序依次访问各个结点.

按照森林和树相互递归的定义,可以得到森林的两种遍历方法:

  1. 先序遍历森林:若森林为非空,那么就按照下列的方式进行遍历:
  • 访问森林中第一棵树的根结点;
  • 先序遍历第一棵树中根结点的子树森林;
  • 先序遍历出去第一棵树之后剩余的树构成的森林.
  1. 中序遍历森林:若森林为非空,则按照如下的规则进行遍历:
  • 中序遍历森林中第一棵树的根节点的子树森林;
  • 访问第一棵树的根节点;
  • 中序遍历除去第一棵树之后剩余的树构成的森林.

用程序实现树或者是森林的遍历如下所示(使用孩子兄弟方法存储)
层序遍历

template<typename ElementType>
void LevelOrderWithQe(){
    //树或者是森林的层次遍历
    if(this->GetRoot()==NULL) return;
    BiNode<ElementType>*ptr = this->GetRoot();
    Queue<BiNode<ElementType>*>qe;
    while(ptr!=NULL){
        qe.Push(ptr);
        ptr=ptr->GetRight();
    }
    while(!qe.IsEmpty()){
        ptr=qe.Pop();
        std::cout<<ptr->GetData()<<" ";
        if(ptr->GetLeft()!=NULL) {
            ptr = ptr->GetLeft();
            while(ptr!=NULL){
                qe.Push(ptr);
                ptr=ptr->GetRight();
            }
        }
    }
    std::cout<<std::endl;
}

先序遍历(先根遍历)与二叉树的先序遍历一致、中序遍历森林(后根遍历)与二叉树的中序遍历一致,此处不再演示.
其他的一些算法很重要,例如下面的几个问题
A. 打印树中的叶子结点
栈的方法

template<typename ElementType>
void ChildBrotherTree<ElementType>::PrintLeavesWithSt(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){
            if(ptr->GetLeft()==NULL) std::cout<<ptr->GetData()<<" ";
            prev = ptr;
            ptr = NULL;
            st.Pop();
        }else ptr= ptr->GetRight();
    }
    std::cout<<std::endl;
}

队列的方法

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

递归的方法

template<typename ElementType>
void PrintLeaves(BiNode<ElementType>*root){
        if(root==NULL) return;
        else{
            if(root->GetLeft()==NULL) std::cout<<root->GetData()<<" ";
            PrintLeaves(root->GetLeft());
            PrintLeaves(root->GetRight());
        }
}

计算叶子结点的个数和打印叶子结点算法类似,只需适当修改即可
B. 计算树高度或者是森林的最大高度

template<typename ElementType>
unsigned int GetHeight(BiNode<ElementType>*root){
        BiNode<ElementType>* rtp = root;
        unsigned int height = 0;
        while(rtp!=NULL){
            BiNode<ElementType>*ptr = rtp;
            unsigned int tmph = 0;
            while(ptr!=NULL){
                tmph++;
                ptr=ptr->GetLeft();
            }
            if(height<tmph) height = tmph;
            rtp=rtp->GetRight();
        }
        return height;
    }

4.4 树的应用(并查集)

并查集是一种简单的集合表示,在一些有 N N N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中,这就是并查集最为基本操作.它一般支持以下的三种操作方式:

  1. Union(S,Root1,Root2):表示将S集合中的子集合Root1并入到子集合Root2中.其中要求集合Root1和集合Root2互不相交,否则不执行合并的步骤.
  2. Find(S,x):查找集合S中单元素x所在的子集合,并返回该子集合的名字.
  3. Initial(S):将集合S中每一个元素都初始化为只有一个单元素的子集合.

通常用树(森林)的双亲表示作为并查集的存储结构,每个子集合以一棵树表示.所有表示子集合的树,构成表示全集合的森林,存放在双亲表示的数组内.通常用数组元素的下标表示元素的名字,根结点的下标代表子集合名,根结点的双亲为负数.

合并两个子集合,只需要将其中一个子集合根结点的双亲指针指向另一个集合的根节点即可.并查集以及一些操作的定义如下所示:

# define SIZE 100

int UFSets[SIZE];
//初始化操作
void Initial(int S[]){
    for(int k=0;k<size;k++) S[k]=-1;
}
//查找操作(在并查集S中查找并返回包含元素x的树的根)
void Find(int S[],int x){
    while(S[X]>0) x = S[x];
    return x;
}
//合并操作(要求这两个不相交的集合的并集)
void Union(int S[],int Root1,int Root2){
    S[Root2] = Root1;
}

小结

这一小节主要讲述的是线索二叉树以及一些树的基本概念和应用,比较抽象,较难理解。通过这次的小结,对二叉树以及树的数据结构有了更深的了解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值