读书笔记-----数据结构六(树的知识点)

2018年6月2日思考
树的存储结构的时候,那个时候我觉得很乱,不知道为什么会有这么一节要存在,现在终于明白了,你总要存储的嘛。
在顺序存储和链式存储的时候,我们简单的顺序存储和链式存储肯定是不得行,所以,开辟了另外的空间来进行维护节点之间的关系问题。在这种维护关系中,我们存什么进去就比较关键了,有三种存储的方式,第一种是双亲表示法,存储双亲的关系,但知道某节点的双亲比较简单,但如果想要知道双亲的孩子节点,则需要遍历整个线性存储的结构,效率比较低下,如果我们多扩展索引的部分,不仅存双亲的信息,而且还可以存储孩子节点,或者存储左孩子节点,右孩子节点等等。第二种是孩子表示法。我们混合了线性表和链表,结合了他们的优点,来表述孩子节点的问题,通过扩展,也可以存储双亲之间的关系的问题。第三种是孩子兄弟表示法,用于表示该节点的第一个孩子节点,以及该节点的右兄弟节点。这种表示方法,可以利用二叉树的性质来进行描述。

树的性质涉及到的数学基础:
等比数列求和的问题。
握手定理。边数的2倍等于度和。
二叉树中结点树n = 度为2的中间节点 + 度为1的中间节点 + 度为1的叶子节点(只考虑出度)
二叉树的边数m = 2 * 度为2的中间节点 + 度为1的中间节点 = n - 1
(可以记住的公式,二叉树中的度为2的节点比二叉树中的叶子节点的数目少 1)
对于完全2叉树,因为性质特殊,所有对于其节点编码有独特的性质。

中序遍历是牛逼的,几乎可以说是万能的了,利用中序遍历的顺序,可以很容易的实现线索二叉树,。
如果知道前序遍历+中序遍历是可以唯一确定一棵树,知道后序遍历+中序遍历可以唯一确定一颗树。

树的专业定义:树是n(n>=0)个结点的有限集。n=0时称为空树。在任意一个非空树中, (1)有且仅有一个特定的根的结点;(2)当n>1时,其余结点可分为m个互不相关的有限集,其中每个集合本身又是一棵树,并且称为根的子树。
定义中,强调了根节点唯一,强调了子树之间的互不相交。
树中的一些概念:
叶结点(出度为0,入度为1)
根节点(入度为0)+分支结点(入度为1) = 内部结点
计算机中的树与图论中的树不太一致??
在这里,我们只考虑出度的问题,出度最大的结点的度数为树的度数。
子孙,父节点,祖先(肯定是一系上才能称为祖先),兄弟,堂兄弟
树高指的是树最高的层次。

树结构中,基本的操作

1,InitTree(*T)创建一个空树
2,DestroyTree(*T)销毁一个树
3,CreateTree(*T, definition)
4,ClearTree(*T)
5,TreeEmpty(T)
6,TreeDepth
7,Root(T)
8,Value(T, cur_e)
9,Assign(T, cur_e, value)
10,Parent(T, cur_e)
11,LeftChild(T, cur_e)
12,RightSibling(T, cur_e)
13,InsertChild(*T, *p, i, c)
14,DeleteChild(*T, *p, i)

对于线性表而言,有前驱有后继,所以存储具有逻辑性,可以判断出元素之间的关系,但对于树结构而言,不管按哪种方式来存储,都无法直接反应其逻辑关系,但我们可以通过制定某些规则来达到我们所要存储的逻辑关系的目的。

