树与二叉树,哈夫曼

树的概念与定义

1.树的概念*

树是n(n≥0)个结点的有限集合T。当n=0时,称
为空树;当n>0时,该集合满足如下条 件:
(1) 其中必有一个称为根(root)的特定结点,它没
有直接前驱,但有零个或多个直接后继。
(2) 其余n-1个结点可以划分成m(m≥0)个互不相
交的有限集T1,T2,T3,…,Tm,其中Ti又是一棵树,
称为根root的子树。每棵子树的根结点有且仅有一
个直接前驱,但有零个或多个直接后继。

**

2.有关树的一些术语:**

1.结点:包含一个数据元素及若干指向其它结点的分
支信息。
2.树的度—— 一棵树中最大的结点度数
3.双亲—— 孩子结点的上层结点叫该结点的双亲
4.兄弟—— 同一双亲的孩子之间互成为兄弟
5.祖先—— 结点的祖先是从根到该结点所经分支上的所有结点
6.子孙—— 以某结点为根的子树中的任一结点都成为该结点的子孙
7.结点的层次—— 从根结点算起,根为第一层,它的孩子为第二层……
8.堂兄弟—— 其双亲在同一层的结点互称为堂兄弟。
9.深度—— 树中结点的最大层次数
10.有序树—— 如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
11.森林—— m(m0)棵互不相交的树的集合
在这里插入图片描述如图
BCD是A的孩子节点,树的深度是4

二叉树

1.二叉树的定义与基本操作

➢定义:我们把满足以下两个条件的树型结构叫做
二叉树(Binary Tree):
(1)每个结点的度都不大于2;
(2)每个结点的孩子结点次序不能任意颠倒。
➢二叉树的五种基本形态:
在这里插入图片描述2.二叉树的基本操作:
(1)Initiate(bt)
(2) Create(bt)
(3) Destory(bt)
(4) Empty(bt)
(5) Root(bt)
(6) Parent(bt,x)
(7) LeftChild(bt,x)
(8) RightChild(bt,x)
(9) Traverse(bt): 遍历操作。按某个次序
依次访问二叉树中每个结点一次且仅一
次。
(10) Clear(bt)

3.二叉树的性质

性质1 在二叉树的第 i 层上至多有 2^(i-1)个 结点(i>=1)

用数学归纳法证明:
归纳基础:i=1时,有2^(i-1)=2^0=1。因为第1层上只有一个根结点,所以命题成立。
归纳假设:假设对所有的 j ( 1<=j < i ) 命题成立,即第j层上至多有 2^(j-1) 个结点,证明j=i时命题亦成立。
归纳步骤:根据归纳假设,第 i-1 层上至多有2^(i-2)个结点。由于二叉树的每个结点至多有两个孩子,故第 i 层上的结点数至多是第 i-1 层上的最大结点数的2倍。即 j=i 时,该层上至多有2×2^(i-2)=2^(i-1)个结点,故命题成立。

性质2 深度为k的二叉树至多有2^k-1个结点(k≥1)。

至多,即视之为满二叉树
证明:计算等比数列 2^0+2^1+…+2^(k-1)=2^k-1

性质3 在任意-棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则no=n2+1。

回顾 m叉树 的性质
1. 设m叉树中,度数为 i 的结点树为 Ni, 则总结点数为: N = N0 + N1 + … + Nm;
2. N = 分支数 + 1 , 1 为根结点
3. 于是 N = N0 + N1 + …+ Nm = 0*(N0) + 1*(N1) + … + m*(Nm) + 1
4. 对应于现在所讨论的二叉树,于是有 N = N0 + N1 + N2 = N1 + 2*(N2) + 1,于是等到结论 N0 = N2 + 1

详细证明

证明:因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)应等于0度结点数、1度结点(记为n1)和2度结点数之和:
n=no+n1+n2 (式子1)
  另一方面,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:
nl+2n2
  树中只有根结点不是任何结点的孩子,故二叉树中的结点总数又可表示为:
n=n1+2n2+1 (式子2)
  由式子1和式子2得到:
no=n2+1

4.两种特殊的二叉树

满二叉树,每层结点都是满的,即每层结点都具有
最大结点数。
在这里插入图片描述*完全二叉树
*在这里插入图片描述

5.二叉树的存储结构

1.二叉树的存储结构有两种:顺序存储结构和链式
存储结构。
2.二叉树的结构是非线性的,每一结点最多可有
两个后继。
.顺序存储结构:是用一组连续的存储单元来存
放二叉树的数据元素 。如

