目录
一、树的性质
1、易错术语
1.1 树的度:节点的最大度数
1.2 节点的深度:节点所在层次
1.3 树的高度/深度:节点的最大层数
1.4 节点的高度:以该节点为根的子树的高度
1.5 节点间路径长度:路径上所经过的边的个数
1.6 树的路径长度:从树根到每个节点的路径长度的总和
1.7 结点的带权路径长度:从树的根到一个结点的路径长度与该结点上权值的乘积
1.8 树的带权路径长度:树中所有叶节点的带权路径长度之和
1.9 编码加权平均长度:WPL/结点频次之和
2、基本性质
2.1 n个节点的树有n-1条边 (每个节点上面都有一条边,除去根)
2.2 树的节点数n等于所有节点的度数之和+1 (每个节点上面都有一个度,除去根)(度之和=边之和)
2.3 度为m的树中第i层上至多有个节点
2.4 高度为h的m叉树至多有个节点(满,等比数列);至少有h个
2.5 度为m,具有n个节点的树的最小高度为
2.6 度为m,具有n个节点的树的最大高度为n-m+1
2 7 一颗m叉树有N1个度数为1的节点,N2个度数为2的节点....Nm个度数为m的节点,则该树中共有个叶节点 (出度N1+2N2+...+mNm+1=入度N1+N2+...Nm+x)
2.8 一棵树对应二叉树的分支数=叶节点数=有兄弟节点数+1
二、二叉树
1、几种特殊二叉树
1.1 满二叉树:对于编号i的节点,其双亲为.,左孩子为2,右孩子为2i+1
1.2 完全二叉树:编号按顺序一一对应
1.3 二叉排序树:左<根<右
1.4 平衡二叉树:树中任意一个节点的左子树和右子树的高度之差的绝对值不超过1
1.5 正则二叉树:每个分支节点都有2个孩子
2、基本性质
2.1 非空二叉树:n0=n2+1
2.2 完全二叉树:若i<=,则i为分支节点,否则为叶节点。因此叶节点比非叶节点多1/相等
若有度为1的节点,则最多只有一个
一旦出现i为叶节点或只有左孩子,则大于i的节点均为叶节点
n为奇数时,则每个分支节点都有左右孩子
当i>1时,节点i的双亲为
节点i所在层次为
具有n个节点的完全二叉树的高度为或
2.3 一颗有n个节点的满二叉树有(n-1)/2个分支节点,(n+1)/2个叶节点
3、存储结构
//顺序存储
#define Maxsize 100
struct TreeNode {
int value;
bool isEmpty;
};
//初始化
void Init(TreeNode t[]) {
for (int i = 0; i < Maxsize; i++)
t[i].isEmpty = true;
}
//链式存储
typedef struct BiTNode{
int data;
struct BiTNode* lchild, * rchild;
}BiTNode,*BiTree
重要结论:在含有n个节点的二叉链表中,含有n+1个空指针域
三、二叉树的遍历
1、递归遍历
void Preorder(BiTree T) {//前序遍历
if (T) {
visit(T);
Preorder(T->lchild);
Preorder(T->rchild);
}
}
void Inorder(BiTree T) {//中序遍历
if (T) {
Inorder(T->lchild);
visit(T);
Inorder(T->rchild);
}
}
void Postorder(BiTree T) {//后序遍历
if (T) {
Postorder(T->lchild);
Postorder(T->rchild);
visit(T);
}
}
//求树高
int TreeDepth(BiTree T) {
if (T == NULL)return 0;
else {
int l = TreeDepth(T->lchild);
int r = TreeDepth(T->rchild);
return l > r ? l + 1 : r + 1;
}
}
时间复杂度:O(n),递归工作栈的栈深=树的深度
若要删除二叉树表的所有结点并释放占用的存储空间,后序遍历方法更合适。因为删除一个结点时,需要先递归地删除左右孩子,并释放他们的占用存储空间,再然后删除该结点。
2、非递归遍历
以中序遍历为例:
①沿着根的左孩子,依次入栈,直到左孩子为空
②栈顶元素出栈访问;若其右孩子为空,重新执行②,也就是往上退。若右孩子不空,将右孩子转为①
void Inorder(BiTree T) {
InitStack(S);
BiTree p = T;
while (p || !IsEmpty(S)){ //树不空且栈不空
if (p) { //当前节点不空,那么就入栈,并往左走
push(S, p);
p = p->lchild;
}
else { //一直走到了左尽头,先出栈,并访问,转为右走
pop(S, p);
visit(p);
p = p->rchild;
}
}
}
void Preorder(BiTree T) {
InitStack(S);
BiTree p = T;
while (p || !IsEmpty(S)) { //树不空且栈不空
if (p) {
visit(p);
push(S, p);
p = p->lchild;
}
else {
pop(S, p);
p = p->rchild;
}
}
}
在后序非递归遍历中:访问某个节点p,此时栈中节点恰好是p的所有祖先,从栈底到栈顶再加上p,刚好构成从根节点到p的一条路径、因此后序非递归遍历可以求根到某节点的路径,求两个节点的最近公共祖先等
3、层次遍历
借助队列
①根节点入队
②若队列不空,则队头节点出队,访问该节点,若有左孩子,左孩子入队;若有右孩子,右孩子入队。重复直至队列为空
void LevelOder(BiTree T) {
InitQueue(Q);
BiTree p;
EnQueue(Q,T);//根入队
while(!IsEmpty(Q)){//队不空
DeQueue(Q, p);//队头出队
visit(p);
if (p->lchild != NULL)
EnQueue(Q, p->lchild);
if (p->rchild != NULL)
EnQueue(Q, p->rchild);
}
4、出题
比如说:先序序列为abcd的不同的二叉树的个数是多少=以abcd入栈次序,则出栈序列的个数为多少。即公式:前序序列和中序序列的关系相当于以前序序列为入栈次序,以中序序列为出栈次序。而可以唯一确定一棵二叉树。
因为:
四、线索二叉树
目的:加快查找结点前驱和后继的速度
二叉树是一种逻辑结构,但线索二叉树是加上线索后的链表结构,即它是二叉树在计算机内部的一种存储机构,是物理结构
规定:若无左子树,lchild指向前驱(此操作是在p指向当前结点时进行);若无右子树,rchild指向后继(此操作是在pre指向当前结点的时候进行)
1、存储结构
//存储结构
typedef struct ThreadNode{
int data;
int ltag, rtag;
struct ThreadNode *lchild, *rchild;
}ThreadNode, *ThreadTree;
2、中序线索二叉树的构造
线索化的实质就是遍历一遍二叉树
在写代码的时候可以画一棵树来模拟一下
void InThread(ThreadTree &p, ThreadTree &pre) {
if (p) {
InThread(p->lchild, pre);
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild, pre);
}
}
void CreatInThread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
InThread(T, pre);
pre->rchild = NULL;//处理最后一个结点
pre->rtag = 1;
}
}
如何找前驱:p==x时,pre为x的前驱 pre==x时,q为x的后继
3、中序线索二叉树的遍历
①找到遍历的第一个结点
②找后继:若其右标志为1,则右链为线索,指向后继;否则,遍历右子树中第一个访问的结点为后继。
ThreadNode* FirstNode(ThreadNode* p) {
while (p->ltag == 0)p = p->lchild;
return p;
}
//找遍历的最后结点
ThreadNode* LastNode(ThreadNode* p) {
while (p->rtag == 0)p = p->rchild;
return p;
}
//找结点p的后继
ThreadNode* Nextnode(ThreadNode* p) {
if (p->rtag == 0)return FirstNode(p->rchild);//右子树中遍历的第一个结点为其后继
else return p->rchild;
}
//找结点p的前驱
ThreadNode* Prenode(ThreadNode* p) {
if (p->ltag == 0)return LastNode(p->lchild);//左子树中遍历的最后一个结点为其前驱
else return p->lchild;
}
void Inorder(ThreadNode* T) {
for (ThreadNode* p = FirstNode(T); p; p = Nextnode(p))
visit(p);
}
同上,编写代码时可以画树模拟
在先序线索二叉树中,若有左孩子,左孩子就是后继;若无左孩子但是有右孩子,右孩子为后继;若为叶节点,其右链指向后继。但查找前驱需要直到该结点的双亲
在后序线索二叉树中,找后继需要直到结点的双亲。后序线索树的遍历需要栈的支持
五、树、森林
1、树的存储结构
1.1、双亲表示法
每个结点增加一个parent域,表示双亲结点在数组中规定位置
规定:根结点的下标为0,parent域为-1
//双亲表示法
#define Max_Tree_Size 100
typedef struct {
int data;
int parent;
}PTNode;
typedef struct { //树的定义
PTNode nodes[Max_Tree_Size];
int n; //结点数
}PTree;
此方法可以很快地找到结点的双亲,但求结点的孩子时要遍历整个结构(并查集)
注意:二叉树可以用树的存储结构来存储,但树不能都用二叉树的存储结构来存储
1.2、孩子表示法
将每个结点的孩子排成一个单链表,n个结点就有n个孩子链表。n个头指针组成一个线性表
//孩子表示法
struct CTNode{ //孩子链表结点的定义
int child; //孩子结点在数组中的位置
struct CTNode* next;
};
typedef struct { //头指针的定义
int data;
struct CTNode* FirstChild;
}CTBox;
typedef struct { //树的类型定义
CTBox nodes[Max_Tree_Size];
int n, r; //结点数和根的位置
}CTree;
寻找孩子的操作非常方便,而寻找双亲的操作则需要遍历n个结点中孩子链表的指针域所指向的n个孩子链表
1.3、孩子兄弟表示法
//孩子兄弟表示法
typedef struct CSNode {
int data;
struct CSNode* firstchild, * nextsibling; //指向结点第一个孩子的指针,指向结点下一个兄弟结点的指针
}CSNode,*CSTree;
优点:可以方便地实现树转换为二叉树的操作,易于查找结点的孩子。但找双亲比较麻烦。
在森林中,根结点看作平级的兄弟
2、树、森林、二叉树的转换
2.1、树转换为二叉树
左孩子右兄弟,注:树转换为二叉树没有右子树
2.2、森林转换为二叉树
把每一棵树转换为二叉树,而树的根为平级的兄弟。相连即可
2.3、二叉树转换为森林
二叉树的右子树的右边均是各树的头节点。二叉树转换为树或森林是唯一的
3、树和森林的遍历
3.1、树-先根遍历
先根,再依次遍历根节点的每棵子树。其遍历序列与对应二叉树的先序序列相同
3.2、树-后根遍历
先依次遍历根结点的每棵子树,再访问根节点、其遍历序列与对应二叉树的中序 序列相同
3.3、森林-先序遍历
先访问森林中第一树根根节点,对该树先序遍历,结束后,遍历下一课树
3.4、森林-中序遍历
中序遍历第一棵树的根节点的子树,再访问第一棵树的根节点,依次-
3.5、森林与二叉树遍历方法对应关系
在森林转换为二叉树时,第一棵树的子树是二叉树的左子树,剩余树为二叉树的右子树。所以森林的先序和中序遍历对应其二叉树的先序和中序遍历。
树和森林的后根=二叉树的中序
六、树与二叉树的应用
1、哈夫曼树和哈夫曼编码
1.1、定义
带权路径长度(WPL)最小的二叉树,也称最优二叉树
1.2、哈夫曼树的构造
每次选出两个权值最小的结点或树,进行结合,新结点的权值为左右子树上根节点的权值之和,重复
1.3、性质
①每个初始结点都称为叶节点,且权值越小的结点到根节点的路径长度越大
②构造过程中新建了n-1个结点,因此哈夫曼树的总结点数为2n-1
③不存在度为1的结点
④度为m的哈夫曼树,叶子节点个数为n,则非叶子结点的个数为
结点总数为N=n0+nm ,而因N个结点的哈夫曼树有N-1个分支,则m*nm=N- 1=nm+n0-1 整理可得
1.4、哈夫曼编码
固定长度编码:对每个字符用相等长度的二进制位表示
可变长度编码:对不同字符用不等长的二进制位表示(压缩数据。对频率高的字符赋以短编码,反之长一些的编码,使字符的平均编码长度减短)
前缀编码:没有一个编码是另一个编码的前缀---二叉树设计二进制前缀编码
哈夫曼编码:数据压缩编码;权值为频率,构造相应哈夫曼树,然后将从根到叶节点的路径上分支标记的字符串作为该字符的编码
2、并查集
2.1、作用
①判断连通性,判断环
②Krnskal算法:排序+并查集
2.2、存储结构
通常用树的双亲表示法作为存储结构
数组下标为元素名称,数组内容为双亲下标。根结点的下标代表子集合名
2.2、基本实现
#define SIZE 100
int UPSets[SIZE];
//初始化
void Initial(int S[]) {
for (int i = 0; i < SIZE; i++)
S[i] = -1;
}
//在并查集S中查找并返回包含元素X的根
int Find(int S[], int x) { //数组元素的下标代表元素名
while (S[x] >= 0) //S[x]双亲的下标
x = S[x]; //一层一层往上找
return x;
}
//把集合S中的子集合ROOT2并入子集合ROOT1,要求两者不相交
void Union(int S[], int Root1, int Root2) {
if (Root1 == Root2)return;
S[Root2] = Root1; //将根Root2连接到另一根Root1下面
}
Find和Union的时间复杂度分别为O(d),O(1) ,D为树的深度
2.3、实现的优化
①极端情况下,并查集可能是一条线的形式,改进:把小树合并到大树,令根节点的绝对值保存集合树种的成员数量
void Union(int S[], int Root1, int Root2) {
if (Root1 == Root2)return;
if (S[Root1] > S[Root2]) {
S[Root1] += S[Root2];
S[Root2] = Root1;
}
else {
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
树的高度不超过,其中Find为O(
),Union为O(1)
②压缩路径,为进一步减少确定元素所在集合的时间,把根从元素x路径上的元素都变成根的孩子
int Find(int S[], int x) {
int root = x;
while (S[root] >= 0)
root = S[root];//找到根
while (x != root) {//压缩路径
int t = S[x]; //t为x的父节点
S[x] = root; //将x挂到根节点下面
x = t; //进行上一层的路径压缩
}
return root;
}
压缩后,集合树的深度不超过O(a(n)),通常a(n)<=4