8.1开场白+8.2查找概论
8.3顺序表查找
8.3.1顺序查找算法
8.3.2顺序表查找优化
8.4有序表查找
8.4.1折半查找
8.4.2插值查找
8.4.7裴波那契查找
8.5线性索引查找
8.5.1稠密索引
8.5.2分块索引
8.5.3倒排索引
8.6二叉排序树
8.6.1二叉排序树查找操作
8.6.2二叉排序数插入操作
8.6.3二叉排序树删除操作
8.6.4二叉排序树总结
8.7(平衡二叉树)AVL树
8.7.1平衡二叉树实现原理
8.7.2平衡二叉树实现算法
8.8多路查找树
8.9散列表查找(哈希表)概述
8.10散列函数的构造方法
8.11处理散列冲突的方法
8.12散列表查找实现
8.12.1散列表查找算法实现
8.12.2散列表查找性能分析
8.1开场白+8.2查找概论
1.查找表:静态查找表+动态查找表
a.静态查找表:查询某个特定的数据元素是否在查找表中+检索某个特定的数据元素和各种属性
b.动态查找表:查找时插入元素,查找时删除元素
2.静态查找----线性结构组织数据+顺序查找法+对于主关键字排序可以使用折半查找
3.动态查找----二叉排序树的查找技术
8.3顺序表查找
/*顺序查找,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;
}
由于上面的查找中每次都要i<=n的判断,实际上设置一个哨兵即可,如下代码
/*有哨兵顺序查找*/
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则查找失败
}
此代码从尾部开始查找,a[0]=key,如果在a[i]中有key则返回i值,查找成功。否则一定在最终的a[0]处等于key,此时返回0,说明查找失败
8.4有序表查找
思想:折半查找的前提是线性表中的记录必须是关键码有序(通常从小到大)。在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右边区继续查找。不断重复上述过程,直到查找成功,或查找区域无记录,查找失败
参考代码:折半查找时间复杂度o(logn)
/*折半查找*/
int Binary_Search(int *a,int n, int key)
{
int low,high,mid;
low=1; //定义最低下标为记录首位
high=n; //定义最高下标为记录末位
while(low<=high)
{
mid=(low+high)/2; //折半
if(key<a[mid]) //若查找值比中指小
high=mid-1;
else if(key>a[mid]) //若查找值比中指大
low=mid+1; //最底下标调整到中位下标大一位
else
return mid; //若相等则说明mid即为查找到的位置
}
return 0;
}
将折半查找的第8行代码改为: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; //定义最高下标为记录末位
k=0;
while(n>F[k]-1) //计算n位于裴波那契数列的位置,F[k]就是裴波那契数列,a[n]就是要查找的数列
k++;
for(i=n; i<F[k]-1; i++) //将不满的数值补全
a[i]=a[n];
while(low<=high)
{
mid=low+F[k-1]-1; //计算当前分隔的下标
if(key<a[mid]) //若查找记录小于当前分隔记录
{
high=mid-1; //最高下标调整到分隔下标mid-1处
k=k-1; //裴波那且数列下标减一位
}
else if(key>a[mid]) //若查找记录大于当前分隔记录
{
low=mid+1; //最低下标调整到分隔下标mid+1处
k=k-2; //裴波那契数列下标减2位
}
else
{
if(mid<=n)
return mid; //若相等则说明mid即为查找到的位置
else
return n; //若mid>n说明是补全数值,返回n
}
}
return 0;
}
时间复杂度为o(logn),但就平均性能而言要优于折半查找
8.5线性表的索引查找
1.稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,索引项一定是按照关键码的有序排列
2.查找关键字可以用折半查找,插值,裴波那契…查找方法
3.如果对于右边的表进行查找,只能进行顺序查找
如下图所示
分块索引的条件:
1.块内无序:
2.块间有序:要求后一块中所记录的关键字都大于第一块中所有记录的关键字,并且无序排列(如下图)
分块索引的数据项:
1.最大关键码:记录块间中的最大关键字
2.存储块中记录的个数,便于循环使用
3.用于指向块首数据元素的指针,便于进行遍历
分块索引的步骤:
1.在块内进行折半查找,插值查找…比如找62, 57<62<96在第三个块中
2.根据块首指针找到相应的块,然后只能进行顺序查获
索引项的通用结构是:
1.次关键码:英文单词,相当于属性
2.记录号表:文章编号,相当于记录的地址
根据属性来找地址,称为倒排
1.查找时插入或删除的查找表称为动态查找表,用二叉排序树这样的数据结构进行存储
比如下图中将58插入进去,对于线性表而言还要进行移位,但对于二叉树结构而言直接将58插入到62的左子树。这就是二叉排序树的方便之处。
2.二叉树性质:
若他的左子树不空,则左子树上所有的结点的值小于它的根结构的值
若他的右子树不空,则右子树上所有的结点的值大于它的根结构的值
3.对于数列{35,37,47,51,58,62,73,88,93,99}而言,具体的二叉排序树如下图所示
8.6.1二叉排序树查找操作
//二叉树的二叉链表结点结构定义:
typedef struct BiTNode //结点结构
{
int data; //结点数据
struct BiTNode *lchild,*rchild; //左右孩子指针
}
//递归查找二叉排序树T中是否存在key
//指针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;
rerturn TRUE;
}
else if(key<T->data)
return SearchBST(T-lchild,key,T,p); //在左子树继续查找
else
return SearchBST(T->rchild,key,T,p); //在右子树继续查找
}
详细过程见下图:
//当前二叉排序树T中不存在关键字等于key的数据元素
//插入key并返回TRUE,否则返回FALSE
Status InsertBST(BiTree *T,int key)
{
BiTree p,s;
if(!SearchBST(*T,key,NULL,&p)) //查找不成功
{
s=(BiTree)malloc(sizeof(BiTNode));
s->data=key;
s->lchild=s->rchild=NULL;
if(!p)
*T=s; //插入s为新的根结点
else if(key<p->data) //插入s为左孩子
p->lchild=s;
else
p->rchild=s; //插入s为右孩子
return TRUE;
}
else
return FALSE; //树中已有关键字相同的结点,不再插入
}
下面代码展示如何将数组a[10]={62,88,58,47,35,73,51,99,37,93},插入到一颗二叉树中
int i;
int a[10]={62,88,58,47,35,73,51,99,37,93};
BiTree T=NULL;
for(i=0; i<10; i++)
InsertBST(&T,a[i);
上述代码如下图:
8.6.3二叉排序树删除操作
分为3 种情况:
下面代码这个算法是递归方式对二叉排序树T查找key,查找时删除 |
//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素的结点
//并返回TRUE,否则返回FALSE
Status DeleteBST(BiTree *T,int key)
{
if(!*T) //不存在关键字等于key的数据元素
return FALSE;
else
{
if(key==(*T)->data) //找到关键字等于key的数据元素
return FALSE;
else if(key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(*T)->rchild,key);
}
}
Delete代码 |
//从二叉排序树中删除结点p,并重接他的左或者右子树
Status Delete(BiTree *p)
{
BiTree q,s;
//下if和else if只适合只有左或者右子树的结点
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指向被删除结点的直接前驱
if(q!=*p)
q->rchild=s->lchild; //重接q的右子树
else
q->lchild=s->lchild; //重接q的左子树
}
return TRUE;
}
上述代码的详细过程,如下:
8.6.4二叉排序树总结
1.优点:插入时小的数字插入到结点左子树,大的数字插入到结点的右子树,操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可。
2.对于二叉树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。
3.极端情况就是,最少为1次,即根结点就是要找到的结点,最多不会超过树的深度
4.如下图中,左边的图查找99要2次,右边的图查找99要10次。左图的时间复杂度就是o(logn),右图的时间复杂度为o(n)
8.7平衡二叉树(AVL树)
AVL树相关概念:
·1.定义:平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1,也就是高度差的绝对值<=1
2.平衡因子(BF):将二叉树的左子树深度—右子树深度的值称为平衡因子
3.平衡二叉树上所有结点的平衡因子只可能是-1,0,1。只要二叉树上有一个结点的平衡因子的绝对值>1,则该树就是不平衡的,
4.平衡二叉树首先是一个二叉排序树(即小的值在左子树,大的值在右子树上)。详见下图中的几个例子:
8.7.1 平衡二叉树实现原理
平衡二叉树构建的基本思想:每当插入一个结点时,先检查是够因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。(详细见下图)
8.7.2 平衡二叉树实现算法
1.首先是需要改进二叉排序树的结点结构,增加一个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; //L指向p的左子树的根结点
(*p)->lchild=L->rchild; //L的右子树挂接为p的左子树
L->rchild=(*p);
*p=L; //p指向新的根结点
}
//详见如下图:
//对以p为根的二叉排序树做左旋处理,处理之后p指向新的树根结点,即旋转处理之前的右子树根结点
void L_Rotate(BiTree *P)
{
BiTree R;
R=(*P)->rchild; //R指向p的右子树根结点
(*P)->rchild=R->lchild; //R的左子树挂接为p的右子树
R->lchild=(*P);
*P=R; //P指向新的根结点
}
//详见下图:
#define LH +1 //左高
#define EH 0 //等高
#define RH -1 //右高
//对以指针T所指结点为根的二叉树做左平衡旋转处理,本算法结束时,指针T指向新的根结点
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild; //L指向T的左子树根结点
switch(L->bf)
{
//检查T的左子树的平衡度,并做相应的平衡处理
case LH: //新结点插入在T的左孩子的左子树上,要做单右旋转处理
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH: //新结点插入在T的左孩子的左子树上,要做单右旋处理
Lr=L->rchild; //Lr指向T的左孩子的右子树根
switch(Lr->bf) //修改T及其左孩子的平衡因子
{
case LH:
(*T)->bf=RH;
L->bf=EH;
break;
case EH:
(*T)->bf=L->bf=EH;
break;
case RH:
(*T)->bf=EH;
break;
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild); //对T的左子树做左旋平衡处理
R_Rotate(T); //对T做右旋平衡处理
}
} //详见代码如下图所示:
//若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
//数据元素为e的新结点并返回1,否则返回0。若因插入而使二叉排序树失去平衡,则做平衡旋转处理,布尔变量taller反映T长高与否
Status InsertAVL(BiTree *T, int e, Status *taller)
{
if(!*T) //不存在与e有相同关键字的结点
{
*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=FALSE;
return FALSE;
}
if(e<(*T)->data)
{
//应继续在T的左子树中进行搜索
if(!InsertAVL(&(*T)->lchild,e,taller)) //未插入
return FALSE;
if(*taller) //已插入到T的左子树中且左子树长高
{
switch((*T)->bf) //检查T的平衡度
{
case LH: //原本左子树比右子树高,需要做左平衡处理
LeftBalance(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(((*T)->bf) //检查T的平衡度
{
case LH: //原本左子树比右子树高,现左右子树的等高
(*T)->bf=EH;
*taller=FALSE;
break;
case EH: //原本左右子树等高,现因右子树增高而树增高
(*T)->bf=RH;
*taller=TRUE;
break;
case RH: //原本右子树比左子树高,需要做右平衡处理
RightBalance(T);
*taller=FALSE;
break;
}
}
}
}
return TRUE;
}//具体过程如下图:
构建一个数组为a[10]={3,2,1,4,5,6,7,10,9,8} 代码如下:
int i;
int a[10]={3,2,1,4,5,6,7,10,9,8};
BiTree T=NULL;
Status taller;
for(i=0; i<10; i++)
InsertAVL(&T,a[i],&taller);
如果二叉排序树是平衡二叉排序树,查找+插入+删除 时间复杂度为o(logn) |
8.8多路查找树
1.多路查找树出现的原因:我们之前谈的树,都是一个结点可以有很多孩子,但是它自身只存储一个元素。二叉树限制更多,结点最多只能有两个孩子。一个结点只能存储一个元素,在元素非常多的时候,就使得要么树的度(结点拥有子树的个数最大值)非常大,要么树的度非常高,甚至两者都必须足够大才行。这就使得内存存取次数非常多,这显然成立时间效率上的瓶颈,这迫使我们要打破每一个结点只存储一个元素的限制
2.多路查找树的定义:
3.2-3树定义,插入,查找:
4 2-3-4树
5.B树的定义+属性+最坏的查找次数:
6.B+树的引用+定义+m阶B+树和B树的差异+B+树的优点:
、
8.9散列表查找(哈希表)概述
1.散列表查找定义+查找步骤+散列技术适用于什么+散列表缺点+冲突:
8.10散列函数的构造方法
1.构造散列函数的原则:
2.直接定址法:
3.数字分析法:
4.平方取中法:
5.折叠法:
6.除留余数法:
7.随机数法:
8.采用不同散列函数考虑的因素:
8.11处理散列冲突的方法
1.开放地址法+二次探测法+随机探测法:
2.再散列函数法:
3.链地址法:
4.公共溢出区法:
除留
8.12 散列表查找实现
8.12.1 散列表查找算法实现
1.散列表结构和一些相关常数。其中HashTable是散列表结构,结构中的elem为一个动态数组
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表长为数组长度
#define NULLKEY -32768
typedef struct
{
int *elem; //数据元素存储基址,动态分配数组,就是数组的初始地址
int count; //当前数据元素的个数
}HashTable;
int m=0; //散列表表长,全局变量
对散列表进行初始化
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->elem=(int *)malloc(m*sizeof(int));
for(i=0; i<m; i++)
H->elem[i]=NULLKEY;
return OK;
}
散列函数:
int Hash(int key)
{
return key%m; //除留余数法
}
对散列表的插入操作
//插入关键字进散列表
void InsertHash(HashTable *H, int key)
{
int addr=Hash(key); //求散列地址
while(H->elem[addr]!=NULLKEY) //如果不为空,则冲突
addr=(addr+1)%m; //开放地址法的线性探测
H->elem[addr]=key; //直到有空位后插入关键字
}
对散列表的查找记录的操作
//散列表查找关键字
Status SearchHash(HashTable H, int key, int *addr)
{
*addr=Hash(key); //求关键字的散列地址
while(H.elem[*addr]!=key) //如果不为key,则继续循环查找
{
*addr=(*addr+1)%m; //开放地址法线性探测
if(H.elem[*addr]==NULLKEY || *addr=Hash(key)) //前面表示压根数组中就没有要查找的key,或者循环找了一圈,又回到起点
return UNSUCCESS;
}
return SUCCESS;
}
8.12.2 散列表查找性能分析
1.时间复杂度是O(1)
2.散列查找的平均查找长度取决于哪些因素?
1.散列函数是否均匀:散列函数的好坏直接影响出现冲突的频繁程度
2.处理冲突的方法:相同的关键字,相同的散列函数,但处理冲突的方法不同,会使得平均查找长度不同。比如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能
3.散列表的装填因子:装填因子α=填入表中的记录个数/散列表长度。α标志着散列表的装满程度,当然α越大表示装满程度越高,也就是产生冲突的可能性越大。也就是说,散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。不管记录个数n有多大,总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围,此时散列表的复杂度为O(1),通常将散列表的空间设置得比查找集合大,虽然浪费空间,但效率提升。