树
- 树
-
- [1.2 结点的分类](#1.2 结点的分类)
- [1.3 结点间的关系](#1.3 结点间的关系)
- [1.4 树的其他概念](#1.4 树的其他概念)
- [1.5 树的性质](#1.5 树的性质)
- [2. 二叉树](#2. 二叉树)
-
- [2.1 满二叉树](#2.1 满二叉树)
- [2.2 完全二叉树](#2.2 完全二叉树)
- [2.3 二叉排序树(二叉查找树)](#2.3 二叉排序树(二叉查找树))
- [3. 二叉树的存储结构](#3. 二叉树的存储结构)
-
- [3.1 二叉树顺序存储结构](#3.1 二叉树顺序存储结构)
- [3.2 二叉树的链式存储结构](#3.2 二叉树的链式存储结构)
- [4. 二叉树的遍历](#4. 二叉树的遍历)
-
- [4.1 层次遍历](#4.1 层次遍历)
- [4.1 前序遍历](#4.1 前序遍历)
- [4.2 中序遍历](#4.2 中序遍历)
- [4.3 后序遍历](#4.3 后序遍历)
- [5. 线索二叉树](#5. 线索二叉树)
-
- [5.1 线索二叉树的数据结构](#5.1 线索二叉树的数据结构)
- [5.2 线索二叉树求前驱和后继](#5.2 线索二叉树求前驱和后继)
- [6. 二叉排序树](#6. 二叉排序树)
-
- [6.1 二叉排序树的插入](#6.1 二叉排序树的插入)
- [6.2 创建二叉排序树](#6.2 创建二叉排序树)
- [6.3 二叉排序树的查找](#6.3 二叉排序树的查找)
- [6.4 二叉排序树的遍历](#6.4 二叉排序树的遍历)
- [6.5 删除二叉排序树的结点](#6.5 删除二叉排序树的结点)
- [6.6 二叉排序树的查找效率](#6.6 二叉排序树的查找效率)
- [6.7 完整实现](#6.7 完整实现)
- [7. 哈夫曼树(最优二叉树)](#7. 哈夫曼树(最优二叉树))
-
- [7.1 构造哈夫曼树](#7.1 构造哈夫曼树)
- [7.2 哈夫曼编码](#7.2 哈夫曼编码)
树
树(Tree)是 n ( n ≥ 0 ) n(n\ge0) n(n≥0)个结点的有限集。 n = 0 n=0 n=0时称为空树。在任意一颗非空树中:①有且仅有一个特定的称为根(Root)的结点②当 n > 1 n>1 n>1时,其余结点可分为 m m m( m > 0 m>0 m>0)个互不相交的有限集 T 1 、 T 2 、 . . . 、 T m T_1、T_2、...、T_m T1、T2、...、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。如图中所示
子树 T 1 T_1 T1和 T 2 T_2 T2是根结点A的子树。当然DGHI组成的树,又是以B为根结点的子树,EJ是以C为根结点的子树。
|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| | |
注意: 1. n>0时,树的根结点是唯一的,不可能存在多个根结点。 2. m>0时,子树的个数没有限制,但它们一定是互不相交的。
1.2 结点的分类
树的结点包含一个数据元素及其若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree) 。度为0的结点称为叶结点(Leaf)或终端结点 ;度不为0的结点称为非终端结点或分支节点。除根结点之外,分支节点也称为内部节点。树的度是树内各结点的度的最大值(不是和) 。如下图中所示,这棵树的度是3。
1.3 结点间的关系
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)结点。同一个双亲的孩子之间称为兄弟(Sibling)结点。结点的祖先是从根到该结点所经分支上的所有结点。对于图中所示的树来说。B是A的Child,A是B的Parent,B和C是Sibling。对H来说,GIJ都是它的Sibling,D是他的Parent,ABD都是它的祖先。
1.4 树的其他概念
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。树中结点的最大层次称为树的深度(Depth)或高度,上面图中所示的树的深度为4。
1.5 树的性质
- 树中的结点总数等于所有结点的度加一(对于上图:(2+1+2+3+1)+1 =10 个结点)
- m叉树中第 i , i ≥ 1 i,i\ge1 i,i≥1层上至多可以有 m i − 1 m^{i-1} mi−1个结点。
2. 二叉树
二叉树(Binary Tree)是树形结构中最重要的类型,它的规律性强,应用广泛。
二叉树的每个结点最多只能拥有两棵子树,分别称为左子树和右子树。二叉树可以有五种基本形态:
- 空树
- 只有根结点
- 只有左子树
- 只有右子树
- 左右子树都有
2.1 满二叉树
一颗高度为h,且含有 2 h − 1 2^{h}-1 2h−1个结点的二叉树称为满二叉树。
对于编号为i的结点,左子结点编号为2i,右结点编号为2i+1,双亲结点为 ⌊ \lfloor ⌊i/2 ⌋ \rfloor ⌋(向下取整)。
2.2 完全二叉树
一颗高度为h,有n个结点的完全二叉树,它的每个结点都与高度相同的满二叉树中的结点编号一一对应。(完全二叉树除了最后一层,其他的都是满的)。如果最后一层中有度为1的结点,那么它的子结点一定是左节点。
性质:
- 对于编号为i的结点,左子节点为2i,右结点为2i+1,双亲结点为 ⌊ \lfloor ⌊i/2 ⌋ \rfloor ⌋。
- 若结点编号i ≤ \le ≤ ⌊ \lfloor ⌊n/2 ⌋ \rfloor ⌋,则结点为分支结点,否则为叶结点。
- 如果编号为i的结点为叶结点或只有左孩子,则编号大于i的结点均为叶结点。
2.3 二叉排序树(二叉查找树)
树上任意结点,如果存在左子树和右子树,则左子树上所有结点元素值都小于该结点,右子树上所有结点元素值都大于该结点。
3. 二叉树的存储结构
3.1 二叉树顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标能够体现结点之间的逻辑关系。
完全二叉树 :编号为i的结点,左子节点为2i,右子节点为2i+1,双亲结点 ⌊ \lfloor ⌊i/2 ⌋ \rfloor ⌋。
体现在数组中下表为i的结点,左子节点下标为2i+1,右子节点下标为2i+2,双亲结点下标为 ⌊ \lfloor ⌊(i-1)/2 ⌋ \rfloor ⌋
普通二叉树:补齐成为完全二叉树,按照完全二叉树存放,数组中利用特殊值来填充。但利用顺序存储的方法存储普通二叉树,补齐成为完全二叉树再存储,容易造成空间的浪费,比较适合顺序存储结构。
3.2 二叉树的链式存储结构
cpp
struct BiTNode{
TElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子结点指针
struct BiTNode *parent; // 指向双亲结点指针
}BiTNode;
typedef BiNode* BiTree;
4. 二叉树的遍历
二叉树的遍历,是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
4.1 层次遍历
规则:树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下的逐层遍历,同一层中,按从左到右的顺序对结点逐个访问。
方法:初始化将二叉树根结点插入队列中。开始,出队队头元素,并将队头元素的左右子结点入队(没有子结点入队为空),直到队列中为空。依次出队的元素就是层次遍历的顺序。
图示:
代码实现:
cpp
void LevelOrder(BiTree TT)
{
std::queue<BiTreeNode*> qq; // 队列
qq.push(TT); //根结点入队
while (!qq.empty())
{
// 队头出队
BiTreeNode* out = qq.front();
qq.pop();
cout << out->data << " ";
if (out->lchild != nullptr) // 左子结点
{
qq.push(out->lchild);
}
if (out->rchild != nullptr) // 右子结点
{
qq.push(out->rchild);
}
}
cout << endl;
}
4.1 前序遍历
规则:二叉树为空,则空操作返回,否则从根结点开始,先访问根结点,然后前序遍历左子树,再前序遍历右子树。
代码实现:
cpp
// 前序遍历
void PreOrder(BiTree TT)
{
// 为空返回
if (TT==nullptr)
{
return;
}
// 访问根结点
cout << TT->data << " ";
// 访问左子树
PreOrder(TT->lchild);
// 访问右子树
PreOrder(TT->rchild);
}
4.2 中序遍历
规则:二叉树为空,则空操作返回,否则从根结点开始,先中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
代码实现:
cpp
// 中序遍历
void MidOrder(BiTree TT)
{
// 为空返回
if (TT == nullptr)
{
return;
}
MidOrder(TT->lchild);// 访问左子树
cout << TT->data << " "; // 访问根结点
MidOrder(TT->rchild); // 访问右子树
}
4.3 后序遍历
规则:二叉树为空,则空操作返回,否则从左到右,先叶子后结点的方式遍历访问左右子树,最后访问的是根结点。
代码实现:
cpp
// 后序遍历
void PostOrder(BiTree TT)
{
if (TT == nullptr)
{
return;
}
// 访问左子树
PostOrder(TT->lchild);
// 访问右子树
PostOrder(TT->rchild);
// 访问根结点
cout << TT->data << " ";
}
5. 线索二叉树
把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded Binary Tree) 。
二叉树线索化是将二叉链表中的空指针指向前驱或后继结点。而前驱或后继结点的信息只有遍历时才能得到,因此二叉树的线索化又分为先序线索二叉树、中序线索二叉树和后序线索二叉树。
- 如果该结点没有左子结点(左子树),则将左指针指向遍历序列中它的前驱结点。
- 如果该结点没有右子节点(右子树),则将右指针指向遍历序列中它的后继节点。
5.1 线索二叉树的数据结构
cpp
struct TBtNode
{
ElemType data;
TBtNode* lchild;
TBtNode* rchild;
bool ltag,rtage; // 左右指针的类型,0-非线索指针,1-线索指针
};
typedef TBtNode* TBtree;
5.2 线索二叉树求前驱和后继
先序线索二叉树:可求后继:
- 如果当前结点的右指针存放的是线索,右指针指向的结点就是后继结点。
- 如果当前结点的右指针存放的是结点,如果结点存在取左子结点,否则取右子结点。
后序线索二叉树:可求前驱:
- 如果当前结点的左指针存放的是线索,左指针指向的结点就是前驱结点
- 如果当前结点的左指针存放的是结点,如果结点存在取右子结点,否则取左子结点。
中序线索二叉树:可求前驱和后继:
求后继:
- 如果当前结点右指针存放的是线索,右指针指向的结点就是后继节点
- 如果当前结点右指针存放的是结点,右子树中序遍历的第一个结点即后继结点
求前驱
- 如果当前结点左指针存放的是线索,左指针指向的结点就是后继结点
- 如果当前结点左指针存放的是结点,左子树中序遍历的第一个结点即后继结点
6. 二叉排序树
二叉排序树(二叉搜索树,二叉查找树,Binary Sort Tree BST),一颗飞控的二叉排序树具有下列性质:
- 如果左子树不空,则左子树上所有结点的值都小于根结点值
- 如果右子树不空,则右子树上所有结点的值都大于根结点值
- 左右子树也分别是二叉排序树
左子树<根<右子树
数据结构
cpp
typedef int ElemType;
struct BSTNode
{
ElemType data; // 数据域
BSTNode* lchild; // 左子结点
BSTNode* rchild; // 右子结点
};
typedef BSTNode* BSTree;
6.1 二叉排序树的插入
- 从根结点开始,递归插入,小于当前结点的值,递归左子树,大于当前结点的值递归右子树。
- 插入元素肯定是在叶结点或根结点
- 如果插入元素重复,则返回异常
cpp
// 在二叉排序树中插入结点
bool InsertBST(BSTree& tree, ElemType* data)
{
if ( tree== nullptr) // 当树为空,创建根结点
{
tree = new BSTNode;
memcpy(&(tree->data), data, sizeof(ElemType));
tree->lchild = tree->rchild = nullptr;
return true;
}
// 如果元素已存在返回
if (*data == (tree)->data)
{
return false;
}
// 插入
if (*data < (tree)->data)
{
return InsertBST((tree)->lchild, data); // 向左递归
}
else
{
return InsertBST((tree)->rchild, data); // 向右递归
}
}
6.2 创建二叉排序树
- 相同的序列创建的二叉排序树是唯一的
- 同一集合创建的二叉排序树是不同的(根结点不同,从而二叉排序树不同)
- 用二叉树的先序遍历创建的二叉排序树与原树相同
cpp
// 创建二叉排序树
void CreateBST(BSTree& tree, ElemType arr[], int len)
{
tree = NULL;
for (int i = 0; i < len; i++)
{
InsertBST(tree, &arr[i]);
}
}
6.3 二叉排序树的查找
根据带查找元素值与结点值的大小比较,小于结点值,递归左子树,大于结点值递归右子树。
cpp
// 在二叉排序树中查找结点
BSTNode* FindNode(BSTree tree, ElemType data)
{
if (tree == nullptr) // 查找失败
{
return nullptr;
}
if (data == tree->data)
{
return tree;
}
if (data < tree->data)
{
return FindNode(tree->lchild, data); //向左递归
}
else
{
return FindNode(tree->rchild, data); //向右递归
}
}
6.4 二叉排序树的遍历
利用中序遍历输出的二叉排序树是按照从小到大的顺序(先左子树(即先小的),然后根结点,最后右子树(最后大的))
cpp
// 中序遍历二叉排序树
void InOrder(BSTree* tree)
{
if (*tree == nullptr)
{
return;
}
// 先左子树
InOrder(&((*tree)->lchild));
// 根结点
cout << (*tree)->data << " ";
// 右子树
InOrder(&((*tree)->rchild));
}
6.5 删除二叉排序树的结点
- 如果树只有根结点,并且待删除的结点就是根结点
- 如果待删除的结点是叶结点,直接删除,不会破坏二叉排序树的性质
- 如果待删除的结点只有左子树或右子树,则让子树代替自己
- 如果待删除的结点有左子树和右子树,让左子树最右侧的结点代替自己,然后删除左子树最右侧的结点。(也可以让右子树最左侧的结点代替自己,然后删除右子树最左侧的结点。)
cpp
bool DeleteNode(BSTree& tree, ElemType* data)
{
if (tree == nullptr) // 树为空
{
return false;
}
// (1) 树只有根节点,并且待删除结点就是根结点
if ((tree->lchild == nullptr && tree->rchild == nullptr) && *data == tree->data)
{
// 删除根结点
delete tree;
tree = nullptr;
return true;
}
//
BSTNode* ptr = tree; //
BSTNode* pre_ptr = nullptr; // 记录双亲结点
int r_or_l = 0; // 记录结点是双亲结点的左子树还是右子树
while (ptr != nullptr)
{
if (ptr->data == *data) // 找到结点
{
break;
}
pre_ptr = ptr; // 记录双亲结点
if (*data < ptr->data) // 向左递归
{
ptr = ptr->lchild;
r_or_l = 1;
}
else // 向右递归
{
ptr = ptr->rchild;
r_or_l = 0;
}
}
if (ptr == nullptr) // 未找到
{
return false;
}
// (2) 如果待删除的结点是叶结点,直接删除
if (ptr->lchild == nullptr && ptr->rchild == nullptr)
{
if (r_or_l == 0) // 当前结点是双亲结点的右子结点
{
pre_ptr->rchild = nullptr;
}
else // 当前结点是双亲结点的左子结点
{
pre_ptr->lchild = nullptr;
}
delete ptr; // 删除当前结点
ptr = nullptr;
return true;
}
// (3) 如果待删除结点只有左子树或只有右子树
if (ptr->lchild == nullptr || ptr->rchild == nullptr)
{
if (ptr->lchild != nullptr) // 只有左子树
{
// 左子树取代当前结点
// 双亲结点指向当前结点的左子树
if (r_or_l == 0) // 当前结点的左子树,是双亲结点的右子树
{
pre_ptr->rchild = ptr->lchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
else // 当前结点的左子树,是双亲结点的左子树
{
pre_ptr->lchild = ptr->lchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
}
else // 只有右子树
{
// 右子树取代当前结点
// 双亲结点指向当前结点的右子树
if (r_or_l == 0) // 当前结点的右子树,是双亲结点的右子树
{
pre_ptr->rchild = ptr->rchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
else // 当前结点的右子树,是双亲结点的左子树
{
pre_ptr->lchild = ptr->rchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
return true;
}
}
// (4) 如果待删除结点已经有左子树和右子树,让左子树最右侧的结点取代自己,然后再删除左子树最右侧的结点
BSTNode* tmp_ptr = ptr->lchild;
BSTNode* pre_ptr2 = nullptr; // 记录最右侧结点的双亲结点位置
while (tmp_ptr->rchild) // 找到当前结点的左子树最右侧结点
{
pre_ptr2 = tmp_ptr;
tmp_ptr = tmp_ptr->rchild;
}
// 最右侧结点替代当前结点
ptr->data = tmp_ptr->data;
// 左子树最右侧结点必定没有右子树
// 双亲结点右指针最右侧结点的左子树
pre_ptr2->rchild = tmp_ptr->lchild;
// 删除最右侧结点
delete tmp_ptr;
tmp_ptr = nullptr;
return true;
}
6.6 二叉排序树的查找效率
在查找操作中,需要对比结点值的次数即查找长度 ,反映了查找运算的时间复杂度。
查找成功的平均查找长度(ASL ,Average Search Length)。ASL=∑(每层结点个数 X 该层层数)/结点个数
例如对二叉排序树
的ASL=(11+2 2+33+4 2)/8=2.75
查找失败的ASL=(21+3 4+4*4)/9=3.33
最好的情况:平均查找长度O(log2n)
最坏的情况:平均查找长度O(n)
6.7 完整实现
cpp
#include <iostream>
using namespace std;
typedef int ElemType;
struct BSTNode
{
ElemType data; // 数据域
BSTNode* lchild; // 左子结点
BSTNode* rchild; // 右子结点
};
typedef BSTNode* BSTree;
// 在二叉排序树中插入结点
bool InsertBST(BSTree& tree, ElemType* data)
{
if ( tree== nullptr) // 当树为空,创建根结点
{
tree = new BSTNode;
memcpy(&(tree->data), data, sizeof(ElemType));
tree->lchild = tree->rchild = nullptr;
return true;
}
// 如果元素已存在返回
if (*data == (tree)->data)
{
return false;
}
// 插入
if (*data < (tree)->data)
{
return InsertBST((tree)->lchild, data); // 向左递归
}
else
{
return InsertBST((tree)->rchild, data); // 向右递归
}
}
// 创建二叉排序树
void CreateBST(BSTree& tree, ElemType arr[], int len)
{
tree = NULL;
for (int i = 0; i < len; i++)
{
InsertBST(tree, &arr[i]);
}
}
// 在二叉排序树中查找结点
BSTNode* FindNode(BSTree tree, ElemType data)
{
if (tree == nullptr) // 查找失败
{
return nullptr;
}
if (data == tree->data)
{
return tree;
}
if (data < tree->data)
{
return FindNode(tree->lchild, data); //向左递归
}
else
{
return FindNode(tree->rchild, data); //向右递归
}
}
// 删除二叉树结点
bool DeleteNode(BSTree& tree, ElemType* data)
{
if (tree == nullptr) // 树为空
{
return false;
}
// (1) 树只有根节点,并且待删除结点就是根结点
if ((tree->lchild == nullptr && tree->rchild == nullptr) && *data == tree->data)
{
// 删除根结点
delete tree;
tree = nullptr;
return true;
}
//
BSTNode* ptr = tree; //
BSTNode* pre_ptr = nullptr; // 记录双亲结点
int r_or_l = 0; // 记录结点是双亲结点的左子树还是右子树
while (ptr != nullptr)
{
if (ptr->data == *data) // 找到结点
{
break;
}
pre_ptr = ptr; // 记录双亲结点
if (*data < ptr->data) // 向左递归
{
ptr = ptr->lchild;
r_or_l = 1;
}
else // 向右递归
{
ptr = ptr->rchild;
r_or_l = 0;
}
}
if (ptr == nullptr) // 未找到
{
return false;
}
// (2) 如果待删除的结点是叶结点,直接删除
if (ptr->lchild == nullptr && ptr->rchild == nullptr)
{
if (r_or_l == 0) // 当前结点是双亲结点的右子结点
{
pre_ptr->rchild = nullptr;
}
else // 当前结点是双亲结点的左子结点
{
pre_ptr->lchild = nullptr;
}
delete ptr; // 删除当前结点
ptr = nullptr;
return true;
}
// (3) 如果待删除结点只有左子树或只有右子树
if (ptr->lchild == nullptr || ptr->rchild == nullptr)
{
if (ptr->lchild != nullptr) // 只有左子树
{
// 左子树取代当前结点
// 双亲结点指向当前结点的左子树
if (r_or_l == 0) // 当前结点的左子树,是双亲结点的右子树
{
pre_ptr->rchild = ptr->lchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
else // 当前结点的左子树,是双亲结点的左子树
{
pre_ptr->lchild = ptr->lchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
}
else // 只有右子树
{
// 右子树取代当前结点
// 双亲结点指向当前结点的右子树
if (r_or_l == 0) // 当前结点的右子树,是双亲结点的右子树
{
pre_ptr->rchild = ptr->rchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
else // 当前结点的右子树,是双亲结点的左子树
{
pre_ptr->lchild = ptr->rchild;
delete ptr; // 删除当前结点
ptr = nullptr;
}
return true;
}
}
// (4) 如果待删除结点已经有左子树和右子树,让左子树最右侧的结点取代自己,然后再删除左子树最右侧的结点
BSTNode* tmp_ptr = ptr->lchild;
BSTNode* pre_ptr2 = nullptr; // 记录最右侧结点的双亲结点位置
while (tmp_ptr->rchild) // 找到当前结点的左子树最右侧结点
{
pre_ptr2 = tmp_ptr;
tmp_ptr = tmp_ptr->rchild;
}
// 最右侧结点替代当前结点
ptr->data = tmp_ptr->data;
// 左子树最右侧结点必定没有右子树
// 双亲结点右指针最右侧结点的左子树
pre_ptr2->rchild = tmp_ptr->lchild;
// 删除最右侧结点
delete tmp_ptr;
tmp_ptr = nullptr;
return true;
}
// 先序遍历二叉排序树
void PreOrder(BSTree* tree)
{
if (*tree == nullptr)
{
return;
}
// 根结点
cout << (*tree)->data << " ";
// 左子树
PreOrder(&((*tree)->lchild));
// 右子树
PreOrder(&((*tree)->rchild));
}
// 中序遍历二叉排序树
void InOrder(BSTree* tree)
{
if (*tree == nullptr)
{
return;
}
// 先左子树
InOrder(&((*tree)->lchild));
// 根结点
cout << (*tree)->data << " ";
// 右子树
InOrder(&((*tree)->rchild));
}
// 后序遍历二叉排序树
void PostOrder(BSTree* tree)
{
if (*tree == nullptr)
{
return;
}
// 先左子树
InOrder(&((*tree)->lchild));
// 右子树
InOrder(&((*tree)->rchild));
// 根结点
cout << (*tree)->data << " ";
}
int main(void)
{
BSTree tree;
//ElemType arr[] = { 1,3,5,2,4,8,6,7 };
//
// 1
// 3
// 2 5
// 4 8
// 6
// 7
ElemType arr[] = { 50,30,20,40,32,31,38,35,39,34,36,37,33 };
CreateBST(tree, arr, sizeof(arr) / sizeof(ElemType));
PreOrder(&tree);
cout << endl;
InOrder(&tree);
cout << endl;
PostOrder(&tree);
cout << endl;
ElemType e = 38;
DeleteNode(tree, &e);
InOrder(&tree);
cout << endl;
return 0;
}
7. 哈夫曼树(最优二叉树)
结点的路径 :从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数称为路径长度。
结点的权 :结点的数值有某种现实的含义(如重要性、两个点之间的距离等)
结点的带权路径长度 :从树的根结点到该结点的路径长度与该结点上的权值的乘积
树的带权路径长度 :为树中所有叶子结点的带权路径长度之和(WPL,Weight Path Length)
在含有n个带权结点的二叉树中,WPL最小的二叉树称为哈夫曼树(最优二叉树) 。如同图中所示,第三棵树才是哈夫曼树。
哈夫曼树并不唯一:将第三棵树左右结点位置调换依然是一颗哈夫曼树
7.1 构造哈夫曼树
详细过程图解:
初始结点看作是只有单个结点的树
先挑两个根结点权值最小的树,构建一颗新的树,并新增一个结点,权值为两子树权值之和
继续挑选权值最小的树,构建新的树
继续挑选权值最小的树,构建新的树
继续挑选权值最小的树,构建新的树
继续挑选权值最小的树,构建新的树
继续挑选权值最小的树,构建新的树
继续挑选权值最小的树,构建新的树
最终
1. 初始结点都会成为叶结点,叶结点的权值越大,离根结点越近。
-
如果叶结点有n个,共合并n-1次,哈夫曼树的结点总数为2n-1
-
哈夫曼树不存在度为1的结点
-
哈夫曼树不唯一,只要WPL最小就行
7.2 哈夫曼编码
可变长度编码,任何一个字符的编码都不是另一个字符编码的前缀,这种编码称作前缀编码。
利用哈夫曼树来设计前缀编码,用0和1表示左子树和右子树。