树及其应用c语言实现

版权声明:欢迎转载,但转载时请注明原文地址 https://blog.csdn.net/weixin_42110638/article/details/83743818

一.树的基本概念

 

 

二.二叉树

1.二叉树的定义

2.二叉树的性质 

此外在这里在介绍下完美二叉树的概念及重要性质

 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

性质:

(1)所有的叶结点都出现在第k层或k-l层(层次最大的两层)

(2)对任一结点,如果其右子树的最大层次为L,则其左子树的最大层次为L或L+l。

3.二叉树的存储结构

4.二叉树的遍历(重点) 

仔细看这篇博客:https://blog.csdn.net/weixin_42110638/article/details/83796618

二叉树遍历的应用:

【例】二叉树的建立

Status CreateBiTree(BiTree &T) //&的意思是传进来节点指针的引用,括号内等价于 BiTreeNode* &T,目的是让传递进来的指针发生改变
{                        
    char c;
    cin >> c;
    if(c == ' ')             //当遇到 时,令树的根节点为NULL,从而结束该分支的递归
        T = NULL;
    else
    {
        if(!(T = (BiTNode*)malloc(sizeof(BiTNode))));
            exit(OVERFLOW);
        //T = new BiTreeNode;

        T->data=c;//生成根节点
        createBiTree(T->lchild);//构造左子树
        createBiTree(T->rchild);//构造右子树
    }
}

 

重点例题:已知两种遍历序列让你还原二叉树

给道例题:https://blog.csdn.net/weixin_42110638/article/details/83796077

5.线索二叉树

