Transcript Document

第八章 查找
§
§
§
§
§
8.1 查找的基本概念
8.2 顺序表查找
8.3 索引查找
8.4 树表查找
8.5 散列表查找
8.查找的基本概念
1.查找表
查找表(Search Table)是由记录序列组成的文件或线性表。
2.查找表上常见的操作
(1)查询某个“特定的”记录是否在查找表中;(2)检索某个“
特定的”记录的信息;(3)在查找表中插入记录;(4)在
查找表中删除记录。根据在查找表上实施的操作不同,可将
查找表分为。
3.静态查找表和动态查找表
静态查找表只做前两项统称为“查找”的操作,在查找的过程
中不再动态地改变查找表,即不做插入和删除记录的操作;
动态查找表的表结构本身是在查找过程中动态生成的,即在
查找过程中同时插入查找表中不存在的记录,或者从表中删
除已经存在的某个记录。
4.查找依据:一般是把记录的关键字作为查找的依据
5.查找(Search)的定义:给定某个特定值k,在查找表中找
出关键字等于给定值k的记录,若找到,则查找成功,返回
该记录在表中的序号;否则查找不成功,给出查找失败的信
息。
6.评价查找算法的效率
平均查找长度ASL(Average Search Length),其计算公
式为:
其中,n是记录的个数;Ci是找到第i个记录需要进行的比较
次数;Pi是查找第i个记录的概率,这里
P1=P2=…=Pi=…=Pn=1/n。
7.查找表常用的存储方式:顺序、链接、索引和散列四种来
存储。本章共介绍了四类查找方法,即顺序表查找、索引查
找、树表查找、散列表查找。
8.2顺序表查找
§ 顺序存储结构存储的查找表,就是顺序表。
§ 顺序表的存储结构用C语言描述如下:
#define N 100
typedef struct
{ KeyType
key;
DataType
other;
} RecType ;
RecType R[N+1];
N是记录的个数;R[0]闲置的主要原因为:
(1)使下标和记录序号对应;
(2)留作他用,比如用作监视哨。
§ 基于顺序表的查找两种方法:顺序查找和二分查找
8.2.1顺序查找
顺序查找(Sequential Search)又称线性查找
,是最简单、最基本的查找方法。
顺序查找的基本思想是:从表的一端开始,依
次将每个记录的关键字与给定值k进行比较,
若某个记录的关键字与k相等,表明查找成功
,并给出记录在表中的位置(序号);若整
个表检测完,仍未找到与k相等的关键字,表
明查找失败,给出查找失败的信息。
顺序查找算法的C函数如下 :
int seqSearch(RecType R[ ], KeyType k)
/*在数组R中顺序查找关键字等于k的记录,若找到
返回记录序号,否则返回0*/
{ int i;
R[0].key = k;
/*R[0]用作监视哨*/
i=N;
/*从表的尾端向前查找 */
while(R[i].key!=k)
i--;
return i;
}
顺序查找算法的性能分析
§ 平均查找长度
查找成功时,平均查找长度为:
查找不成功时,与关键字的比较次数总是n+1次。
§ 查找算法的平均时间复杂度O(n)。
§ 优缺点
优点是算法简单,可以方便地插入记录。
缺点是当n很大时,平均查找长度较大,查找效率
较低。
顺序查找方法不仅适用于顺序表,也同样适合于单
链表。
8.2.2
二分查找
§ 二分查找(Binary Search)又称折半查找,是一种
效率较高的查找方法。二分查找要求顺序表是有序
表
§ 二分查找的基本思想是:初始时,查找区间为整个
有序表,取查找区间的中间记录作为比较对象,若
中间记录的关键字与给定值相等,则查找成功;若
给定值小于中间记录的关键字,则在中间记录的前
半区继续查找;若给定值大于中间记录的关键字,
则在中间记录的后半区继续查找。不断重复上述查
找过程,直至新区间中间记录的关键字等于给定值
,则查找成功,或者查找区间不断缩小直到为空,
表明查找失败。
§ 二分查找的具体查找步骤为:
(1)设变量low和high表示查找区间的起始和终端下
标,初始时查找区间是R[1]~R[N],low取值为1,
high取值为N。设变量mid表示查找区间中间位置的
下标,计算公式:mid= (low+hign)/2
(2)当low≤high(查找区间非空)时,求mid=
(low+hign)/2,进行如下比较:
若k==R[mid].key,查找成功,返回记录在表中位置
若k<R[mid].key,则high=mid-1,在前半区继续查找
若k>R[mid].key,则low=mid+1,在后半区继续查找
(3)当low>high时,查找区间为空,说明查找失败
例8.1一个有序表中有13个记录,记录的关键字
序列为(7,14,18,21,23,29,31,35,38
,42,46,49,52),给定值k分别为14和22,
在表中查找关键字与k相等的记录。
0
1
2
3
4
5
6
7
8
9
7
14 18 21 23 29 31 35 38 42 46 49 52
↑
low=1
10
11
12 13
↑
high=13
↑
mid=7
↑
low=1
↑
high=6
k<R[mid],调整到前半区:high=mid-1
↑
mid=3
↑
↑
low=1 high=2
↑
mid=1
↑↑
low=high=2
↑
mid=2
k<R[mid],调整到前半区:high=mid-1
k>R[mid],调整到后半区:low=mid+1
k==R[mid],查找成功,返回找到的记录序号2
查找k=14的过程示意图
0
1
2
3
4
5
6
7
14 18 21 23 29 31 35 38 42 46 49 52
↑
low=1
↑
low=1
7
8
9
10
11
12 13
↑
mid=7
↑
mid=3
↑
low=4
↑
high=6
k<R[mid],high=mid-1
↑
high=6
k>R[mid],low=mid+1
↑
mid=5
↑↑
low=high=4
↑
mid=4
↑ ↑
high=4 low=5
k<R[mid],high=mid-1
k>R[mid],low=mid+1
low>high 查找区间为空,查找失败。
查找k=22的过程示意图
↑
high=13
二分查找非递归算法的C函数:
int binarySearch(RecType R[ ], KeyType k)
/*用二分查找法
在数组R中查找关键字为k的记录,若找到返回该记录的位置,否则返回
0。*/
{ int low, hign, mid;
low=1; high=N;
/*设置初始查找区间*/
while(low<=high)
/*查找区间非空*/
{ mid=(low+high)/2;
/*计算查找区间中间位置的下标*/
if(k==R[mid].key) return mid; /*查找成功,返回记录的位置*/
else if(k<R[mid].key) high=mid-1; /*调整到前半区*/
else low=mid+1;
/*调整到后半区*/
}
return 0;
}
§ 二分查找过程判定树
31
18
42
7
23
14
21
35
29
49
38
46
52
例8.1的二分查找过程对应的判定树
查找成功情况下的平均查找长度为:
ASL=(1+2×2+3×4+4×6)/13=36/13。
查找不成功情况下的判定树
31
42
18
7
23
14
21
49
35
29
38
46
52
查找不成功时的平均查找长度ASL为:
ASL不成功=(3×2+4×12)/14=54/14。
二分查找性能分析
§ 二分查找的平均查找长度
以树高为k的满二叉树为例,树中共有n=2k-1个
结点,第i层有2i-1个结点,则二分查找的平均查
找长度为:
n
1
0
1
k 1


