查找算法
查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找。
查找表:用于查找的数据集合称为查找表,它由同一类型的数据元素组成,可以是一个数组或链表等数据类型。从名义上来看,查找表是一种新的数据结构,但实际上它指的就是用线性表,树或者图结构来存储数据,只不过数据间的逻辑关系是人为赋予的。数据结构中,将存储无逻辑关系数据的线性表,树或者图结构统称为查找表。
查找表常见操作:
- 查找特定元素是否在查找表中;
- 获取查找表中某个元素的值;
- 查找失败时,将目标元素插入到查找表中;
- 查找成功时,将目标元素从查找表中删除。
如果只对查找表做前俩种操作,不改变查找表的存储结构,这样的查找表称为静态查找表;反之,如果对查找表做插入或者删除操作,查找表的结构发生了变化,这样的查找表称为动态查找表。
动态查找表可以在查找过程中动态建立,而静态查找表只能先建立然后再执行查找操作。
平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvOF1w5i-1662212577600)(vx_images/200001011239279.png)]
n是查找表的长度;p是查找第i个数据元素的概率,一般认为每个元素的查找概率相等,即p=1/n,C是找到第i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要指标。 ASL值越大,表明查找算法的性能越差,执行效率越低。
顺序查找算法
顺序查找算法又称顺序搜索算法或者线性搜索算法,是所有查找算法中最基础,最简单的。顺序查找算法适用于绝大多数场景,查找表中存放有序序列或者无序序列,都可以使用此算法。
顺序查找算法的实现思路:从查找表的一端开始,将表中的元素逐一和目标元素作比较,直至找到目标元素。如果表中的所有元素和目标元素对比了一遍,最终没有找到目标元素,表明查找表中没有目标元素,查找失败。
顺序查找算法的具体实现
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct
{
keyType key;//查找表中每个数据元素的值
//如果需要还可以添加其他属性
}ElemType;
//顺序表表示查找表
typedef struct
{
ElemType* elem;//存放查找表中数据元素的数组
int length;//记录查找表中数据的总数量
}SSTable;
//创建查找表
void Create(SSTable* st, int length)
{
int i;
st->elem = (ElemType*)malloc((length+1)*sizeof(ElemType));
//加1是为了给监视哨流出位置
st->length = length;
printf("输入表中的数据元素:\n");
//根据查找表中的数据元素的总长度,在存储时数组下标为1的空间开始存储数据
for(i = 0;i<length;i++)
{
scanf("%d",&(st->elem[i].key));
}
}
//查找表查找的功能函数,其中key为关键字
int Search_seq(SSTable st,keyType key)
{
int i ;
st.elem[0].key=key;
//将目标元素放在顺序表0的位置,起监哨作用
//从查找表最后一个元素开始,直至找到目标元素
for(i=st.length;st.elem[i].key!=key;--i);
//如果i=0,说明查找失败;
//反之,返回的是含有关键字key的数据元素在查找表中的位置
return i;
}
int main()
{
int key,location,len;
SSTable st;
printf("输入查找表的元素个数");
scanf("%d",&len);
Create(&st,len);
printf("请输入查找数据的关键字:");
scanf("%d",&key);
location = Search_seq(st,key);
if(location == 0)
{
printf("查找失败");
}
else
{
printf("目标元素在查找表中的位置为:%d",location+1);
}
free(st.elem);
}
顺序查找算法的性能分析
顺序查找算法的时间复杂度可以用O(n)表示。查找表找中的元素越多,顺序查找算法的执行效率越低。
默认情况下,表中各个元素被查找到的概率都是相同的,都是1/n,所以各个元素对应的Pi就是1/n。在给定的查找表中,顺序查找表从表的一端开始查找,第一个元素对应的C1=1,第二个元素对应的C2,依此类推,所以表中第i个元素对应的Ci就是i。
可得,顺序查找算法对应的ASL值为(n+1)/2,几乎为查找表长度的一半。这也就意味着,查找表中包含的元素越多,顺序查找算法的ASL值越大,查找性能越差,执行效率越低。
顺序查找算法的优点是实现简单,适用于绝大多数场景。和其他算法相比,顺序擦护照算法的时间复杂度较大,同样平均查找长度也较大,查找表中的元素数量越多,算法的性能越差。
折半查找
折半查找又称二分查找,二分搜索,折半搜索等,是一种在静态查找表中查找特定元素的算法。
使用二分查找算法,必须保证 查找表中存放的是有序序列。换句话说,存储无序序列的静态查找表,除非先对数据进行排序,否则不能使用二分查找算法。
折半查找算法的实现思路:首先将给定值key与表中中间位置,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素意外的前半部分或后半部分(例如,在查找表升序排序时,若给定值key大于中间元素,则所查找的元素只可能在后半部分)。然后在缩小的范围内继续进行同样的查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。
折半查找算法具体实现
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct
{
keyType key;//查找表中每个数据元素的值
//如果需要还可以添加其他属性
}ElemType;
//顺序表表示查找表
typedef struct
{
ElemType* elem;//存放查找表中数据元素的数组
int length;//记录查找表中数据的总数量
}SSTable;
//创建查找表
void Create(SSTable* st, int length)
{
int i;
st->elem = (ElemType*)malloc((length+1)*sizeof(ElemType));
//加1是为了给监视哨流出位置
st->length = length;
printf("输入表中的数据元素:\n");
//根据查找表中的数据元素的总长度,在存储时数组下标为1的空间开始存储数据
for(i = 0;i<length;i++)
{
scanf("%d",&(st->elem[i].key));
}
}
//折半查找算法
int Search_Bin(SSTable st,keyType key)
{
int low = 0;//初始状态low指针指向第一个关键字
int high = st.length-1;
int mid;
while(low <= high){
mid = (low + high) / 2;
//int 本身为整形,所以mid每次为取整的整数
if(st.elem[mid].key == key)
//如果mid指向的同要查找的相等,返回id所指向的位置
{
return mid;
}
else if (st.elem[mid].key > key){//从前半部分继续查找
high = mid - 1;
}
else{//从后半部分继续查找
low = mid + 1;
}
}
return -1;//查找失败,返回-1
}
int main()
{
int key,location,len;
SSTable st;
printf("输入查找表的元素个数");
scanf("%d",&len);
Create(&st,len);
printf("请输入查找数据的关键字:");
scanf("%d",&key);
location = Search_Bin(st,key);
//如果返回值为-1,证明查找表中为查到key值
if(location == -1)
{
printf("查找失败");
}
else
{
printf("目标元素在查找表中的位置为:%d",location + 1);
}
free(st.elem);
return 0;
}
二分查找算法性能分析
二分查找算法的时间复杂度可以用O(
log
2
n
\log_2n
log2n) 表示n为查找表中的元素数量,底数2可以省略)。和顺序查找算法的O(n)相比,二分查找算法的效率更高,且查找表中的元素越多,二分查找算法效率高的优势就越明显。
平均查找长度ASL=
log
2
(
n
+
1
)
−
1
\log_2(n+1)-1
log2(n+1)−1 ,二分查找算法只适用于有序的静态查找表,且通常选择用顺序表表示查找表结构。
分块查找
分块查找又称索引顺序查找,它吸收了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找。
分块查找的基本思想:将查找表分为若干子块。块内的元素可以无序,但是块间是有序的,即第一个块中的最大关键字小于第二块中的最大关键字,第二块中的最大关键字小于第三块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块的第一个元素的地址,索引表按关键字有序排列。
分块查找的过程:第一步是在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表;第二步是在块内顺序查找。
分块查找的平均长度为索引查找和块内查找的平均长度之和。设索引查找和块内查找的平均查找长度分别为Li,Ls,则分块查找的平均查找长度为ASL=Li+Ls,将长度为n的查找表均匀地分为b块,每块有s个记录.
在等概率的情况下,若在块内和索引表中均采用顺序查找,
则平均查找长度为ASL=Li+Ls=(b+1)/2+(s+1)/2 = (s^2+2s+n)/2s;
若对索引表采用折半查找时,则平均查找长度为ASL=Li+Ls=
⌈
log
2
(
b
+
1
)
⌉
\lceil\log_2(b+1)\rceil
⌈log2(b+1)⌉+(s+1)/2。
二叉排序树
二叉排序树又叫二叉查找树和二叉搜索树,时一种实现动态查找表的树型存储结构。
二叉排序树的本质时一棵二叉树,它的特别之处在于:
- 对于树中的每个结点,如果他有左子树,那么左子树上所有结点的值都比该结点小;
- 对于树中的每个结点,如果它有右子树,那么右子树上所有结点的值都比该结点大。
二叉排序数的具体应用
二叉排序数的常见操作有3种,分别是:
SearchBST(Key):查找指定元素Key;
InsertBST(Key):若二叉排序树中不存在Key,将Key作为新结点插入到树上的适当位置;
DeleteBST(Key):若二叉排序树中存在元素Key,将存储Key的结点从树中摘除。
二叉排序数查找元素
在二叉排序树中查找目标元素,就是从根结点出发,先将树根结点和目标元素做比较:
若当前结点不存在,则查找失败;若当前结点的值和目标元素相等,则查找成功;
若当前结点的值比目标元素大,目标元素只可能位于当前结点的左子树中,继续进入左子树查找;
若当前结点的值比目标元素小,目标元素只可能位于当前结点的右子树,继续进入右子树查找;
代码实现:
BiTree SearchBST(BiTree T,KeyType key){
//如果T为空,则查找失败,返回NULL;或者查找成功,返回指向存有目标元素结点的指针
if(!=T||key==T->data){
return T;
}
else if(key<T->data){
//继续去左子树中查找目标元素
return SearchBST(T->lchild,key);
}
else{
//继续去右子树中查找目标元素
return SearchBST(T->rchild,key);
}
}
二叉排序树插入元素
二叉排序树中各个结点的值都不相等,因此新插入的元素一定是原二叉排序树没有的,否则插入操作会失败。此外插入新元素后,必须保证整棵树还是一棵二叉排序树。
二叉排序树插入新元素的方法是:在树中查找新元素,最终查找失败时找到的位置,就是放置新元素的位置。
代码:
Status InsertBST(BiTree* T,ElemType e){
//如果本身为空树,则直接添加e为根结点
if((*T)==NULL){
(*T) = (BiTree)malloc(sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
return true;
}
//如果找到目标元素,插入失败
else if(e==(*T)->data){
return false;
}
//如果e小于当前结点的值,表明应该将e插入到该结点的左子树中
else if(e<(*T)->data){
InsertBST(&(*T)->lchild,e);
}
//如果e大于当前结点的值,表明应该将e插入到该结点的右子树中
else{
InsertBST(&(*T)->rchild,e);
}
}
InserBST()函数本意是将指定元素插入到二叉排序树中,当二叉排序树不存在(为NULL)时,此函数还能完成二叉排序树的构建工作。
作为实现动态查找表的树形结构,二叉排序树通常不会一次性创建好,而是一边查找一边创建,InsertBST()就是实现此过程的函数。
二叉排序树删除元素
二叉排序树删除树中已有元素,必须确保整棵树还是一棵二叉排序树。
假设被删除的元素是P,删除的同时需要妥善处理它的左,右子树。根据结点P是否有左,右孩子可以归结为以下3中情况:
- P是叶子结点:可以直接删除,整棵树还是二叉排序树。
- P只有一个孩子(左孩子或右孩子):若P是双亲结点(用F表示)的左孩子,直接将P的孩子结点作为F的左孩子;反之若P是F的右孩子,直接将P的孩子结点作为F的右孩子。
- P有俩个孩子:中序遍历整颗二叉排序数,在中序序列里找到P的直接前驱结点S,将P结点修改成S结点,然后再将之前的S结点从树中摘除。
在二叉排序树中,对于用有俩个孩子的结点,它的直接前驱结点要么是叶子结点,要么是没有右孩子的结点,所以删除直接前驱结点可以套用前面俩种情况的实现思路。
代码如下:
//实现删除 p 结点的函数
Status Delete(BiTree* p)
{
BiTree q = NULL, s = NULL;
//情况 1,结点 p 本身为叶子结点,右孩子也为 NULL,用 NULL 直接替换 p 结点即可
//情况 2,结点 p 只有一个孩子
if (!(*p)->lchild) { //左子树为空,只需用结点 p 的右子树根结点代替结点 p 即可;
q = *p;
*p = (*p)->rchild;
free(q);
}
else if (!(*p)->rchild) {//右子树为空,只需用结点 p 的左子树根结点代替结点 p 即可;
q = *p;
*p = (*p)->lchild;
free(q);
}
//情况 3,结点 p 有两个孩子
else {
q = *p;
s = (*p)->lchild;
//遍历,找到结点 p 的直接前驱,最终 s 指向的就是前驱结点,q 指向的是 s 的父结点
while (s->rchild)
{
q = s;
s = s->rchild;
}
//直接改变结点 p 的值
(*p)->data = s->data;
//删除 s 结点
//如果 q 和 p 结点不同,删除 s 后的 q 将没有右子树,因此将 s 的左子树作为 q 的右子树
if (q != *p) {
q->rchild = s->lchild;
}
//如果 q 和 p 结点相同,删除 s 后的 q(p) 将没有左子树,因此将 s 的左子树作为 q(p)的左子树
else {
q->lchild = s->lchild;
}
free(s);
}
return true;
}
//删除二叉排序树中已有的元素
Status DeleteBST(BiTree* T, int key)
{
//如果当前二叉排序树不存在,则找不到 key 结点,删除失败
if (!(*T)) {
return false;
}
else
{
//如果 T 为目标结点,调用 Delete() 删除结点
if (key == (*T)->data) {
Delete(T);
return true;
}
else if (key < (*T)->data) {
//进入当前结点的左子树,继续查找目标元素
return DeleteBST(&(*T)->lchild, key);
}
else {
//进入当前结点的右子树,继续查找目标元素
return DeleteBST(&(*T)->rchild, key);
}
}
}
二叉排序树的具体实现
#include<stdio.h>
#include<stdlib.h>
#define ElemType int
#define KeyType int
typedef enum { false, true } Status;
/* 二叉排序树的节点结构定义 */
typedef struct BiTNode
{
int data;
struct BiTNode* lchild, * rchild;
} BiTNode, * BiTree;
//在 T 二叉排序树中查找 key
BiTree SearchBST(BiTree T, KeyType key) {
//如果 T 为空,则查找失败,返回NULL;或者查找成功,返回指向存有目标元素结点的指针
if (!T || key == T->data) {
return T;
}
else if (key < T->data) {
//继续去左子树中查找目标元素
return SearchBST(T->lchild, key);
}
else {
//继续去右子树中查找目标元素
return SearchBST(T->rchild, key);
}
}
//向二叉排序树 T 中插入目标元素 e
Status InsertBST(BiTree* T, ElemType e) {
//如果本身为空树,则直接添加 e 为根结点
if ((*T) == NULL)
{
(*T) = (BiTree)malloc(sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
return true;
}
//如果找到目标元素,插入失败
else if (e == (*T)->data)
{
return false;
}
//如果 e 小于当前结点的值,表明应该将 e 插入到该结点的左子树中
else if (e < (*T)->data) {
InsertBST(&(*T)->lchild, e);
}
//如果 e 大于当前结点的值,表明应该将 e 插入到该结点的右子树中
else
{
InsertBST(&(*T)->rchild, e);
}
}
//实现删除 p 结点的函数
Status Delete(BiTree* p)
{
BiTree q = NULL, s = NULL;
//情况 1,结点 p 本身为叶子结点,右孩子也为 NULL,用 NULL 直接替换 p 结点即可
//情况 2,结点 p 只有一个孩子
if (!(*p)->lchild) { //左子树为空,只需用结点 p 的右子树根结点代替结点 p 即可;
q = *p;
*p = (*p)->rchild;
free(q);
}
else if (!(*p)->rchild) {//右子树为空,只需用结点 p 的左子树根结点代替结点 p 即可;
q = *p;
*p = (*p)->lchild;
free(q);
}
//情况 3,结点 p 有两个孩子
else {
q = *p;
s = (*p)->lchild;
//遍历,找到结点 p 的直接前驱,最终 s 指向的就是前驱结点,q 指向的是 s 的父结点
while (s->rchild)
{
q = s;
s = s->rchild;
}
//直接改变结点 p 的值
(*p)->data = s->data;
//删除 s 结点
//如果 q 和 p 结点不同,删除 s 后的 q 将没有右子树,因此将 s 的左子树作为 q 的右子树
if (q != *p) {
q->rchild = s->lchild;
}
//如果 q 和 p 结点相同,删除 s 后的 q(p) 将没有左子树,因此将 s 的左子树作为 q(p)的左子树
else {
q->lchild = s->lchild;
}
free(s);
}
return true;
}
//删除二叉排序树中已有的元素
Status DeleteBST(BiTree* T, ElemType key)
{
//如果当前二叉排序树不存在,则找不到 key 结点,删除失败
if (!(*T)) {
return false;
}
else
{
//如果 T 为目标结点,调用 Delete() 删除结点
if (key == (*T)->data) {
Delete(T);
return true;
}
else if (key < (*T)->data) {
//进入当前结点的左子树,继续查找目标元素
return DeleteBST(&(*T)->lchild, key);
}
else {
//进入当前结点的右子树,继续查找目标元素
return DeleteBST(&(*T)->rchild, key);
}
}
}
//中序遍历二叉排序树
void INOrderTraverse(BiTree T) {
if (T) {
INOrderTraverse(T->lchild);//遍历当前结点的左子树
printf("%d ", T->data); //访问当前结点
INOrderTraverse(T->rchild);//遍历当前结点的右子树
}
}
//后序遍历,释放二叉排序树占用的堆内存
void DestroyBiTree(BiTree T) {
if (T) {
DestroyBiTree(T->lchild);//销毁左孩子
DestroyBiTree(T->rchild);//销毁右孩子
free(T);//释放结点占用的内存
}
}
int main()
{
int i;
int a[10] = { 41,20,11,29,32,65,50,91,72,99 };
BiTree T = NULL;
for (i = 0; i < 10; i++) {
InsertBST(&T, a[i]);
}
printf("中序遍历二叉排序树:\n");
INOrderTraverse(T);
printf("\n");
if (SearchBST(T, 20)) {
printf("二叉排序树中存有元素 20\n");
}
printf("删除20后,中序遍历二叉排序树:\n");
DeleteBST(&T, 20);
INOrderTraverse(T);
//后续遍历,销毁整棵二叉排序树
DestroyBiTree(T);
}
平衡二叉树(AVL树)
平衡二叉树,又称为AVL树。实际上就是遵循以下两个特点得二叉树:
- 每颗子树中得左子树和右子树得深度差不能超过1;
- 二叉树中每颗子树都是要求是平衡二叉树;
其实就是在二叉树的基础上若树种每颗子树都满足其左子树和右子树深度差都不超过1,则这颗二叉树就是平衡二叉树。
平衡因子:每个结点都有其各自的平衡因子,表示的就是其左子树深度同右子树深度的差。 **结点的平衡因子=左子树高-右子树高 **
平衡二叉树中各结点平衡因子的取值只可能是:0,1,-1。
在二叉排序树中插入新结点后,如何保持平衡?
从插入点往回找到第一个不平衡结点,调整以该结点为根的子树
每次调整的的对象都是“最小不平衡子树”。
在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡
二叉排序树的特性:左子树结点值<根结点值<右子树结点值
如何调整最小不平衡子树
-
LL平衡旋转(右单旋转)。由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A为根结点,将A结点向右旋转称为B的右子树的根结,而B的原右子树则作为A结点的左子树。
-
RR平衡旋转(左单旋转)。由于在结点A的右孩子®的右子树®上插入了新结点,A的平衡因子由-1减至-2,导致以A根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根结点,将A结点向下左旋转称为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
-
LR平衡旋转(先左后右双旋转)。由于在A的左孩子(L)的右子树®上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后把该C结点左上旋提升到A结点的位置。
-
RL平衡旋转(先右后左双旋转)。由于在A的右孩子®的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后把该C结点向左旋转提升到结点的位置。
散列表
散列表,又称哈希表。是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关。
若不同的关键字通过散列函数映射到同一值,则称它们为同义词
同一散列函数确定的位置已经存放其他元素,则称这种情况为冲突。
对于哈希表而言,冲突只尽可能地少,无法完全避免。
处理冲突的方法–拉链法,用拉链法(又称链接法,链地址法)处理冲突,把所有的同义词存储在同一个链表中。
查找长度:在查找运算中,需要对比关键字的次数称为查找长度。
装填因子a=表中记录数/散列表长度
哈希函数的构造
常用的哈希函数的构造有6中:直接定址法,数学分析法,平方取中法,折叠法,除留余数法和随机数法。
直接定址法:其哈希函数为一次函数,即以下两种形式:
H(key)=key 或者H(key)=a*key+b
H(key)表示关键字为key对应的哈希地址,a和b都为常数。
数字分析法:如果关键字由多为字符或者数字组成,就可以考虑抽取其中的2位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。
平方取中法:对关键字做平方操作,取中间得几位作为哈希地址。此方法也是比较常用的构造哈希函数的方法。
折叠法:是将关键字分割位位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。
除留余数法:若已知整个哈希表的最大长度m,可以取一个不大于m的数p,然后对该关键字key做取余运算,即H(key) = key%p。p的取值十分重要,由经验得知p可以为不大于m得质数。
随机数法:是取关键字的一个随机函数,作为它得哈希地址,即:
H(key) = random(key)
此方法适用于关键字长度不等得情况。
这里得随机函数其实是伪随机函数,随机函数是即使每次给定得key相同,但是H(key)都是不同;而为随机函数正好相反,每个key都对应的是固定的H(key)。
在选择的时候,需要根据实际的查找表的情况采取适当的方法。通常考虑的因素有以下几个方面:
- 关键字的长度。如果长度不相等,就选随机数法。如果关键字位数较多,就选用折叠法或者数字分析法;反之如果位数较短,可以考虑平方取中法;
- 哈希表大大小。如果大小已知,可以选用除留余数法;
- 关键字的分布情况;
- 查找表的查找频率;
- 计算哈希函数所需的时间(包括硬件指令的因素)
处理冲突的方法
- 开放定址法
H(key) = (H(key)+d) MOD m (其中m为哈希表的表长,d为一个增量)
当得出的哈希地址产生冲突时,选取以下3种方法中的一种获取d的值,然后继续计算,直到计算出的哈希地址不在冲突位置,这3种方法为:
- 线性探测法:d=1,2,3,……,m-1
- 二次探测法:d=12,-12,22,-22,32……
- 伪随机数探测法:d=伪随机数
在线性探测法中,当遇到冲突时,从发生冲突位置起,每次+1,向右探测,直到有空闲的位置为止;二次探测法中,从发生冲突的位置起,按照+12,-12,+22……如此探测,直到有空闲的位置;伪随机探测,每次加上一个随机数,直到探测到空闲位置结束。
-
再哈希法
当通过哈希函数求得的哈希地址同其他关键字产生冲突时,使用另一个哈希函数计算,直到冲突不再发生。 -
链地址法
将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。 -
建立一个公共溢出区
建立两张表,一张为基本表,另一张为溢出表。基本表存储没有发生冲突的数据,当关键字由哈希函数生成的哈希地址产生冲突时,就将数据填入溢出表。
哈希查找算法及其实现
在哈希表中进行查找的操作同哈希表的构建过程类似,其具体实现思路为:对于给定的关键字K,将其带入哈希函数中,求得与该关键字对应的数据的哈希地址,如果该地址中没有数据,则证明该查找表中没有存储该数据,查找失败,如果哈希地址中右数据,就需要做进一步的证明(排除冲突的影响),找到该数据对应的关键字同K进行比对,如果相等,则查找成功;反之,如果不相等,说明在构造哈希表时发生了冲突,需要根据构造表时设定的处理冲突的方法找到下一个地址,或者比对成功。
#include <stdio.h>
#include <stdlib.h>
#define HASHSIZE 7 //定义散列表长为数组的长度
#define NULLKEY -1
typedef struct{
int *elem;//数据元素存储地址,动态分配数组
int count;//当前数据元素个数
}HashTable;
//哈希表进行初始化
void Init(HashTable *hashTable)
{
int i ;
hashTable->elem=(int *)malloc(HASHSIZE*sizeof(int));
hashTable->count=HASHSIZE;
for(i=0;i<HASHSIZE;i++)
{
hashTable->elem[i]=NULLKEY;
}
}
//哈希函数(除留余数法)
int Hash(int data)
{
return data%HASHSIZE;
}
//哈希表的插入函数,可用于构造哈希表
void Insert(HashTable *hashTable,int data)
{
int hashAddress = Hash(data);//求哈希地址
//发生冲突
while(hashTable->elem[hashAddress]!=NULLKEY)
{
//利用开放定址法解决冲突
hashAddress = (++hashAddress)%HASHSIZE;;
}
hashTable->elem[hashAddress]=data;
}
//哈希表的查找算法
int Search(HashTable *hashTable,int data)
{
int hashAddress=Hash(data);
while(hashTable->elem[hashAddress]!=data){//发生冲突
//利用开放定址法解决冲突
hashAddress =(++hashAddress)%HASHSIZE;
//如果查找到的地址中数据为NULL,或者经过过一圈的遍历回到原位置,则查找失败
if(hashTable->elem[hashAddress]==NULLKEY||hashAddress==Hash(data)){
return -1;
}
}
return hashAddress;
}
int main()
{
int i,result;
HashTable hashTable;
int arr[HASHSIZE] = {13,29,27,28,26,30,38};
//初始化哈希表
Init(&hashTable);
//利用插入函数构造哈希表
for(i=0;i<HASHSIZE;i++){
Insert(&hashTable,arr[i]);
}
//调用查找算法
result = Search(&hashTable,29);
if(result==-1) printf("查找失败");
else printf("29在哈希表中的位置是:%d\n",result+1);
printf("Hello world!\n");
return 0;
}
哈希查找算法的效率分析
在构造哈希表的过程中,由于冲突的产生,使得哈希表的查找算法仍然会涉及到比较过程,因此对于哈希表的查找效率仍需以平均查找长度来衡量。
在哈希表的查找过程中需和给定值进行比较的关键字的个数取决于以下3个元素:
- 哈希函数:哈希函数的好坏,取决于影响出现冲突的频繁程度。但是一般情况下,哈希函数相比于后俩中的影响,可以忽略不计。
- 处理冲突的方式:对于同一组关键字,设定相同的哈希函数,使用不同的处理冲突的方式得到的哈希表是不同的,表的平均查找长度也不同。
- 哈希表的装填因子:在一般情况下,当处理冲突的方式相同的情况下,其平均查找长度取决于哈希表的装满程度,装的越满,插入数据时越有可能发生冲突;反之则越小。
装填因子=哈希表中数据的个数/哈希表的长度,装填因子越小,表示哈希表中空闲的位置就越多。
α
经过计算,在假设查找表中的所有数据的查找概率相等的情况下,对于表长为m,数据个数为n的哈希表: - 其查找成功的平均查找长度约为:-1/α* In(1-α)
- 其查找不成功的平均查找长度约为:1/(1-α)
通过公式可以看到,哈希表的查找效率只同装填因子有关,而同哈希表中的数据的个数无关,所以在选用哈希表做查找操作时,选择一个合适的装填因子是非常有必要的。