树和二叉树
数的定义和基本术语
- 度:结点拥有的子树数称为度。
- 叶子结点:度为0的结点
- 非终端结点(分支结点、内部结点):度不为0的结点
- 层次:从根开始,根为第一层,根的孩子为第二层。
- 深度:树中结点的最大层次称为树的深度或高度。
- 有序树:如果将树中结点的个子树看成从左至右是有次序的(即不能互换),则称为有序树
- 森林:对于树种每个结点而言,其子树的集合即为森林。
二叉树
二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树的性质
性质1:在二叉树的第i层上,最多有2^i-1个结点
性质2:深度为k的二叉树最大结点数为:2^k - 1
性质3:对于任何一颗二叉树,如果其终端结点数为n0, 度为2的结点数为n2, 那么n0 = n2 + 1.因为:
- n = 分支数 + 1
- n = n0 + n1 + n2
- 分支数 = n1 + 2*n2
性质4:具有n个结点的完全二叉树,深度为 log(n)向下取整 + 1
二叉树的存储结构
1. 顺序存储结构
用一块连续的地址依次自上而下、自左向右存储完全的二叉树。
2. 链式存储结构
表示二叉树的结点至少有三个域:数据域、左、右指针。
遍历二叉树和线索二叉树
遍历二叉树
先序遍历:
- 先访问根
- 然后左、右子树
中序遍历:
//非递归,用栈解决
status InoderTraverse(BiTree T, Status(*visit)(TElemType e)){
InitStack(S);
push(S,T);
while(!Stackempty()){
while(GetTop(S,p) && p) push(S, p->left);
pop(S,p); //空指针退栈
if(!Stackempty()){
pop(S,p);
Visit(p->data);
push(S, p->right);
}
}
return ok;
}
//递归算法
status InoderTraverse(BiTree T, Status(*visist)(TElemType e)){
if(T->left) {
InoderTraverse(T->left, T->left->data);
}
if(T) Visit(T->data);
if(T->right){
InoderTraverse(T->right, T->right->data);
}
return ok;
}
- 先左子树
- 根
- 右子树
后序遍历:
//后序遍历非递归写法
//中序遍历中,是一路将左儿子压栈、然后弹栈输出、如果它的右儿子不为空再压栈
//后序遍历中,也是一路将左儿子压栈,然后取栈顶元素查看,如果右儿子不为空则压栈,否则输出。
status LastTraverse(BiTree T) {
InitStack(S);
push(S,T);
while(!Stackempty(S)){
while(T->left) push(S, T->left);
pop(S, p); //空指针退栈
if(!Stackempty()){
GetTop(S, p);
if(p->right == NULL) Visit(p->data);
else push(S, p->right);
}
}
return ok;
}
//递归写法
status LastTraverse(BiTree T) {
if(T->left) LastTraverse(T->left);
if(T->right) LastTraverse(T->right);
Visit(T->data);
}
- 先左、右子树
- 后根
线索二叉树
在前面遍历二叉树的时候,当以二叉链表作为存储结构时,只能找到结点的左右孩子的信息,而不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到。
一种简单的保存这种信息的方法是在每个结点上增加两个指针域fwd和bkwd,分别指示结点在任一次序遍历的时得到的前驱和后继信息。
现做如下规定:若结点有左子树,那么它的Lchild域指示其左孩子,否则令Lchild指示其前驱。若结点有右子树,那么它的Rchild域指示其右孩子,否则令Rchild指示其后继。为避免混淆,增加两个标志位LTag、RTag,0表示指示孩子,1表示指示前驱或后继。
1. 概念
线索链表: 这种结点结构构成的二叉链表就是线索链表
线索: 指向结点前驱和后继的指针,叫做线索
线索化:对二叉树以某种次序遍历使其变成线索二叉树的过程叫做线索化
2. 中序线索化
//写这种递归的核心:
1. 知道递归框架,如中序遍历:先递归左子树、后遍历根、后遍历右子树
2. 然后知道初始条件:这里preT = NULL;
3. 然后知道在某一个状态的时候,递归函数中该怎么处理。比如到P结点。此时如果有左子树,显然指针指向左子树,否则左边可以前驱线索化。再看此时前驱结点有无右子树,如果有,那么指向右子树,否则可以直接后继线索化
preT = NULL;
status InThread(BiTree p) {
if(p){
InThread(p->left);
if(!p->left){
p->Ltag = Thread;
p->left = preT;
}
if(!preT->right){
preT->Rtag = thread;
preT->right = p;
}
preT = p;
InThread(p->right);
}
}
哈夫曼(赫夫曼、最优二叉树)树
构造一颗有n个叶子结点的二叉树,每个叶子结点带权Wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树。
二叉排序树和平衡二叉树
二叉排序树(二叉查找树)
二叉排序树或者是一颗空树,或者是具有下列性质的二叉树。
1. 若它的左子树不为空,则左子树上所有结点的值均小于它的根结点值
2. 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点值
3. 它的左右子树也分别为二叉排序树
1. 插入
// 查找元素
status SearchBST(BiTree T, KeyType key, BiTree f, BiTree &p) {
//f为T的父亲结点,初始化为NULL, 如果没找到那么f就是叶子结点,这样便于后面插入
if(!T) {
p = f;
return false;
}
else if(EQ(key, T->data.key)) {
p = T;
return true;
}
else if(key < T->data.key) return SearchBST(T->lchild,key,T,p);
else return SearchBST(T->rchild,key,T,p);
}
//插入
Status InsertBST(BiTree &T, ElemType e) {
if(!SearchBST(T, e.key, NULL, p)){
s = (BiTree)malloc(sizeof(BiTree));
s->data = e;
s->lchild = s->rchild = NULL;
if(!p) T = s;
else if(e.key < p->data.key) p->lchild =s;
else p->rchild = s;
return true;
}
else return false;
}
二叉排序树的构造过程就是不断插入值的过程。
2. 删除
设删除的元素为P。
1. 如果P有右儿子,把右子树接在P的父亲的儿子上。如果P是父亲的左儿子,那么接在左儿子上,如果P是父亲的右儿子,那么接在右儿子上。然后把P的左儿子(如果有左儿子)接在P的右儿子的左儿子尽头上。
2. 如果P没有右儿子,那么左子树直接接上去
3. 如果P没有左儿子,那么右儿子直接接上去
4. 如果P没有儿子,直接删除
平衡二叉树
或者为一颗空树,或者满足以下的条件:
1. 它的左、右子树均为平衡二叉树
2. 或者左、右子树的深度之差的绝对值不超过1
一般要将二叉排序树平衡化,防止二叉排序树的退化。
平衡二叉树都是在构建的过程中做平衡化,或者在删除操作后做平衡化,所以此时的不平衡状态主要有四种: (旋转点为首先失去平衡的点)
1. LL:在根的左子树的左子树上插入结点,向右的顺时针旋转。
2. LR:在根的左子树的右子树上插入结点,先向左、后向右。
3. RR:在根的右子树的右子树上插入结点,向左旋转。
4. RL:在根的右子树的左子树上插入结点,先向右、后向左。
B- 和 B+树
B-树
B-树是一种平衡的多路查找树,主要用作文件的索引:
一颗m阶的B-树,或为空树,或者满足以下条件:
1. 树中每个结点至多m颗子树,内结点至少 m/2向上取整 颗子树
2. 这里表示每个内结点最多只能有m-1个关键字
3. 内结点信息:(n, A0, K1, A1, K2, A2, …. , Kn, An)
4. 注意叶子结点,其实所有的叶子结点都出现在同一层次上,并且不带信息。(可以看做是外部结点或查找失败的结点,实际上这些结点不存在)
1. 插入
因为B-树结点中的关键字个数必须 大于等于 m/2向上取整。因此每次插入一个关键字的时候,不是在树中添加一个叶子结点,而是首先在某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1,则插入完成,否则要产生结点的分裂。这里需要注意,一颗m阶的B-树,每个结点至多m颗子树,所以每个内结点最多只能有m-1个关键字
分裂:
1. 一般找到该结点中的中间点,
2. 把它拉到父亲结点
3. 把它左边的关键字连接到该关键字左边的指针,右边的接到右边指针
2. 删除
- 首先找到关键字所在结点,并删除。
- 若该结点为非叶结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。 因此,把在非叶结点删除关键字k的问题就变成了删除叶子结点中的关键字的问题了。
删除叶子结点中的关键字:
1. 如果被删关键字所在结点的原关键字个数n>=ceil(m/2),说明删去该关键字后该结点仍满足B-树的定义。这种情况最为简单,只需从该结点中直接删去关键字即可。
2. 如果被删关键字所在结点的关键字个数n等于ceil(m/2)-1,说明删去该关键字后该结点将不满足B-树的定义,需要调整。调整过程为:如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于ceil(m/2)-1。则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。
3. 如果左右兄弟结点中没有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目均等于ceil(m/2)-1。这种情况比较复杂。需把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,合并到Ai(是双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲结点中关键字个数小于ceil(m/2)-1,则对此双亲结点做同样处理。以致于可能直到对根结点做这样的处理而使整个树减少一层。
B+树
B+树是应文件系统需求而出的一种B-树的变型树。他们的差异如下:
1. 有n颗子树的结点中包含有n个关键字。(关键字两侧没有指针)
2. 所有的叶子结点中包含了全部的关键字信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最小或者最大关键字。
键树(字典树)
堆
二叉堆
二叉堆一般都是完全二叉树。
二叉堆满足二个特征:
1. 父结点的键值总是小于或大于所有子结点的键值
2. 左右子树都是二叉堆
如果键值总是大于或等于所有子结点,那么称为最大堆。 如果总是小于或等于所有子结点,那么称为最小堆。
堆的存储
一般都用数组存储。i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。
堆的操作
从底开始,考虑每个内结点,调整、筛选为堆,再依次向上。那么直到根结点后肯定结果为堆。
筛选
对于一个堆,输出堆顶元素后,如何将剩下的元素调整为堆。这里就可以考虑为,对于以N为根的二叉树,它的左右子树都是堆,如何调整整颗树为堆。
筛选方法很简单(这里以最小堆为例):
1. 对于根N,比较左、右根,小的与根N替换。因为左右子树都为最小堆,所以左右根选出的最小值必定是整个堆的最小值。
2. 就这样一层层往下筛选,直到叶子结点。
建堆
给出一个序列,比如{49, 38, 65, 97, 76, 13, 27},最后一个非终端结点是第 n/2 向下取整个元素。。这里一共7个元素,所以第一个非终端结点一定是第3个元素,这里是65.
算法思路(建最小堆):
1. 先按照原序列建立一个二叉树。
2. 从第一个非终端结点开始,从该序列的左到右依次做筛选操作。这里先对65做筛选,直到49.
可行性:
对于第一个内结点,以它为根的二叉树肯定是一个两层的二叉树,所以只需要一次筛选便成为了一个堆。然后把它的兄弟结点变为堆。然后依次向上扩展,这个过程刚好就是筛选的过程。所以结果肯定是建成一个堆了。
删除
删除就和筛选差不多。比如删除一个结点N。
1. 这个结点是叶子结点,直接删除
2. 这个结点是内结点。这个时候对以这个结点为根的子树做一次筛选操作,可以把这个内结点沉入叶子结点,然后进行删除操作。