树和森林(包括哈夫曼树)的概念及算法实现

1、树:n个结点的有限集,n=0时称为空树

n>0时:有且仅有一个特定的称为结点的根、其余结点可以分为m个互不相交的有限集

森林:是m棵互不相交的树的集合

2、树的存储结构

(1)双亲表示法:定义结构数组,存放树的结点,每个结点含两个域:数据域和双亲域

数据域:存放结点本身信息

双亲域:指示本结点的双亲结点在数组中的位置

特点:找双亲容易,找孩子难

类型描述

typedef structure PTNode{
  TElemType data;
  int parent;  //双亲位置域
}PTNode;

树结构:

#define MAX_TREE_SIZE 100
typedef struct{
  PTNode nodes[MAX_TREE_SIZE};
  int r,n;  //根结点的位置和结点个数
}PTree;

(2)孩子链表

把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储。

n个结点有n个孩子链表(叶子结点的孩子链表为空表),n个头指针又组成一个线性表,用顺序表存储

typedef struct CTNode{
  int child;
  struct CTNode *next;
}*ChildPtr;
typedef struct{
  TElemType data;
  ChildPtr first child; //孩子链表头指针
}CTBox;

  

树结构:

typedef struct{
  CTBox nodes[MAX_TREE_SIZE];
 int n,r;   //结点数和根结点的位置
}CTree;

带双亲的孩子链表如图:

(3)孩子兄弟表示法(二叉树表示法、二叉链表表示法):用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向第一个孩子结点和下一个兄弟结点。
typedef struct CSNode{
  ElemType data;
  struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;

3、树域二叉树的转换

由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以到导出树和二叉树的一个对应关系。

(1)将树转换成二叉树:兄弟相连留长子

a)加线:在兄弟间加一条线。b)抹线:对每个结点,除了左孩子,去除其与其余孩子之间的关系

c)旋转:以树的根结点为轴心,将整树顺时针旋转45度

(2)将二叉树转换成树:左孩右右连双亲,去掉原来右孩线
a)加线:若p结点是双亲结点的左孩,即将p的右孩子,右孩子的右孩子。。。沿分支找到的所有右孩子,都与p的双亲用线连起来
b)抹线:抹掉原二叉树中双亲与右孩子之间的连线
c)调整:将结点按层次排列,形成树结构

4、森林与二叉树的转换
(1)森林转换成二叉树(二叉树与多棵树之间的关系):森林变二叉树,树变二叉根相连
a)将各棵树分别转换成二叉树
b)将每棵树的根结点用线相连
c)以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树形结构

(2)二叉树转换成森林:去掉全部右孩线,孤立二叉再还原
a)抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间的连线全部抹掉,使之成为孤立的二叉树
b)还原:将孤立的二叉树还原为树

5、树的遍历(3种)
(1)先根遍历
若树不空,现访问根结点,然后依次先根遍历各棵子树
(2)后根遍历
若树不空,则先依次后根遍历各棵子树,然后访问根结点
(3)按层次遍历:
若树不空,则自上而下从左至右访问树中每个结点
6、森林的遍历
***将森林看成三个部分:
(1)森林中第一颗树的根结点
(2)森林中第一颗树的子树森林
(3)森林中其他树构成的森林
(1)先序遍历
若森林不空,则:
a)访问森林中第一棵树的根结点 
b)先序遍历森林中第一棵树的子树森林
c)先序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林的每一棵树进行先根遍历
(2)中序遍历
7、哈夫曼树的基本概念
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
结点的路径长度:两结点间路径上的分支数
树的路径长度:从树根到每个结点的路径长度之和(结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树)
权:将树中结点赋给一个有着某种含义的数值,则这个数值称为结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和
哈夫曼树:最优树(在度相同的树中带权路径长度最短的树)
注意点:
(1)满二叉树不一定是哈夫曼树(2)哈夫曼树中权越大的叶子离根越近 (3)具有相同带权结点的哈夫曼树不唯一
*贪心算法:构造哈夫曼树时首先选择权制小的叶子结点
8、哈夫曼树的构造算法
(1)构造哈夫曼树的方法:
构造森林全是根、选用两小造新树、删除两小添新人、重复23剩单根
具体为:
(2)包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个结点
包含n个叶子结点的哈夫曼树共有2n-1个结点
哈夫曼树的结点的度数为0或2,没有度数为1的结点。
eg:
9、哈夫曼树构造算法的实现
(1)采用顺序存储结构:一维结构数组 HuffmanTree H;
结点类型定义
typedef struct{
  int weight;
  int parent,lch,rch;
}HTNode,*HuffmanTree;

哈夫曼树共有2n-1个结点,不使用0下标,数组大小为2n

(2)具体算法

void CreateHuffmanTree(HuffmanTree HT,int n){
  if(n<=1) return;
  m=2*n-1;  //数组中共有m个元素
  HT=new HTNode[m+1];     //0号单元未用,HT[m]表示根结点
  for(I=1;i<=m;++i){.  //将2n-1个元素的lcc\rch\parent置为0
    HT[I].lch=0;
    HT[I].rch=0;
    HT[I].parent=0;
  }
  for(I=1;i<=n;++I)
  cin>>HT[i].weight;    //输入前n个元素的weight值
  //初始化结束
  
  for(I=n+1;i<=m;i++){.      //合并产生n-1个结点--构造哈夫曼树
    Select(HT,i-1,s1,s2).    //在HT[k]中选择两个其双亲域为0且权值最小的结点,并返回他们在HT中的序号s1s2
    HT[s1].parent=I;
    HT[s2].parent=I;    //分别从F中删除s1s2
    HT[I].lch=s1;
    HT[I].rch=s2;    //s1s2分别作为I的左右孩子
    HT[I].weight=HT[s1].weight+HT[s2].weight;  //I的权值为左右孩子权值之和
  }
}

10、哈夫曼编码

若将编码设计为长度不等的二进制编码,即让待穿传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。

关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀

在哈夫曼树的每个分支上标0或1:结点左分支标0,右分支标1,把从根到每个叶子接的路径上的标号连接起来,作为该叶子代表的字符的编码。

两个问题:

***从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中

void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){
  HC=new char *[n+1];      //分配n个字符编码的头指针矢量
  cd=new char [n];         //分配临时存放编码的动态数组空间
  cd[n-1]='\0';            //编码结束符
  for(I=1;i<=n;++i){.      //逐个字符求哈夫曼编码
    start=n-1; c=I; f=HT[I].parent;
    while(f!=0){.          //从叶子结点开始向上回溯,知道根结点
      --start;             //回溯一次start向前指一个位置
      if(HT[f].child==c)  cd[start]='0';   //结点c是f的左孩子,则生成代码0
      else
      cd[start]='1';   //结点c是f的右孩子,则生成代码
      c=f;f=HT[f].parent;        //继续向上回溯
    }           //求出第i个字符的编码
    HC[I]=new char [n-start];     //为第I个字符串编码分配空间
    strcpy(HC[I],&cd[start]);     //将求得的编码从临时空间cd复制到HC的当前行中
  }
  delete cd;     //释放临时空间
}

11、文件的编码和解码

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值