目录
什么是树
树是由 n (n >= 0) 个结点组成的一个具有层次关系的有限集合。
(树也可以说成是由树根和若干棵子树构成的)
树的结点
根
在任意一棵非空树中,存在且仅存在一个根结点,简称根,根结点只有后继结点,没有前驱结点。
子节点 父结点 兄弟结点
每个结点的直接后继结点称为结点的子结点。相应的该结点称为孩子结点的父结点。具有同一父结点的结点称为兄弟结点。
叶子结点
在一棵树中,度不为零的结点称为非终端结点或分支结点,度为零的结点称为终端结点或叶子结点。
结点的度
一个结点所拥有的子树的数目称为结点的度
树的度
树中各结点度的最大值称为树的度
树的深度
树的层次从从根开始定义,根为第一层,根的孩子为第二层,以此类推,结点所在层数为其父结点层次加一,树的最大层次称为树的高度或深度。
路径
从一个结点到达另一个结点的路线。路径的长度是路线上所经过的边(连接两个结点的线段)的数目,等于结点个数减一。
二叉树
基本概念
二叉树是 n (n >= 0) 个结点的有限集合,由一个根结点及两棵互不相交的分别称为左子树和右子树的 二叉树组成。二叉树也是树的一种给,在二叉树中,每个结点最多只能有两个孩子结点。
基本特征
- 每个结点最多只能有两个孩子结点(不存在度大于二的结点)
- 二叉树是有序树,左子树和右子树的顺序不能颠倒。
二叉树基本形态有五种:
- 空二叉树
- 仅有根结点的二叉树
- 仅有一棵左子树的二叉树
- 仅有一棵右子树的二叉树
- 有两颗子树的二叉树
二叉树的分类
非完全二叉树
非完全二叉树就是普通的二叉树
完全二叉树
完全二叉树除最后一层外,每一层结点上的点数均达到最大值,在最后一层上只缺少右边的若干结点。
完全二叉树最后一层结点都优先填在左边,。若结点L在结点F的右侧,则该树不是完全二叉树。
特点
- 叶子结点只能出现在最下两层
- 最下层的叶子结点一定集中在左边连续位置
- 如果结点的度为1,那么该结点只存在左孩子,不存在右孩子。
- 同样结点的二叉树,完全二叉树的深度最小
满二叉树
满二叉树除叶子结点外,每个结点都有孩子结点,每一层的结点数都达到最大。
在同样深度的二叉树中,满二叉树的结点最多。
满二叉树是完全二叉树的特殊情况。
斜数
一颗只有左孩子或者只有右孩子的二叉树叫做斜树。
斜树的每一层都只有一个结点,结点的个数和二叉树的深度相同。
二叉树的性质
- 在 二 叉 树 的 第 i 层 上 至 多 有 2 i − 1 个 结 点 ( i > 0 ) \ 在二叉树的第 i 层上至多有2^{i-1}\,个结点(i > 0) 在二叉树的第i层上至多有2i−1个结点(i>0)
- 深 度 为 k 的 二 叉 树 至 多 有 2 k − 1 个 结 点 ( k > 0 ) \ 深度为 k 的二叉树至多有 2^{k-1}\,个结点(k > 0) 深度为k的二叉树至多有2k−1个结点(k>0)
- 对于任意一棵二叉树,若度为二的结点数为n2个,则叶子结点数(n0)必定为 n2 + 1 (n0 = n2 + 1)
- 对 于 有 n 个 结 点 的 完 全 二 叉 树 , 它 的 深 度 必 为 ⌊ l o g 2 n ⌋ ( 向 下 取 整 ) + 1 . 对于有 n 个结点的完全二叉树,它的深度必为 ⌊ \ log_{2}{n}⌋(向下取整)+1\,. 对于有n个结点的完全二叉树,它的深度必为⌊ log2n⌋(向下取整)+1.
- 若完全二叉树中的结点从上至下,从左到右编号,则编号为 i (1 <= i <= n)的结点,其左孩子编号必为 2i ,其右孩子编号必为 2i + 1,其双亲编号必为 i / 2(i = 1时除外)。
二叉树的存储结构
顺序存储
二叉树的顺序存储也是用一组连续的存储单元来存放二叉树中的结点元素,对于完全二叉树,分配一段相应大小的空间,对树中的结点自上而下,自左至右进行存储。
对于一般的二叉树,并不能靠顺序存储来还原出一棵正确的二叉树,可以在二叉树空缺的部分补上空结点,使之成为一棵完全二叉树,然后再用一维数组进行存储。
对于完全二叉树而言,顺序存储既简单又节省内存空间。但对于一般的二叉树,这样存储势必会造成大量的空间浪费,在最坏的情况下(右斜树),一颗深度为 k 的右斜树只有 k 个结点,却需要分配 2的k次方减一个存储单元。
链式存储
二叉树的链式存储也是利用链表来实现的,链表中的结点存储树结点中的元素和树结点之间的关系。
使用链式存储表示树,树中每个结点由三部分组成:数据域和左、右指针
typedef struct listTree
{
char date;//数据域
struct listTree *left,*right;//左右孩子指针
} Tree;
二叉树的遍历
首先创造一棵树
Tree* Create()
{
char n;
Tree *tree;
scanf("%c",&n);
if (n == '#')//若输入为# 则循环截止,停止输入
{
tree = NULL;
}
else
{
tree = (Tree *)malloc(sizeof(Tree));
tree->date = n;
tree->left = initTree();
tree->right = initTree();
}
return tree;
}
层序遍历
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
前序遍历
先访问根结点,然后访问左子树,最后访问右子树
实现
void show(Tree *tree)
{
if (tree == NULL)
{
return ;
}
else
{
printf("%c",tree->date);
show(tree->left);
show(tree->right);
}
}
中序遍历
先访问树的左子树,然后访问根结点,最后访问右子树
实现
//中序
void preOrder(Tree *tree)
{
if (tree == NULL)
{
return ;
}
else
{
preOrder(tree->left);
printf("%c",tree->date);
preOrder(tree->right);
}
}
后序遍历
实现
先访问树的左子树,然后访问树的右子树,最后访问根结点
//后序
void lastOrder(Tree *tree){
if (tree == NULL)
{
return ;
}
else
{
lastOrder(tree->left);
lastOrder(tree->right);
printf("%c",tree->date);
}
}
二叉树的其它操作
求叶子结点数
树中左右指针都为空的救赎叶子结点。
实现
int sum = 0;//记录叶子结点个数
void getLeafNum(Tree *tree){
if(tree == NULL)
return;
if(tree->left == NULL && tree->rigth == NULL)
sum++;//左右孩子指针为空,该结点为叶子结点,sum加一。
getLeaNum(tree->left);//递归调用计算左子树叶子结点个数
getLeaNum(tree->right);//递归调用计算右子树叶子结点数
}
二叉树的高度
将左右子树分别视为一颗独立的二叉树,根结点是树的第一层高度,分别求左右子树的高度,比较左右子树的高度,取较大的值加上根结点的高度 1 就是整树的高度加粗样式。
int Depth(Tree *tree){
int depth = 0;
int dleft = 0,dright = 0;
//定义左右子树高度
if(tree == NULL)
return 0;
dleft = Depth(tree->left);
//求左子树高度
drigth = Depth(tree->rigth);
//求右子树高度
return 1 + (dleft > drigth ? dleft : dright);
//取左右子树中较大的一个加 1 并返回
}
哈夫曼树(赫夫曼树)
路径
指从某一结点到另一结点的线路
树的路径长度
从树根到树中每一个结点的路径长度之和。
(结点数目相同的所有二叉树中,完全二叉树的路径长度最短)
树的带权路径长度
结点的带权路径长度是结点到树根之间的路径长度与该结点上权值的乘积。
树的带权路径长度是树中所有叶结点的带权路径长度之和,树的带权路径长度也称树的代价。
带权路径最小(即代价最小)的二叉树称为最优二叉树,也叫哈夫曼树(赫夫曼树)。
哈夫曼树的构造
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
哈夫曼编码
在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。例如,需传送的报文为“AFTER DATA EAR ARE ART AREA”,这里用到的字符集为“A,E,R,T,F,D”,各字母出现的次数为{8,4,5,3,1,1}。现要求为这些字母设计编码。要区别6个字母,最简单的二进制编码方式是等长编码,固定采用3位二进制,可分别用000、001、010、011、100、101对“A,E,R,T,F,D”进行编码发送,当对方接收报文时再按照三位一分进行译码。显然编码的长度取决报文中不同字符的个数。若报文中可能出现26个不同字符,则固定编码长度为5。然而,传送报文时总是希望总长度尽可能短。在实际应用中,各个字符的出现频度或使用次数是不相同的,如A、B、C的使用频率远远高于X、Y、Z,自然会想到设计编码时,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码。
为使不等长编码为前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀),可用字符集中的每个字符作为叶子结点生成一棵编码二叉树,为了获得传送报文的最短长度,可将每个字符的出现频率作为字符结点的权值赋予该结点上,显然字使用频率越小权值越小,权值越小叶子就越靠下,于是频率小编码长,频率高编码短,这样就保证了此树的最小带权路径长度效果上就是传送报文的最短长度。因此,求传送报文的最短长度问题转化为求由字符集中的所有字符作为叶子结点,由字符出现频率作为其权值所产生的哈夫曼树的问题。利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证报文编码总长最短。
每个字符的出现频率作为字符结点的权值赋予该结点上,规定左权值分支为 0 ,右权值分支为 1 。则从根结点到叶子结点经过的分支所组成的 0 和 1 串便是对应字符的编码。这就是哈夫曼编码。