这是本人根据王道考研数据结构课程整理的笔记,希望对您有帮助。
5.1 树
5.1.1 树的定义和基本术语
树的基本概念
非空树的特性:
- 有且仅有一个根节点
- 没有后继的结点称为“叶子结点”(或终端结点)
- 有后继的结点称为“分支结点”(或非终端结点)
- 除了根节点外,任何一个结点都有且仅有一个前驱
- 每个结点可以有0个或多个后继
空树:结点数为0的树
子树:
森林:森林是
m
(
m
≥
0
)
m(m\ge0)
m(m≥0)棵互不相交的树的集合(森林可以和树发生转换)
结点之间的关系描述
- 祖先结点:父亲、爷爷
- 子孙结点:K、L
- 父结点(双亲结点):父亲
- 孩子结点:K、L
- 兄弟结点:F
- 堂兄弟结点:G、H、I、J
- 两个结点之间的路径:只能从上往下
- 路径长度:经过几条边
- 结点的层次(深度):从上往下数(默认从1开始)
- 结点的高度:从下往上数
- 树的高度(深度):总共多少层
- 结点的度:有几个孩子(分支)
- 树的度:各结点的度的最大值
- 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
- 无序树:逻辑上看,树中结点的各子树从左至右是无次序的,不能互换
5.1.2 树的性质
-
结点数 = 总度数 + 1
-
度为m的数 和 m叉树 的区别
-
【度为 m m m 的树 / m m m 叉树】第 i i i 层至多有 m i − 1 m^{i-1} mi−1 个结点( i ≥ 1 i\ge1 i≥1)
-
高度为 h h h 的 m m m 叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1 个结点
-
高度为 h h h 的 m m m 叉树至少有 h h h 个结点。
-
高度为 h h h ,度为 m m m 的树至少有 h + m − 1 h+m-1 h+m−1 个结点
-
具有 n n n 个结点的 m m m 叉树的最小高度为 ⌈ log m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m(n(m-1)+1) \rceil ⌈logm(n(m−1)+1)⌉(向上取整)
5.2 二叉树
5.2.1 二叉树的定义和基本术语
二叉树的特点
- 每个结点至多只有两颗子树
- 左右子树不能颠倒(二叉树是有序树)
- 二叉树由一个根结点和两个互不相交的左子树和右子树组成。左子树和右子树又分别是一颗二叉树。
二叉树的五种状态
特殊的二叉树
满二叉树:一棵高度为
h
h
h,且含有
2
h
−
1
2^h-1
2h−1个结点的二叉树
特点:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序从1开始编号,结点 i i i 的左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1;结点 i i i 的父节点为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor ⌊2i⌋(如果有的话)
完全二叉树:当且仅当其每个结点都与高度为
h
h
h 的满二叉树中编号为
1
∼
n
1 \sim n
1∼n 的结点一一对应时,称为完全二叉树。
特点:
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点
- 按层序从1开始编号,结点 i i i 的左孩子为 2 i 2i 2i,右孩子为 2 i + 1 2i+1 2i+1;结点 i i i 的父节点为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor ⌊2i⌋(如果有的话)
- i ≤ ⌊ n 2 ⌋ i \le\lfloor \frac{n}{2} \rfloor i≤⌊2n⌋为分支结点, i > ⌊ n 2 ⌋ i \gt \lfloor \frac{n}{2} \rfloor i>⌊2n⌋为叶子结点
- 如果某结点只有一个孩子,那么一定是左孩子
二叉排序树(可用于元素的排序、搜索)
- 左子树上所有结点的关键字均小于根节点的关键字
- 右子树上所有结点的关键字均大于根节点的关键字
- 左子树和右子树又各是一颗二叉排序树
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
特点:平衡二叉树能有更高的搜索效率
5.2.2 二叉树的性质
二叉树的性质
-
设非空二叉树中度为0、1和2的结点个数分别为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1(叶子结点比二分支结点多一个)
-
二叉树第 i i i 层至多有 2 i − 1 2^{i-1} 2i−1 个结点 ( i ≥ 1 ) (i\ge1) (i≥1)
-
高度为 h h h 的二叉树至多有 2 h − 1 2^h-1 2h−1 个结点(满二叉树)
完全二叉树的性质
-
具有 n n n 个 ( n > 0 (n\gt0 (n>0) 结点的完全二叉树的高度 h h h 为 ⌈ log 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil ⌈log2(n+1)⌉ 或 ⌊ log 2 n ⌋ + 1 \lfloor \log_2 n \rfloor +1 ⌊log2n⌋+1。
-
对于完全二叉树,可以由结点数 n n n 推出度为0、1和2的结点个数为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2。
5.2.3 二叉树的存储结构
顺序存储
#define MaxSize 100
struct TreeNode
{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
};
TreeNode t[MaxSize];
//定义一个长度为MaxSize的数组t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点
基本操作:
- i i i 的左孩子: 2 i 2i 2i
- i i i 的右孩子: 2 i + 1 2i+1 2i+1
- i i i 的父结点: ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor ⌊2i⌋
- i i i 所在的层次: ⌈ log 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil ⌈log2(n+1)⌉ 或 ⌊ log 2 n ⌋ + 1 \lfloor \log_2 n \rfloor +1 ⌊log2n⌋+1
若完全二叉树中共有 n n n 个结点,则:
- 判断 i i i 是否有左孩子: 2 i ≤ n 2i\le n 2i≤n
- 判断 i i i 是否有右孩子: 2 i + 1 ≤ n 2i+1\le n 2i+1≤n
- 判断 i i i 是否是叶子/分支结点: i > ⌊ n 2 ⌋ i\gt\lfloor \frac{n}{2} \rfloor i>⌊2n⌋
✨二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。(只适合存储完全二叉树)
链式存储
typedef struct BiTNode
{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
✨ n n n 个结点的二叉链表共有 n + 1 n+1 n+1 个空链域(可用于构造线索二叉树)
//【使用示例】
struct ElemType
{
int value;
}
typedef struct BiTNode
{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根结点
root = (BiTNode *)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p; //作为根结点的左孩子
如果在使用场景中需要寻找父结点,可以考虑使用三叉链表,方便寻找父结点:
typedef struct BiTNode
{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父结点指针
}BiTNode, *BiTree;
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
先序遍历:根左右(NLR)
void PreOrder(BiTree T)
{
if(T != NULL)
{
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
中序遍历:左根右(LNR)
void InOrder(BiTree T)
{
if(T != NULL)
{
PreOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
PreOrder(T->rchild); //递归遍历右子树
}
}
后序遍历:左右根(LRN)
void PostOrder(BiTree T)
{
if(T != NULL)
{
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
5.3.2 二叉树的层序遍历
算法思想:
- 初始化一个辅助队列
- 根节点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 重复3直至队列为空
//二叉树的结点(链式存储)
typedef struct BiTNode
{
char data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode
{
BiTNode *data; //存指针而不是结点
struct LinkNode *next;
}LinkNode;
typedef struct
{
LinkNode *front, *rear; //队头队尾
}LinkQueue;
//【层序遍历】
void LevelOrder(BiTree T)
{
LinkQueue Q;
InitQueue(Q); //初始化辅助序列
BiTNode* 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); //右孩子入队
}
}
5.3.3 由遍历序列构造二叉树
若只给出一棵二叉树的前序/中序/后序/层序遍历序列中的一种,不能唯一确定一棵二叉树。
前序+中序/后序+中序/层序+中序可以唯一确定一棵二叉树。(找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根节点)
5.3.4 线索二叉树的概念
约定:根据遍历序列定义前驱/后继的概念
中序线索二叉树
typedef struct ThreadNode
{
ElemType data; //数据域
struct ThreadNode *lchild, *rchild; //左、右孩子指针/前驱、后继线索指针
int ltag,rtag; //左、右线索标志
//【tag=0,表示指针指向孩子】
//【tag=1,表示指针是“线索”】
}ThreadNode, *ThreadTree;
先序线索二叉树
后续线索二叉树
5.3.5 二叉树的线索化
中序线索化
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
//中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
pre = NULL; //pre初始化为NULL
if(T != NULL) //非空二叉树才能线索化
{
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
//线索二叉树结点
typedef struct ThreadNode
{
ElemType data; //数据域
struct ThreadNode *lchild, *rchild; //左、右孩子指针/前驱、后继线索指针
int ltag,rtag; //左、右线索标志
//【tag=0,表示指针指向孩子】
//【tag=1,表示指针是“线索”】
}ThreadNode, *ThreadTree;
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T)
{
if(T != NULL)
{
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根结点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q)
{
if(q->lchild == NULL) //左子树为空,建立前驱线索
{
q->lchild = pre;
q->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rchild = q; //建立前驱结点的后继搜索
pre->rtag = 1;
}
pre = q;
}
(先序和后序略,如果用到去视频找)
5.3.6 在线索二叉树中找前驱后继
中序线索二叉树找中序后继
//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p)
{
//循环找到最左下结点(不一定是叶结点)
while(p->ltag == 0)
p = p->lchild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p)
{
//右子树中最左下结点
if(p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T)
{
for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
visit(p);
}
中序线索二叉树找中序前驱
//找到以p为根的子树中最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p)
{
//循环找到最右下结点(不一定是叶结点)
while(p->rtag == 0)
p = p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p)
{
//左子树中最右下结点
if(p->ltag == 0)
return Lastnode(p->lchild);
else
return p->lchild; //ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历(利用线索实现的非递归算法)
void RevInorder(ThreadNode *T)
{
for(ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p))
visit(p);
}
(先序和后序略,如果用到去视频找)
5.4 树、森林
5.4.1 树的存储结构
双亲表示法(顺序存储):每个结点中保存指向双亲的“指针”
#define MAX_TREE_SIZE 100 //树中最多结点树
//树的结点定义
typedef struct
{
ElemType data; //数据元素
int parent; //双亲位置域
}PTNode;
//树的类型定义
typedef struct
{
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
缺点:查找操作特别困难,需要从头到尾遍历
孩子表示法(顺序+链式存储)
struct CTNode
{
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct
{
ElemType data;
struct CTNode *firstChild; //第一个孩子
}CTBox;
typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int n, r; //结点数和根的位置
}CTree;
特点:查找孩子很方便,但是查找双亲很困难
✨孩子兄弟表示法(链式存储)
typedef struct CSNode
{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针
}CSNode, *CSTree;
相当于二叉链表中的左指针指向孩子,右指针指向兄弟
优点:可以用熟悉的二叉树操作来处理树
森林和二叉树的转换
5.4.2 树和森林的遍历
先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历。——深度优先遍历
void PreOrder(TreeNode *R)
{
if(R != NULL)
{
visit(R); //访问根结点
while(【R还有下一个子树T】)
PreOrder(T); //先根遍历下一棵子树
}
}
树的先根遍历序列与这棵树相应的二叉树的先序序列相同。
后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。——深度优先遍历
void PostOrder(TreeNode *R)
{
if(R != NULL)
{
while(【R还有下一个子树T】)
PreOrder(T); //先根遍历下一棵子树
visit(R); //访问根结点
}
}
树的后根遍历序列与这棵树相应的二叉树的中序序列相同。
层次遍历:与二叉树一致(用队列实现)——广度优先遍历
森林的先序遍历:对各个子树进行先根遍历 【or】 将森林转成二叉树,对二叉树进行先序遍历
森林的中序遍历:对各个子树进行后根遍历 【or】 将森林转成二叉树,对二叉树进行中序遍历
5.5 树与二叉树的应用
5.5.1 二叉排序树(BST)
二叉排序树,又叫二叉查找树,BST, Binary Search Tree
- 左子树上所有结点的关键字均小于根结点的关键字
- 右子树上所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一棵二叉排序树
- 左子树结点值 < 根结点值 < 右子树结点值
- 对二叉排序树进行中序遍历,可以得到一个递增的有序序列
二叉排序树的查找
//【二叉排序树结点】
typedef struct BSTNode
{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//【在二叉排序树中查找值为key的结点】
BSTNode *BST_Search(BSTree T, int key)
{
while(T != NULL && key != T->key) //若树空或等于根结点值,则结束循环
{
if(key < T->key) //小
T = T->lchild;
else
T = T->rchild;
}
return T;
}
//【在二叉排序树中查找值为key的结点(递归实现)】
BSTNode *BST_Search(BSTree T, int key)
{
if(T == NULL)
return NULL; //查找失败
if(key == T->key)
return T; //查找成功
else if(key < T->key)
return BSTSearch(T->lchild, key); //在左子树中找
else
return BSTSearch(T->rchild, key); //在右子树中找
}
return T;
}
二叉排序树的插入
若原二叉排序树为空,则直接插入结点。否则,若关键字 k k k小于根结点值,则插入到左子树;若关键字 k k k 大于根结点值,则插入到右子树。
//【在二叉排序树插入关键字为k的新结点(递归实现)】
int BST_Insert(BSTree &T, int k)
{
if(T == NULL) //原树为空,新插入的结点为根结点
{
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //返回1,插入成功
}
else if(k == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key) //插入到T的左子树
return BST_Insert(T->lchild, k);
else //插入到T的右子树
return BST_Insert(T->rchild, k);
}
二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T, int str[], int n)
{
T = NULL; //初始时T为空树
int i = 0;
while(i < n) //依次将每个关键字插入到二叉排序树中
{
BST_Insert(T, str[i]);
i++;
}
}
二叉排序树的删除
先搜索找到目标结点:
- 若被删除结点 z z z 是叶结点,则直接删除,不会破坏二叉排序树的性质。
- 若结点 z z z 只有一棵左子树或右子树,则让 z z z 的子树成为 z z z 父节点的子树,替代 z z z 的位置。
- 若结点 z z z 有左、右两棵子树,则令 z z z 的直接后继(或直接前驱)替代 z z z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
注:
- z z z 的前驱: z z z 的左子树中最右下结点(该结点一定没有右子树)
- z z z 的后继: z z z 的右子树中最左下结点(该结点一定没有左子树)
5.5.2 平衡二叉树(AVL)
平衡二叉树:树上任一结点的左子树和右子树的高度之差不超过1。
结点的平衡因子 = 左子树高 - 右子树高
//平衡二叉树结点
typedef struct AVLNode
{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;
最小不平衡子树:从插入点往回找到的第一个不平衡结点。
每次调整的对象都是“最小不平衡子树”。
调整最小不平衡子树:
- LL:在A的左孩子的左子树中插入导致不平衡
- RR:在A的右孩子的右子树中插入导致不平衡
- LR:在A的左孩子的右子树中插入导致不平衡
- RL:在A的右孩子的左子树中插入导致不平衡
调整最小不平衡子树(LL)
调整最小不平衡子树(RR)
调整最小不平衡子树(LR)
调整最小不平衡子树(RL)
5.5.3 哈夫曼树
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
WPL
=
∑
i
n
w
i
l
i
\text{WPL}=\sum_i^n{w_il_i}
WPL=i∑nwili
在含有 n n n 个带权结点的二叉树中,其中带权路径长度WPL最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树的构造
- 将这 n n n 个结点分别作为 n n n 棵仅含一个结点的二叉树,构成森林 F F F。
- 构造一个新结点,从 F F F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从 F F F 中删除刚才选出的两棵树,同时将新得到的树加入 F F F 中。
- 重复步骤2和3,直至
F
F
F 中只剩下一棵树为止。
结论:
- 每个初始结点最终都称为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为 2 n − 1 2n-1 2n−1
- 哈夫曼树中不存在度为1的结点
- 哈夫曼树并不唯一,但WPL必然相同且为最优
应用:哈夫曼编码