结点:树中的数据元素
树:是n个结点的有限集合(当n=0时,称为空树)
结点的度、树的度
叶子结点、分支结点
孩子结点、双亲结点、兄弟结点
路径、路径长度
路径:如果树的结点序列n1、n2、…、nk满足如下关系:结点ni是结点ni+1的双亲(1 <= i < k),则把n1、n2、…、nk称为一条由n1至nk的路径
路径长度:路径经过的边数称为路径长度
祖先、子孙
结点的层数、树的深度(高度)
层:规定根结点的层数为1,对其余任何结点,若某结点在第k层,则其孩子结点在第k+1层
树的深度(高度):树中所有结点的最大层数称为树的深度/高度
层序编号
有序树、无序树
有序树/无序树:如果有一棵树中的结点的各子树 从左到右 是有次序的,称这棵树为有序树;反之称为无序树
森林
若树为空,则空操作返回
否则:
(1)先访问根结点
(2)然后按照从左到右的顺序(由上到下)前序遍历根结点的每一棵子树
若树为空,则空操作返回
否则:
(1)首先按照从左到右的顺序(由下到上)后序遍历根结点的每一棵子树
(2)然后访问根结点
存储方式:一维数组,数据元素为树中对应的一个结点(一般按层序存储)
表示:
data | parent |
---|
private:
DataType data; //树中结点的数据信息
int parent; //该结点的双亲在数组中的下标
原理:树中每个结点都 有且仅有 一个双亲结点
优缺点
优点 | 便于查找双亲结点和根结点 |
---|---|
缺点 | 不能反映各兄弟结点之间的关系 |
定义:链表中每个结点包括 一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点
表示:
法1: 各结点不同构(不易实现),指针域的个数等于 各个结点的度
data | degree | child1 | child2 | … | childx |
---|
法2: 各结点同构,指针域的个数等于 树的度
data | child1 | child2 | … | childx |
---|
缺点:对于法2,链表中各结点同构,存在存储空间浪费的问题
n x (k - 1) + 1
个空链域定义:数组+单链表
表示:
(1)孩子结点
child | next |
---|
(2)表头结点
data | firstChild |
---|
定义:链表中的每个结点除数据域外,还设置了两个指针分别指向该结点的第一个孩子和右兄弟
表示:
firstChild | data | rightSib |
---|
定义:二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为 根结点的左子树和右子树
特点:
二叉树的五种基本形态:
定义:对一棵具有n个结点的二叉树 按层序 编号,如果编号i(1 <= i <= n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树
特点:
性质1:二叉树 的底i层上最多有 2的i-1次方 个结点(i >= 1)
性质2:在一棵深度为k的 二叉树 中,最多有 2的k次方 - 1 个结点(满二叉树),最少有 k 个结点(斜树)
性质3:在一棵 二叉树 中,如果叶子结点的个数为n0,度为2的结点个数为n2,则 n0 = n2 + 1
n = n0 + n1 + n2 //式1
n = B + 1 //除根结点外,每个结点都只有一个分支进入
B = n1 + 2*n2 //度为1的结点射出1个分支,度为2的结点射出2个分支
n = n1 + 2*n2 + 1 //式2
n0 = n2 + 1 //综合式1、式2得出结论
性质4:具有n个结点的 完全二叉树 ,假设其深度为k,则 k =「log以2为底的n」+ 1 (「」表示向下取整)
最后一层的结点序号从左到右第一个为 2的k-1次方 ,最后一个为 2的k次方-1
2的k-1次方 <= n < 2的k次方 //取对数后,向下取整得出结论
性质5:对一棵具有n个结点的 完全二叉树 中的结点从1开始按层序编号,则对于任意的编号为i(1 <= i <= n)的结点有
(1)如果 i > 1
,则结点i的 双亲 的编号为 i/2
(2)如果 2i =< n
,则结点i的 左孩子 的编号为 2i
(3)如果 2i + 1 =< n
,则结点i的 右孩子 的编号为 2i+1
若二叉树为空,则空操作返回
否则:
(1)访问根结点 //只有“访问”时才返回值
(2)前序遍历根结点的左子树 //存在递归
(3)前序遍历根结点的右子树
若二叉树为空,则空操作返回
否则:
(1)中序遍历根结点的左子树 //存在递归
(2)访问根结点
(3)中序遍历根结点的右子树 //存在递归
若二叉树为空,则空操作返回
否则:
(1)后序遍历根结点的左子树 //存在递归
(2)后序遍历根结点的右子树 //存在递归
(3)访问根结点
从上到下,从左往右
实质上是 广度优先 遍历
存储方式:一维数组
步骤:
应用场景:一般仅适合存储完全二叉树
结点结构:
lchild | data | rchild |
---|
void BiTree::PreOrder(BiNode * bt) //* bt为根指针
{
if (bt == NULL) return; //递归调用的结束条件
else {
cout << bt -> data; //访问根结点bt的数据域
PreOrder(bt -> lchild); //前序递归遍历bt的左子树
PreOrder(bt -> rchild); //前序递归遍历bt的右子树
}
}
void BiTree::InOrder(BiNode * bt)
{
if (bt == NULL) return; //递归调用的结束条件
else {
InOrder(bt -> lchild); //中序递归遍历bt的左子树
cout << bt -> data; //访问根结点bt的数据域
InOrder(bt -> rchild); //中序递归遍历bt的右子树
}
}
void BiTree::PostOrder(BiNode * bt)
{
if(bt == NULL) return; //递归调用的结束条件
else {
PostOrder(bt -> lchild); //后序递归遍历bt的左子树
PostOrder(bt -> rchild); //后续递归遍历bt的右子树
cout << bt -> data; //访问根结点bt的数据域
}
}
结构:队列
思想:设置一个队列存放已访问的结点。遍历从二叉树的根结点开始,首先将 根指针 入队,然后从队头取出一个元素并执行下列操作
算法实现:
void BiTree::LevelOrder()
{
front = rear = -1; //队列Q初始化(采用顺序队列,并假定不会发生上溢)
if (root == NULL) return; //二叉树为空,算法结束
Q[++rear] = root; //根指针入队
while(front != rear)
{
q = Q[++front]; //队头元素出队
cout << q -> data; //访问结点q的数据域
if (q -> lchild != NULL) //若结点q存在左孩子,则将左孩子指针入队
Q[++rear] = q -> lchild
if (q -> rchild != NULL) //若结点q存在右孩子,则将右孩子指针入队
Q[++rear] = q -> rchild;
}
}
构造 扩展二叉树 :将每一个结点的空指针引出一个虚结点
扩展二叉树建立二叉链表的算法实现:
bt = NULL;
bt -> data
,之后依次递归建立它的左子树和右子树??? 域前加*
BiNode * BiTree::Create(BiNode * bt)
{
cin >> ch; //输入结点的数据信息,假设为字符
if (ch == '#') bt = NULL; //建立一棵空树(将parent的对应指针域置空)
else{
bt = new BiNode; //生成一个结点,数据域为输入的ch(假设为前序遍历)
bt -> data = ch;
bt -> lchild = Creat(bt -> lchild); //递归建立左子树
bt -> rchild = Creat(bt -> rchild); //递归建立右子树
}
}
方法:后序遍历
算法实现:
void BiTree::Release(BiNode * bt)
{
if (bt != NULL) {
Release(bt -> lchild); //释放左子树
Release(bt -> rchild); //释放右子树
delete bt; //释放根结点
}
}
定义:在二叉链表结点结构的基础上 增加parent域指向父节点
lchild | data | rchild | parent |
---|
定义:在二叉链表中,若lchild和rchild为空,则可将其指向二叉树 在某种遍历序列 中的前驱结点或后继结点
结构:
ltag | lchild | data | rchild | rtag |
---|
ltag:
值 | 含义 |
---|---|
0 | lchild指向该结点的左孩子 |
1 | lchild指向该结点的前驱结点 |
rtag:
值 | 含义 |
---|---|
0 | rchild指向该结点的右孩子 |
1 | rchild指向该结点的后继结点 |
代码实现
struct ThrNode
{
DataType data;
ThrNode * lchild, * rchild;
flag ltag, rtag;
}
关键点:前序遍历过某结点的整个左子树后,如何找到该结点的右子树的根指针
二叉树的根指针bt值的两种情况:
bt != null
,则表明当前的二叉树不空,此时,应输出根结点bt的值并将bt保存到栈中,准备继续遍历bt的左子树bt = null
,则表明以bt为根指针的二叉树遍历完毕,并且bt是栈顶指针所指结点的左子树
void BiTree::PreOrder(BiNode * bt)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (bt != NULL || top != -1) //bt为空且栈也为空时才退出循环
{
while (bt != NULL)
{
cout << bt -> data; //访问bt指向的结点
s[++top] = bt; //将根指针bt入栈
bt = bt -> lchild; //继续遍历bt指向结点的左子树
}
if (top != -1) //栈非空
{
bt = s[top--]; //将栈顶元素出栈赋值给bt
bt = bt -> rchild; //继续遍历bt指向结点的右子树
}
}
}
void BiTree::PreOrder(BiNode * bt)
{
top = -1; //采用顺序栈,并假定不会发生上溢
while (bt != NULL || top != -1) //bt不为空且栈也不为空时才退出循环
{
while (bt != NULL)
{
s[++top] = bt; //将根指针bt入栈
bt = bt -> lchild; //继续遍历bt指向结点的左子树
}
if (top != -1) //栈非空
{
bt = s[top--]; //将栈顶元素出栈赋值给bt
cout << bt -> data; //访问bt指向的结点
bt = bt -> rchild; //继续遍历bt指向结点的右子树
}
}
}
关键点:在后序遍历中,结点要 入两次栈,出两次栈
出栈情况 | 含义 |
---|---|
第一次出栈 | 只遍历完左子树,右子树尚未遍历,则该结点不出栈,利用栈顶结点找到它的右子树,准备遍历它的右子树 |
第二次出栈 | 遍历完右子树,将该结点出栈,并访问它 |
flag
flag值 | 含义 |
---|---|
flag = 1 | 第一次出栈,只遍历完左子树,该结点不能访问 |
flag = 0 | 第二次出栈,遍历完右子树,该结点可以访问 |
二叉树的根指针bt值的两种情况:
bt != null
,则bt及标志flag(置为1)入栈,遍历其左子树bt == null
,此时
flag = 1
,则表明栈顶结点的左子树已遍历完毕,将flag修改为2flag = 2
,则表明栈顶结点的右子树也遍历完毕,输出栈顶结点代码实现:
void BiTree::PostOrder(BiNode * bt)
{
top = -1; //采用顺序栈s,并假定栈不会发生上溢
while(bt != null || top != -1) //bt为空且栈s也为空时才退出循环
{
while(bt != null)
{
top++;
s[top].ptr = bt; //根结点bt入栈(ptr暂存bt用于遍历右子树或再次将左子树入栈)
s[top].flag = 1; //更新flag值为1
bt = bt -> lchild; //bt指向原根结点的左子树
}
while(top != -1 && s[top].flag == 2) //当栈s非空且栈顶元素的标志等于2时,出栈并输出栈顶结点
{
bt = s[top--].ptr; //出栈(两次出栈都在这里,第二次时的top--避免死循环)
cout << bt -> data;
}
}
}
方法:
步骤 | 方法 | 具体操作 |
---|---|---|
1 | 加线 | 树中所有相邻兄弟结点之间加一条连线 |
2 | 去线 | 对树中的每个结点,只保留它与第一个孩子结点之间的连线,删除它与其他孩子结点之间的连线 |
3 | 层次调整 | 以根结点为轴心,将树顺时针转动一定的角度,使之层次分明 |
规则:右边的兄弟当作右儿子
特点:
(1)在二叉树中,左分支上的各结点在原来的树中是父子关系,右分支上的各结点在原来的树中是兄弟关系
(2)二叉树根结点的右子树必定为空(由于转换前树的根结点没有兄弟)
转换前后的遍历关系:
转换前 | 转换后 |
---|---|
树的前序遍历 | 二叉树的前序遍历 |
树的后序遍历 | 二叉树的中序遍历 |
思想:将森林中的每棵树转换为二叉树,再将每棵树的根结点视为兄弟
方法:
转换成树/森林的判定依据:二叉树根结点有无右子树
方法:
步骤 | 方法 | 具体操作 |
---|---|---|
1 | 加线 | 若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、…,都与结点y用线连接起来 |
2 | 去线 | 删去原二叉树中所有的双亲结点与右孩子结点的连线 |
3 | 层次调整 | 整理由(1)(2)两步所得到的树或森林,使之层次分明 |
叶子结点的权值:叶子结点的权值是对叶子结点赋予的一个有意义的数值量
二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和叫做二叉树的带权路径长度(是对整个带权二叉树的范围求和)
哈夫曼树:给定一组具有确定权值的叶子结点,可以构造出不同的二叉树,将其中带权路径长度最小的二叉树称为哈夫曼树(又称 最优二叉树)
要点:一棵二叉树要使其带权路径长度最小,必须 使权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点
基本思想:
步骤 | 方法 | 具体操作 |
---|---|---|
1 | 初始化 | 由给定的n个权值{w1、w2、…、wn} 构造 只有一个根结点的二叉树,从而得到一个二叉树 集合 F={T1,T2,…,Tn} |
2 | 选取与合并 | 在F中选取根结点的 权值最小 的两颗二叉树分别作为左右子树,构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和 |
3 | 删除与加入 | 在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中 |
4 | 重复2、3 | 当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树 |
特点:
存储结构:数组huffTree[2n-1]
weight | lchild | rchild | parent |
---|
过程:首先将n个权值的叶子结点存放到数组huffTree的前n个分量中,然后不断将两颗子树合并为一棵子树,并将新子树的根结点顺序存放到数组huffTree的前n个分量的后面
parent = -1
的为根结点算法:将n个叶子的权值保存在w[n]中
1、数组huffTree初始化,所有数组元素的双亲、左右孩子都置为-1
2、数组huffTree的前n个元素的权值置给定权值
3、进行n-1次合并
3.1、在二叉树集合中选取两个权值最小的根结点,其下标分别为i1、i2;
3.2、将i1、i2合并为一棵新的二叉树k
void HuffmanTree(element huffTree[], int w[], int n) {
//初始化所有结点均没有双亲和孩子(置-1)
for (i = 0; i < 2 * n - 1; i++) {
huffTree[i].parent = -1;
huffTree[i].lchild = -1;
huffTree[i].rchild = -1;
}
//构造n棵只含有根结点的二叉树
for (i = 0; i < n; i++) {
hufTree[i].weight = w[i]
}
//n-1次合并
for (k = n; k < 2 * n - 1; k++) { //k为全局变量
//查找权值最小的两个根结点,并将其下标的值赋给全局变量i1,i2
Select(huffTree, i1, i2);
huffTree[i1].parent = k;
huffTree[i2].parent = k;
huffTree[k].weight = hufTree[i1].weight + huffTree[i2].weight;
huffTree[k].lchild = i1;
huffTree[k].rchild = i2;
}
}
编码:在进行程序设计时,通常给每一个字符标记一个单独的代码来表示一组字符,我们称之为编码
不等长编码:根据字符出现的频率不同,让 出现频率高的字符尽可能使用较短的编码 ,以减少用于区分不同字符使用的代码位数
构造哈夫曼编码树:设需要编码的字符集为{d1, d2, … ,dn}作为叶子结点,它们在字符串中出现的频率{w1, w2, …, wn}作为为叶子结点的权值,构造一棵哈夫曼树
规定哈夫曼编码树的 左分支代表0,右分支代表1
作用:哈夫曼树棵用于构造 最短的不等长编码 方案
哈夫曼编码:则从根结点到每个叶子结点所经过的路径组成的0和1的序列便为该叶子结点所对应字符的编码,称为哈夫曼编码
优点:
序号 | 优点 | 含义 |
---|---|---|
1 | 使字符串的编码总长度最短 | 哈夫曼编码树中,树的带权路径长度的含义是 各个字符的码长与出现次数的乘积之和 ,所以采用哈夫曼树构造的编码是一种能 使字符串的编码总长度最短 的不等长编码 |
2 | 保证了解码的唯一性 | 哈夫曼树的每个字符结点都是叶子结点,不可能在根结点到其他字符结点的路径上,保证了前缀编码,保证了解码的唯一性 |