ASL   PC

1

2

2

2

...

k

2
i i

n
i 1
n 1

log 2 (n  1)  1  log 2 (n  1)  1
n
§ 二分查找算法的平均时间复杂度为O(log2n)
§ 二分查找的特点
优点:比较次数相对较少,查找效率较高。
缺点:在查找之前需要建立有序表,二分查
找要求顺序存储有序表,因而在表中插入或
删除记录都需要移动大量的记录,
二分查找方法适合于数据相对稳定的静态查
找表。
二分查找只适合顺序存储结构,而不适合链
接存储结构。
8.3 索引查找
索引查找是建立在索引存储结构上的查找方法。
例如在英文字典里查找某个单词的过程就是一个索
引查找。把字典看作是索引查找的对象,其中,字
典的正文是主要部分,称作是主表,字母索引表是
为了方便查找而建立的索引,称作是索引表。
8.3.1 索引表的组织
§
索引存储的基本思想是:除了存储主表的记录
外,还要为主表建立一个或若干个附加的索引表
。索引表用来标识主表记录的存储位置,它由若
干个被称为索引项的数据元素组成。
§
索引项的一般形式为(索引关键字,地址),
索引关键字是记录的某个数据项,一般会是记录
的关键字,地址是用来标识一个或一组记录的存
储位置。
§
若一个记录对应一个索引项,则该索引表为稠
密索引(Dense Index)。若一组记录对应一个
索引项,则该索引表为稀疏索引(Sparse Index
)。
例8.2
为表8.1所示的通讯录建立索引表。
编号
姓名
性别
职称
电话号码
所在院系
1001
刘林
女
讲师
82626777
法学院
1002
赵红
男
教授
1003
陈曲
女
副教授
1004
南方
男
讲师
67891234
68889245
89891900
法学院
法学院
理学院
1005
朱红
男
讲师
1006
刘微微
女
副教授
1007
陈俊亮
男
副教授
23452345
56347812
34512345
理学院
外语学院
外语学院
1008
赵婷婷
女
讲师
1009
陈华
女
教授
1010
佟晓伟
男
讲师
67645321
89764567
34523455
外语学院
艺术学院
艺术学院
(1)稠密索引: 稠密索引为每个记录建立索引项(索引
关键字,地址)
主表:0
1
2
3
4
5
6
7
8
9
…
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 ……
索引表:
关键字
地址
1001
0
1002
1
1003
2
1004
3
1005
4
1006
5
1007
6
1008
7
1009
8
1010
9
1)稀疏索引:把主表的记录按照某种规则划分为几
个子表,然后再为每个子表建立索引项(索引关键
字,地址)
按照所在院系划分,可以有4个子表,分别为:法学院
LA1=(1001,1002,1003),理学院LA2=(1004,
1005),外语学院LA3=(1006,1007,1008),艺术学
院LA4=(1009,1010)。
索引表:
索引关键字
起始下标
结束下标
法学院
0
2
理学院
3
4
外语学院
5
7
艺术学院
8
9
这种按照主表的非关键字建立的索引表称为辅助索
引表。
8.3.2 分块查找
§ 分块查找(Block Search)又称索引顺序查找,是对
顺序查找方法的一种改进。
§ 分块查找要求对顺序存储的主表建立索引:
(1)主表“分块”有序:将主表划分为几个子表,即分块,块内可以无序,
但块与块之间必须有序,即前一个块中的最大关键字必须小于后一个块
中的最小关键字;
(2)建立有序的索引表:为每一块建立索引项,索引项包括:每一块中的
最大关键字和每一块在主表中的起止位置。 由于主表分块有序,所以
索引表一定是个递增的有序表。
§ 分块查找的基本思想是:
首先在索引表中查找与给定值k对应的索引项,以确定下一步的查找在
主表中的哪个分块中进行;然后在分块中继续查找与给定值k对应的记
录。
设:记录的关键字序列为(14,31,8,22,18,43,62
,49,35,52,88,78,71,83)实现分块查找 。
下标
关键字
起始下标
结束下标
0
1
2
31
63
88
1
6
11
5
10
14
索引表
主表
数组R
14
31
下标
1
2
8
3
22
18
43
62
49
35
52
4
5
6
7
8
9
10
88
78
71
83
11 12 13 14
索引表存储结构的C语言描述如下:
# define MAXSIZE 10
typedef KeyType IndexType;
typedef struct
{ IndexType index;
/*IndexType是索引关键字的类型*/
int start,end;
/*子表在主表中的起始下标和结束下标*/
} IndexTable;
/*IndexTable是索引项的类型*/
IndexTable Index[MAXSIZE]; /*MAXSIZE的值应该大于索引
项数*/
实现分块查找算法的C函数:
int blockSearch(RecType R[], IndexTable Index[], int m,
KeyType k)
/*在主表R和长度为m的索引表Index中查找关键字为k的记录,查找成功返
回记录序号,否则返回0*/
{ int i,j;
for(i=0;i<m;i++)
/*在索引表中顺序查找与k对应的索引项*/
if (k<=Index[i].index)
break;
if (i<m)
/*在第i个子表中顺序查找关键字为k的记录*/
for (j= Index[i].start;j<= Index[i].end;j++)
if (k==R[j].key)
return j;
return 0;
}
分块查找性能分析:
§ 分块查找的平均查找长度
设n个记录的查找表分为m个子表,且每个子表均
有t个元素,则t= n/m。
ASL  ASL索引表  ASL子表 
1
1 n
1
n
(m  1)  (  1)  (m  )  1
2
2 m
2
m
当m取 时,ASL= +1达到了最小值,
§ 平均时间复杂度为O(
)
§ 分块查找的性能介于顺序查找和二分查找之间,
分块查找可以方便地进行插入和删除记录的操作
,不仅查找效率较高,还适合动态查找表使用。
8.4 树表查找
§ 树表,是查找表的一种树形组织形式,并且
使用链接方式进行存储。
§ 树表查找既具有二分查找的效率,同时还能
高效率地实现插删。
§ 本节主要介绍二叉排序树和B-树。
8.4.1 二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找树(Binary
Search Tree),它或者是一棵空树,或者是具有下列性质
的二叉树:
63
(1)若左子树不空,则左子树上
所有结点的值均小于根结点的值;
55
90
(2)若右子树不空,则右子树上
42
58
98
所有结点的值均大于根结点的值;
70
(3)左、右子树也都是二叉排序树。
10
45
67
83
对二叉排序树做中序遍历得到的
图8.6 一棵二叉排序树
结点序列是一个有序序列。
二叉排序树存储结构的C语言描述如下:
typedef struct node
{ KeyType key;
/*关键字域*/
DataType
other;
/*其他数据项域*/
struct node *lchild,*rchild; /*左、右指针域*/
} BstNode;
1.二叉排序树的查找
二叉排序树的查找过程为:
(1)若二叉排序树为空,查找失败。
(2)若二叉排序树非空,将给定值k与根结点
的关键字比较,如果相等,查找成功;否则
,当k小于根结点的关键字时,查找将在左子
树上继续进行;当k大于根结点的关键字时,
查找将在右子树上继续进行。
(3)在子树上的查找与前面的查找过程相同
。
二叉排序树查找算法的C函数:
BstNode * searchBst(BstNode *t, KeyType k)
/*已知二叉排序树的根结点*t,在其中查找关键字为k的记录,若找到返回
结点的地址,否则返回空地址*/
{
}
while (t!=NULL)
{ if (t->key==k)
return t;
if (t->key > k)
t=t->lchild;
else t=t->rchild;
}
return NULL;
2.二叉排序树的插入及建立
二叉排序树中插入一个结点的过程如下:设待插入结
点为*s,若二叉排序树为空,则将新结点*s作为根
结点插入;若二叉排序树非空,比较结点*s与根结
点的关键字,可分为三种情况:
(1)若s->key与根结点的关键字相等,说明树中已
经存在该结点,不用插入;
(2)若s->key小于根结点的关键字,则将*s插入到
根结点的左子树中;
(3)若s->key大于根结点的关键字,则将*s插入到
根结点的右子树中。
在左、右子树中的插入过程和二叉排序树的插入过
程是相同的。如此进行下去,直到左子树或右子树
为空时,新结点*s作为叶子结点插入到二叉排序树
中。
63
55
42
10
90
58
45
98
70
60 67
83
一棵二叉排序树插入结点60
二叉排序树上插入运算的C函数:
BstNode *insertNode (BstNode *t, BstNode *s)
/*在根结点为*t的二叉排序树上插入新结点*s ,并返回根结点*t */
{ BstNode *p,*q;
if (t==NULL) return s; /*二叉排序树为空,新结点为根结点*/
p=t;
/*二叉排序树非空,开始查找插入位置*/
while(p)
{ q=p;
if(s->key== p->key) return t; /*二叉排序树中已经存在该结点*/
if(s->key < p->key) p=p->lchild;
/*在子树中寻找插入位置*/
else p=p->rchild;
}
if (s->key < q->key) q->lchild=s;
/*插入新结点*/
else q->rchild=s;
return t;
}
建立一棵二叉排序树的过程就是逐个插入新
结点的过程,反复调用插入运算即可。
例8.3设记录的关键字序列为(63,90,70,
55,67,42,98,83,10,45,58),
依次插入各个关键字,建立一棵二叉排序
树。
63
90
55
10
70
58
42
45
67
98
83
建立二叉排序树的算法的C函数如下:
BstNode *creatBst() /*建立一棵二叉排序树,返回这棵树的根结点*/
{ BstNode *root,*s;
KeyType key;
DataType data;
root=NULL;
scanf(“%d”,&key);
/*从键盘读入新插入结点的关键字*/
while(key!=-1)
/*遇到-1,表明插入操作结束*/
{ s=(BstNode *)malloc(sizeof(BstNode)); /*为新结点申请空间*/
s->lchild=NULL;
s->rchild=NULL;
s->key=key;
scanf(“%f”,&data);
/*读入新插入结点的其他的数据项*/
s->other=data;
t=insertNode(root,s);
/*调用插入算法*/
scanf(“%d”,&key);
}
return root;
/*返回根结点*/
}
3.二叉排序树的删除
设待删除结点为*p,其双亲结点为*f,以下分三种情况进行讨论。
(1)*p结点为叶子结点。
只需将被删结点的双亲结点相应指针域置为空即可,这种情况最简单。
(2)*p结点为单分支结点。
*p结点只有一棵子树。若只有左子树,根结点为*pl;或者只有右子树,
根结点为*pr,此时,只需用*pl或*pr替换*p结点即可,这种情况也比较
简单。
(3)*p结点为双分支结点。
*p结点既有左子树,又有右子树,其根结点分别为*pl和*pr。这种情况下
删除*p结点比较复杂,可按中序遍历保持有序的原则进行调整,有两种
调整方法:第一种方法,把*p的右子树链接到*p的中序遍历前趋结点的
右指针域上,*p的中序前趋结点是它的左子树最右下结点,其右指针域
一定为空,从而使得*p结点的右子树为空,变成单分支点,然后用*pl 替
换*p结点;第二种方法,用*p结点的中序前趋结点(中序后继结点)的
值替换*p结点的值,然后删除*p的中序前趋结点(中序后继结点)。*p
的中序前趋(中序后继)不是叶子结点就是单分支结点,可以按照(1
)或(2)的方法将它删除。
36
36
20
15
15
50
25
6
48
50
18
48
25
6
18
30
30
(a) 一棵二叉排序树
(b) 第一种方法删除结点20
30
36
18
15
6
50
25
20
48
30
(c) 第二种方法删除结点20
15
6
50
25
48
18
(d) 第二种方法删除结点36
4.二叉排序树查找性能分析
§ 二叉排序树的形态与结点的插
入顺序有关
如结点的插入顺序为:
63,90,70,55,67,42,98,10,45,5
8,构成的二叉排序树如图 (a)
ASL=(1+2×2+3×4+4×3)/10=2.9
如果插入顺序为:
10,42,45,55,58,63,67,70,90,9
8,构成的二叉排序树如图 (b)
63
55
42
10
90
58
45
98
70
67
(a)
10
42
45
55
58
63
ASL=(1+2+3+4+5+6+7+8+9+
10) /10=5.5
67
70
90
(b)
98
§ 二叉排序树的查找效率与树的形态有关。

