一、线性查找
简介
线性查找是一种在数组中查找数据的算法。与二分查找不同(下面讲解),即便数据没有按顺序存储,也可以应用线性查找,线性查找的操作很简单,只要在数组中从头开始依次往下查找即可。
顺序查找算法是最简单的查找算法,其意思为:线性的从一个端点开始,将所有的数据依次访问,并求得所需要查找到的数据的位置,因此,线性查找可以称呼为遍历。
解析
开始线性查找,我们以查找 1 位例。
首先,检测数组中最左边的数字,将其与1进行比较。如果结果一致,查找结束!不一致则向右检测下一个数字。
不一致检测下一个。
重复上面的操作直到找到1结束查询,或者检测至数组最右端未查找到1。
找1,查找结束!
代码
#include<stdio.h>
int main()
{
int a[8] = {7,13,4,5,8,1,11,9};
for(int i=0;i<8;i++)
if(a[i] == 1){
printf("找到数组%d,位置在%d",a[i],i);
return 0;
}
printf("未找到数字!");
return 0;
}
总结
线性查找需要从头开始不断地按顺序检查数据,因此在数据量大且目标数据靠后,或者目标数据不存在时,比较的次数就会更多,也更为耗时。若数据量为n,线性查找的时间复杂度为O(n)。
二、二分查找
简介
也叫折半查找,二分查找也是一种在数组中查找数据的算法,它只能查找已经排好序的数据。二分查找通过比较数组中间的数据与目标数据的大小,可以得知目标数据是在数组的左边还是右边。因此,一次比较就可以把查找范围缩小一半。重复执行该操作就可以找到目标数据,或得出目标数据不存在的结论。
解析
下面以查找int类型数据为例,解析二分查找,其他查找可以类似。
区间[l, r]被划分成[l, mid]和[mid + 1, r]
我们利用二分查找查找4。
r = 7,l = 0,l + r >> 1 = 3,计算出的中间数组标记为3,数据为7。
将要查找的数与7进行比较,判断查找数在7的哪边,从而剔除一半的数据。
我们将r=3,从而剔除了被排除的那一半数据。l和r并没有相遇,所以继续查找中间值。
4<=4!所以可以把右半剔除。
我们将r=1,从而剔除了被排除的那一半数据。l和r并没有相遇,所以继续查找中间值。
l与r相遇结束查找,返回l。查看l下标的数据是否与查找数据一致。
总结
二分查找利用已排好序的数组,每一次查找都可以将查找范围减半。宣找泥围内只剩一个数据时查找结束。
数据量为n的数组,将其长度减半log,n次后,其中便只剩一个数据了。也就是说,在二分查找中重复执行“将目标数据和数组中间的数据进行比较后将查找范围减半”的操作log,n次后,就能找到目标数据(若没找到则可以得出数据不存在的结论),因此它的时间复杂度为O(logn)。
代码
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
// 当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
// 当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
例题
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int a[100000];
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (a[mid] >= m) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (a[mid] <= m) l = mid;
else r = mid - 1;
}
return l;
}
int main()
{
int q;
scanf("%d %d",&n,&q);
for (int i = 0; i < n; i ++ )
scanf("%d", &a[i]);
while (q -- ){
scanf("%d", &m);
int l = bsearch_1(0,n-1);
// printf("1 %d\n",l);
int r = bsearch_2(l,n-1);
if(a[l] != m)
printf("-1 -1\n");
else
printf("%d %d\n",l,r);
}
return 0;
}
三、分块查找*
简介
分块查找,也叫索引顺序查找,分块查找是结合二分查找和顺序查找的一种改进方法。在分块查找里有索引表和分块的概念。索引表就是帮助分块查找的一个分块依据,其实就是一个数组,用来存储每块的最大存储值,也就是范围上限;分块就是通过索引表把数据分为几块。
在每需要增加一个元素的时候,我们就需要首先根据索引表,知道这个数据应该在哪一块,然后直接把这个数据加到相应的块里面,而块内的元素之间本身不需要有序。因为块内无须有序,所以分块查找特别适合元素经常动态变化的情况。
分块查找只需要索引表有序,当索引表比较大的时候,可以对索引表进行二分查找,锁定块的位置,然后对块内的元素使用顺序查找。这样的总体性能虽然不会比二分查找好,却比顺序查找好很多,最重要的是不需要数列完全有序。
块间有序指的是第二个子表中所有关键字都要大于第一个子表中的最大关键字,第三个子表的所有关键字都要大于第二个子表中的最大关键字,依次类推。
解析
思路: 先对索引表进行二分查找或顺序查找,确定待查关键字在哪一块中;然后在已确定的块中用顺序法进行查找。
步骤:
- 在索引表中确定待查记录所属的分块(可顺序、可折半)
- 在块内顺序查找
代码
索引表存储结构
typedef struct{
ElemType data;
int low, high;
}Index;
顺序表存储结构
ElemType List[100000];
折半查找分块
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct{
ElemType data;
int low, high;
}Index;
Index indexList[5];//索引表
ElemType keyList[15] = {5,1,10,20,11,15,19,30,30,25,33,40,45,42,50};//顺序表
int n = 15, m = 5;//n为顺序表长度,m为索引表长度
int findLump(int key){//索引表查找
int start = 0;
int end = m;
while(start <= end){
int mid = (start + end) / 2;
if(indexList[mid].data > key)
end = mid - 1;
else
start = mid + 1;
}
return start;
}
int findKey(int index,int key){//块内查找
for(int i = indexList[index].low; i <= indexList[index].high; i++)
if(keyList[i] == key)
return i;
return -1;
}
int main(){
indexList[0].data = 10;indexList[0].low = 0;indexList[0].high = 2;
indexList[1].data = 20;indexList[1].low = 3;indexList[1].high = 6;
indexList[2].data = 30;indexList[2].low = 7;indexList[2].high = 9;
indexList[3].data = 40;indexList[3].low = 10;indexList[3].high = 11;
indexList[4].data = 50;indexList[4].low = 12;indexList[4].high = 14;
int index = findLump(11);
printf("第%d块 序号%d",index,findKey(index,11));
return 0;
}
总结
设长度为 n 的表均匀地分成 b 块,每块含有 s 个记录,则 b = n / s;
顺序查找所在块,分块查找的平均查找长度 = (b+1)/2 + (s+1)/2 = (n/s+s)/2+1;
A
S
L
顺
序
查
找
=
b
+
1
2
+
s
+
1
2
=
s
2
+
2
s
+
n
2
s
ASL_{顺序查找}=\dfrac{b+1}{2}+\dfrac{s+1}{2}=\dfrac{s^{2}+2s+n}{2s}
ASL顺序查找=2b+1+2s+1=2ss2+2s+n
折半查找所在块,分块查找的平均查找长度 = log2(n/s+1)+s/2;
A
S
L
折
半
查
找
=
⌈
log
2
(
b
+
1
)
⌉
+
s
+
1
2
ASL_{折半查找}=\lceil \log _{2}\left( b+1\right) \rceil +\dfrac{s+1}{2}
ASL折半查找=⌈log2(b+1)⌉+2s+1
优点:在表中插入或删除一个记录时,只要找到该记录所在块,就在该块中进行插入或删除运算(因快内无序,所以不需要大量移动记录)。
缺点:增加了一个辅助数组的存储空间和将初始表分块排序的运算。
性能:介于顺序查找和二分查找之间。
分块查找在现实生活中也很常用。例如,一个学校有很多个班级,每个班级有几十个学生。给定一个学生的学号,要求查找这个学生的相关资料。显然,每个班级的学生档案是分开存放的,没有任何两个班级的学生的学号是交叉重叠的,那么最好的查找方法实现确定这个学生所在的班级,然后再在这个学生所在班级的学生档案中查找这个学生的资料。上述查找学生资料的过程,实际上就是一个典型的分块查找。
四、总结
二分查找的时间复杂度为O(logn),与线性查找的O(n)相比速度上得到了指数倍提高(x=logn,则n=2x )。
但是,二分查找必须建立在数据已经排好序的基础上才能使用,因此添加数据时必须加到合适的位置,这就需要额外耗费维护数组的时间。
而使用线性查找时,数组中的数据可以是无序的,因此添加数据时也无须顾虑位置,直接把它加在末尾即可,不需要耗费时间。
综上,具体使用哪种查找方法,可以根据查找和添加两个操作哪个更为频繁来决定。