在这里插入图片描述
链式存储结构:对于任意的二叉树来说,每个结点最多有两个孩子,一个双亲结点。
在这里插入图片描述
二叉树的二叉链表节点的结构

typedef struct Node
{ DataType data;
struct Node *LChild;
struct Node *RChild;
} BiTNode, *BiTree; 

6.二叉树的遍历与线索化

➢二叉树的遍历:指按一定规律对二叉树中的每个
结点进行访问且仅访问一次。
➢二叉树的基本结构由根结点、左子树和右子树组成
在这里插入图片描述用L、D、R分别表示遍历左子树、访问根结
点、遍历右子树,那么对二叉树的遍历顺序
就可以有:
①DLR ②LDR ③LRD
④DRL ⑤RDL ⑥RLD
➢先序、中序、后序遍历是递归定义的,即在其子
树中亦按上述规律进行遍历。
*

*三种遍历方法的递归定义

:**
◆先序遍历(DLR)操作过程
若二叉树为空,则空操作,否则依次执行如下操
作:
(1)访问根结点;
(2)按先序遍历左子树;
(3)按先序遍历右子树。
中序遍历(LDR)操作过程
若二叉树为空,则空操作,否则依次执行如下操作:
(1)按中序遍历左子树;
(2)访问根结点;
(3)按中序遍历右子树。
后序遍历(LRD)操作过程:
若二叉树为空,则空操作,否则依次执行如下操作:
(1)按后序遍历左子树;
(2)按后序遍历右子树;
(3)访问根结点。
如下图的二叉树,其先序、中序、后序遍历的序列
为:

在这里插入图片描述
先序遍历: A、B、D、F、G、C、E、H 。
中序遍历: B、F、D、G、A、C、E、H 。
后序遍历: F、G、D、B、H、E、C、A 。
1) 先序遍历算法

void PreOrder(BiTree root)
/*先序遍历二叉树, root为指向二叉树(或某一子树)
根结点的指针*/
{if(root!=NULL)
{ Visit(root ->data); /*访问根结点*/
PreOrder(root ->LChild);/*先序遍历左子树*/
PreOrder(root ->RChild);/*先序遍历右子树*/
}
} 

2) 中序遍历

void InOrder(BiTree root)
/*中序遍历二叉树, root为指向二叉树(或某一子树)
根结点的指针*/
{if(root!=NULL)
{
InOrder(root->LChild); /*中序遍历左子树*/
Visit(root->data); /*访问根结点*/
InOrder(root->RChild); /*中序遍历右子树*/
}
} 


3) 后序遍历

void PostOrder(BiTree root)
{if(root!=NULL)
{
PostOrder(root ->LChild);/*后序遍历左子树*/
PostOrder(root ->RChild);/*后序遍历右子树*/
Visit(root ->data); /*访问根结点*/
}
} 

➢以中序遍历为例来说明中序遍历二叉树的递归过程
在这里插入图片描述

*二叉树遍历算法应用

  **1.前序遍历二叉树:**
    (1)若二叉树为空,则为空操作,返回空。
    (2)访问根结点。
    (3)前序遍历左子树。
    (4)前序遍历右子树。

  a.二叉树前序遍历的递归算法:
 void PreOrderTraverse(BiTree BT)
   {
     if(BT)
     {
        printf("%c",BT->data);              //访问根结点
        PreOrderTraverse(BT->lchild);       //前序遍历左子树
        PreOrderTraverse(BT->rchild);       //前序遍历右子树
     }
   }
b.使用栈存储每个结点右子树的二叉树前序遍历的非递归算法:
  (1)当树为空时,将指针p指向根结点,p为当前结点指针。
  (2)先访问当前结点p,并将p压入栈S中。
  (3)令p指向其左孩子。
  (4)重复执行步骤(2)、(3),直到p为空为止。
  (5)从栈S中弹出栈顶元素,将p指向此元素的右孩子。
  (6)重复执行步骤(2)~(5),直到p为空并且栈S也为空。
  (7)遍历结束。
  使用栈的前序遍历的非递归算法:
void PreOrderNoRec(BiTree BT)
  {
    stack S;
    BiTree p=BT->root;
    while((NULL!=p)||!StackEmpty(S))
    {
      if(NULL!=p)
      {
        printf("%c",p->data);
        Push(S,p);
        p=p->lchild;
      }
      else
      {
        p=Top(S);
        Pop(S);
        p=p->rchild;
      }
    }
  }