最好情况下,平均查找长度大约为log2n,时间复杂度
为O(log2n)。
 最坏的情况下,平均查找长度为(n+1) /2,时间复杂度
为O(n)。
 查找运算的平均时间复杂度仍为O(log2n)。
§ 二叉排序树的平均查找性能与二分查找近似,查找
效率较高,同时使用链式存储结构,它的插入和删
除操作也较为方便,所以二叉排序树非常适合作动
态查找表。
8.5 散列表查找
顺序表、索引表和树表中,记录的存储位置与记录
的关键字之间不存在确定的关系,在表中查找记录
需要进行一系列的比较,所以查找效率主要取决于
查找过程中执行的比较次数。
如果记录的存储位置与关键字之间有某种确定的关
系,在理想的情况下,记录的存储位置与关键字存
在一一对应关系,则可以不通过比较就能找到对应
的记录。本节探讨的散列表查找就是这样的查找方
法。
8.5.1 散列表的概念
§
散列存储是专为查找而设计的存储结构,它的基
本思想是:以表中每一个记录的关键字k为自变量
,通过某个函数Hash(k)计算出函数值,把这个值
解释为记录的存储位置,将记录存储在Hash(k)所
指的位置上。散列存储实现了关键字到存储地址的
转换,所以也称关键字-地址转换法。
§ 散列表:使用散列存储方式存储的线性表,也称作
哈希表(Hash Table)。
§ 散列存储中使用的函数Hash(k),称为散列函数(
哈希函数),Hash(k)的值称为散列地址(哈希地
址)。
若有一个长度为9的线性表,其中记录的关键字序列为(
23,15,36,99,6,14,65,93,75),使用散列存储
方式存储该线性表 .
(1)直接定址法
设散列函数Hash1(key)=key,将线性表存储在长度为100的数组空间
上,散列表的存储情况如图所示。
0 ... 6 ... 14 15 ... 23 ...36 ... 65 ...75 ... 93 ... 98 99
…
6
…
14
15
…
23
…
36
…
65
…
75
…
93
…
99
直接定址法一般形式为:Hash(key)=a·key+b,其中,a、b为常数,
这里a为1,b为0。
设线性表的长度为n,散列表(一维数组)的长度为m,则称α=n/m
为散列表的装填因子。
装填因子的取值区间为[0.6..0.9]比较合适。本例中n=9,m=100,
则α为0.09,显然这样的散列函数是不可取的,在实际应用中较少使
用。
(2)除留余数法
散列函数为 Hash2(key)=key % 11,将线性表存储在长
度为11的数组中,则每个记录的散列地址为:
Hash2(23)=23%11=1 Hash2(15)=15%11=4
Hash2(36)=36%11=3 Hash2(99)=99%11=0
Hash2(6)=6%11=6
Hash2(14)=14%11=3
Hash2(65)=65%11=10 Hash2(93)=93%11=5
Hash2(75)=75%11=9
一般地,若某个散列函数Hash(k)对于不相等的关键字key1
和key2,得到相同的散列地址,则称该现象为冲突。发生冲
突的两个不同的关键字key1和key2被称为同义词,这里36
和14就是同义词。
§ 如何设计一个好的散列函数和确定一种解决
冲突的办法,是散列存储方式中需要解决的
两个最重要问题。
8.5.2 散列函数的设计
散列函数的设计原则是:计算简单,节省时
间;函数值取值范围合理,避免空间的浪费
,保证α在合理的取值区间;散列地址尽可能
均匀地分布在散列空间上,避免太多的冲突
。
1.数字分析法
例8.5有一组由7位数字组成的关键字,使用数字分析
法设计散列函数。
关键字
3470524
3491487
3482697
3485273
3486305
3498058
3479671
3473919
散列地址1(0-999)
052
148
269
527
630
805
967
391
散列地址2(0-99)
29
01
22
25
68
38
67
58
2.除留余数法
Hash(key)=key % p
(p是一个整数)
若线性表的长度为n,散列表的长度为m,则
一般选取p为质数,且 n<p≤m。根据装填因
子α的取值区间为[0.6..0.9],p应为1.1n~1.7n
之间的一个质数。
3.平方取中法
平方取中法是对关键字取平方后,按散列空间的大
小,取中间的若干位作为散列地址。
例8.6 有一组关键字为{0100,0111,0101,1001
,0011},取平方的结果是:{0010000,0012321
,0010201,1002001,0000121},如果散列空间
的长度为1000,则可取中间的三位作为散列地址
{100,123,102,020,001}。
4.折叠法
折叠法是将关键字自左到右分成位数相等的几部分,最后一部分位数可
以短些,然后将这几部分叠加求和,并根据散列表的表长,选取后几位
作为散列地址。
例8.7 已知关键字 key=5326248725,散列表长度为1000,使用折叠法计
算散列地址。
按照每三位为一部分来分割关键字,关键字分割为如下四组:
532 624 872 5
532
624
872
+ 5
───
2033
Hash(key)=033
折叠法适合于关键字的位数较多,而散列地址的位数较少,同时关键字
中的每一位的取值又较集中的情况。
8.5.3 解决冲突的方法
1.开放地址法
所谓开放地址法,就是散列地址单元一旦发
生了冲突,就按照某种方法寻找下一个开放
地址。开放地址是指该地址单元为空,可以
存储记录。
寻找开放地址的方法有很多,它们的区别是
形成的探测序列不同。
(1)线性探测法
线性探测法的基本思想是:将散列表看成一个首尾相接的环
形表,设散列表的长度为m,当向散列表中插入关键字为
key的记录时,若地址d=Hash (key)的单元为空,即将记录
存入该地址单元。若发生冲突,则依次探测下列地址单元:
d+1,d+2,...,m-1,0,1,...,d-1,直到找到
一个空的地址单元,然后将记录存入其中。或者在探测过程
中,遇到关键字为key的记录,说明表中已有该记录,无需
插入。如果按照这种探测序列搜索整个散列表后又回到了地
址空间d,则说明散列表已满。
线性探测法计算下一个开放地址的公式为:
Hi=(Hash(key)+i)% m
其中:i取整数,取值范围 1≤i≤m-1;m为散列表的长度。
例8.8 依次向长度为11的散列表(数组R)中插入关键字为
47,7,29,11,16,92,22,8,3的记录,散列函数
为:Hash(key)=key % 11,用线性探测法处理冲突。
散列表的存储结构如图所示。
下标
0
1
2
11 22
3
4
5
6
7
47 92 16 3
8
9 10
7 29 8
47 , 7 , 11 , 16 , 92 没 有 发 生 冲 突 ; 29 , 22 , 8 发 生 冲 突 , 由
H1=(Hash(k)+1) % 存入其中;Hash(3)发生冲突,H1=(Hash(3)+1) % 11=4
,仍然冲突;H2=(Hash(3)+ 2) % 11=5,仍然冲突;H3=(Hash(3)+3) %
11=6,找到空的地址单元R[6],存入其中。
在例子8.8中,成功查找到每一个记录需要与关键字的比较次数
如表所示。
关键字
比较次数
47
7
29
11
16
92
22
8
3
1
1
1
1
1
1
2
2
4
查找成功时,它的平均查找长度为:
ASL=(1+1+1+1+1+1+2+2+4)/9=14/9≈1.56
查找不成功时,它的平均查找长度为:
ASL不成功=(3+2+1+8+7+6+5+4+3+2+1)
/11=42/11≈3.8
2.链地址法
链地址法就是把发生冲突的同义词记录用单链表链
接起来,散列表中的每一个单元不是用来存储记录
,而是存储单链表的头指针。所有散列地址相同(
冲突)的记录都存储在同一个单链表中。由于单链
表每一个结点是动态生成的,所以插入和删除记录
非常方便。散列表的长度只要与散列函数的取值范
围对应即可。
例8.9 已知关键字序列和例
8.8相同,散列函数仍为
Hash(key)=key % 11,
使用链地址法处理冲突,
用头插法向单链表中插入
结点。
查找成功时的平均查找长度
ASL=(6×1+2×3)/9=12/9≈1.
33
查找不成功时的平均查找长
度:
ASL=(2+0+0+2+1+1+0+2+
1+0+0)/11=9/11≈0.82
0
22
11
3
47
3
4
92
5
16
1
2
6
7
29
8
8
9
10
7
结论:
(1)散列表中的平均查找长度要比顺序查找和二分查找小。
查找表的长度n=9,散列表的平均查找长度分别为:
线性探测法:ASL=14/9≈1.56
链地址法:ASL=12/9≈1.33
顺序查找和二分查找的平均查找长度分别为:
ASL=(9+1)/2=5
ASL=(1+2×2+3×4+4×2)/9=25/9≈2.78
(2)散列表的平均查找长度与散列函数的设计、冲突的解决
方法有关。
同样一组关键字,不同的散列函数使得冲突的发生频繁程
度不同,导致平均查找长度是不同的。
同样一组关键字,设定相同的散列函数,但用不同的冲突
解决方法构造散列表,其平均查找长度仍然是不同的。
(3)一般情况下,当散列函数的设计方法相同时,
散列表的平均查找长度与装填因子α有关。装填因
子α反映了散列空间的装满程度,α越小,发生冲突
的可能性就越小,查找时与关键字比较的次数较少
;反之,α越大,发生冲突的可能性就越大,查找
时与关键字比较的次数就越多。但α越小,造成空
间的浪费也越大。
(4)散列表的优点:关键字与记录的存储地址存在
确定的对应关系,使得插入和查找操作效率很高,
所以散列表是较优的动态查找表。
散列表的缺点:根据关键字计算散列地址的操作
需要一定的时间开销;散列表存储方式浪费存储空
间;在散列存储结构中,无法体现出记录之间的逻
辑关系。