本篇文章用于记录,不适合参考
。
查找(一)
9.1 查找基本概念
查找又称为检索,是根据关键字寻找指定元素的一种操作。
查找表也称为被查找对象,是一组元素(或者记录)的集合。
查找表由元素组成,每个元素由若干个数据项组成,这若干个数据项中有一个数据项可以被指定为关键字。每个元素,其关键字的取值唯一。
- 查找表的分类
查找过程中会对查找表进行修改(如插入或删除),那么这个查找表属于动态查找表。
查找过程中不会对表进行修改,这个查找表属于静态查找表。
- 查找的分类
内查找:查找过程在内存中进行
外查找:查找过程需要访问外存
- 平均查找长度(Average Search Length)
意义:衡量查找算法性能好坏的重要指标。 A S L ASL ASL越小,算法时间性能越好,反之,越差。
定义:
A
S
L
=
∑
i
=
1
n
p
i
c
i
ASL = \sum^{n}_{i=1}p_{i}c_{i}
ASL=i=1∑npici
n
n
n表示查找表中元素的个数,
p
i
p_i
pi表示查找第
i
i
i个元素的概率,
c
i
c_i
ci表示找到第
i
i
i个元素所需的关键字比较次数。通常假设每个元素的查找概率相等,此时
p
i
p_i
pi=
1
n
\frac{1}{n}
n1。
A S L ASL ASL分为查找成功 A S L 成 功 ASL_{成功} ASL成功和查找失败 A S L 不 成 功 ASL_{不成功} ASL不成功两种情况。
对于 A S L 成 功 ASL_{成功} ASL成功, p i p_i pi为查找到第 i i i个元素的概率。
对于 A S L 不 成 功 ASL_{不成功} ASL不成功,假设有 m m m种查找失败的情况, p i p_i pi为第 i i i种情况的概率。
9.2 线性表的查找
对于每个元素,其采用如下结构:
typedef int KeyType;
typedef struct{
KeyType key;
InfoType date;
}RecType;
9.2.1 顺序查找
顺序查找是最简单的查找方法。
算法:从表的一端向表的另一端逐个元素地查找。
时间复杂度: O ( n ) O(n) O(n)
优点:简单
缺点:效率低, n n n大时不适用
实现:
int SeqSearch(RecType R[], int n, KeyType k){
int i = 0;
while(i<n && R[i]!=k){
++i;
}
if(i>=n){
return 0;
}
else{
return i+1;
}
}
- 算法优化:哨兵
上述算法中可以在顺序表R的末尾添加一个关键字记录k,该记录称为哨兵
。
算法优化后的实现:
int SeqSearch(RecType R[], int n, KeyType k){
int i = 0;
R[n].key = k;
while(R[i].key!=k){
++i;
}
if(i==n){
return 0;
}
else{
return i+1;
}
}
优化分析:这个优化主要是在while语句的判定条件处,优化后查找过程中不用再判断是否越界。这个优化在查找表规模较大时有明显的效益。
9.2.2 折半查找
算法:顺序表 R [ l o w ] R[low] R[low]到 R [ h i g h ] R[high] R[high]为当前的查找范围。 m i d = ⌊ ( l o w + h i g h ) / 2 ⌋ mid= \lfloor (low + high)/2 \rfloor mid=⌊(low+high)/2⌋。
- 若 k = R [ m i d ] . k e y k=R[mid].key k=R[mid].key,查找成功,返回元素逻辑编号。
- 若 k < R [ m i d ] k<R[mid] k<R[mid],收缩查找范围, h i g h = m i d − 1 high = mid - 1 high=mid−1。再次查找。
- 若 k > R [ m i d ] k>R[mid] k>R[mid],收缩查找范围, l o w = m i d + 1 low = mid + 1 low=mid+1。再次查找。
元素不在查找表中时: l o w > h i g h low > high low>high终止算法。
思路:每一次比较都将查找范围减半。
时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n)
前提:查找表有序
优点:查找效率高
缺点:由于需要查找表有序,对元素要能够随机读取。故更加适用于顺序表,对于链表的折半查找的实现相对复杂。
9.2.3 索引存储结构
- 索引存储结构
索引存储结构就是在存储数据时,额外建立一个索引表。
- 索引存储结构的优点:提高查找效率。
- 缺点:建立索引表增加额外的时间和空间开销
索引表中的每个元素称为索引项,每个索引项的一般形式为(关键字,地址)
- 分块查找
分块查找是一种性能介于顺序查找和折半查找之间的查找算法。
算法:
将整个表 R [ 0... n − 1 ] R[0...n-1] R[0...n−1]均分为 b b b块,前 b − 1 b-1 b−1块中各有 s = ⌈ n / b ⌉ s=\lceil n/b \rceil s=⌈n/b⌉个元素,最后一块中含有剩下的元素(最后一块中的元素个数可能小于等于 s s s。每一块中的元素不必有序,但是必须满足,前一块中的最大关键字小于后一块中的最小关键字。即要求整个表是"分块有序"。
抽取各个块中的最大关键字及其起始位置(对应地址)作为索引项存入索引表中。索引表按关键字有序。
实现:
索引表的数据类型声明:
#define MAXI <索引表的最大长度>
typedef struct{
KeyType key; //KeyType为关键字的类型
int link; //指向对应块的起始下标
} IdxType;
实现:使用二分查找检索索引表,使用顺序查找检索块。
int IdxSearch(IdxType I[], int b, RecType R[], int n, KeyType k){
int s = (n+b-1)/b;
int low = 0, high = b-1, mid, i;
while(low <= high){ //在索引表中查找目标区块,使用折半查找
mid = (low + high)/2;
if(I[mid].key >= k){ //这里if中的判断条件与折半查找还是有区别的,需要注意。
//因为这里需要找到的是某个区块,而不是某个具体元素。
high = mid-1;
}else{
low = mid + 1;
}
}
//下一步,在主数据表(目标区块)中使用顺序查找
i = I[high + 1].link;
while(i<=I[high + 1].link + s-1 && R[i].key!=k){
++i;
}
if(i<=I[high + 1].link + s-1){
return i+1;
}else{
return 0; //查找失败
}
}
- 检索索引表时使用二分查找还是顺序查找?
分块查找中每个块不要求有序,所以一般不能使用二分查找。但是索引表是有序的,这就可以使用二分查找或者顺序查找。那哪种方法更好呢?
根据其 A S L ASL ASL得到结果,这里直接给出结果,不做分析。
若有 n n n个元素,每块中有 s s s个元素(R中总块数 b = ⌈ n s ⌉ b=\lceil \frac{n}{s}\rceil b=⌈sn⌉)。
折半查找确定块时,分块查找成功时的
A
S
L
ASL
ASL为:
A
S
L
b
l
k
=
l
o
g
2
(
b
+
1
)
−
1
+
s
+
1
2
≈
l
o
g
2
(
n
/
s
+
1
)
+
s
2
ASL_{blk} = log_2(b+1) -1+\frac{s+1}{2} \approx log_2(n/s + 1)+\frac{s}{2}
ASLblk=log2(b+1)−1+2s+1≈log2(n/s+1)+2s
显然,当 s s s越小时,折半查找更好
顺序查找确定块时,分块查找成功时的
A
S
L
ASL
ASL为:
A
S
L
b
l
k
=
1
2
(
n
s
+
s
)
+
1
ASL_{blk} = \frac{1}{2}(\frac{n}{s}+s)+1
ASLblk=21(sn+s)+1
显然,当
s
=
n
s=\sqrt{n}
s=n时,
A
S
L
b
l
k
ASL_{blk}
ASLblk取极小值
n
+
1
\sqrt{n}+1
n+1。此时顺序查找更好。
参考资料
[1] 李春葆.数据结构教程(第五版).清华大学出版社.