一.树和森林
•树:一对多的结构(可1对0,1对1,一对多),有一个起点‘根结点’
•结点:树的一个数据元素
•孩子:1对多里的‘多’
•子树:以某个孩子结点为根的一棵树
•叶子结点:没有孩子的结点
•森林:多棵树
二.二叉树
•二叉树:每个结点至多有两个孩子(可以1个或0个),分别称为左孩子和右孩子
•左孩子(若有)是左子树的根,右孩子(若有)是右子树的根
•高度(深度):最深的叶子结点所在层数
•二叉树的重要性质:
•第i层至多有2的i-1次方个结点
•高度为h的树至多有2的h次方-1个结点
*key1:二叉树每个结点有0或1或2个孩子,叶子结点没有孩子
*key2:二叉树的高度指从根节点向下直至最深叶子结点的高度
1.由3个结点可以构成多少颗结构不同的二叉树?()
A.2 B.3 C.4 D.5
分析:根节点左右子树的结点数量存在1-1,0-2,或2-0三种可能,而2个结点构成的二叉树结构只可能有2种,因此答案为1+2+2=5。
2.一颗二叉树有1025个结点,则其高度为()
A.10 B.11 C.11至1025之间 D.10至1025之间
分析:高度为10的二叉树至多有,因此高度至少为11,1025个结点至少向下延申1025层,因此答案选C.
两种特殊的二叉树:
•满二叉树:
装满的二叉树,高为h=>有2的h次方-1个结点
•完全二叉树
只在最下一层的最右边空缺
*key1:满二叉树是满的,完全二叉树只是完整了,但不一定满
1.一颗完全二叉树有1001个结点,则叶子结点个数为()
A.250 B.254 C.500 D.501
树/森林转换为二叉树
•树转换为二叉树:
•每个结点只保留第一个孩子(老大)作为左孩子,剩下的孩子(老大的兄弟们)依次接到老大的右孩子链上
•森林转为二叉树:
1.各树分别转为二叉树
2.各树根用右孩子链相连
举例:
*key1:森林向二叉树转化是确定且唯一的过程
1.将一颗树转换为二叉树后,其形态()
A.是唯一的 B.有多种 C.有多种,但根结点都没有右孩子 D.有多种,但根结点都没有左孩子
分析:是唯一的,且根结点没有右孩子
二叉树顺序存储实现(逻辑结构)
•顺序二叉树(底层是数组)
•顺序树中结点i的左右孩子分别是2i+1和2i+2(i从0开始计数)
•若结点为空,使用特殊值(如0)表示
二叉树链式实现
•链式树(二叉链表)
/*二叉树数据结构定义*/
typedef struct TreeNode{
int data;
struct TreeNode *left;
struct TreeNode *right;
}TreeNode;
三.二叉树的遍历
•遍历:按某种确定的次序逐个访问所有结点(时间复杂度为O(n))
•先序遍历:当前结点-左子树-右子树
•中序遍历:左子树-当前结点-右子树
•后序遍历:左子树-右子树-当前结点
• *层序遍历:逐层从左向右遍历各个结点
•注意:右孩子是右子树的根,左孩子是左子树的根
•注意:一棵树是用其根结点表示的,因从根节点出发我们足以访问整棵树
二叉树先序/中序/后序遍历
*key1:二叉树三种遍历顺序中,左子树都先于右子树,区别在于访问根的次序
*key2:可以通过中序+先序/后序序列之一来还原二叉树结构,先+后则不行
1.一颗二叉树的先序遍历序列为3541982,中序遍历序列为5413892,请还原其结构
分析:中+先/后还原树结构的问题,关键点在于可以通过先/后序序列确定根结点,再通过中序遍历里根结点的位置划分左右子树,问题减小为分别还原左右子树,依次逐层往下直至还原完成。
2.为什么不一定能通过先序+后序序列还原二叉树?
分析:实际是问为何必需中序序列才可以保证还原,思考中序序列提供了什么重要信息?
提供了左右子树如何划分的重要信息,而仅仅依靠先+后无法划分左右子树
四.哈夫曼树
哈夫曼树与哈夫曼编码
•先学如何构建一颗哈夫曼树,再学为什么要有哈夫曼树!
•初学时,一堆独立结点,结点有自己的权值
•重复地让当前权值最小的两个根结点作为左右孩子,生成新的根结点,新结点权值为它们的权值之和,直至形成一颗二叉树
Why哈夫曼树?
上述过程有什么特点?
•越靠近根节点,权值越大
•初始结点全是后来的叶子结点
•叶子结点权值越大,离根结点越近=>路径越短
•有什么好处?如果把路径标上0和1.....
•每个叶子结点有一个唯一编码(不定长)
•思考:如果权值表示在文章中的出现频率,这种编码有什么优势?
•最大程度节省空间!越常用的字符码长越短
•这就是哈夫曼编码
•题型:给一个字符-频率表,构造哈夫曼树来求哈夫曼编码表
五.代码实现
/*
二叉树
*/
/* 链式二叉树数据结构定义 */
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 注意:一棵树用它的根结点来表示,因为只要有根结点,就能访问整棵树
// 二叉树先序(前序)遍历的递归实现
#include <cstdio>
void PreTraverse(TreeNode *node) {
if (!node) {
return; // 递归出口
}
printf("%d\n", node->data); // 先访问当前结点
PreTraverse(node->left); // 其次递归遍历左子树
PreTraverse(node->right); // 最后递归遍历右子树
}
// 二叉树中序遍历的递归实现
void MidTraverse(TreeNode *node) {
if (!node) {
return; // 递归出口
}
MidTraverse(node->left); // 先递归遍历左子树
printf("%d\n", node->data); // 其次访问当前结点
MidTraverse(node->right); // 最后递归遍历右子树
}
// 二叉树后序遍历的递归实现
void PostTraverse(TreeNode *node) {
if (!node) {
return; // 递归出口
}
PostTraverse(node->left); // 先递归遍历左子树
PostTraverse(node->right); // 其次递归遍历右子树
printf("%d\n", node->data); // 最后访问根结点
}
// 求树的高度(深度)的递归实现
int GetHeight(TreeNode *node) {
if (!node) {
return 0;
}
int lh = GetHeight(node->left);
int rh = GetHeight(node->right);
// 返回 1 + 左右子树中高度较大者,这里1是指当前结点贡献1个高度
return 1 + (lh > rh ? lh : rh);
}
// 二叉树的层序遍历(利用队列)
#include <queue>
void LevelTraverse(TreeNode *node) {
if (!node) {
return;
}
std::queue<TreeNode *> q; // 队列元素是TreeNode*
q.push(node);
while (!q.empty()) {
TreeNode *cur = q.front(); // 获取当前队首
printf("%d\n", cur->data); // 访问之
q.pop(); // 队首离开队伍
if (cur->left) {
q.push(cur->left); // 左孩子(若有)入队
}
if (cur->right) {
q.push(cur->right); // 右孩子(若有)入队
}
}
}
练习: