树和森林
树与二叉树的转换
把树转化为二叉树进行处理,利用二叉树的算法来实现对树地操作
由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表做媒介导出树与二叉树之间的一个对应关系
树转化为二叉树步骤:
①**加线:在兄弟之间加一连线
②抹线:对每个结点,除了左孩子外,去除其与其余孩子之间的关系
③旋转:以树的根结点为轴心,将整树顺时针旋转45°
树变二叉树:兄弟相连留长子
二叉树转化为树步骤:
①加线:若p结点是双亲结点的左孩子,则将p的右孩子、右孩子的右孩子…沿分支找到所有右孩子,都与p的双亲用线连起来
②抹线:抹掉原二叉树中双亲与右孩子之间的连线
③调整:**将结点按层次排列,形成树结构
二叉树变树:左孩右右连双亲,去掉原来右孩线
森林与二叉树的转化
森林转换为二叉树(二叉树与多棵树之间的关系):
①将各棵树分别转换为二叉树
②将每棵树的根结点用线相连
③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
森林变二叉树:树变二叉根相连
二叉树转换为森林:
①**抹线:将二叉树中根结点与其右孩子连线,及沿右分支所搜到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
②还原:**将孤立的二叉树还原成树
二叉树变森林:去掉全部右孩线,孤立二叉再还原
树和森林的遍历
树的遍历(三种方式)
- 先根(次序)遍历:若树不空,则先访问根结点,然后依次先根遍历各子树
- 后根(次序)遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点
- 层次遍历:若树不空,则自上而下从左至右访问树中每个结点
森林的遍历
将森林看作由三部分构成:
- 森林中第一棵树的根结点
- 森林中第一棵树的子树森林
- 森林中其他树构成的森林
- 先序遍历:若森林不空,则①访问第一棵树的根结点;②先序遍历森林中第一棵树的子树森林;③先序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行先根遍历 - 中根遍历:若森林不空,则①中序遍历森林中第一棵树的子树森林;②访问第一棵树的根结点;③中序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行后根遍历
哈夫曼树
引子
基本概念
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
- 结点的路径长度:两结点间路径上的分支数
- 树的路径长度:从树根到每一个结点的路径长度之和,记作TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树,但是路径最短的二叉树不一定是完全二叉树,如b的H和I作为E的孩子时 - 权:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树中所有叶子节点的带权路径长度之和,记作WPL
- 哈夫曼树:最优树,带权路径长度(WPL)最短的树
“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树等 - 哈夫曼树:最优二叉树,带权路径长的(WPL)最短的二叉树,构造哈夫曼树的算法叫做哈夫曼算法
由第一个图可知:满二叉树不一定是最优二叉树;
哈夫曼树中权较大的叶子离根越近;
具有相同带权结点的哈夫曼树不唯一
哈夫曼树的构造算法
哈夫曼算法:构造哈夫曼树的步骤
算法实现
顺序存储结构——一维结构数组
- 定义
typedef struct {
int weight;
int parent, lch, rch;
}HTNode, *Huffman Tree;
初始化
- 初始化HT[1…2n-1]:lch = rch = parent = 0
- 输入初始n个叶子结点:置HT[1…n]的weight值
void CreatHuffmanTree(Huffman Tree HT, int n) {
if (n <= 1) return;
m = 2*n-1; //数组共2n-1个元素
HT = (Huffman Tree)malloc(sizeof(HTNode)*(m+1)); //0号单元未用,HT[m]表示根结点
for (i = 1; i <= m; i++) {
HT[i].lch = 0; HT[i].rch = 0; HT[i].parent = 0;
}
for (i = 1; i <= n; i++)
scanf("%d", &HT[i].weight);
}
构造产生新结点
进行n-1次合并,依次产生n-1个结点HT[i],i = n+1…2n-1
①在HT[1…i-1]中选两个未被选过的(从parent==0的结点中选)的weight最小的两个结点HT[s1]和HT[s2],s1、s2为两个最小结点的下标
②修改HT[s1]和HT[s2]的parent的值:HT[s1].parent = i;HT[s2].parent=i
③修改新产生的HT[i]:HT[i].weight = HT[s1].weight + HT[s2].weight; HT[i].lch = s1; HT[i].rch = s2
for (i = n+1; i <= m; i++) { //合并产生n-1个结点——构造Huffman树
Select(HT, i-1, s1, s2); //在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i; HT[s2].parent = i; //表示从F中删除s1和s2
HT[i].lch = s1; HT[i].rch = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
哈夫曼编码
左分支0,右分支1
保证是前缀码:因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码不可能是其它叶结点编码的前缀
保证是总长最短:因为哈夫曼树就是带权路径长度最短的,因此字符编码的总长最短
性质:
- 哈夫曼编码是前缀码
- 哈夫曼编码是最优前缀码
算法实现:
void CreatHuffmanCode (HuffmanTree HT, HaffumanCode &HC, int n) {
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
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].lchild == c) cd[start] = '0';
else cd[start] = '1';
c = f; f = HT[i].parent; //继续向上回溯
}
HC[i] = new char[n-start]; //为第i个字符串编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}//CreatHuffmanCode