树的应用
树的应用
树的存储结构
双亲表示法(顺序存储)
定义:每个结点中保存指向双亲的“指针”(位置下标)。根结点固定存储在0,-1表示没有双亲。
新增:直接添加无需按照逻辑上的次序。
删除:①指针设为-1。(空数据导致遍历更慢)②将最后一个结点移到待删除结点的位置。
优点:查找指定结点的双亲很方便。
缺点:查找指定结点的孩子只能从头遍历。
应把二叉树的结点编号与完全二叉树对应。,结点编号反映存储位置以及各结点间逻辑关系。
//树中最多结点数
#define MAX_TREE_SIZE 100
//树结点定义
typedef struct{
ELemType data;
int parent;
}PTNode
//树类型定义
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
孩子表示法(顺序+链式存储)
定义:顺序存储各个结点,每个结点中保存孩子结点的链表头指针。每个结点的孩子结点都用单链表链接起来形成一个线性结构。
优点:寻找孩子结点
缺点:寻找双亲结点需要遍历n个结点中孩子链表指针域所指向的n个孩子链表。
struct CTNode{
//孩子结点在数组中的位置
int child;
//下一个孩子
struct CTNode *next;
};
typedef struct{
ElemType data;
struct CTNode *firstChild;
}
孩子兄弟表示法(链式存储)
树和二叉树相互转换。用二叉链表表示的树。
左指针:第一个孩子结点
右指针:孩子结点的兄弟结点
//树的存储
森林和二叉树的转换
左指针:孩子结点
右指针:兄弟结点
用二叉链表存储森林。森林中各个树的根结点视为兄弟关系。
树和森林的遍历
树的遍历
先根遍历(深度优先搜索DFS)
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
若树非空,先访问根结点,再依次对每棵子树进行先根遍历。
后根遍历(深度优先搜索DFS)
树的后根遍历序列与这棵树相应二叉树的中序序列相同。
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
层序遍历
用队列实现(广度优先搜索BFS)
①若树非空,则根结点入队
②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
重复②直到队列为空。
森林的遍历
由m(m≥0)棵互不相交的树集合。每棵树去掉根结点后,其各子树组成森林。
先序遍历
若森林为非空,则
①访问森林中第一棵树的根结点。
②先序遍历第一棵树中根结点的子树森林
③先序遍历第一棵树之后剩余树构成的森林
等同于依次对各个树进行先根遍历。
将森林转化为二叉树,对二叉树进行先序遍历。
中序遍历
若森林非空,则
①中序遍历森林中第一棵子树的根结点的子树森林
②访问第一棵树的根结点
③中序遍历除去第一棵树之后剩余的树构成的森林
效果等同于依次对各个树进行后根遍历。
将森林转化为二叉树,对二叉树进行中序遍历。
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
二叉排序树
定义
二叉查找树(BST Binary Search Tree)
一棵二叉树或者是空二叉树或者是具有如下性质的二叉树:
①左子树上所有结点的关键字均小于根结点的关键字;
②右子树上所有结点的关键字均大于根结点的关键字。
③左子树和右子树又各是一棵二叉树。
左子树结点值<根结点值<右子树结点值
进行中序遍历,可以得到一个递增的有序序列。
二叉排序树可用于元素的有序组织、搜索。
默认不允许两个结点的关键字相同。
操作
构造
不同的关键字序列可能得到同样的二叉排序树。
第一个为根结点,后续关键字大于结点右插入,小于结点左插入。
//按照str[]中的关键字建立二叉排序树
void Creat_BST(BSTree &T,int str[],int n){
T=NULL;
int i=0;
//依次将每个关键字插入到二叉排序树中
while(i<n){
BST_Insert(T,str[i])
i++;
}
}
查找
左子树结点值<根结点值<右子树结点值
①若树非空,目标值与根结点的值比较。
(1)若相等,则查找成功
(2)若小于根结点,则在左子树上查找
(3)若大于根结点,则在右子树上查找
⑤查找成功返回结点指针
⑥查找失败返回NULL
//在二叉排序树种查找值为key的结点
//最坏空间复杂度O(1)
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;
}
//递归实现
//最坏空间复杂度O(h)
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);
}
插入
应该插入的位置一定是叶子结点,注意修改其父结点指针
//在二叉排序树插入关键字为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;
}
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);
}
查找效率分析
查找长度:需要对比关键字的次数。
反映了查找操作时间复杂度。
平均查找长度(ASL)
在查找运算中,每个结点需要比对关键字的次数之和,与关键字个数的比值。
树越矮,查找次数越少。
查找成功
若树高h,找到最下层结点需要对比n次。
最好情况:n个结点的二叉树最小高度为[(log2) n]+1,平均查找长度=O((log2)n)
最坏情况:每个结点只有一个分支,树高h=结点数n,平均查找长度=O(n)
查找失败
需要补充失败结点。
删除
始终保证:左子树结点值<根结点值<右子树结点值。
进行中序遍历,可以得到一个递增的有序序列。
被删除结点z是:
叶结点
则直接删除,不会破坏二叉排序树的性质。
只有一棵左子树或右子树
则让z的子树成为z父结点的子树,代替z的位置。
有左右两棵子树
则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一种或第二种情况。
前驱:左子树中最右下的结点。
后继:右子树中最左下的结点。
平衡二叉树
定义
平衡树(AVL树),树上任一结点的左子树和右子树的高度之差不超过1。
结点平衡因子=左子树高-右子树高。
平衡二叉树就是二叉排序树。
性质
平衡二叉树结点的平衡因子的值只可能是-1,0或1。
只要有任意结点的平衡因子绝对值大于1,就不是平衡二叉树。
保持平衡
在二叉排序树中插入新结点后如何保持平衡:
从插入点往回找到第一个不平衡结点,调整以该结点为根的子树。
每次调整的对象都是最小不平衡子树。插入操作中只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡。
最小不平衡子树
往一个平衡二叉树(本文中均指“平衡二叉排序树”)插入新的叶子结点,从插入点由下往上,依次遍历插入点的各个祖先结点,记录第一个遍历到的 |平衡因子|≥2 (即不平衡)的祖先结点,以该结点为根结点的子树即为这棵树的最小不平衡子树。
调整最小不平衡子树A
假定所有子树高度都为H(仅能为H)。
目标:①恢复平衡②保持二叉排序树特性。
为了方便讨论,我们使用连续的两个字母来表示平衡因子,以表示各种不同的情况。第一个字母表示最小不平衡子树根结点的平衡因子,第二个字母表示最小不平衡子树较高子树的根结点的平衡因子。
LL
在A的左孩子的左子树中插入导致不平衡。
平衡旋转(右单旋转)。
不平衡子树的右子树进行了旋转。
由于在结点A的左孩子(L)的左子树(L)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
RR
在A的右孩子的右孩子中插入导致不平衡。
左单旋转。
不平衡子树的左孩子进行了旋转。
由于在结点A的右孩子的右子树上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
LR
在A的左孩子的右孩子中插入导致不平衡。
先左后右双旋转。
由于在A的左孩子的右子树上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要再进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置。
拆分B的右子树,在B的右子树上插入新结点等同于在C的右子树上插入新结点。
RL
在A的右孩子的左子树中插入导致不平衡