数据结构 读书笔记
查找表是由同一类型的数据元素(或记录)构成的集合。
查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值得数据元素(或记录)
查找表按操作方式来分:静态查找表和动态查找表
静态查找表:只作查找操作的查找表。
动态查找表:查找时涉及的操作有两种:插入数据元素,删除数据元素
顺序查找又叫做线性查找,其过程就是从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定的值比较。
//顺序表查找算法
//a为数组,n为要查找的数组长度,key为要查找的关键字
int Sequential_Search(int *a,int n,int key)
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
return i;
}
return 0;
}
//顺序表查找优化
int Sequential_Search2(int *a,int n,int key)
{
int i;
a[0]=key;//设置a[0]为关键字,我们称之为哨兵
i=n;
while(a[i]!=key)
{
i--;
}
return i;//返回0说明查找失败
}
//设置哨兵之后不用每次都对i是否越界进行判断
时间复杂度O(n)
折半查找又称二分查找,它的前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储。
//折半查找
int Binary_Search(int *,int n,int key)
{
int low,high;
low=1;
high=n;
while(low<=high)
{
mid=(low+high)/2;//折半
if(keya[mid])
low=mid+1;
else
return mid;//若相等则说明mid即为查找到的位置
}
return 0;
}
时间复杂度O(logn)
根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式。
其算法与折半查找完全一样,不过是把mid=(low+high)/2替换成mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);
插值查找对于表长较大,而关键字分布又比较均匀的查找表,其效率比折半查找要好的多
//斐波那契查找
int Fibonacci_Search(int *a,int n,int key)
{
int low,high,mid,i,k;
low=1;
high=n;
while(n>F[k]-1)//计算n位于斐波那契数列中的位置
k++;
for(i=n;ia[mid])
{
low=mid+1;
k=k-2;
}
else
{
if(mid<=n)
return mid;
else
return n;//若mid>n说明是补全数组,而补全数组的值等于a[n]的值
}
}
return 0;
}
斐波那契的时间复杂度为O(logn),但就其平均性能来说,要优于折半查找
三种有序表的查找本质上是分隔点的选择不同,各有优劣。另外折半查找求分隔点是进行加法和除法运算(mid=(low+high)/2),插值查找进行复杂的四则运算(mid=low+(high-low)*(key-a[low])/(a[high]-a[low])),而斐波那契查找只是简单的加减法运算(mid=low+F[k-1]-1),在海量的数据查找中,细微的差别可能最终影响查找效率。
索引就是把一个关键字与它对应的记录相关联的过程;
索引按照结构可以分为线性索引,树形索引和多级索引;
线性索引就是将索引项集合组织为线性结构,也称索引表;
线性表有稠密索引,分块索引和倒排索引;
是指在线性索引中,将数据集中的每个记录对应一个索引项,索引项一定是按照关键码有序的排列。缺点是数据集非常大时,查找性能会下降
对数据集进行分块,使其分块有序,然后再对每一块建立一个索引,从而减小索引项的个数。(分块有序要求块内无序(每一块内的记录不要求有序),块间有序(例如第二块的所有记录的关键字都必须大于第一块里面的。。依次类推))
分块索引的索引项的结构分为三个数据项:最大关键码(存储了每一块中的最大管子字),块中的记录个数,用于指向块首元素的指针。
分块索引查找的查找长度依赖于数据集的总记录数n,以及每一块的记录数t;当分的块数m与t相同时,效率最高,此时查找长度为n^(1/2),效率比顺序查找的O(n)提高了不少
倒排索引的通用结构是次关键码和记录号表(即记录的位置)
倒排排序不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称作倒排索引
二叉排序树又称为二叉查找树,它或者是一棵空树,或者具有以下性质:
*若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
*若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
*它的左,右子树也分别为为二叉排序树
二叉排序树的查找和删除关键字的速度都大大提高;
//二叉排序树的查找
//指针f指向T的双亲,其初始值调用值NULL
//若查找成功,则指针p指向该数据元素结点,并返回TRUE
//若查找失败,则指针p指向查找路径上访问的最后一个结点并返回FALSE
Status SearchBST(BiTree T,int key ,BiTree f,BiTree *p)
{
if(!T)//为空树
{
*p=f
return FALSE;
}
else if(key==T->data)
{
*p=T;
return TRUE;
}
else if(keydata)
return SearchBST(T->lchild,key,T,p);
else
return SearchBST(T->rchild,key,T,p);
}
二叉排序树的插入就是将关键字放入合适的位置,可以借用二叉排序树的查找函数找到这个位置
//当二叉排序树种不存在关键字等于key的数据元素时,插入key
Status InsertBST(BitTree *T,int key)
{
BitTree p,s;
if(!SearchBST(*T,key,NULL,&p))//找到要插入的位置
{
s=(BitTree)malloc(sizeof(Bi));
s->data=key;
s->lchild=s->rchild=NULL;
if(!p)//T为空树时,p==NULL;
*T=s;//插入s为新的根结点
else if(keydata)
p->lchild=s;
else
p->rchild=s;
return TRUE;
}
else
return FALSE;
}
分三种情况:
1待删除的结点没有孩子
2待删除的结点只有一个
3待删除的结点有两个孩子
//二叉排序树的删除操作
Status DeleteBST(BiTree *T,int key)
{
if(!*T)//为空树
return FALSE;
else
{
if(key==(*T)->data)(定义二叉树结构时BitTree 为指向结点的指针)
return Delete(T);
else if(key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(*T)->rchild,key);
}
}
//从二叉排序树中删除结点p,并重接它的左或右子树
Status Delete(BitTree *p)
{
BitTree q,s;
if((*p)->rchild==NULL)
{
q=*p;*p=(*p)->lchild;free(q);
}
else if((*p)->lchild==NULL)
{
q=*p;*p=(*p)->rchild;free(q);
}
else
{
q=*p;s=(*p)->lchild;
while(s->rchild)//转左,然后向右到尽头(即找到待删除节点的前驱)
{
q=s;s=s->rchild;
}
(*p)->data=s->data;//用s(即待删除结点的前驱)替换待删除的结点,然后重新释放掉s
if(q!=*p)
q->rchild=s->lchild;
else
q->lchild=s->lchild;//重接q的左子树,为q原来左子树的左子树
free(s);
}
return TRUE;
}
当二叉排序树左右两边比较平衡时的效率最高为O(logn),最坏的情况就是二叉排序树是斜树其时间复杂度为O(n),这等同于顺序查找。
平衡二叉树是一种二叉树,其中每一个结点的左子树和右子树的高度差至多等于1
二叉树结点的左子树深度减去右子树的深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树的BF只可能是-1,0,1
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
当最小不平衡子树的根结点的平衡因子BF是大于与它的子树的BF符号相反时,就要对结点先进行一次旋转以使得符号相同时,再反向旋转一次才能够完成平衡操作。
//二叉树的二叉链表结点结构定义
typedef struct BitNode
{
int data;
int bf;//结点的平衡因子
struct BitNode *lchild,*rchild;
}BiTNode,*BiTree;
//对以p为根的二叉排序树作右旋处理
//处理之后p指向新的树根结点,即旋转处理志之前的左子树的根结点
void R_Rotate(BiTree *P)
{
BiTree L;
L=(*P)->lchild;
(*P)->lchild=L->rchild;
L->rchild=(*P);//右旋右挂,原来的根结点挂在新的根结点的右边
*P=L;//P指向新的根结点
}
//左旋处理
void L_Rotate(BiTree *P)
{
BiTree R:
R=(*P)->rchild;
(*P)->rchild=R->lchild;
R-lchild=(*P);//左旋左挂,原来的根结点挂在新的根结点的左边
*P=R;
}
//左平衡旋转处理的函数代码
#define LH +1//左高
#define EH 0//等高
#define RH -1//右高
//对以指针T所指向根结点为根的二叉树作左平衡旋转处理
void LeftBanlance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild;
switch(L->bf)
{
case LH://新结点插入在T的左孩子的左孩子树上,要作单右旋处理
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH://新插入结点在T的左孩子的右子树上,需要作双旋处理
Lr=L->rchild;
switch(Lr->bf)
{//调整平衡因子,这里为调整后为最终的平衡因子
//我觉得作者在这里写的最终结果有问题
//我们可以通过画图来得到最终结果
//我得到的结果是
//case LH:(*T)->bf=EH;
//L->bf=RH;
//case RH:(*T)->bf=LH;
//L->bf=EH;
case LH:
(*T)->bf=RH;
L->bf=EH;
break;
case EH:
(*T)->bf=L->bf=EH;
break;
case RH:
(*T)->bf=LH;
L->bf=LH;
break;
}
Lr-bf=EH;
L_Rotate(&(*T)->lchild);//对T的左子树作左旋平衡处理
R_Rotate(T);//对T作右旋平衡处理
平衡二叉排序树的构建过程 :
//Stauts InsertAVL(BiTree *T,int e ,Status *taller)
{
if(!T)
{//插入新结点,树“长高”,置taller为true
*T=(BiTree)malloc(sizeof(BitNode));
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
*taller=TRUE;
}
else
{
if(e==(*T)->data)
{//树种已经存在和e有相同关键字的结点则不再插入
*taller=FLASE;
return FALSE;
}
if(e<(*T)->data)
{//应该继续在左子树中搜索
if(!InsertAVL(&(*T)->lchild,e,taller))//未插入,因为树中已经存在相同的关键字
return FALSE;
if(*taller)//已插入到T的左子树中且左子树长高
{
switch((*T)->bf)//检查T的平衡度
{
case LH://原本左子树比右子树高,需要做左平衡处理
LeftBanlance(T);
*taller=FALSE;
break;
case EH:
(*T)->bf=LH;
*taller=TRUE;
break;
case RH:
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
}
else
{//应该在T的右子树进行搜索
if(!InsertAVL(&(*T)->rchild,e,taller))
return FALSE;
if(*taller)//已经插入到T的右子树中且右子树长高
{
switch(case)
{
case LH:
(*T)->bf=EH;
*taller=FALSE;
break;
case EH:
(*T)->=RH;
*taller=TRUE;
break;
case RH://原本右子树比左子树高,需要作右平衡处理
RightBalance(T)//右平衡处理的代码与左平衡类似,所以没有在前面给出来
*taller=FALSE;
break;
}
}
}
}
return TRUE;
}
例如执行这样的代码:
int i;
int a[10]={3,2,1,4,5,7,10,9,8};
BiTree T=NULL;
Status taller;
for(i=0;i<10;i++)
{
InsertAVL(&T,a[i],&taller);
}
就可以创建一棵平衡二叉排序树
平衡二叉排序树执行查找的时间复杂度就是O(logn)
多路查找树(mutil-way search tree),其每一个结点的孩子树可以多余两个,且一个结点可以储存多个元素。
2-3树是这样一棵多路查找树:其中的每一个结点都具有两个孩子(我们称它为2结点)或3个孩子(我们称它为3结点)。
(注意:一个2结点包含一个元素和两个孩子(或没有孩子),不能只有一个孩子。一个3结点包含一大一小两个元素和三个孩子(或没有孩子),不能有1个或两个孩子。)
2-3树中所有叶子都在同一层次上。结点分布按大小排列(2结点左子树包含的元素小于该元素,右子树包含的元素大于该元素;3结点中间子树包含的元素大小介于父节点之间)
插入时,始终保持插入后的树为2-3树,所以在插入过程中需要调整结点(2结点边3结点,结点重新组合等等)或者同时增加树的层次,总的来说有规律,这里略。
和插入时的要求相同,即需要最终的树是2-3树,所以也需要调整结点或者同时调整树的层次。这里也需要分情况讨论,这里略
2-3-4树就是2-3树的扩展,包括了4结点的使用。
(同样结点的分布按大小排列)
B(B-Tree)树是一种平衡的多路查找树,2-3树和2-3-4树是B树的特例
。结点最大的孩子数目,称为B树的阶(order)
B树具有一系列的性质:
1如果结点不是叶结点则其至少有两棵子树;
2每一个非根的分支结点都有k-1个元素和k个孩子,每一个叶结点都有k-1个元素,其中[m/2]<=k<=m(m为B树的阶,[m/2]在这里表示的是不小于m/2的最小整数),
3所有的叶子结点都位于同一层次
4.略
由于B树每一个结点具有的元素比二叉树多的多,减少了必须访问结点和数据块的数量,所以提高了性能
B树查找的时间复杂度为O(log((n+1)/2+1)) (log的底为[m/2],[m/2]表示不小于m/2的最小整数;n表示的是B树具有的关键字)
B树也是有缺点的,每次我们经过结点遍历时,都会对结点的元素进行访问,然后进行遍历,这是非常糟糕的,因为这一且都是在内存中进行的,可能会占据大量的内存。如果我们在遍历时只让每个元素访问一次,这样效率就会有大量提高。我们在原来B树的结构基础上增加新的元素的组织方式就形成了新的B+树。
B+树中,出现在分支结点的元素会被当作它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每一个叶子结点都会保存一个指向后一个叶子结点的指针。
一棵m阶的B+树与m阶的B树的差异在于:
*有n棵子树的结点包含有n个关键字;
*所有的叶子结点包含全部的关键字的信息,及指向含这些关键字记录的指针。叶子结点本身依关键字的大小自小而大顺序链接;
*所有的分支结点可以看成索引,结点中仅含有其子树中最大(或最小)的关键字