c.使用二叉链表存储的二叉树前序遍历非递归算法:
 void PreOrder(pBinTreeNode pbnode)
    {
      pBinTreeNode stack[100];
      pBinTreeNode p;
      int top;
      top=0;
      p=pbnode;
      do
      {
        while(p!=NULL)
        {
          printf("%d\n",p->data);      //访问结点p
          top=top+1;
          stack[top]=p;
          p=p->llink;                  //继续搜索结点p的左子树
        }
        if(top!=0)
        {
          p=stack[top];
          top=top-1;
          p=p->rlink;                  //继续搜索结点p的右子树
        }
      }while((top!=0)||(p!=NULL));
    }
2.中序遍历二叉树:
  (1)若二叉树为空,则为空操作,返回空。
  (2)中序遍历左子树。
  (3)访问根结点。
  (4)中序遍历右子树。
  a.二叉树中序遍历的递归算法:
  void InOrderTraverse(BiTree BT)
    {
      if(BT)
      {
         InOrderTraverse(BT->lchild);        //中序遍历左子树
         printf("%c",BT->data);              //访问根结点
         InOrderTraverse(BT->rchild);        //中序遍历右子树
      }
    }
  b.使用栈存储的二叉树中序遍历的非递归算法:
   (1)当树为空时,将指针p指向根结点,p为当前结点指针。
   (2)将p压入栈S中,并令p指向其左孩子。
   (3)重复执行步骤(2),直到p为空。
   (4)从栈S中弹出栈顶元素,将p指向此元素。
   (5)访问当前结点p,并将p指向其右孩子。
   (6)重复执行步骤(2)~(5),直到p为空并且栈S也为空。
   (7)遍历结束。
     使用栈的中序遍历的非递归算法:
  void IneOrderNoRec(BiTree BT)
     {
       stack S;
       BiTree p=BT->root;
       while((NULL!=p)||!StackEmpty(S))
       {
         if(NULL!=p)
         {
           Push(S,p);
           p=p->lchild;
         }
         else
         {
           p=Top(S);
           Pop(S);
           printf("%c",p->data);
           p=p->rchild;
         }
       }
     }
   c.使用二叉链表存储的二叉树中序遍历非递归算法:
  void InOrder(pBinTreeNode pbnode)
    {
         pBinTreeNode stack[100];
         pBinTreeNode p;
         int top;
         top=0;
         p=pbnode;
         do
         {
           while(p!=NULL)
           {
             top=top+1;
             stack[top]=p;                //结点p进栈
             p=p->llink;                  //继续搜索结点p的左子树
           }
           if(top!=0)
           {
             p=stack[top];                //结点p出栈
             top=top-1;
             printf("%d\n",p->data);      //访问结点p
             p=p->rlink;                  //继续搜索结点p的右子树
           }
         }while((top!=0)||(p!=NULL));
    }
3.后序遍历二叉树:
  (1)若二叉树为空,则为空操作,返回空。
  (2)后序遍历左子树。
  (3)后序遍历右子树。
  (4)访问根结点。
  a.二叉树后序遍历的递归算法:
void PostOrderTraverse(BiTree BT)
 {
   if(BT)
   {
      PostOrderTraverse(BT->lchild);        //后序遍历左子树
      PostOrderTraverse(BT->rchild);        //后序遍历右子树
      printf("%c",BT->data);                //访问根结点
   }
 }
  b.使用栈存储的二叉树后序遍历的非递归算法:
  算法思想:首先扫描根结点的所有左结点并入栈,然后出栈一个结点,扫描该结点的右结点并入栈,再扫描该右结点的所有左结点并入栈,当一个结点的左、右子树均被访问后再访问该结点。因为在递归算法中,左子树和右子树都进行了返回,因此为了区分这两种情况,还需要设置一个标识栈tag,当tag的栈顶元素为0时表示从左子树返回,为1表示从右子树返回。
   (1)当树为空时,将指针p指向根结点,p为当前结点指针。
   (2)将p压入栈S中,0压入栈tag中,并令p指向其左孩子。
   (3)重复执行步骤(2),直到p为空。
   (4)如果tag栈中的栈顶元素为1,跳至步骤(6)。
   (5)如果tag栈中的栈顶元素为0,跳至步骤(7)。
   (6)将栈S的栈顶元素弹出,并访问此结点,跳至步骤(8)。
   (7)将p指向栈S的栈顶元素的右孩子。
   (8)重复执行步骤(2)~(7),直到p为空并且栈S也为空。
   (9)遍历结束。
    使用栈的后序遍历非递归算法:
  void PostOrderNoRec(BiTree BT)
   {
     stack S;
     stack tag;
     BiTree p=BT->root;
     while((NULL!=p)||!StackEmpty(S))
     {
       while(NULL!=p)
       {
         Push(S,p);
         Push(tag,0);
         p=p->lchild;
       }
       if(!StackEmpty(S))
       {
         if(Pop(tag)==1)
         {
           p=Top(S);
           Pop(S);
           printf("%c",p->data);
           Pop(tag);    //栈tag要与栈S同步
         }
         else
         {
           p=Top(S);
           if(!StackEmpty(S))
           {
             p=p->rchild;
             Pop(tag);
             Push(tag,1);
           }
         }
       }
     }
   }
 c.使用二叉链表存储的二叉树后序遍历非递归算法:

