数据结构——6.图
考纲
- 查找的基本概念;静态查找与动态查找
- 顺序查找、折半查找、索引查找
- 哈希查找
哈希函数的基本构造方法,解决地址冲突的基本策略 - 各种查找算法的时间复杂度和空间复杂度
选填考的概率大,算法考的概率较小,其中折半最可能考算法。此处的“时间复杂度和空间复杂度”实际多考察ASL。
一、查找的基本概念
- 查找:在数据集合中寻找满足某种条件的数据元素的过程。
- 查找表(查找结构):用于查找的数据集,由同一类型的数据元素或记录组成。
- 关键字:数据元素中唯一标识该元素的某个数据项的值。
- 静态查找表:只需进行操作①;
动态查找表:也要进行操作②。
①查找符合条件的数据元素
②插入、删除某个数据元素 - 查找长度:查找运算中,需要比对关键字的次数。
- 平均查找长度ASL:所有查找过程中进行关键字的比较次数的平均值。通常认为查找任何一个元素的概率都相同。ASL的数量级反应了查找算法时间复杂度。通常考虑查找成功、查找失败两种情况下的ASL。
A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^{n}P_iC_i ASL=i=1∑nPiCi
n:数据元素个数;P:查找第i个元素的概率;C:查找第i个元素的查找长度。
二、顺序查找
- 顺序查找:也称线性查找,通常用于线性表(顺序/链式存储)
- 从头到尾或从尾到头依次查找。
- 对线性的链表只能进行顺序查找
1、一般线性表的顺序查找
- 0号位置存“哨兵”,不必判断数组是否会越界,效率更高。
- 查找效率分析(若
P
i
=
1
n
P_i=\frac{1}{n}
Pi=n1):
- A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) = ( 1 + 2 + 3 + ⋅ ⋅ ⋅ + n ) n = n + 1 2 ASL_{成功}=\sum_{i=1}^{n}P_i(n-i+1)=\frac{(1+2+3+···+n)}{n}=\frac{n+1}{2} ASL成功=∑i=1nPi(n−i+1)=n(1+2+3+⋅⋅⋅+n)=2n+1
- A S L 失 败 = n + 1 ASL_{失败}=n+1 ASL失败=n+1
- 即时间复杂度 O ( n ) O(n) O(n)
2、有序表的顺序查找
- 顺序查找判定树
- 一个成功结点的查找长度=自身所在层数;一个失败结点的查找长度=父节点所在层数。有n+1个失败结点,n个成功结点
- 查找效率分析:
A S L 失 败 = ∑ j = 1 n q j ( l j − 1 ) = ( 1 + 2 + 3 + ⋅ ⋅ ⋅ + n + n ) n + 1 = n 2 + n n + 1 ASL_{失败}=\sum_{j=1}^{n}q_j(l_j-1)=\frac{(1+2+3+···+n+n)}{n+1}=\frac{n}{2}+\frac{n}{n+1} ASL失败=j=1∑nqj(lj−1)=n+1(1+2+3+⋅⋅⋅+n+n)=2n+n+1n
q j q_j qj是到达第j个失败结点的概率,假设为 1 n + 1 \frac{1}{n+1} n+11 - 优化:被查概率不等时,把被查概率大的放在靠前位置,ASL更小。
三、折半查找
- 折半查找,又称二分查找,仅适用于有序顺序表。顺序表具有随机访问的特性,链表没有。
1、算法
- 思想
- 在[low, high]之间找目标关键字,每次检查mid=(low+high)/2。
- 根据mid所指元素与目标关键字的大小调整low或high,不断缩小low和high的范围。
- 若low>high则查找失败。
- 手动推导
- 算法实现
typedef struct{ //查找表的 ElemType *elem; //动态数组基址 int TableLen; //表的长度 }SSTable; //折半查找(假设升序) int Binary_Search(SSTable 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 }
2、判定树及效率分析
- 判定树举例(向下取整)
分析:
A S L 成 功 = ( 1 ∗ 1 + 2 ∗ 2 + 3 ∗ 4 + 4 ∗ 4 ) / 11 = 3 ASL_{成功}=(1*1+2*2+3*4+4*4)/11=3 ASL成功=(1∗1+2∗2+3∗4+4∗4)/11=3
A S L 失 败 = ( 3 ∗ 4 + 4 ∗ 8 ) / 12 = 11 / 3 ASL_{失败}=(3*4+4*8)/12=11/3 ASL失败=(3∗4+4∗8)/12=11/3 - 判定树的构造(向下取整时)
- 如果当前low和high之间有奇数个元素,则mid分割后,左右两部分元素相等。
- 若偶数个元素,则左半部分比右半部分少一个元素。
- 折半查找的判定树中,向下取整时,对于任何一个结点,必有右子树结点数-左子树结点数=0或1。向上取整时,则是左-右=0/1。
- 折半查找的判定树一定是平衡二叉树,只有最下面一层是不满的,因此树高 h = [ l o g 2 ( n + 1 ) ] h=[log_2(n+1)] h=[log2(n+1)]。计算方法与完全二叉树相同。树高不包含失败节点。
- 判定树结点关键字:左<中<右,满足二叉排序树定义。
- 对于n个成功结点的判定树,失败结点n+1个(等于成功结点的空链域数量)。
- 查找成功/失败的 A S L ≤ h ASL≤h ASL≤h。
- 时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)。
四、分块查找/索引查找
- 定义:又称索引顺序查找
- 分块查找示意图:
- 特点:块内无序、块间有序
- 算法过程:
①在索引表中确定待查记录所属的分块(可顺序、可折半)
②在块内顺序查找 - 若索引表中不包含目标关键字,则折半查找索引表最终停在low>high,要在low所指分组。
原因:最终low左边一定小于目标关键字,high右边一定大于目标关键字。分块存储的索引表中保存的是各个分块的最大关键字。 - 查找效率分析ASL
索引查找ASL为 L L L_L LL,块内查找ASL L S L_S LS长度为n的查找表分为b块,每块s个记录,等概率情况下:- 顺序查找: A S L = L L + L S = b + 1 2 + s + 1 2 = s 2 + 2 s + n 2 s ASL=L_L+L_S=\frac{b+1}{2}+\frac{s+1}{2}=\frac{s^2+2s+n}{2s} ASL=LL+LS=2b+1+2s+1=2ss2+2s+n,此时若 s = n s=\sqrt{n} s=n,则ASL取最小值 n + 1 \sqrt{n}+1 n+1。
- 折半查找: A S L = L L + L S = [ l o g 2 ( b + 1 ) ] + s + 1 2 ASL=L_L+L_S=[log_2(b+1)]+\frac{s+1}{2} ASL=LL+LS=[log2(b+1)]+2s+1
五、哈希查找/散列查找
考的多,基本为选填
1、基本概念
- 散列表/哈希表:根据关键字而直接进行访问的数据结构。散列表建立了关键字和存储地址之间的一种直接映射关系。特点是数据元素的关键字与其存储地址直接相关。
- 散列函数:把查找表中的关键字映射成该关键字对应地址的函数。 H a s h ( k e y ) = A d d r Hash(key)=Addr Hash(key)=Addr
- 同义词:不同的关键字通过散列函数映射到同一个值。
- 冲突:通过散列函数确定的位置已经存放了其他元素。
- 装填因子a/负载因子:一个表的装满程度。装填因子a=表中记录数/散列表长度。装填因子会直接影响散列表的查找长度
- 散列函数的设计目标:让不同关键字的冲突尽可能少。要结合实际的关键字分布特点来考虑。
- 理想情况下,时间复杂度 O ( 1 ) O(1) O(1)。
- 典型的“用空间换时间”的算法。只要散列函数设计的合理,则散列表越长,冲突的概率越低。
- 查找效率取决于:散列函数、处理冲突的方法、装填因子a。
2、常见的散列函数计算
- 除留余数法——
H
(
k
e
y
)
=
k
e
y
%
p
H(key)=key \% p
H(key)=key%p
- 散列表表长为m,取一个不大于m但最接近或等于m的质数p。
- 用质数取模,分布更均匀,冲突更少。
- 直接定址法——
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*key+b
H(key)=a∗key+b
- 不会产生冲突,适合关键字的分布基本连续的情况。
- 数字分析法——选取数码分布较为均匀的若干位作为散列地址
- 设关键字为r进制,r个数码在各位上出现的频率不一定相同,选取在某些位上较为均匀的若干位作为散列地址。
- 适合于已知的关键字集合。更换关键字后需要重新构造哈希。
- 平方取中法——取关键字的平方值的中间几位作为散列地址
- 得到的散列地址与关键字的每位都有关系。使分布较为均匀。
- 适合关键字的每位取值都不够均匀或均小于散列地址所需的位数。
3、处理冲突的方法
(1)拉链法(含查找分析)
- 拉链法/链接法/链地址法:为了避免非同义词发生冲突,把所有同义词存储在一个链表中。适用于经常进行插入和删除的情况。
- 上例中,查找目标若为21时,21的查找长度=0。(因为查找长度=对比关键字的次数,而21%13=8中为空指针,无比较)
- A S L 成 功 = 1 ∗ 6 + 2 ∗ 4 + 3 + 4 12 = 1.75 ASL_{成功}=\frac{1*6+2*4+3+4}{12}=1.75 ASL成功=121∗6+2∗4+3+4=1.75
- A S L 失 败 = 0 + 4 + 0 + 2 + 0 + 0 + 2 + 1 + 0 + 0 + 2 + 1 + 0 13 = 0.92 ASL_{失败}=\frac{0+4+0+2+0+0+2+1+0+0+2+1+0}{13}=0.92 ASL失败=130+4+0+2+0+0+2+1+0+0+2+1+0=0.92,即装填因子。
- 在插入新元素时,保持关键字有序,可略微提高查找效率。
(2)开放定址法
定义+计算方法
- 开放定址法:可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。
- 递推公式:
H
I
=
(
H
(
k
e
y
)
+
d
i
)
%
m
,
i
=
0
,
1
,
2
,
⋅
⋅
⋅
,
k
(
k
≤
m
−
1
)
H_I=(H(key)+d_i)\%m,i=0,1,2,···,k(k≤m-1)
HI=(H(key)+di)%m,i=0,1,2,⋅⋅⋅,k(k≤m−1)
m m m:散列表表长; d i d_i di:增量序列; i i i:第i次发生冲突 - 采用“开放定址法”时,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之前的散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除。
- 确定增量序列的方法如下:
①线性探测法(含查找分析)
- 线性探测法——
d
i
=
0
,
1
,
2
,
3
,
⋅
⋅
⋅
,
m
−
1
d_i=0,1,2,3,···,m-1
di=0,1,2,3,⋅⋅⋅,m−1。发生冲突时,每次往后探测相邻的下一个单元是否为空。
注:上例中冲突处理函数可能落于[0,15],但哈希函数取值落在[0, 12] - 查找操作
- 空位置的判断也要算作一次比较
- 越早遇到空位置,就可以越早确定查找失败
- 查找效率分析ASL
A S L 成 功 = 1 + 1 + 1 + 2 + 4 + 1 + 1 + 3 + 3 + 1 + 3 + 9 12 = 2.5 ASL_{成功}=\frac{1+1+1+2+4+1+1+3+3+1+3+9}{12}=2.5 ASL成功=121+1+1+2+4+1+1+3+3+1+3+9=2.5
A S L 失 败 = 1 + 13 + 12 + 11 + 10 + 9 + 8 + 7 + 6 + 4 + 3 + 2 13 = 7 ASL_{失败}=\frac{1+13+12+11+10+9+8+7+6+4+3+2}{13}=7 ASL失败=131+13+12+11+10+9+8+7+6+4+3+2=7 - 线性探测法容易造成同义词、非同义词的“聚集(堆积)”现象,严重影响查找效率。因为冲突后再探测一定是放在某个连续的位置。
②平方探测法
- 平方探测法,又称二次探测法。 d i = 0 2 , 1 2 , − 1 2 , ⋅ ⋅ ⋅ , k 2 , − k 2 , 其 中 k ≤ m / 2 d_i=0^2,1^2,-1^2,···,k^2,-k^2,其中k≤m/2 di=02,12,−12,⋅⋅⋅,k2,−k2,其中k≤m/2
- 比起线性探测法更不易产生“聚集(堆积)”问题。
- 散列表长度m必须是一个可以表示成
4
j
+
3
4j+3
4j+3的素数,才能探测到所有位置。
③伪随机序列法
- 伪随机序列法: d i d_i di是一个伪随机序列,如 d i = 0 , 5 , 24 , 11 , ⋅ ⋅ ⋅ d_i=0,5,24,11,··· di=0,5,24,11,⋅⋅⋅
(3)再散列法
- 再散列法/再哈希法:除了原始的散列函数 H ( k e y ) H(key) H(key)之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止。 H i = R H i ( k e y ) , i = 1 , 2 , 3 , ⋅ ⋅ ⋅ , k H_i=RH_i(key),i=1,2,3,···,k Hi=RHi(key),i=1,2,3,⋅⋅⋅,k