文章目录
一、树的基本定义
树是n( n ≥ 0 n \geq 0 n≥0)个节点的有限集。当n=0时,称为空树。每一棵非空树应该满足:
- 有且仅有一个根结点
- 当n>1时,其余结点分为m(m>0)个互不相交的有限集合T1,T2,…,Tn,每个集合本身又是一棵树,称为根的子树(递归结构、分层结构)
- 没有后继的结点称为“叶子结点”(度为0),没有前驱的结点为“根结点",有后继的结点称为”分支结点”(度大于0,每个结点的分分支数为该结点的度)
- 除了根节点之外所有结点都有且只有一个前驱,树的所有结点可以有零个或多个后继
关系描述:
- 祖先结点(根结点到结点A的唯一路径上的所有结点)、子孙结点、双亲结点(父结点:一个结点的直接前驱)、孩子结点(一个结点的直接后继)、兄弟结点(同一个父亲的孩子)、堂兄弟结点
- 度:树中一个结点的孩子个数为该结点的度;树的度:树中结点的最大度数
- 层次:根节点为第一层(有些教材为第0层,看题目要求,一般默认为一层),它的子结点为第二层,依次类推;树的深度(高度):树中结点的最大层数。高度:从下往上;深度:从上向下(从根结点开始)
- 有序树:树中结点的各个子树从左到右是由次序的,不能互换,否则为无序树
- 路径:从上到下(同一双亲不同孩子之间不存在路径),结点A到结点B经过的结点序列;路径长度:路径中经过的边的个数
- 森林:m( m ≥ 0 m \geq 0 m≥0)棵互不相交的树的集合(树—>森林:去掉根节点;森林—>树:添加根节点)
二、树的性质
-
结点数=总度数+1(结点的度:结点有几个孩子)
-
树的度——各结点的度的最大值 m叉树——每个结点最多只能有m个孩子的树 度为m的树 m叉树 任意结点的度 ≤ m \leq m ≤m(最多有m个孩子) 任意结点的度 ≤ m \leq m ≤m(最多有m个孩子) 至少有一个结点度=m(有m个孩子) 允许所有结点的度都<m 一定是非空树 可以是空树 -
度为m的树第i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥1),也可以说m叉树的第i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥1)
-
高度为h的m叉树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点(等比数列求和)
-
高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有h+m-1个结点
-
具有n个结点的m叉树最小高度为 ⌈ l o g m n ( m − 1 ) + 1 ⌉ \lceil log_m^{n(m-1) + 1} \rceil ⌈logmn(m−1)+1⌉
三、二叉树
二叉树(一种有序树)是n( n ≥ 0 n \geq 0 n≥0)个结点的有限集合:
- 或者为空二叉树,即n=0。
- 或者有一个根结点和两个互不相交的被称为根的左子树和右子树组成(有左右之分,次序不可颠倒)。左子树和右子树分别是一棵二叉树。(二叉树中不存在度大于2 的结点)
特殊二叉树
- 满二叉树:一棵高度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 i/2\rfloor ⌊i/2⌋(如果存在的话)
- 完全二叉树:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。特点如下
- 只有最后两层可能有叶子节点
- 最多只有一个度为1的结点
- 同上③
- i ≤ ⌊ n / 2 ⌋ i\leq \lfloor n/2\rfloor i≤⌊n/2⌋为分支结点, i > ⌊ n / 2 ⌋ i>\lfloor n/2\rfloor i>⌊n/2⌋为叶子结点
- 完全二叉树如果某结点只有一个孩子,那么一定是左孩子
- 最后两层出现叶子结点,对于最大层次中的叶子结点,都依次排列在该层的最左边的位置上
- 按照层序编号,一旦发现某结点i为叶子结点或者只有左孩子,则编号大于i的结点均为叶子结点
- 若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,编号最大的分支节点(编号n/2)只有左孩子,没有右孩子,其余分支结点左右孩子都有
-
二叉排序树(BST):首先是二叉树(空树或者是非空树)
- 左子树上所有结点的关键字都小于根节点的关键字
- 右子树上所有结点的关键字都大于根节点的关键字
- 左子树和右子树又各自是一棵二叉排序树
- 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1(拥有更高的搜索效率)
二叉树的性质
普通二叉树
- 二叉树叶子结点数等于度为2的结点数加一
- 二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 1 i \geq 1 i≥1)(m叉树第i层至多有 m i − 1 m^{i-1} mi−1个结点( i ≥ 1 i \geq 1 i≥1))
- 高度为h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点(满二叉树)–>等比数列求和
完全二叉树
- 具有n(n>0)个结点的完全二叉树的高度 ⌈ l o g 2 n + 1 ⌉ \lceil log_2^{n+1} \rceil ⌈log2n+1⌉或者 ⌊ l o g 2 n + 1 ⌋ \lfloor log_2^n+1 \rfloor ⌊log2n+1⌋
- 对于完全二叉树,可以有结点数推出度为0、1和2的个数n0、n1和n2
- 若完全二叉树有2k(偶数)个结点,则必有n1=1,n0=k,n2=k-1
- 若完全二叉树有2k-1(奇数)个结点,则必有n1=0,n0=k,n2=k-1
- 对于完全二叉树,层序排列为i,若 2 i ≤ n 2i \leq n 2i≤n,2i为其左孩子,否则无左孩子;若 2 i + 1 ≤ n 2i+1 \leq n 2i+1≤n,2i+1为其右孩子,否则无右孩子
- 结点i所在的层次(深度)为 ⌊ log 2 i ⌋ + 1 \lfloor\log_2i\rfloor+1 ⌊log2i⌋+1
满m叉树
- 第k层结点的个数为 m k − 1 m^{k-1} mk−1
- 编号为 i ( i ≥ 1 ) i(i\geq1) i(i≥1)的结点的双亲结点的编号为 ⌊ ( i − 2 ) / m ⌋ + 1 \lfloor (i-2)/m \rfloor+1 ⌊(i−2)/m⌋+1
- 编号为 i i i的结点第一个孩子结点(若存在)的编号为 j = ( i − 1 ) ∗ m + 2 j=(i-1)*m+2 j=(i−1)∗m+2,第k ( 1 ≤ k ≤ m ) (1\leq k \leq m) (1≤k≤m)个孩子的编号为 j = ( i − 1 ) ∗ m + k + 1 j=(i-1)*m+k+1 j=(i−1)∗m+k+1
二叉树的存储结构
顺序存储
顺序存储比较适合——>满二叉树或者完全二叉树
// 二叉树的顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value;
bool isEmpty;
};
// 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来,可以用0表示不存在的空结点
TreeNode t[MaxSize];
若是存储二叉树的数组下标是从1开始,则有以下性质:i的左孩子: 2 i 2i 2i;i的右孩子: 2 i + 1 2i+1 2i+1;i的父节点: ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋;i所在的层次: ⌊ log 2 i ⌋ + 1 \lfloor\log_2i\rfloor+1 ⌊log2i⌋+1或者 ⌈ l o g 2 ( i + 1 ) ⌉ \lceil log_2(i+1)\rceil ⌈log2(i+1)⌉;判断i是否使叶子结点/分支结点: s > ⌊ n / 2 ⌋ ? s> \lfloor n/2\rfloor? s>⌊n/2⌋?
链式存储
n个结点一共有2n个指针域,共有n-1个指针指向其他结点,故有n+1个空链域(用于构造线索二叉树)
// 二叉树的链式存储
// 二叉树的结点
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
二叉树的先中后遍历
遍历:按照某种次序,把所有结点都访问一边
二叉树的递归特性:①要么是个空二叉树;②要么是由“根结点+左子树+右子树”组成的二叉树
先序遍历:根左右;中序遍历:左根右;后序遍历:左右根
前、中、后序遍历算法如下:——》递归版
// 二叉树的先序遍历
void PerOrder(BiTree T){
if(T!=NULL){
visit(T); // 访问根节点
PerOrder(T->lchild); // 递归遍历左子树
PerOrder(T->rchild); // 递归遍历右子树
}
}
// 二叉树的中序遍历
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); // 递归遍历左子树
visit(T); // 访问根节点
InOrder(T->rchild); // 递归遍历右子树
}
}
// 二叉树的后序遍历
void PostOrder(BiTree T){
if(T!=NULL){
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; // 树的深度=Max{左子树,右子树}+1
}
}
前、中、后序遍历算法如下:——》非递归版
// 先序遍历
void PreOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
visit(p); // 访问栈顶元素
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
p=p->rchild; // 转向右子树
}
}
}
// 中序遍历
void InOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
visit(p); // 访问栈顶元素
p=p->rchild; // 转向右子树
}
}
}
后序遍历较为特殊:
// 后序遍历
void PostOrder(BiTree T){
InitStack(S);// 初始化栈S用来存放结点
BiTree p=T; // 遍历指针p
r=NULL; // 辅助指针r,指向最近访问过的结点
while(p || !Empty(S)){ // 栈不空或者p不空时候循环
if(p){
Push(S,p); // 当前结点进栈
p=p->lchild; // 左孩子不空,一直向前走
}
else{ //向右
GetTop(S,p); // 读取栈顶元素,栈顶元素不出栈
if(p->rchild && p->rchild!=r){ // 右子树存在并且未被访问
p=p->rchild;
}
else{ // 出栈转向右子树
Pop(S,p); // 栈顶元素出栈
visist(p); //访问p结点
r=p; // 记录最近被访问过的结点
// 每次出栈访问完毕一个结点就相当于遍历完以该结点为根的子树,需要将p置为null
p=NULL; // 结点访问完毕后,重置p指针
}
}
}
}
二叉树的层次遍历
算法基本思想:
①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果存在)
④重复③直至队列为空
// 二叉树的链式存储
// 二叉树的结点
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
// 链式队列结点!!!!!!!!!此处存储的为指针
typedef struct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct {
LinkNode *front,*rear;
}LinkQueue;
void LevelQrder(BiTree T){
LinkQueue Q;
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); // 右孩子入队
}
}
}
遍历序列构造二叉树
只给出一个二叉树的前/中/后/层序遍历序列中的一种:不能唯一确定一棵二叉树
- 给出先序序列和中序序列可以唯一确定一棵二叉树
- 给出后序序列和中序序列可以唯一确定一棵二叉树
- 给出层序序列和中序序列可以唯一确定一棵二叉树
线索二叉树
如何寻找指定结点p在中序遍历序列中的前驱?如何寻找p的中序后继?思路:从根节点出发,重新进行依次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点:①当q=p时,pre为前驱;②当pre=p时,q为后继
缺点:找前驱、后继很不方便;遍历操作必须从根开始
因此提出线索二叉树:利用n个结点的二叉树有n+1个空链域:可以用来记录前驱、后继的信息——<含有这n+1个空链域的结点是:度为1(只有一个孩子)和度为0(叶子结点)>
前驱线索:左孩子指针充当;后继线索:右孩子指针充当
tag=0,表示指针指向孩子;tag=1,表示指针是“线索”
线索二叉树的存储结构:
// 线索二叉树的存储
typedef struct ThreadTNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;// 左、右线索标志
}ThreadNode,*ThreadTree;
中序线索二叉树——线索指向中序前驱、中序后继
先序线索二叉树——线索指向先序前驱、先序后继
后序线索二叉树——线索指向后序前驱、后序后继
二叉树的线索化
中序线索化
// 中序线索化二叉树T
void CreateInThread(ThreadTree T){
ThreadTree pre=NULL; // pre初始为NULL
if(T!=NULL){
InThread(T,pre); // 中序线索化二叉树
pre->rchild=NULL;
pre->ltag=1; // 处理遍历最后一个结点,右子树指向空
}
}
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre); // 递归遍历左子树
// visit; // 访问根节点
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 Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p)){
visit(p);
}
}
// 在中序线索二叉树中给找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode* p){
// 右子树最左下角
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
// 循环找到最左下结点(不一定为叶结点)
while(p->ltag==0){
p=p->lchild;
}
return p;
}
四、树与森林
树的存储结构
双亲表示法(顺序存储)
双亲表示李永乐每个结点(根节点除外)只有唯一双亲的性质,可以很快地得到每个结点的双亲结点,但是求结点的孩子时候需要遍历整个结构。
// 双亲表示法
// 存储结构
#define MAX_TREE_SIZE 100 // 树中最多结点数
typedef struct{ // 树中结点定义
ElemType data; // 数据元素
int parent; // 双亲位置域
}PTNode;
typedef struct{ // 树的类型表示
PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
int n; // 结点数
}PTree;
孩子表示法(顺序+链式存储)
孩子表示法:顺序存储各个结点,每个结点中保存孩子链表头指针
将各个结点的孩子结点都用单链表链接起来形成一个新的线性结构
#define MAX_TREE_SIZE 100 // 树中最多结点数
// 孩子表示法
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;
树、森林和二叉树的转换
森林:森林是m(
m
≥
0
m\geq0
m≥0)棵互不相交的树的集合
森林中各个树的根节点之间是为兄弟关系
树和森林的遍历
树:
- 树的先根遍历与这棵树对应的二叉树的先序序列相同
- 树的后根遍历与这棵树对应的二叉树的中序序列相同
- 先根、后根遍历:深度优先遍历;
- 层次遍历:广度优先遍历(需要用到队列)
森林:
- 森林先序遍历效果等同于对各个树进行先根遍历,也等同于依次对二叉树先序遍历
- 森林中序遍历效果等同于对各个树进行后根遍历,也等同于依次对二叉树中序遍历
总结:
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
五、树与二叉树的应用
二叉排序树
定义见上文:按住Ctrl键,鼠标点击此处——左子树结点值<根结点值<右子树结点值
查找:
// 二叉排序树
//二叉排序树结点
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;
}
插入:
// 二叉排序树的插入
// 二叉排序树不允许由两个相同的数值
// 二叉排序树插入结点一定为叶子结点
// 算法思想:如果原二叉树为空,则直接插入结点;
// 否则,若关键字k小于根节点数值,则插入到左子树,
// 若关键字k大于根结点值,则插入到右子树
int BST_Insert(BSTree &T,int k){
if(T==NULL){
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=0;
return 1;
}
else if(k==T->key){
return 0;
}
else if(k<T->key){
return BST_Insert(T->lchild,k);
}
else{
return BST_Insert(T->rchild,k);
}
}
构造:
// 二叉排序树的构造
// 按照输入的str[]中的关键字序列构造二叉排序树
// 不同关键字序列可能得到同款二叉排序树
void Create_BST(BSTree &T,int str[],int n){
T==NULL;
int i=0;
while(i<n){
BST_Insert(T,str[i]);
i++;
}
}
删除:
- 删除的为叶子结点:则直接删除,不会破坏二叉排序树的性质
- 删除的结点只有一棵左子树或者右子树:让该结点的子树成为该结点父结点的子树,替代该结点的位置
- 删除的结点z有左、右两棵子树:令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删除这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
注:直接前驱或者后继来自对二叉排序树的中序遍历,得到的是一个递增的有序序列,
z的后继:z的右子树中最左下结点(该结点一定没有左子树)
平衡二叉树
平衡二叉树(AVL),简称平衡树——树上任一结点的左子树和右子树的高度之差不超过1
结点的平衡因子=左子树高-右子树高=(0/1/-1)
二叉排序树中插入新节点后如何保持平衡?不平衡时:每次调整对象都是“”最小不平衡子树“
二叉平衡树保证平衡的基本思想为:每当在二叉排序树中插入(删除)一个节点是,首先检查其插入路径上的结点是否因为此次操作导致了不平衡。若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,在对以A为根的子树,在保证二叉排序树特性的前提下,调整个结点的位置关系,使之重新达到平衡。
- LL平衡旋转(右单旋转):由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A称为根结点,将A结点向右下旋转称为B的右子树的根节点,而B的原右子树则作为A结点的左子树。
- RR平衡旋转(左单旋转):由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由-1减到-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A称为根节点,将A结点向坐下旋转称为B的左子树的根节点,而B的原左子树则作为A结点的右子树。
- LR平衡旋转(先左后右双旋转):由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置。
- RL平衡旋转(先右后左双旋转)。由于在A的右孩子(R) 的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋转提升到A结点的位置。
哈夫曼树
结点的权:有某种显示含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上的权值的乘积
树的带权路径长度(WPL):树中所有叶结点的带权路径长度之和
哈夫曼树(最优二叉树):在含有n个带权叶结点的二叉树中,其中 带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
构造哈夫曼树:
给定n个权值分别为w1,w2,w3,…,wn的结点,构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复步骤2)和3),直至F中只剩下一棵树为止。
注意:
1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
2)哈夫曼树的结点总数为
2
n
−
1
2n-1
2n−1
3)哈夫曼树中不存在度为1的结点。
4)哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼树应用:
固定长度编码——每个字符用相等长度的二进制为表示
可变长度编码——允许对不同字符用不等长的二进制位表示