void PosOrder(pBinTreeNode pbnode)
{
pBinTreeNode stack[100]; //结点的指针栈
int count[100]; //记录结点进栈次数的数组
pBinTreeNode p;
int top;
top=0;
p=pbnode;
do
{
while(p!=NULL)
{
top=top+1;
stack[top]=p; //结点p首次进栈
count[top]=0;
p=p->llink; //继续搜索结点p的左子树
}
p=stack[top]; //结点p出栈
top=top-1;
if(count[top+1]==0)
{
top=top+1;
stack[top]=p; //结点p首次进栈
count[top]=1;
p=p->rlink; //继续搜索结点p的右子树
}
else
{
printf("%d\n",p->data); //访问结点p
p=NULL;
}
}while((top>0));
}
B 线索化二叉树:
线索化二叉树的结点结构图:

   线索化二叉树的结点类型说明:
 typedef struct node
  {
    DataType data;
    struct node *lchild, *rchild;       //左、右孩子指针
    int ltag, rtag;                     //左、右线索
  }TBinTNode;         //结点类型
  typedef TBinTNode *TBinTree;
    在线索化二叉树中,一个结点是叶子结点的充分必要条件是其左、右标志均为1.
 中序线索化二叉树及其对应的线索链表如下图:
                   
  (1)中序线索化二叉树的算法:
  void InOrderThreading(TBinTree p)
  {
    if(p)
    {
      InOrderThreading(p->lchild);   //左子树线索化
      if(p->lchild)
        p->ltag=0;
      else
        p->ltag=1;
      if(p->rchild)
        p->rtag=0;
      else
        p->rtag=1;
      if(*(pre))      //若*p的前驱*pre存在
      {
        if(pre->rtag==1)
          pre->rchild=p;
        if(p->ltag==1)
          p->lchild=pre;
      }
      pre=p;                         //另pre是下一访问结点的中序前驱
      InOrderThreading(p->rchild);   //右子树线索化
    }
  }
(2)在中序线索化二叉树下,结点p的后继结点有以下两种情况:
  ①结点p的右子树为空,那么p的右孩子指针域为右线索,直接指向结点p的后继结点。
  ②结点p的右子树不为空,那么根据中序遍历算法,p的后继必是其右子树中第1个遍历到的结点。
  
 中序线索化二叉树求后继结点的算法:
 TBinTNode *InOrderSuc(BiThrTree p)
    {
       TBinTNode *q;
       if(p->rtag==1)   //第①情况
         return p->rchild;
       else            //第②情况
       {
         q=p->rchild;
         while(q->ltag==0)
           q=q->lchild;
         return q;
       }
    }

      中序线索化二叉树求前驱结点的算法:

    TBinTNode *InOrderPre(BiThrTree p)
    {
       TBinTNode *q;
       if(p->ltag==1)
         return p->lchild;
       else
       {
         q=p->lchild;         //从*p的左孩子开始查找
         while(q->rtag==0)
           q=q->rchild;
         return q;
       }
    }
 (3)遍历中序线索化二叉树的算法
 void TraversInOrderThrTree(BiThrTree p)
    {
      if(p)
      {
        while(p->ltag==0)
          p=p->lchild;
        while(p)
        {
          printf("%c",p->data);
          p=InOrderSuc(p);
        }
      }
    }

**将一棵树转换为二叉树的方法:

