二、树的基本概念
树是一种逻辑结构
基本术语
- 祖先:根A到结点K的唯一路径上的任意结点。(K的祖先:A、B、E)
- 子孙:若B是K的祖先,则K为B的子孙。(B的子孙:E、F、K、L)
- 双亲:路径上最接近K的结点。(K的双亲:E根节点A无双亲)
- 孩子:E为K的双亲,K为E的孩子。(E的孩子:K、L)
- 兄弟:结点K和结点L有相同的双亲E,既K和L为兄弟。(K与L为兄弟)
- 堂兄弟:同一层结点(E、F、G、H、I、J为堂兄弟)
- 结点的度:一个结点的孩子个数。(A的度为3)
- 树的度:树中结点的最大度数。
- 分支结点(非终端节点):度大于0的结点
- 叶子结点(终端结点):度为0的结点
- 结点的层次:从根节点开始定义,根节点为第一层。(一共四层结点)
- 结点深度:从根节点开始自顶向下组成累加。
- 结点的高度:从叶节点开始自底向上组层累加。
- 树的高度(或深度):树中结点的最大层数(图中树的高度为4)
1.有序树(无序树):树中结点各子树从左到右是有次序(无序)的,不能互换(可互换)。
2. 路径:两个结点之间所经过的结点序列构成的。(树中的路径是从上往下的)
3. 路径长度:路径上所经过边的个数。
树的性质
- 结点数 = 总度数+1 (加的1为根节点)、
- 度为m的树、m叉树 的区别:
- 度为m的树第 i 层至多有mi-1
m叉树第i层至多有**mi-1**个结点
- 高度为h的m叉树至多有 (mh-1)/(m-1)。
1+m+m2+……+mn
- 高度为h的m叉树至少有h个结点,高度为h、度为m的树至少有h+(m-1)个节点
- 具有n个结点的m叉树的最小高度为logm(n(m-1)+1)【向上取整】
每一层都有孩子
(mh-1-1)/(m-1) < n ≤ (mh-1)/(m-1)
mh-1-1 < n/(m-1) ≤ mh-1
h-1< logm[n(m − 1) + 1] ≤ h
ℎ𝑚𝑖𝑛 = logm[n(m − 1) + 1]
二、二叉树
二叉树的定义及其主要特征
特点:
- 每个结点至多只有两棵子树,并且二叉树的子树有左右之分
- 二叉树的五种形态
(a)空二叉树 (b)只有根节点 ©只有左子树 (d)左右子树都有 (e)只有右子树- 二叉树和度为2的有序树区别:
(1)度为2的树至少有三个结点,二叉树可以为空
(2)度为2的树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子,则无序区分左右。二叉树的结点次序不是相对于另一结点而言的。
几个特殊的二叉树
满二叉树
一颗高度为h,且含有2h-1个结点的二叉树。
特点:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层序序号为1开始编号,结点 i 的左孩子为 2i,右孩子为2i+1;结点i的父节点为 [i/2]向下取整
完全二叉树
每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应
特点:
- 只有最后两层可能有叶节点
- 最多只有一个度为1的结点
- 按层序序号为1开始编号,结点 i 的左孩子为 2i,右孩子为2i+1;结点i的父节点为 [i/2]向下取整
- i ≤ n/2 为分支结点,i>n/2 为叶子节点(向下取整)
- 如果某结点只有一个孩子,那么一定是左孩子
(若对二叉树的根结点从0开始编号,则相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的编号为2i+2。)
二叉排序数
定义:(左小右大)
左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有结点的关键字均大于根结点的关键字。
左子树和右子树又各是一棵二叉排序树
平衡二叉树
树上任一结点的左子树和右子树的深度只差不超过1
二叉树的性质
- 非空二叉树上的叶子结点树等于度为2的结点数+1(n0 = n2 + 1)
n = n0 + n1 + n2
n = n1 + 2n2 +1
→n0 = n2+1
- 二叉树第 i层至多有2i-1个结点
m叉树第i层至多mi-1个结点- 高度为h的二叉树至多有2h-1个结点
- 具有n个(n>0)结点的完全二叉树高度为log 2 (n + 1)【向上取整】
- 第i个结点所在层次为log 2 (n + 1)【向上取整】
- 完全二叉树最多只有一个度为1的结点,即n1 =0或1
n0 = n2 + 1 → n0 + n2一定是奇数
若完全二叉树有2k(偶数)个结点,则必有n1 = 1,n0 =k,n2 =k-1
若完全二叉树有2k-1个(奇数)个结点,则
必有n1 = 0,n0 =k,n2 =k-1
二叉树的存储结构
顺序存储
用一组地址连续的存储单元依次自上而下、自左到右存储完全二叉树上的结点元素。
#define MaxSize 100
struct TreeNode{
ElemType value;
bool isEmpty;
};
TreeNode t[MaxSize];
二叉树的顺序存储结构,只适合存储完全二叉树。
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来
链式存储
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *lchild,*rchild;
//左指针域,右指针域
}BiTNode,*BiTree;
n个结点的二叉链表共有n+1个指针域
【每个结点贡献两个空指针,共2n个,指向n-1(除根节点)个结点,还剩下2n-(n-)=n+1 】
扩展:
三叉链表
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *lchild,*rchild;
struct BiTNode *parent;//父节点指针
}BiTNode,*BiTree;
方便找父亲
三、二叉树的遍历
递归
- 先序遍历(PreOrder)【根左右】
第一次路过时访问结点
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
- 中序遍历(InOrder)【左根右】
第二次路过时访问结点
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
- 后序遍历(PostOrder)【左右根】
第二次路过时访问结点
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
InOrder(T->rchild);
visit(T);
}
}
- 每个结点都只访问一次:时间复杂度:O(n)
- 在递归遍历中,递归栈的栈深恰好为树的深度,最坏情况下,二叉树是有n个结点且深度为n的单只树遍历算法的空间复杂度为O(n)
非递归
- 中序遍历
①、沿着根的左孩子,依次入栈,直到左孩子为空,转去执行②
②、栈顶元素出栈并访问:若其右孩子为空,继续执行②;若其右孩子不空,将右子树转执行①
- 沿着根A、B、D依次入栈; (底)【A B D】(顶) 【】
- D左孩子为空,栈顶D出栈并访问;(底)【A B 】(顶) 【D】
- D右孩子不空,右孩子G入栈并查看其左孩子; (底)【A B G】(顶) 【D】
- G左孩子为空,G出栈并访问;(底)【A B 】(顶) 【D G】
- G右孩子为空,栈顶B出栈访问;(底)【A 】(顶) 【D G B】
- B右孩子不空,E入栈并查看其左孩子;(底)【A E】(顶) 【D G B】
- E左孩子为空,栈顶E出栈并访问;(底)【A 】(顶) 【D G B E】
- E右孩子为空,栈顶A出栈并访问;(底)【】(顶) 【D G B E A】
- A右孩子不空,C入栈并查看其左孩子;(底)【C】(顶) 【D G B E A】
- C左孩子F入栈,并查看F左孩子(底)【C F】(顶) 【D G B E A】
- F左孩子为空,栈顶F出栈并访问;(底)【C】(顶) 【D G B E A F】
- F右孩子为空,栈顶C出栈并访问;(底)【】(顶) 【D G B E A F C】
入栈看左指针,出栈看右指针
void InOeder2(BiTree){
InitStack(S);BiTree p = T;
while(p||!IsEmpty(S)){//当栈不空或p不空时循环
if(p){
Push(S,p);
p=p->lchild;
}else{
Pop(S,p);visit(p);//访问
p=p->rchild;
}
}
}
- 先序与中序类似
void PreOeder2(BiTree){
InitStack(S);BiTree p = T;
while(p||!IsEmpty(S)){//当栈不空或p不空时循环
if(p){
visit(p);Push(S,p);//访问
p=p->lchild;
}else{
Pop(S,p);
p=p->rchild;
}
}
}
- 后序遍历
① 、 沿着根的左孩子,依次入栈,直到左孩子为空。
② 、 读栈顶元素:若其右孩子不空且为被访问过,将右子树转执行①;否则栈顶元素出栈并访问。
void PostOrder(BiTree T){
InitSack(S);
BiTree p = T,r == NULL;
while(p!||!IsEmpty(S)){
if(p){//走到最右
push(S,p);
p=p->lchild;
}else{
GetTop(S,p);
if(p->rchild&&p->rchild!=r) p=p->rchild;//右子树存在且未被访问
//右子树都是顺都是按右下到左上依次出栈,r指针也从右下依次记录到左上
else {
pop(S,p);
visit(p->>data);
r=p;//记录最近访问过的结点
p=NULL;
}
}//else
}//while
}
二叉树的遍历(手算练习)
红色:先序遍历
绿色:中序遍历
紫色:后续遍历
扩展:
二叉树的应用(求树的深度)
int treeDepth(BiTree T){
if(T==NULL){return 0;}
else {
int r = treeDepth(T->rchild);
int l = treeDepth(T->lchild);
//树的深度=MAX(左子树深度,右子树深度)+1
return l>r? l+1 : r+1;
}
}
二叉树的层次遍历
算法思想:
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 重复 3 直至队列为空
void LevelOrder(BiTree T){
InitQueue(Q);
EnQueue(Q,T);
BiTree p = NULL;
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p->lchild!=NULL) EnQueue(Q,p->lchild);
if(p->rchild!=NULL) EnQueue(Q,p->rchild);
//入的是结点指针,而非结点本身
}
}
遍历序列构造二叉树
必定包含中序
- 由先序序列+中序序列唯一确定一颗二叉树
- 由后序序列+中序序列唯一确定一颗二叉树
- 由层序序列+中序序列唯一确定一颗二叉树(最先出现的充当root)
四、线索二叉树
- 引入线索二叉树正是为了加快查找结点前驱和后继的速度(中序/先序/后序 前驱/后续)
- 利用二叉树中n+1个空指针域
- 将二叉树中的空指针改为指向前驱或后续的线索
规定:
若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其前驱结点;还需加两个标制域标识指针是指向孩子还是指向前驱。
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,* ThreadTree;
中序线索二叉树的构造
附设pre指针指向刚刚访问过的结点,指针p指向正在访问的结点,既pre指向p的前驱。在中序遍历中,检查p的左指针是否为空,若为空就将他指向pre;检查pre右指针是否为空,若为空就将它指向p
递归算法
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild);//线索化右子树
//线索化当前结点
if(p->lchild==NULL){
p->lchild = pre;
p->ltag = 1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild = p;
p->rlag = 1;
}
pre = p;
InThread(p->rchild);//线索化左子树
}
}
void CreateInTread(ThreadTree T){
ThreadTree pre = NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild = NULL;
pre->rtag = 1;
//最后一个肯定无右孩子
}
}
后续线索二叉树的构造
void PostThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild);//线索化右子树
InThread(p->rchild);//线索化左子树
visit(pre,p);
}
}
void visit(ThreadTree &pre,ThreadTree &p){
//线索化当前结点
if(p->lchild==NULL){
p->lchild = pre;
p->ltag = 1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild = p;
p->rlag = 1;
}
pre = p;
}
void CreateInTread(ThreadTree T){
ThreadTree pre = NULL;
if(T!=NULL){
InThread(T,pre);
if(pre->rchild = NULL)pre->rtag = 1;
}
}
先序线索二叉树的构造
与中序线索或和后续线索化的区别:
中序:左根右。
根结点
左结点的线索化在左结点
遍历之后(根节点的线索化不会影响遍历);根节点
右节点的线索化在右节点
遍历之后(在本轮的下一轮才会修改)。后序:左右根
根节点
的线索化在左右节点
遍历之后,不影响遍历。先序:根左右
根节点左结点
的线索化会在左结点遍历之前,在遍历左结点之前需要判断是否是前驱线索。根节点
右节点的线索化在右节点
遍历之后(在本轮的下一轮才会修改)。
void PreThread(ThreadTree &p,ThreadTree &pre){
if(T!=NULL){
visit(p,pre);
if(p->ltag==0) PreThread(T->lchild);
//lchild不是前驱线索
PreThread(p->rchild);
}
}
void visit(ThreadNode *p,ThreadNode *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;
}
void CreatPost(ThreadTree T){
pre = NULL;
if(T!=NULL){
PostThtrad(T);
if(pre->rchild==NULL) pre->rtag = 1;
}
}
五、线索二叉树的遍历
中序线索二叉树
中序线索二叉树的遍历
中序线索二叉树找中序后续
中序线索二叉树中寻找结点后续的规律:
- 若右标制为“1”,则右链为线索
- 否则遍历右子树中第一个访问节点的结点
(右子树的最左下结点)
为其后续
- 找到以p为根的子树中,
第一个
被中序遍历的结点
ThreadNode *Firstnode(ThreadNNode *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;
}
- 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p)){
visit(p);
}
}
空间复杂度O(1)
中序线索二叉树找中序前驱
左子树的最右下结点
- 找到以p为根的子树中,最后一个被中序遍历的结点
Node *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);
}
}
先序线索二叉树
先序线索二叉树找后续
先序线索二叉树找先序前驱
左右子树中的结点只有可能是根的后继,不可能是前驱。
用三叉链表
找前驱:
-
若能找到p的父节点,且p是左孩子。
p的父节点既其前驱
-
若能找到p的父节点,且p是右孩子,其左兄弟为空。
p的父节点既为其前驱
-
若能找到p的父节点,且p是右孩子,其左兄弟非空。
p的前驱为左兄弟子树最后一个被先序遍历的结点
-
若p是根节点,则p无前驱
后序线索二叉树
后序线索二叉树找后序前驱
后序线索二叉树找后序后继
左右根:后序遍历中,左右子树中的结点只可能是根的前驱,不可能是后继。
用三叉链表
找后继
-
若能找到p的父节点。且p是右孩子。
p的父节点即为其后续
-
若能找到p的父节点,且p是左孩子,其右兄弟为空。
p的父节点即为其后继
-
若能找到p的父节点,且p是左孩子,其右兄弟非空。
p的后继为右兄弟子树中最后一个被后序遍历的结点
-
p是根节点,则p没有后序后继
总结
六、树、森林
树的存储结构
- 双亲表示法
每个结点保存指向双亲的指针
- 可以很快得到每个结点的双亲结点;
求结点孩子或删除某一分支需要遍历。
树不都能用二叉树的存储结构来存储
存储结构描述
#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;
树、森林与二叉树的转换
本质:用二叉链表存储森林
树和森林的遍历
树的先根遍历
若树非空,先访问根结点,再依次对每棵子树进行先根遍历。
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
树的后根遍历
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
树的层次遍历
用队列实现
①、若树非空,则根节点入队
②、若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
③、重复②直到队列为空
森林的先序遍历
若森林为非空,则按如下规则进行遍历:
- 访问森林中第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林
效果等同于依次对各个树进行先根遍历
效果等同于依次对二叉树的先序遍历
森林的中序遍历
若森林为非空,则按如下规则进行遍历:
- 中序遍历森林中第一棵树的根结点的子树森林。
- 访问第一棵树的根结点。
- 中序遍历除去第一棵树之后剩余的树构成的森林。
总结
七、二叉排序数 BST
(二叉查找树)
- 左子树上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字。
左小右大
:
左子树结点值 < 根结点值 < 右子树结点值- 进行中序遍历,可以得到一个递增的有序序列
二叉排序树的查找
- 若树非空,目标值与根结点的值比较:
- 若相等,则查找成功;
- 若小于根结点,则在左子树上查找,否则在右子树上查找。
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode ,*BSTree;
//在二叉排序树中查找值为key,的结点
BSTNode *BST_Search(BSTree T,int key){
//最坏空间复杂度:O(1)
while(T!=NULL&&key!=T->key){
if(key>T->key) T = T->rchild;
else T = T->lchild;
}
return T;
}
递归实现:
最坏空间复杂度:O(h)
查找效率分析
查找长度——在查找运算中,需要
对比关键字的次数
称为查找长度,反映了查找操作时间复杂度
最好情况:平衡二叉树
最坏情况:只有左(右)孩子的单支树
平均时间性能上看,二叉排序树与二分查找差不多;但二叉排序树的查找不唯一,二分查找判断树唯一
二叉排序树的插入
插入的结点一定是是个新的叶子结点,且是查找失败时路径上访问的最后一个结点的左孩子或右孩子
int BST_Insert(BSTree &T,int k){
if(T==NULL){
T = (BSTree)malloc(sizeof(BSTNode));
T->key = key;
T->lchild = T->rchild = NULL;
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);
}
二叉排序树的构造
(不同元素序列可能得到不同的二叉排序树序列,也可能相同)
二叉排序树的删除
①、 若被删除结点z是叶结点
,则直接删除,不会破坏二叉排序树的性质。
②、若结点z只有一棵左子树或右子树
,则让z的子树成为z父结点的子树,替代z的位置。
③ 、若结点z有左、右
两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
直接后续,右子树中
最小的
;(右子树最左下结点,必无左子树)
直接前驱,左子树中最大的
;(左子树中最右下结点,必无右子树)
八、平衡二叉树 AVL
- 树上任一结点的左子树和右子树的高度之差不超过1
- 结点的平衡因子=左子树高 - 右子树高
- 平衡二叉树结点的平衡因子的值只可能是
−1、0或1
。
平衡二叉树的插入
- 从插入点往回找到第一个不平衡结点,调整以该结点为根的子树
- 每次调整的对象都是最小不平衡子树
调整最先不平衡因子
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为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。
2… 先将A结点的右孩子B的左子树的根结点C
向右上旋转
提升到B结点的位置,然后再把该C结点向左上旋转
提升到A结点的位置
总结
二叉平衡树的查找
比较关键字个数不超过树的深度