树的三种表示方法:
1,双亲表示法
利用了结点(除了根结点之外的)都具有双亲,所以,我们在存储结点的时候,附设一个指示器,指示其双亲结点在数组中的位置。
在这种存储方式中,我们可以很快的找到结点的父结点,此时时间复杂度为O(1),但如果我们要找到结点的儿子结点,则需要遍历整个结构才行,比较麻烦。
对于此种的改进,加入第一个子结点,即长子域,没有子结点即为-1;
在我们需要关注兄弟结点的情况中,我们长子域替换为右兄弟域,来表示兄弟关系。我们需要根据我们的运算需要来灵活的改变存储结构。
2,孩子表示法
我们考虑在结点中存储孩子结点,即在每个结点中都有多个指针域,其中每个指针指向一棵子树的根结点,这种存储方式叫做多重链表表示法。
在多重链表表示法中,对于开辟多少的指针域,是我们需要考虑的问题:
一,开辟定量的指针域
在定量的指针域中,采用树的度(最大结点的度,即有最多儿子结点的个数)作为目标,每个结点都开辟定量的指针域,但在这种存储方式中,存在问题就是浪费,但如果结点之间的度相差不大的情况中,这种缺点反而会成为优点。
二,开辟不定量的指针域
需要开辟一个额外的空间来存储结点指针域的个数,即结点的儿子结点的个数。这种办法可以克服空间上的浪费,对空间利用率极高,但由于每个链表的结构不同,增加了维护和操作的困难性。
改进上面二种,综合二种方案的优劣,我们采用了顺序存储的方式来存储结点,结点除了有数据域以外,还存储本结点的儿子结点链表的头指针,对于每一个结点都有一个儿子结点的单链表,可以很快的知道结点的儿子结点。但在这种方式中,我们不容易知道结点的双亲是谁,所以我们可以将双亲表示法和孩子表示法结合一下,得到双亲孩子表示法,可以很快的得到结点的双亲以及儿子结点。
3,孩子兄弟表示法
1,2二种方法研究的是双亲和儿子的情况,在3中我们研究的是兄弟结点,但如果我们只知道兄弟结点,我们是无法构造出树的结构的,所以,我们需要添加新的信息来构建整个树的情况。我们将孩子信息加入进去,来得到新的存储方式。在这种方式中,我们只需要第一个孩子,一个右兄弟即可定位数据,得到完整的树的结构。
在这种结构中,我们可以很快找到某结点的某个孩子结点,通过这个结点的第一个孩子结点的指针域,然后这个孩子结点的兄弟域来进行遍历,即可找到某个孩子结点。但在寻找双亲的时候存在困难,这个时候,我们可以通过增加一个双亲域即可解决问题。
孩子兄弟表示法中,最大的一个好处在于把一棵复杂的树变成了一颗二叉树。我们可以利用二叉树的独特的性质来解决问题。