**
❖树中所有相邻兄弟之间加一条连线。
树转换为二叉树的例子:
❖对树中的每个结点,只保留其与第一个孩子结点
之间的连线,删去其与其它孩子结点之间的连线。
❖以树的根结点为轴心,将整棵树顺时针旋转一
定的角度,使之结构层次分明。
在这里插入图片描述
结论:
➢树中的任意一个结点都对应于二叉树中的一个结
点。
➢树中某结点的第一个孩子在二叉树中是相应结点
的左孩子,树中某结点的右兄弟结点在二叉树中是
相应结点的右孩子。
➢树的根结点没有兄弟,所以变换后的二叉树的根
结点的右孩子必然为空。

树与二叉树的对应关系及转换方法

在这里插入图片描述

哈夫曼树

基本概念:
路径:指从一个结点到另一个结点之间的分支序列。
路径长度:指从一个结点到另一个结点所经过的分支
数目。
结点的权:给树的每个结点赋予一个具有某种实际意
义的实数,我们称该实数为这个结点的权。
带权路径长度:在树形结构中,我们把从树根到某
一结点的路径长度与该结点的权的乘积,叫做该结
点的带权路径长度。
树的带权路径长度:为树中所有叶子结点的带权路
径长度之和,通常记为: WPL= wi×li
i=1
n
其中n为叶子结点的个数, wi为第i个叶子结点的
权值,li为第i个叶子结点的路径长度。
例如下图所示的具有不同带权路径长度的二叉树
WPL(a)=7×2+5×2+2×2+4×2=36
WPL(b)=4×2+7×3+5×3+2×1=46
WPL©=7×1+5×2+2×3+4×3=35
在这里插入图片描述
哈夫曼树
➢哈夫曼树又叫最优二叉树,它是由n个带权叶子结
点构成的所有二叉树中带权路径长度WPL最短的二
叉树。

构造哈夫曼算法的步骤:

(1)用给定的n个权值{w1
,w2
, … ,wn
}对应的n个
结点构成n棵二叉树的森林F={T1
,T2
, …,Tn
},其中
每一棵二叉树Ti (1≤i≤n)都只有一个权值为wi的
根结点,其左、右子树为空。
(2)在森林F中选择两棵根结点权值最小的二叉树,
作为一棵新二叉树的左、右子树,标记新二叉树的
根结点权值为其左右子树的根结点权值之和。
(3)从F中删除被选中的那两棵二叉树,同时把新
构成的二叉树加入到森林F中。
(4)重复(2)、(3)操作,直到森林中只含有
一棵二叉树为止,此时得到的这棵二叉树就是哈夫
曼树。
例如
有一份电文中共使用了5个字符:a,b,c,d,e,它们的出现频率一次为4、7、5、2、9,试画出对应的哈夫曼树,并求出每个字母的哈夫曼编码。
27
/
11 16
/ \ /
5 6 7 9
/
2 4
a、b、c、d、e的哈夫曼编码依次为011、10、00、010、11

哈夫曼编码

➢哈夫曼树最典型的应用是在编码技术上的应用。
利用哈夫曼树,我们可以得到平均长度最短的编码。
在这里插入图片描述
使用变长编码虽然可以使得程序的总位数达到最
小,但机器却无法解码。
➢如对编码串0010110该怎样识别呢?

**前缀编码

:任意一个编码不能成为其它任意编码的前缀。
我们可以设计出最优的前缀编码。
首先以每条指令的使用频率为权值构造哈夫曼树。
在这里插入图片描述
例如**:传送数据
state,seat,act,tea,cat,set,a,eat,如何使传
送的长度最短?
➢先看字符出现的次数,然后将出现次数当作权。
➢规定二叉树的构造为左走0,右走1
在这里插入图片描述
构造满足哈夫曼编码的最短最优性质:
(1)若di≠dj(字母不同),则对应的树叶不同。
因此前缀码(任一字符的编码都不是另一个字符编
码 )不同,一个路径不可能是其他路径的一部分,
所以字母之间可以完全区别。
(2)将所有字符变成二进制的哈夫曼编码,使带
权路径长度最短,相当总的通路长度最短。
哈夫曼编码算法的实现
➢由于哈夫曼树中没有度为1的结点,则一棵有n个
叶子的哈夫曼树共有2×n-1个结点,可用一个大小
为2×n-1的一维数组来存放各个结点。
➢每个结点同时还包含其双亲信息和孩子结点信息,
所以构成一个静态三叉链表。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值