什么是顺序查找呢?顺序查找的原理很简单,就是遍历整个列表,逐个进行记录的关键字与给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录。如果直到最后一个记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找失败。
一、顺序查找
在前面讲解了顺序表的存储结构,针对顺序查找,即适用于线性表的顺序存储结构,也适用于线性表的链式存储结构。其中针对顺序查找有个关键点:哨兵。
哨兵:为了在for循环中设置循环的停止条件,避免全部循环。
#include<stdio.h> int Search_seq(int R[],int length,int key) { int i; R[0]=key; for(i=length-1;R[i]!=key;--i);//找不到时i为0,否则R[i]就是要找的结点; return i; } int main() { int R[11]={0,13,29,18,27,6,15,34,33,2,1}; int k=Search_seq(R,11,15); printf("%d ",k); getchar(); }
成功时的顺序查找的平均查找长度:
在等概率情况下,pi=1/n(1≤i≤n),故成功的平均查找长度为
(n+…+2+1)/n=(n+1)/2
即查找成功时的平均比较次数约为表长的一半。
若K值不在表中,则须进行n+1次比较之后才能确定查找失败。最终的算法复杂度为O(n)。
顺序查找的优点
算法简单,且对表的结构无任何要求,无论是用向量还是用链表来存放结点,也无论结点之间是否按关键字有序,它都同样适用。
顺序查找的缺点
查找效率低,因此,当n较大时不宜采用顺序查找。
其实顺序查找只要掌握哨兵的应用就可以了。
二、二分查找
二分查找又称折半查找,是一种效率很高的查找方法。二分查找要求:线性表是有序表,即表中结点按关键字有序排列,并且用向量作为表的存储结构,例如递增有序的数组。
1、二分查找基本思想
① low=1;high=length; // 设置初始区间
② 当low>high 时,返回查找失败信息// 表空,查找失败
③ low≤high,mid=(low+high)/2; //确定该区间的中点位置
a. 若kx<tbl.elem[mid].key,high = mid-1;转② // 查找在左半区进行
b. 若kx>tbl.elem[mid].key,low = mid+1; 转② // 查找在右半区进行
c. 若kx=tbl.elem[mid].key,返回数据元素在表中位置// 查找成功
2、代码
int BinSearch(int R[],int length,int key) { int low=1; int high=length; int mid; while(low<=high)//要加上=,因为这样指针先可以指到最后一个数0 1 2 4 比如找4 ,第一次low=1 high=3;第二次mid+1=3,这样low==high /2 指到最后 { mid=(low+high)/2; if(key==R[mid]) return mid; else if(key>R[mid]) low=mid+1;//继续在R[mid+1..high]中查找 else high=mid-1; //继续在R[low..mid-1]中查找 } return 0;//当low>high时表示查找区间为空,查找失败 }
3、性能分析
平均查找长度 = log2(n+1)-1.从折半查找过程看,以表的中点为比较对象,并以中点将表分割为两个子表,对定位到的子表继续这种操作.很类似二叉树查找。
(7,14,18,21,23,29,31,35,38,42,46,49,52)折半查找的判定树,可以看到,查找表中任一元素的过程,即是判定树中从根到该元素结点路径上各结点关键码的比较次数,也即该元素结点在树中的层次数。对于n 个结点的判定树,树高为k,则有2k-1 -1<n≤2k-1,即k-1<log2(n+1)≤k,所以k=log2(n+1) 。时间复杂度为O(logn)。
虽然折半查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算,所以二分法比较适用于顺序存储结构。为保持表的有序性,在顺序结构中插入和删除都必须移动大量的结点。因此,折半查找特别适用于那种一经建立就很少改动而又经常需要查找的线性表。
三、插分查找
在一个0~10000之间的100个元素从小到大均匀分布的数组中查找5,我们自然会考虑从数组下标较小的开始查找,因此二分查找还有提高空间。
mid = (low+high)/2 = low+1/2(high-low)。在这公式中我们将这个1/2进行修改:
mid = low + (key-a[low])/a[high]-a[low](high-low)。
比如在a[11] = {0,1,16,4,35,47,59,62,73,88,99},查找16,按照原来二分查找需要四次,改进后为:二次。其中mid= 1+ (16-1)/(99-1) *(10-1) = 2.377.针对代码只需将:
mid = low + (high-low)*(key-a[low])/(a[high]-a[low])即可。
加入数组分布不均匀,用插值查找未必是合适的选择。如{0,1,2,2000,2001….}。
四、斐波那契查找
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=Fk-1;
1、算法思想
斐波那契查找的核心是:
1)当key=a[mid]时,查找成功;
2)当key<a[mid]时,新的查找范围是第low个到第mid-1个,此时范围个数为F[k-1] - 1个,即数组左边的长度,所以要在[low, F[k - 1] - 1]范围内查找;
3)当key>a[mid]时,新的查找范围是第mid+1个到第high个,此时范围个数为F[k-2] - 1个,即数组右边的长度,所以要在[F[k - 2] - 1]范围内查找。
关于斐波那契查找, 如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去,对处于当众的大部分数据,其工作效率要高一些。所以尽管斐波那契查找的时间复杂度也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏的情况,比如这里key=1,那么始终都处于左侧在查找,则查找效率低于折半查找。
还有关键一点,折半查找是进行加法与除法运算的(mid=(low+high)/2),插值查找则进行更复杂的四则运算(mid = low + (high - low) * ((key - a[low]) / (a[high] - a[low]))),而斐波那契查找只进行最简单的加减法运算(mid = low + F[k-1] - 1),在海量数据的查找过程中,这种细微的差别可能会影响最终的效率。
2、代码
// 斐波那契查找.cpp #include "stdafx.h" #include <memory> #include <iostream> using namespace std; const int max_size=20;//斐波那契数组的长度 /*构造一个斐波那契数组*/ void Fibonacci(int * F) { F[0]=0; F[1]=1; for(int i=2;i<max_size;++i) F[i]=F[i-1]+F[i-2]; } /*定义斐波那契查找法*/ int Fibonacci_Search(int *a, int n, int key) //a为要查找的数组,n为要查找的数组长度,key为要查找的关键字 { int low=0; int high=n-1; int F[max_size]; Fibonacci(F);//构造一个斐波那契数组F int k=0; while(n>F[k]-1)//计算n位于斐波那契数列的位置 ++k; int * temp;//将数组a扩展到F[k]-1的长度 temp=new int [F[k]-1]; memcpy(temp,a,n*sizeof(int)); for(int i=n;i<F[k]-1;++i) temp[i]=a[n-1]; while(low<=high) { int mid=low+F[k-1]-1; if(key<temp[mid]) { high=mid-1; k-=1; } else if(key>temp[mid]) { low=mid+1; k-=2; } else { if(mid<n) return mid; //若相等则说明mid即为查找到的位置 else return n-1; //若mid>=n则说明是扩展的数值,返回n-1 } } delete [] temp; return -1; } int _tmain(int argc, _TCHAR* argv[]) { int a[] = {0,16,24,35,47,59,62,73,88,99}; int key=100; int index=Fibonacci_Search(a,sizeof(a)/sizeof(int),key); cout<<key<<" is located at:"<<index; system("PAUSE"); return 0; }
3、代码解析
解析:
首先要明确:如果一个有序表的元素个数为n,并且n正好是(某个斐波那契数 - 1),即n=F[k]-1时,才能用斐波那契查找法。 如果有序表的元素个n不等于(某个斐波那契数 - 1),即n≠F[k]-1,这时必须要将有序表的元素扩展到大于n的那个斐波那契数 - 1才行,这段代码:
for (int i = n; i < F[k] - 1; i++)
{
a[i] = a[high];
}
便是这个作用。
下面回答
第一个问题:看完上面所述应该知道①是为什么了吧。 查找n在斐波那契数列中的位置,为什么是F[k] - 1,而不是F[k],是因为能否用斐波那契查找法是由F[k]-1决定的,而不是F[k]。如果暂时不理解,继续看下面。
第 二个问题:a的长度其实很好估算,比如你定义了有10个元素的有序数组a[20],n=10,那么n就位于8和13,即F[6]和F[7]之间,所以 k=7,此时数组a的元素个数要被扩充,为:F[7] - 1 = 12个; 再如你定义了一个b[20],且b有12个元素,即n=12,那么很好办了,n = F[7]-1 = 12, 用不着扩充了; 又或者n=8或9或11,则它一定会被扩充到12; 再如你举的例子,n=13,最后得出n位于13和21,即F[7]和F[8]之间,此时k=8,那么F[8]-1 = 20,数组a就要有20个元素了。 所以,n = x(13<=x<=20)时,最后都要被扩充到20;类推,如果n=25呢,则数组a的元素个数肯定要被扩充到 34 - 1 = 33个(25位于21和34,即F[8]和F[9]之间,此时k=9,F[9]-1 = 33),所以,n = x(21<=x<=33)时,最后都要被扩充到33。也就是说,最后数组的元素个数一定是(某个斐波那契数 - 1),这就是一开始说的n与F[k]-1的关系。
第三个问题:对于二分查找,分割是从mid= (low+high)/2开始;而对于斐波那契查找,分割是从mid = low + F[k-1] - 1开始的; 通过上面知道了,数组a现在的元素个数为F[k]-1个,即数组长为F[k]-1,mid把数组分成了左右两部分, 左边的长度为:F[k-1] - 1, 那么右边的长度就为(数组长-左边的长度-1), 即:(F[k]-1) - (F[k-1] - 1) = F[k] - F[k-1] - 1 = F[k-2] - 1。
4、程序分析
1、程序开始执行,参数为 a = {0,1,16,24,35,47,59,62,73,88,99},n = 10,查找关键字为key = 59.根据事先生成的斐波那契序列,计算n在序列中位置,我们得到 F[6]<n<F[7],所以k =7.
2、F[7] = 13,所以a[11]=a[12] = a[10] = 99.
3、查找开始,mid = 1+ F[6]-1 = 8,也就是说从下标8开始。此时a[8] =73>key =59,所以在左边,high = mid-1 = 7,k = k-1 =6;
4、再次循环,mid = 1+F[k-1] -1 = 5.此时从a[5] = 47<key开始查找,low = mid +1 = 6,k = 6-2 = 4.继而继续循环。
五、分块查找
分块查找又称索引查找。它是一种介于顺序查找和二分查找之间的查找方法。在分块查找中,主要求索引表是有序的,对块内结点没有排序要求,因此特别适合结点动态变化的情况。
1、存储结构
分块查找是由“分块有序”的线性表和索引表两部分构成。
分块有序的线性表:
假设要排序的表为R[0...N-1],将表均匀分成b块,前b-1块中记录个数为s=N/b,最后一块记录数小于等于s;
每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字。
注:这是使用分块查找的前提条件。
如上将表均匀分成b块后,抽取各块中的最大关键字和起始位置构成一个索引表IDX[0...b-1]。
由于表R是分块有序的,所以索引表是一个递增有序表。
所示为一个索引顺序表。其中包括三个块,第一个块的起始地址为 0,块内最大关键字为 25;第二个块的起始地址为 5,块内最大关键字为 58,最小关键字为28,大于25;第三个块的起始地址为10,块内最大关键字为 88。
2、算法
分块查找算法有两个处理步骤:
(1) 首先查找索引表
因为分块查找表是“分块有序”的,所以我们可以通过索引表来锁定关键字所在的区间。
又因为索引表是递增有序的,所以查找索引可以使用顺序查找或二分查找。
(2) 然后在已确定的块中进行顺序查找
因为块中不一定是有序的,所以只能使用顺序查找。
3、代码
// 建立索引方法,n 是线性表最大长度,gap是分块的最大长度 public IndexType[] createIndex(int list[], int n, int gap) { int i = 0, j = 0, max = 0; int num = n / gap; IndexType idxGroup[] = new IndexType[num]; // 根据步长数分配索引数组大小 while (i < num) { j = 0; idxGroup[i] = new IndexType(); idxGroup[i].link = gap * i; // 确定当前索引组的第一个元素位置 max = list[gap * i]; // 每次假设当前组的第一个数为最大值 // 遍历这个分块,找到最大值 while (j < gap) { if (max < list[gap * i + j]) { max = list[gap * i + j]; } j++; } idxGroup[i].key = max; i++; } return idxGroup; } // 分块查找算法 public int blockSearch(IndexType[] idxGroup, int m, int[] list, int n, int key) { int mid = 0; int low = 0; int high = m -1; int gap = n / m; // 分块大小等于线性表长度除以组数 // 先在索引表中进行二分查找,找到的位置存放在 low 中 while (low <= high) { mid = (low + high) / 2; if (idxGroup[mid].key >= key) { high = mid - 1; } else { low = mid + 1; } } // 在索引表中查找成功后,再在线性表的指定块中进行顺序查找 if (low < m) { for (int i = idxGroup[low].link; i < idxGroup[low].link + gap; i++) { if (list[i] == key) return i; } } return -1; }
六、三种线性查找的PK
(1) 以平均查找长度而言,二分查找 > 分块查找 > 顺序查找。
(2) 从适用性而言,顺序查找无限制条件,二分查找仅适用于有序表,分块查找要求“分块有序”。
(3) 从存储结构而言,顺序查找和分块查找既可用于顺序表也可用于链表;而二分查找只适用于顺序表。
(4) 分块查找综合了顺序查找和二分查找的优点,既可以较为快速,也能使用动态变化的要求。