二叉树
二叉树的定义:是n(n >= 0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和二棵互不相交的,分别成为根结点的左子树和右子树的二叉树来组成。
二叉树特点:
1,每个结点最多只有2棵子树。可以有0个子树,1个子树,2个子树。
2,左子树与右子树之间存在顺序,次序是不能颠倒的。
根据递归式的二叉树,其形态具有五种:空二叉树;只有一个根结点;根结点只有左子树;根结点只有右子树;根结点既有左子树又有右子树。
对于多个结点的多种形态的二叉树的种类,明确根结点,分清左右子树应该也是很容易就可以推出来的~
特殊的二叉树:
1,斜树,分为左,右斜树。对于斜树,可以说是线性表,也就是线性表是树的一种极其特殊的表现形式。
2,满二叉树,对于所有的分支结点都存在左子树和右子树,并且所有叶子都在同一层,这样的二叉树为满二叉树。对于满二叉树而言,叶子结点肯定在最下面一层,而且非叶子结点的度一定为2,在同样深度的二叉树中,满二叉树的叶子结点数目最多。
3,完全二叉树,在这个概念中,首先涉及到了编号的问题,就是从上到下,从左到右编号,如果对于二叉树的编号,与满二叉树的编号相同,但允许适当的放宽满二叉树的条件,这种二叉树称为了完全二叉树。在这种编号的方式中,我们是按照层序编号的,要依次来按层的顺序连续排列。
在完全二叉树中,叶子节点只可能出现在最后二层中,并且最下面的叶子节点一点是左部连续,倒数第二层有叶子节点,一定是右部连续,如果只有一个孩子结点,那么一定是左孩子结点,在同样结点的二叉树中,完全二叉树的深度最小。
二叉树的性质:
这里写图片描述
这里写图片描述
这里写图片描述
第三条性质证明的关键点:对于图而言,出度=入度
这里写图片描述
第四条证明根据完全二叉树与满二叉树之间的关系来得到。
这里写图片描述
第五条证明则是根据完全二叉树的层序编号来得到的性质,可以根据具体的例子来证明。
二叉树的存储结构:
1,顺序存储结构
利用一维数组来存储结点,并且结点的存储位置能够体现出结点之间的逻辑关系,比如亲子关系等。对于完全二叉树根据性质5比较好办,对于一般的二叉树而言,则可以在空着的位置加入空的结点元素也可以办到。但一般顺序存储都是存储的完全二叉树。
2,链式存储结构
对于每个结点有一个数据域,有二个指针域,这样的链表称为了二叉链表。如果需要寻找双亲时,可以再增加一个指针域来存储双亲的结点位置,此时称为了三叉链表。
二叉树的遍历:
1,前序遍历
(根-左-右)
2,中序遍历
(左-根-右)
3,后序遍历
(左-右-根)
4,层序遍历
(从上到下-从左到右)
①前序遍历算法
采用递归来实现(一说递归就应该想到栈~~)

void PreOrderTraverse(BiTree T)
{
   if (T == NULL)
   {
       return;
   }
   printf ("%c", T->data);
   PreOrderTraverse(T->lchild);
   PreOrderTraverse(T->rchild);
}

②中序遍历算法

void PreOrderTraverse(BiTree T)
{
   if (T == NULL)
   {
       return;
   }
   PreOrderTraverse(T->lchild);
   printf ("%c", T->data);
   PreOrderTraverse(T->rchild);
}

③后序遍历算法

void PreOrderTraverse(BiTree T)
{
   if (T == NULL)
   {
       return;
   }
   PreOrderTraverse(T->lchild);
   PreOrderTraverse(T->rchild);
   printf ("%c", T->data);
}

在二叉树遍历中,我们知道前序遍历和中序遍历是可以唯一确定一颗树;知道后序遍历和中序遍历也可以唯一确定一颗树;但我们如果只知道前序遍历和后序遍历,我们只能确定根结点,无法确定左右子树,所以不唯一。
前序遍历建立一颗二叉树(扩展的二叉树,即对没有儿子结点的结点赋值为#特殊符号)

void CreateBiTree(BiTree *T)
{
    TElemType ch;
    scanf("%c", &ch);
    if (ch == '#')
        *T = NULL;
    else
    {
        *T = (BiTree)malloc(sizeof(BiTree));
        if(!*T)
           exit(OVERFLOW!)
        (*T)->data = ch;
        CreateBiTree(&(*T)->lchild);
        CreateBiTree(&(*T)->rchild);
    }
}

中序遍历输入扩展二叉树的值建立二叉树

void CreateBiTree(BiTree *T)
{
    TElemType ch;
    scanf("%c", &ch);
    if (ch == '#')
        *T = NULL;
    else
    {
        *T = (BiTree)malloc(sizeof(BiTree));
        if(!*T)
           exit(OVERFLOW!)
        CreateBiTree(&(*T)->lchild);
        (*T)->data = ch;
        CreateBiTree(&(*T)->rchild);
    }
}

后序遍历输入扩展二叉树的值来得到二叉树

void CreateBiTree(BiTree *T)
{
    TElemType ch;
    scanf("%c", &ch);
    if (ch == '#')
        *T = NULL;
    else
    {
        *T = (BiTree)malloc(sizeof(BiTree));
        if(!*T)
           exit(OVERFLOW!)
        CreateBiTree(&(*T)->lchild);
        CreateBiTree(&(*T)->rchild);
        (*T)->data = ch;
    }
}

线索二叉树
线索二叉树的来源于很多指针域被空着,总希望他们存点什么东西,故将存储着指向某种遍历方式的前驱与后继的指针称为了线索,加上线索的二叉链表称为了线索链表,相应的二叉树称为了线索二叉树。
将结点的一个空指针指向前驱结点,将结点的另一个空指针指向后继结点。此时将二叉树以某种次序遍历使其变成线索二叉树的过程称为线索化。判断指针域到底是存储的是孩子结点还是前后驱,需要二个flag变量来说明。
中序遍历进行中序线索化

void InThreading(BiThrTree p)
{
    if (p)
    {
       InThreading(p->lchild);
       if (!p->lchild)
       {
          p->LTag = Thread;
          p->lchild = pre; 
       }
       if(!p -> rchild)
       {
         pre->RTag = Thread;
         pre->rchild = p;
       }
       pre = p;
       InThreading(p->rchild);
    }
}

为了更好的理解这部分的代码,我们通过一个实例来进行解释,来看数据的走向,判断怎么得到的一个双向链表的结构。
这里写图片描述
有了线索二叉树之后,我们对它进行遍历的时候可以看出,就是一个双向链表结构。在这个树中,我们在二叉线索链表上添加一个头结点,并令其lchild域中的指针指向二叉树的根结点,其rchild域中的指针指向中序遍历的最后一个结点。令中序遍历的第一个结点的lchild域中的指针以及中序遍历的最后一个结点的rchild域中指针指向头结点。

Status InOrderTraverse_Thr(BiThrTree T)
{
   BiThrTree p;
   p = T->lchild;
   while(p != T)
   {
       while(p->LTag == Link)
           p = p->lchild;
       printf("%c", p->data);
       while(p->RTag == Thread && p->rchild != T)
       {
          p = p->rchild;
          printf("%c", p->data);
       }
       p = p->rchild;
   }
   return OK;
}

树,森林与二叉树之间的转换
树转换为二叉树
1,在兄弟之间加线;2,只保留第一个孩子的连线,去掉其他孩子的连线;3,层次调整。经过加线,去线以及层次调整之后,可以得到一棵二叉树。
森林转换为二叉树
1,将每一棵树均变为二叉树;2,将后一棵树的根结点作为前一棵树根结点的右儿子。
二叉树转换为树
1,结点的左孩子的右孩子的右孩子等全变成自己的孩子;2去掉原有的连接右孩子的线,重新变为兄弟;3调整层次
二叉树转换为森林
如果二叉树的根结点有右结点,则他是一个森林,把右结点的部分提出来,再判断提出来的部分是否有右结点,如果对于根结点没有右结点,说明已经分散完成,需要将每棵二叉树都转变为树,最终可以形成森林。
二叉树的遍历有:前序遍历,中序遍历,后序遍历,层序遍历
森林的遍历有:前序遍历和后续遍历(都是对于森林中的每棵树的遍历,遍历完一颗树就遍历下一棵树)
【关系】对于森林以及由森林转变的二叉树而言,森林的前序遍历与其对应的二叉树的前序遍历所对应;森林的后序遍历与其对应的二叉树的中序遍历结果相同。

哈夫曼树(树叶带权值的二叉树)
从树中的一个结点到另一个结点之间的分支构成二个结点之间的路径,路径上的分支数目称为路径长度。树的路径长度是从树根到每一结点的路径长度之和。如果结点带权值,则结点的带权路径是结点到树根之间的路径长度乘以结点上的权值。树的带权路径长度为树中所有的叶子结点的带权路径长度之和。此时哈夫曼树是带权路径长度WPL最小的二叉树。
求哈夫曼树的步骤:
1,带权叶子结点的排序(从小到大)
2,取较小的二个结点作为孩子结点生成一个父结点,(权值从小到大,从左到右),父结点的权值是二个孩子结点权值之和,父结点代替二个孩子结点,加入到排序中。
3,重复2的操作,最终所有的叶子结点都用完,即完成最终的操作。
哈夫曼编码
利用了不等长的编码特性实现压缩编码,利用前缀编码的特性来实现解码。
对于编码的字符集{d1,d2,d3,d4…dn},对于字符出现的次数或频率集合{w1,w2,w3,w4…wn},字符集作为叶子结点,字符出现的次数来作为对应叶子结点的权值,此时构造出来的二叉树为哈夫曼树,每个字符的编码为哈夫曼编码,对于哈夫曼码而言,满足前缀编码的条件,即任何的编码都不是其他码的前缀,也就是我们可以解码出来,并且哈夫曼码是最优的不等长的编码,效率比较高。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值