<1>线索二叉树的原理

    通过考察各种二叉链表,不管儿叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。如下图所示。

    因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索

    记ptr指向二叉链表中的一个结点,以下是建立线索的规则:

    (1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;

    (2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;

    显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。结点结构如下所示。

    其中:

    (1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;

    (2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;

    (3)因此对于上图的二叉链表图可以修改为下图的养子。


<2>线索二叉树结构实现

    二叉线索树存储结构定义如下:


/* 二叉树的二叉线索存储结构定义*/
typedef enum{Link, Thread}PointerTag;    //Link = 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索
 
typedef struct BitNode
{
       char data;                                      //结点数据
       struct BitNode *lchild, *rchild;                //左右孩子指针
       PointerTag  Ltag;                               //左右标志
       PointerTag  rtal;
}BitNode, *BiTree;

  线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。

    中序遍历线索化的递归函数实现如下:

BiTree pre;                 //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiTree p)
{
    if(p)
    {
        InThreading(p->lchild);          //递归左子树线索化
                //===
        if(!p->lchild)           //没有左孩子
        {
            p->ltag = Thread;    //前驱线索
            p->lchild = pre; //左孩子指针指向前驱
        }
        if(!pre->rchild)     //没有右孩子
        {
            pre->rtag = Thread;  //后继线索
            pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
        }
        pre = p;
                //===
        InThreading(p->rchild);      //递归右子树线索化
    }
}

上述代码除了//===之间的代码以外,和二叉树中序遍历的递归代码机会完全一样。只不过将打印结点的功能改成了线索化的功能。


    中间部分代码做了这样的事情:


因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild = p,并且设置pre->rtag = Thread,完成后继结点的线索化。如图:


    if(!p->lchild)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值了pre,所以可以将pre赋值给p->lchild,并修改p->ltag = Thread(也就是1)以完成前驱结点的线索化。

    

    完成前驱和后继的判断后,不要忘记当前结点p赋值给pre,以便于下一次使用。

    

    有了线索二叉树后,对它进行遍历时,其实就等于操作一个双向链表结构。

    和双向链表结点一样,在二叉树链表上添加一个头结点,如下图所示,并令其lchild域的指针指向二叉树的根结点(图中第一步),其rchild域的指针指向中序遍历访问时的最后一个结点(图中第二步)。反之,令二叉树的中序序列中第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点(图中第三和第四步)。这样的好处是:我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。

 中序遍历二叉线索树的非递归函数实现:

//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
int InOrderThraverse_Thr(BiTree t)
{
    BiTree p;
    p = t->lchild;                               //p指向根结点
    while(p != t)                               //空树或遍历结束时p == t
    {
        while(p->ltag == Link)                       //当ltag = 0时循环到中序序列的第一个结点
        {
            p = p->lchild;
        }
        printf("%c ", p->data);                      //显示结点数据,可以更改为其他对结点的操作
        while(p->rtag == Thread && p->rchild != t)
        {
            p = p->rchild;
            printf("%c ", p->data);
        }
 
        p = p->rchild;                         //p进入其右子树
    }
 
    return OK;
}


说明:


    (1)代码中,p = t->lchild;意思就是上图中的第一步,让p指向根结点开始遍历;
    (2)while(p != t)其实意思就是循环直到图中的第四步出现,此时意味着p指向了头结点,于是与t相等(t是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作;
    (3)while(p-ltag == Link)这个循环,就是由A->B->D->H,此时H结点的ltag不是link(就是不等于0),所以结束此循环;
    (4)然后就是打印H;
    (5)while(p->rtag == Thread && p->rchild != t),由于结点H的rtag = Thread(就是等于1),且不是指向头结点。因此打印H的后继D,之后因为D的rtag是Link,因此退出循环;
    (6)p=p->rchild;意味着p指向了结点D的右孩子I;
    (7).....,就这样不断的循环遍历,直到打印出HDIBJEAFCG,结束遍历操作。


    从这段代码可以看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。
    由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

6.树与森林

<1>树的定义

  • 有且仅有一个特点的称为根的节点
  • 当n>1时,其余节点可分为m(m>0)个互不相干的有限交集,每个交集称为根的子树。

<2>森林的定义

  • m个互不相交的森林树的集合,子树的集合称为子树的森林。

<3>树的存储结构

a.双亲表示法

  • 在树中,除了根节点没有双亲外,其他节点的双亲的唯一确定的。

//双亲存储的结构类型定义
typedef struct PTNode{
  TElemType data; //数据域
  int parent;     //双亲的位置,根节点的双亲为-1
}PTNode;        //双亲的节点类型
typedef struct {
  PTNode *nodes;  //初始化分配的结点数组
  int r,nodeNum; //根的位置和结点数
}PTree;         //树的双亲存储结构类型

优点: 可用parent直接找到双亲,并且很容易找到祖先。

缺点:需要查找节点的孩子及其子孙时要遍历整个结构。

b.双亲孩子表示法

  • 是对双亲表示法的扩展,为各节点构造孩子单链表,以便于访问结点的孩子及其子孙,在结点的数组元素中增加firstchild域作为结点的孩子链表头的头指针。

//双亲孩子存储结构的类型定义
typedef struct ChildNode{
  int childIndex;       //孩子在结点数组的位置
  struct ChildNode *nextChild;  //下一个孩子
}ChildNode;         //孩子链表中的节点类型
​
typdef  struct{
  TElemType data;       //元素值
  int parent;           //双亲的位置
  struct ChildNode *firstChild; //孩子链表头指针
}PCTreeNode;            //双亲节点的节点类型
​
typedef struct{
  PCTreeNode *nodes;    //节点数组
  int nodeNum,r;    //结点元素的个数,根位置
}PCTree;            //树的双亲孩子存储结构类型

c.孩子兄弟表示法

  • 在树中,结点的最左孩子(第一个孩子)和右兄弟如果存在则都是唯一的。采取二叉链式存储结构,每个结点包含三个域,元素值data,最左边孩子指针firstchild和右兄弟指针nextsibling。

//孩子兄弟链表的类型定义
typedef struct CSTNode{
    TElemType data;         //数据域
    struct CSTNode *firstChild,*nextSibling; //最左孩子指针,右兄弟指针
}CSTnode,*CSTree,*CSForest;     //孩子兄弟链表

d.孩子兄弟表示法的树的接口

Status InitTree(CSTree &T); //构造空树
CSTree MakeTree(TElemType e,int n....)  //创建根结点为e和n颗子树的树
Status DestroyTree(CSTree &T)  //销毁树
int TreeDepth(CSTree T)  //返回树的深度
CSNode *Search(CSTree T,TElemType e); //查找树T中的节点e并返回其指针
Status InesertChild(CSTree &T,int i,CSTree c) //插入c为T的第i颗子树,c非空并且与T不相交
Status DeleteChild(CSTree &T,int i) //删除第i棵子树

创建树

#include<stdarg.h>// 标准头文件,提供宏va_start、va_arg和va_end,
​
CSTree MakeTree(TElemType e,int n....){
  int i;
  CSTree t,p,pi;
  va_list argptr; //存放变长参数表信息的数组
  t=(CSTree)malloc(sizeof(CSTNode));
  if(t=NULL) return NULL;
  t->data=e;    //根结点的值为e;
  t->firstChild=t->nextSibling=NULL;
  if(n<=0) return t;  //若无子树,则返回根结点
  va_start(argptr,n) //令argptr 指向n后的第一个实参
  p=va_arg(argptr,CSTree); //取第一棵子树的实参转化为CSTree类型
  t->firstChild=p;
  pi=p;
  for(i=1;i<n;i++){
    p=va_arg(argptr,CSTree); //取下一颗子树的实参并转换为CSTree类型
    pi->nextSibling=p;
    pi=p;
  }
  va_end(argptr);
  return t;
}
​
//可变参数的使用
使用可变参数应该有以下步骤(要加入<stdarg.h>): 
​
1)首先在函数里定义一个va_list型的变量,这里是argptr,这个变 量是指向参数的指针. 
​
2)然后用va_start宏初始化变量argptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数. 
​
3)然后用va_arg返回可变的参数,并赋值给变量p(CSTree类型). va_arg的第二个 参数是你要返回的参数的类型,这里是CSTree类型.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数. 
​
4)最后用va_end宏结束可变参数的获取.

插入第i个子树

Status InesertChild(CSTree &T,int i,CSTree c){
  int j;
  CSTree p;
  if(NULL==T||i<1) return ERROR;
  if(i==1){ //c为第一棵插入子树
    c->nextSibling=T->firstChild;
    T->firstChild=c; //T成为T的第一棵子树
  }else{
    p=T->firstChild;
    for(j=2;p!=NULL&&j<i;j++){
      p=p->nextSibling; //寻找插入位置
    }
    if(j==i){
      c->nextSibling = p->nextSibling;
      p->nextSibling = c;
    }else return ERROR;
  }
  return OK;
}

求树的深度

int TreeDepth(CSTree T) {  // 求树T的深度  
    int dep1, dep2, dep;
    if(NULL==T) dep = 0; // 树为空,深度则为0
    else {   
        dep1 = TreeDepth(T->firstChild);    // 求T的子树森林的深度
        dep2 = TreeDepth(T->nextSibling);   // 求除T所在树以外的其余树的深度
        dep = dep1+1>dep2 ? dep1+1 : dep2;  // 树的深度 
     }
     return dep;
} 
 

查找树

CSTreeNode* Search(CSTree T, TElemType e) {  
    // 查找树T中的结点e并返回其指针
    CSTreeNode* result = NULL;       
    if(NULL==T) return NULL;  // 树为空,返回NULL 
    if(T->data==e) return T;  // 找到结点,返回其指针
    if((result = Search(T->firstChild, e))!=NULL) // 在T的子树森林查找
        return result; 
    return Search(T->nextSibling, e); 
        // 在除T所在树以外的其余树构成的森林查找
}
​

<4>树、森林与二叉树的转换(重点)

7.哈夫曼树及其应用

哈夫曼树这里涉及到一些关于其他树的知识,比如二叉搜索树,二叉平衡树,堆(最好这几个知识都要学会,因为非常重要)

本文对这些知识不做详解,详细的可以参考我的这几篇博客

二叉搜索树:https://blog.csdn.net/weixin_42110638/article/details/83963764

二叉平衡树:https://blog.csdn.net/weixin_42110638/article/details/83963954

堆:https://blog.csdn.net/weixin_42110638/article/details/83982381

代码实现:

这里其实就用了堆的思想!!!

这个创建的过程最主要的就是每次选两个最小的,这里其实就是堆的思想,你把结点的权值构造出最小堆,每次取两个最小堆,并在一起,形成的新堆插进去

猜你喜欢

转载自blog.csdn.net/weixin_42110638/article/details/83743818