数据结构学习笔记——第7章 查找
7 查找
7.1 查找的基本概念
- 查找
- 在数据集合中寻找满足某种条件的数据元素的过程称为查找
- 查找的结果一般分为两种:
- 一是查找成功,即在数据集合中找到了满足条件的数据元素
- 二是查找失败
- 查找表(查找结构)
- 用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成,可以是一个数组或链表等数据类型
- 对查找边经常进行的操作一般有 4 种:
- ① 查询某个特定的数据元素是否在查找表中
- ② 检索满足条件的某个特定的数据元素的各种属性
- ③ 在查找表中插入一个数据元素
- ④ 从查找表中删除某个数据元素
- 静态查找表
- 若一个查找表的操作只涉及上述操作的①和②,则无须动态的修改查找表,此类查找表称为静态查找表
- 适合静态查找表的查找方法有顺序查找、折半查找、散列查找等
- 动态查找表
- 需要动态地插入或删除的查找表(上述操作全部涉及)称为动态查找表
- 适合动态查找表的查找方法有二叉排序树的查找、散列查找等,二叉平衡树和 B 树都是二叉排序树的改进
- 关键字
- 数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的
- 平均查找长度
- 在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为 A S L = ∑ i = 1 n P i C i ASL = \sum_{i=1}^{n}P_iC_i ASL=∑i=1nPiCi
- 式中, n n n 是查找表的长度; P i P_i Pi 是查找第 i i i 个数据元素的概率,一般认为每个数据元素的查找概率相等,即 P i = 1 / n P_i = 1/n Pi=1/n; C i C_i Ci 是找到第 i i i 个数据元素所需进行的比较次数
- 平均查找长度是衡量查找算法效率的最主要的指标
7.2 顺序查找和折半查找
7.2.1 顺序查找
- 顺序查找又称线性查找,主要用于在线性表中进行查找
一般线性表的顺序查找
- 对无须线性表进行顺序查找,查找失败时要遍历整个线性表
- 基本思想:
- 从线性表的一端开始,逐个检查关键字是否满足给定的条件
- 若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置
- 若已查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败信息
typedef struct { //查找表的数据结构
ElemType *elem; //元素存储空间基址,建表时按实际长度分配,0号单元留空
int TableLen; //表的长度
}SSTable;
int Search_Seq(SSTable ST, ElemType key) {
ST.elem[0] = key; //“哨兵”
for(int i = ST.TableLen; ST.elem[i] != key; i--); //从后往前找
return i; //若表中不存在关键字为key的元素,将查找到i为0时推出for循环
}
- 上述算法中,将
ST.elem[0]
称为“哨兵”。引入它的目的是使得Search_Seq
内的循环不必判断数组是否会越界,因为蛮子i==0
时,循环一定会推出。引入“哨兵”可以避免很多不必要的判断语句,从而提高程序效率 - 顺序查找的平均查找长度
- A S L 成 功 = ∑ i = 1 n P i C i = ∑ i = 1 n 1 n ( n − i + 1 ) = n + 1 2 ASL_{成功} = \sum_{i=1}^{n}P_iC_i = \sum_{i=1}^{n}\frac{1}{n}(n-i+1) = \frac{n+1}{2} ASL成功=∑i=1nPiCi=∑i=1nn1(n−i+1)=2n+1
- A S L 失 败 = ∑ i = 1 n P i C i = ∑ i = 1 n 1 n ( n + 1 ) = n + 1 ASL_{失败} = \sum_{i=1}^{n}P_iC_i = \sum_{i=1}^{n}\frac{1}{n}(n+1) = n+1 ASL失败=∑i=1nPiCi=∑i=1nn1(n+1)=n+1
- 顺序查找的缺点是当 n n n 比较大时,平均查找长度较大,效率低;优点是对数据元素的存储没有要求,顺序存储或链式存储皆可。对表中记录的有序性也没有要求,无论记录是否按关键字有序,均可应用
- 应注意,对线性的链表只能进行顺序查找
有序表的顺序查找
- 若在查找之前就已经知道表时关键字有序的,则查找失败时可以不用再比较到表的另一端就能返回查找失败的信息,从而降低顺序查找失败的平均查找长度
- 判定树:描述有序顺序表的查找过程的二叉排序树
- 树中的圆形结点表示表示有序顺序表中存在的元素
- 树中的矩形结点称为失败结点(若有 n 个结点,则相应地有 n+1 个查找失败结点),它描述的是那些不在表中的数据值的集合
- 若查找到失败结点,则说明查找不成功
- 平均查找长度
- 查找成功的平均查找长度和一般线性表的顺序查找一样
- 查找失败时,查找指针一定走到了某个失败结点,到达失败结点时所查找的长度等于它上面的一个圆形结点所在的层数。查找不成功的平均查找长度在相等查找概率的情形下为
- A S L 失 败 = ∑ j = 1 n q j ( l j − 1 ) = 1 + 2 + ⋯ + n + n n + 1 = n 2 + n n + 1 ASL_{失败} = \sum_{j=1}^{n}q_j(l_j-1) = \frac{1+2+\cdots +n+n}{n+1} = \frac{n}{2}+\frac{n}{n+1} ASL失败=∑j=1nqj(lj−1)=n+11+2+⋯+n+n=2n+n+1n
- 式中, q j q_j qj 是到达第 j j j 个失败结点的概率; l j l_j lj 是第 j j j 个失败结点所在的层数
7.2.2 折半查找
- 折半查找又称二分查找,它适用于有序的顺序表
- 基本思想:
- 首先将给定值 key 与表中中间位置的元素比较
- 若相等,则查找成功,返回该元素的存储位置
- 若不相等,则所需查找的元素只能在中间元素以外的前半部分或后半部分
- 然后在缩小的范围内继续进行同样的查找
- 如此重复,直到找到为止,或确定表中没有所需查找的元素,则查找不成功,返回失败的信息
- 首先将给定值 key 与表中中间位置的元素比较
int Binary_Search(SeqList L, ElemType key) {
int low = 0, high = L.TableLen-1, mid;
while(low <= high) {
mid = (low + high) / 2; //取中间位置
if(L.elem[mid] == key)
return mid; //查找成功,返回所在位置
else if(L.elem[mid] > key)
high = mid - 1; //从前半部分继续查找
else
low = mid + 1; //从后半部分继续查找
}
return -1; //查找失败,返回-1
}
- 图中有误,16 的左子树应为
(13,16)
,19 的右子树应为(19,29)
- 平均查找长度
- A S L 成 功 = 1 n ∑ i = 1 n l i = 1 n ( 1 × 1 + 2 × 2 + ⋯ + h × 2 h − 1 ) = n + 1 n l o g 2 ( n + 1 ) − 1 ≈ l o g 2 ( n + 1 ) − 1 ASL_{成功} = \frac{1}{n}\sum_{i=1}^{n}l_i = \frac{1}{n}(1 \times 1 + 2 \times 2 + \cdots + h \times 2^{h-1}) = \frac{n+1}{n}log_2(n+1) - 1 \approx log_2(n+1) - 1 ASL成功=n1∑i=1nli=n1(1×1+2×2+⋯+h×2h−1)=nn+1log2(n+1)−1≈log2(n+1)−1
- A S L 失 败 = ∑ j = 1 n q j ( l j − 1 ) = 1 n ∑ j = 1 n ( l j − 1 ) ASL_{失败} = \sum_{j=1}^{n}q_j(l_j-1) = \frac{1}{n}\sum_{j=1}^{n}(l_j-1) ASL失败=∑j=1nqj(lj−1)=n1∑j=1n(lj−1)
- 式中, h h h 是树的高度,并且元素个数为 n n n 时树高为 h = ⌈ l o g 2 ( n + 1 ) ⌉ h = \left \lceil log_2(n+1) \right \rceil h=⌈log2(n+1)⌉
- 所以折半查找的时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)
7.2.3 分块查找
- 分块查找又称索引顺序查找,它吸取了顺序查找和折半查找各自的有点,既有动态结构,又适于快速查找
- 基本思想
- 将查找表分为若干子块。块内的元素可以无序,但块之间是有序的,即第 i i i 个块中的最大关键字小于第 i + 1 i+1 i+1 个块中的所有记录的关键字。再建立一个索引表,索引表中每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列
- 第一步,在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表
- 第二步,在块内顺序查找
- 平均查找长度
- 平均查找长度为索引查找和块内查找的平均长度之和。设索引查找和块内查找的平均查找长度为 L I , L S L_I, L_S LI,LS,则分块查找的平均查找长度为 A S L = L I + L S ASL = L_I + L_S ASL=LI+LS
- 将长度为
n
n
n 的查找表均匀地分为
b
b
b 块,每块有
s
s
s 个记录,在等概率情况下
- 若在块内和索引表中均采用顺序查找,则平均查找长度为
- A S L = L I + L S = b + 1 2 + s + 1 2 = s 2 + 2 s + n 2 s ASL = L_I + L_S = \frac{b+1}{2} + \frac{s+1}{2} = \frac{s^2 + 2s + n}{2s} ASL=LI+LS=2b+1+2s+1=2ss2+2s+n
- 此时若 s = n s=\sqrt{n} s=n,则平均查找长度取最小值 n + 1 \sqrt{n}+1 n+1
- 若在款诶采用顺序查找,索引表采用折半查找时,则平均查找长度为
- A S L = L I + L S = ⌈ l o g 2 ( b + 1 ) ⌉ + s + 1 2 ASL = L_I + L_S = \left \lceil log_2(b+1) \right \rceil + \frac{s+1}{2} ASL=LI+LS=⌈log2(b+1)⌉+2s+1
- 若在块内和索引表中均采用顺序查找,则平均查找长度为
7.3 B 树和 B+ 树
7.3.1 B 树及其基本操作
- B 树,又称多路平衡查找树,B 树中所有节点的孩子个数的最大值称为 B 树的阶,通常用 m m m 表示
- 一棵 m 阶 B 树或为空树,或为满足如下特性的
m
m
m 叉树:
- 1)树中每个结点至多有 m m m 棵子树,即至多包含有 m − 1 m-1 m−1 个关键字
- 2)若根结点不是终端结点,则至少有两棵子树
- 3)除根结点外的所有非叶结点至少有 ⌈ m / 2 ⌉ \left \lceil m/2 \right \rceil ⌈m/2⌉ 棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 \left \lceil m/2 \right \rceil - 1 ⌈m/2⌉−1 个关键字
- 4)所有非叶结点的结构如下:
其中, K i ( i = 1 , 2 , ⋯ , n ) K_i (i=1,2,\cdots ,n) Ki(i=1,2,⋯,n) 为结点的关键字,且满足 K 1 < K 2 < ⋯ < K n K_1 < K_2 < \cdots < K_n K1<K2<⋯<Kn; P i ( i = 1 , 2 , ⋯ , n ) P_i(i=1,2,\cdots ,n) Pi(i=1,2,⋯,n) 为指向子树很结点的指针,且指针 P i − 1 P_{i-1} Pi−1 所指的子树中所有结点的关键字均小于 K i K_i Ki, P i P_i Pi 所指子树中所有结点的关键字均大于 K i K_i Ki, n ( ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 ) n(\left \lceil m/2 \right \rceil - 1 \leq n \leq m-1) n(⌈m/2⌉−1≤n≤m−1) 为结点中关键字的个数 - 5)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)
B 树的高度(磁盘存取次数)
- B 树中的大部分操作所需的磁盘存取次数与 B 树的高度成正比
- 这里明确 B 树的高度不包括最后的不带任何信息的叶结点所处的那一层
- 若
n
≥
1
n \geq 1
n≥1,则对任意一棵包含
n
n
n 个关键字、高度为
h
h
h、阶数为
m
m
m 的 B 树:
- 因为 B 树中每个结点最多有 m m m 棵子树, m − 1 m-1 m−1 个关键字,所以在一棵高度为 h h h 的 m m m 阶 B 树中的关键字的个数应该满足 n ≤ ( m − 1 ) ( 1 + m + m 2 + ⋯ + m h − 1 ) = m h − 1 n \leq (m-1)(1+m+m^2+\cdots +m^{h-1}) = m^h-1 n≤(m−1)(1+m+m2+⋯+mh−1)=mh−1,因此有 h ≥ l o g m ( n + 1 ) h \geq log_m(n+1) h≥logm(n+1)
- 若让每个结点中的关键字个数达到最小,则容纳同样多关键字的 B 树的高度达到最大。由 B 树的定义:第一层至少有 1 1 1 个结点;第二层至少有 2 2 2 个结点;除根结点外的所有非终端结点至少有 ⌈ m / 2 ⌉ \left \lceil m/2 \right \rceil ⌈m/2⌉ 棵子树,则第三层至少有 2 ⌈ m / 2 ⌉ 2\left \lceil m/2 \right \rceil 2⌈m/2⌉ 个结点……第 h + 1 h+1 h+1 层至少有 2 ( ⌈ m / 2 ⌉ ) h − 1 2(\left \lceil m/2 \right \rceil)^{h-1} 2(⌈m/2⌉)h−1 个结点,注意到第 h + 1 h+1 h+1 层是不含任何信息的叶结点。对于关键字个数为 n n n 的 B 树,叶结点即查找不成功的结点为 n + 1 n+1 n+1,由此有 n + 1 ≥ ( ⌈ m / 2 ⌉ ) h − 1 n+1 \geq (\left \lceil m/2 \right \rceil)^{h-1} n+1≥(⌈m/2⌉)h−1,即 h ≤ l o g ⌈ m / 2 ⌉ ( ( n + 1 ) / 2 ) + 1 h \leq log_{\left \lceil m/2 \right \rceil}((n+1)/2)+1 h≤log⌈m/2⌉((n+1)/2)+1
B 树的查找
- B 树的查找和二叉查找树类似,只是每个结点都是多个关键字的有序表,每个结点上所做的是根据该结点的子树所做的多路分支决定
- B 树的查找包含两个基本操作:
- ① 在 B 树中找结点
- ② 在结点内找关键字
- 由于 B 树长存储在磁盘上,因此前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的,即在找到目标结点后,先将结点信息读入内存,然后在结点内采用顺序查找法或折半查找法
- 在 B 树上查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息到所指的子树去查找。查找到叶结点时(对应指针为空指针),则说明树中没有对应的关键字,查找失败。
B 树的插入
- 将关键字 key 插入 B 树的过程如下:
- 1)定位。利用前述的 B 树查找算法,找到插入该关键字的最低层中的某个非叶结点(在 B 树中查找 key 时,会找到表示查找失败的叶结点,这样就确定了最底层非叶结点的插入位置。注意:插入位置一定是最低层中的某个非叶结点)
- 2)插入。在 B 树中,每个非失败结点的关键字个数都在区间 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil - 1, m-1] [⌈m/2⌉−1,m−1] 内。插入后的结点关键字个数小于 m m m,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于 m − 1 m-1 m−1 时,必须对节点进行分裂。
- 分裂的方法是:
- 取一个新结点,在插入 key 后的原结点,从中间位置( ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉)将其中的关键字分为两个部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置( ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉)的结点插入原结点的父结点
- 若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致 B 树高度增 1
B 树的删除
- 当被删关键字
k
k
k 不在终端结点(最低层非叶结点)中时:
- 1)若小于 k k k 的子树中关键字个数 > ⌈ m / 2 ⌉ − 1 > \lceil m/2 \rceil - 1 >⌈m/2⌉−1,则找出 k k k 的前驱值 k ′ k' k′,并用 k ′ k' k′ 来取代 k k k,再递归地删除 k ′ k' k′ 即可(转换成了被删关键字在终端结点中的情形)
- 2)若大于 k k k 的子树中关键字个数 > ⌈ m / 2 ⌉ − 1 > \lceil m/2 \rceil - 1 >⌈m/2⌉−1,则找出 k k k 的后继值 k ′ k' k′,并用 k ′ k' k′ 来取代 k k k,再递归地删除 k ′ k' k′ 即可(转换成了被删关键字在终端结点中的情形)
- 3)若前后两子树中关键字个数均为
⌈
m
/
2
⌉
−
1
\lceil m/2 \rceil - 1
⌈m/2⌉−1,则直接两个子节点合并,然后删除
k
k
k 即可
- 当被删关键字
k
k
k 在终端结点(最低层非叶结点)中时,有下列三种情况:
-
1)直接删除关键字。若被是删除关键字所在结点的关键字个数 ≥ ⌈ m / 2 ⌉ \geq \lceil m/2 \rceil ≥⌈m/2⌉,表明删除该关键字后仍满足 B 树的定义,则直接删去该关键字
-
2)兄弟够借。若被删除关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1 = \lceil m/2 \rceil - 1 =⌈m/2⌉−1,且与此节点相邻的右(或左)兄弟节点的关键字个数 ≥ ⌈ m / 2 ⌉ \geq \lceil m/2 \rceil ≥⌈m/2⌉,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)以达到新的平衡
- 从左兄弟结点借一个
- 从右兄弟结点借一个
- 从左兄弟结点借一个
-
3)兄弟不够借。若被删除关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1 = \lceil m/2 \rceil - 1 =⌈m/2⌉−1,且此时与该结点相邻的左、右兄弟结点的关键字个数均 = ⌈ m / 2 ⌉ − 1 = \lceil m/2 \rceil - 1 =⌈m/2⌉−1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并
-
- 在合并过程中,双亲结点中的关键字个数会减
1
1
1
- 若其双亲结点是根结点且关键字个数减少至 0 0 0,则直接将根结点删除后,刚合并后的新结点称为根
- 若其双亲结点不是根结点,且关键字个数减少到 ⌈ m / 2 ⌉ − 2 \lceil m/2 \rceil - 2 ⌈m/2⌉−2,则又要与它自己的兄弟节点进行调整或合并操作,并重复上述步骤,直至符合 B 树的要求为止
7.3.2 B+ 树的基本概念
- B+ 树是应数据库所需而出现的一种 B 树的变形树
- 一棵
m
m
m 阶的 B+ 树需满足下列条件:
- 1)每个分支结点最多有 m m m 可子树(孩子结点)
- 2)非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 棵子树
- 3)结点的子树个数与关键字个数相等
- 4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
- 5)所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针
-
m
m
m 阶 B+ 树与
m
m
m 阶的 B 树的主要差异如下:
- 1)在 B+ 树中,具有 n n n 个关键字的结点只含有 n n n 棵子树,即每个关键字对应一棵子树;而在 B 树中,具有 n n n 个关键字的结点含有 n + 1 n+1 n+1 棵子树
- 2)在 B+ 树中,每个结点(非根内部结点)的关键字个数 n n n 的范围是 ⌈ m / 2 ⌉ ≤ n ≤ m \lceil m/2 \rceil \leq n \leq m ⌈m/2⌉≤n≤m(根结点: 1 ≤ n ≤ m 1 \leq n \leq m 1≤n≤m);在 B 树中,每个结点(非根内部结点)的关键字个数 n n n的范围是 ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil m/2 \rceil - 1 \leq n \leq m - 1 ⌈m/2⌉−1≤n≤m−1(根结点: 1 ≤ n ≤ m − 1 1 \leq n \leq m-1 1≤n≤m−1)
- 3)在 B+ 树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址
- 4)在 B+ 树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在 B 树中,叶结点包含的关键字和其他结点包含的关键字是不重复的
- 通常在 B+ 树中有两个头指针:一个指向根结点,另一个指向关键字最小的叶结点。因此,可以对 B+ 树进行两种查找运算:一种是从最小关键字开始的顺序查找,另一种是从根结点开始的多路查找
- B+ 树的查找、插入和删除操作和 B 树的基本类似。只是在查找过程中,非叶结点上的关键字值等于给定值时并不终止,而是继续向下查找,直到叶结点上的该关键字为止。所以,在 B+ 树中查找时,无论查找成功与否,每次查找都是一条从根到叶结点的路径
7.4 散列表
7.4.1 散列表的基本概念
- 在前面介绍的线性表和树表的查找中国,记录在表中的为止于记录的关键字之间不存在确定关系,因此,在这些查找记录时需进行一系列的关键字比较,这类查找方法建立在“比较”的基础上,查找的效率取决于比较的次数
- 散列函数:一个把查找表中的关键字映射称该关键字对应的地址的函数,记为 H a s h ( k e y ) = A d d r Hash(key) = Addr Hash(key)=Addr(这里的地址可以是数组的下标、索引或内存地址等)
- 散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词
- 一方面,设计得好的散列函数应尽量减少这样的冲突
- 另一方面,由于这样的冲突总是不可避免的,所以还要设计好处理冲突的方法
- 散列表:根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键和存储地址之间的一种直接映射关系
- 理想情况下,对散列表进行查找的时间复杂度为 O ( 1 ) O(1) O(1),即与表中元素的个数无关
7.4.2 散列函数的构造方法
- 在构造散列函数时,必须注意以下几点:
- 1)散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
- 2)散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生
- 3)散列函数应尽量简单,能够在较短时间内计算出人以关键字对应的散列地址
直接定址法
- 直接取关键字的某个线性函数值为散列地址
- 散列函数为 H ( k e y ) = k e y H(key) = key H(key)=key 或 H ( k e y ) = a × k e y + b H(key) = a \times key + b H(key)=a×key+b,式中, a a a 和 b b b 是常数
- 这种方法计算最简单,且不会产生冲突
- 它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费
除留余数法
- 这是一种最简单、最常用的方法
- 假定散列表表长为 m m m,取一个不大于 m m m 但是最接近或等于 m m m 的质数 p p p,利用以下公式把关键字转换成散列地址
- 散列函数为 H ( k e y ) = k e y % p H(key) = key \% p H(key)=key%p
- 除留余数法的关键是选好 p p p,使得每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性
数字分析法
- 设关键字是 r r r 进制数(如十进制数),而 r r r 个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀地若干位作为散列地址
- 这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数
平方取中法
- 取关键字的平方值的中间几位作为散列地址,具体取多少位要视实际情况而定
- 这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀
- 适用于关键字的每位取值不够均匀,或均小于散列地址所需的位数
折叠法
- 将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址
- 例: H ( 5211252 ) = 521 + 125 + 2 = 648 H(5211252) = 521+125+2=648 H(5211252)=521+125+2=648
- 适用于关键字的位数多,而且关键字中的每位上数字分布大致均匀
7.4.3 处理冲突的方法
- 应注意到,任何设计出来的散列函数都不可能绝对地避免冲突,为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的 Hash 地址
- 用 H i H_i Hi 表示处理冲突中第 i i i 次探测得到的散列地址,假设得到的另一个散列地址 H 1 H_1 H1 仍然发生冲突,只得继续求下一个地址 H 2 H_2 H2,以此类推,直到 H k H_k Hk 不发生冲突位置,则关键字 H k H_k Hk 为关键字在表中的地址
开放定址法
- 指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放
- 其数学递推公式为:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i = (H(key) + d_i) \% m
Hi=(H(key)+di)%m
- 式中, H ( k e y ) H(key) H(key) 为散列函数; i = 0 , 1 , 2 , ⋯ , k ( k ≤ m − 1 ) i = 0, 1, 2, \cdots , k (k \leq m-1) i=0,1,2,⋯,k(k≤m−1); m m m 表示散列表表长; d i d_i di 为增量序列
- 确定某一增量序列后,对应的处理方法就是确定的。通常有以下 4 中取法:
- 1)线性探测法
- 当 d i = 0 , 1 , 2 , ⋯ , m − 1 d_i = 0, 1, 2, \cdots , m-1 di=0,1,2,⋯,m−1 时,称为线性探测法
- 特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m − 1 m-1 m−1 时,下一个探测地质是表首地址 0 0 0),直到找到一个空闲单元(当表未填满时一定能找到一个空闲单元)或查边全表
- 线性探测法可能使第 i i i 个散列地址的同义词存入第 i + 1 i+1 i+1 个散列地址,这样本应存入第 i + 1 i+1 i+1 个散列地址的元素就争夺第 i + 2 i+2 i+2 个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率
- 2)平方探测法
- 当 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , ⋯ , k 2 , − k 2 d_i = 0^2, 1^2, -1^2, 2^2, -2^2, \cdots , k^2, -k^2 di=02,12,−12,22,−22,⋯,k2,−k2 时,称为平方探测法,其中 k ≤ m / 2 k \leq m/2 k≤m/2,散列表的长度 m m m 必须是一个可以表示成 4 k + 3 4k+3 4k+3 的素数,又称二次探测法
- 平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元
- 3)再散列法
- 当 d i = H a s h 2 ( k e y ) d_i = Hash_2(key) di=Hash2(key) 时,称为再散列法,又称双散列法,需要使用两个散列函数,当通过第一个散列函数 H ( k e y ) H(key) H(key) 得到的地址发生冲突时,再利用第二个散列函数 H a s h 2 ( k e y ) Hash_2(key) Hash2(key) 计算该关键字的地址增量
- 它的具体散列函数形式如下: H i = ( H ( k e y ) + i × H a s h 2 ( k e y ) ) % m H_i = (H(key) + i \times Hash_2(key)) \% m Hi=(H(key)+i×Hash2(key))%m
- 初始探测位置 H 0 = H ( k e y ) % m H_0 = H(key) \% m H0=H(key)%m, i i i 是冲突的次数,初始为 0 0 0
- 在再散列法中,最多经过 m − 1 m-1 m−1 次探测就会遍历表中所有位置,回到 H 0 H_0 H0位置
- 4)伪随机序列法
- 当 $d_i = $伪随机数序列时,称为伪随机序列法
- 1)线性探测法
- 注意:
- 在开放定址的情形下,不能随便物理删除表中已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找地址。因此,要删除一个元素时,可给他做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除
拉链法(链接法,chaining)
- 为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识
- 假设散列地址为 i i i 的同义词链表的头指针存放在散列表的第 i i i 个单元中,因而查找、插入和删除操作主要在同义词链中进行
- 拉链法适用于经常进行插入和删除的情况
7.4.4 散列查找及性能分析
- 散列的查找过程与构造散列表的过程基本一致。对于一个给定的关键字
k
e
y
key
key,根据散列函数可以计算出其散列地址,执行步骤如下:
- 初始化: A d d r = H a s h ( k e y ) ; Addr = Hash(key); Addr=Hash(key);
- ① 检测查找表中地址为 A d d r Addr Addr 的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与 k e y key key 的值,若相等,则返回查找成功标志,否则执行步骤②
- ② 用给定的处理冲突方法计算“下一个散列地址”,并把 A d d r Addr Addr 置为次地址,转入步骤①
- 在散列表的查找过程可见:
- (1)虽然散列表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的产生,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程,因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量
- (2)散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子
- 装填因子:散列表的装填因子一般记为 α \alpha α,定义为一个表的装满程度,即 α = 表 中 记 录 数 n 散 列 表 长 度 m \alpha = \frac{表中记录数n}{散列表长度m} α=散列表长度m表中记录数n
- 散列表的平均查找长度依赖于散列表的装填因子 α \alpha α,而不直接依赖于 n n n 或 m m m。直观的看, α \alpha α 越大,表示装填记录越“满”,发生冲突的可能性越大,平均查找长度越长;反之发生冲突的可能性越小,平均查找长度越短