树和森林
- 树:一对多的结构(可1对0,1对1,1对多),有一个起点 ‘根结点’
- 结点:树的一个数据元素
- 孩子:1对多里的 ‘多’
- 子树:以某个孩子结点为根的一棵树
- 叶子结点:没有孩子的结点
- 森林:多棵树
二叉树
- 二叉树:每个结点至多有两个孩子(可以1个或0个),分别称为左孩子和右孩子
- 左孩子(若有)是左子树的根,右孩子(若有)是右子树的根
- 高度(深度):最深的叶子结点所在层数
二叉树的重要性质
- 第i层至多有2 ^ (i - 1)个结点
- 高度为h的树至多有2 ^ h - 1个结点
Key1:二叉树每个结点有0或1或2个孩子,叶子结点没有孩子
Key2:二叉树的高度指从根结点向下直至最深叶子结点的高度
两种特殊二叉树
-
满二叉树:装满的二叉树,高为h => 有2 ^ h - 1个结点
-
完全二叉树:只在最下一层的最右边有空缺
Key1:满二叉树是满的,完全二叉树只是完整了,但不一定满
树/森林转换为二叉树
-
树转为二叉树:
- 每个结点只保留第一个孩子(老大)作为左孩子,剩下的孩子(老大的兄弟们)依次接到老大的右孩子链上
-
森林转为二叉树:
- 各树分别转为二叉树
- 各树根用右孩子链相连
Key1:森林向二叉树转化是确定且唯一的过程
二叉树顺序存储实现
- 顺序二叉树(底层是数组)
- 顺序树中结点i的左右孩子分别是2i + 1和2i + 2(i从0开始计数)
- 若结点为空,使用特殊值(如0)表示
二叉树链式实现
- 链式树(二叉链表)
/*二叉树数据结构定义*/
typedef struct TreeNode
{
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
二叉树的遍历
- 遍历:按某种确定的次序逐个访问所有结点,时间复杂度是O(n)
- 先序遍历:当前结点-左子树-右子树
- 中序遍历:左子树-当前结点-右子树
- 后序遍历:左子树-右子树-当前结点
- *层序遍历:逐层从左向右遍历各个结点
二叉树先序/中序/后序遍历
- 注意:右孩子是右子树的根,左孩子是左子树的根
- 注意:一棵树是用其根结点表示的,因从根结点出发我们足以访问整棵树
先序遍历序列:A B C
中序遍历序列:B A C
后序遍历序列:B C A
先序遍历序列:3 5 4 1 9 8 2 7
中序遍历序列:5 4 1 3 8 9 2 7
后序遍历序列:1 4 5 8 7 2 9 3
Key1:二叉树三种遍历顺序中,左子树都先于右子树,区别在于访问根的次序
Key2:可以通过中序+先序/后序序列之一来还原二叉树结构,先+后则不行
哈夫曼树与哈夫曼编码
- 先学如何构建一颗哈夫曼树,在学为什么要有哈夫曼树!
- 初始时,一堆独立结点,结点有自己的权值
- 重复地让当前权值最小的两个根结点作为左右孩子,生成新的根结点,新结点权值为他们的权值之和,直至形成一颗二叉树
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
Why哈夫曼树?
- 越靠近根节点,权值越大
- 初始结点全是后来的叶子结点
- 叶子结点权值越大,离根结点越近 => 路径越短
- 有什么好处?如果把路径标上0和1…
- 每一个叶子结点有一个唯一编码(不定长)
- 思考:如果权值表示在文章中的出现频率,这种编码有什么优势?
- 最大程度节省空间!越常用的字符码长越短。
- 这就是哈夫曼编码
- 题型:给一个字符-频率表,构造哈夫曼树来求哈夫曼编码表