本文内容,参考自《大话数据结构》(程杰著) ,一部分自己修改,如:把C语言换成了Java语言。写作目的,意在加强记忆。
本文写作工具,使用 Typora。
顺序查找
试想一下,要在散落的一大堆书中找到你需要的那本有多么麻烦。碰到这种情况的人大都会考虑做一件事,那就是把这些书排列整齐,比如竖起来放置在书架上,这样根据书名,就很容易查找到需要的图书。
散落的图书可以理解为一个集合,而将它们排列整齐,就如同是将此集合构造成一个线性表。我们要针对这一线性表进行查找操作,因此它就是静态查找表。
此时图书尽管已经排列整齐,但还没有分类,因此我们要找书只能从头到尾或从尾到头一本一本查看,直到找到或全部查找完为止。这就是顺序查找。
顺序查找又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不相等时,则表中没有所查的记录,查找不成功。
顺序查找的算法实现如下:
public static int search(int[] R,int k){
int i,n=R.length;
for(i=0;i<n;i++){
if(R[i]==k){
return i;
}
}
return -1;
}
复制代码
对于这种顺序查找算法来说,查找成功最好的情况就是在第一个位置就找到了,算法时间复杂度为O(1),最坏的情况是在最后一个位置才找到,需要n次比较,时间复杂度为O(n),当查找不成功时,需要n+1次比较,时间复杂度为O(n)。关键字在任何一位置的概率是相同的,所以平均查找次数为(n+1)/2,所以最终时间复杂度还是O(n)。
很显然,顺序查找算法是有很大缺点的,n很大时,查找效率极为低下,不过优点也是有的,这个算法非常简单,对静态查找表的记录没有任何要求,在一些小型数据的查找时,是可以适用的。
另外,也正由于查找概率的不同,我们完全可以将容易查找到的记录放在前面,而不常用的记录放置在后面,效率就可以有大幅提高。
二分查找
二分查找,它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。二分查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
假设我们现在有这样一个有序表数组{1,16,24,35,47,59,62,73,88,99},对它进行查找是否存在62这个数。二分查找算法实现如下:
public static int binarySearch(int[] arr,int num){
int low=0;
int upper=arr.length-1;
while(low<=upper){
int mid=(upper+low)/2;
if(arr[mid]<num){
low=mid+1;
} else if(arr[mid]>num){
upper=mid-1;
} else {
return mid;
}
}
return -1;
}
复制代码
该算法还是比较容易理解的,同时我们也能感觉到它的效率非常高。二分查找等于是把静态有序表分成了两棵子树,即查找结果只需要找其中的一半数据记录即可,等于工作量少了一半,然后继续二分查找,效率非常高。
最终我们二分查找算法的时间复杂度为O(㏒n),它显然远远好于顺序查找的O(n)时间复杂度了。
不过由于二分查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法已经比较好了。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。
插值查找
现在我们的新问题是,为什么一定要折半,而不是折四分之一或都折更多呢?打个比方,在英文词典查"apple",你下意识地翻开词典,是翻前面的还是后面呢?如果再让你查"zoo",你又怎么查?很显然,这里你绝对不会是从中间开始查找,而是有一定目地往前翻或往后翻。
同样的,比如要在取值范围0~100000之间100个元素从小到大均匀分布的数组中查找5,我们自然会考虑从数组下标较小的开始查找。
看来,我们的二分查找,还是有改进空间。
我们把二分查找,变换之后:
int mid = (upper+low)/2 = low + (upper-low)/2;
复制代码
也就是mid等于最低下标low加上最高下标upper与low的差的一半。算法科学家们考虑的就是将1/2进行改进,改进为下面的计算方案:
int mid = low + ((num-arr[low])/(arr[upper]-arr[low]))*(upper-low)
复制代码
将1/2改成了((num-arr[low])/(arr[upper]-arr[low]))有什么道理呢?假设a[10]={1,16,24,35,47,59,62,73,88,99},low=0,upper=9,则a[low]=1,a[upper]=99,如果我们要找的是16时,按原来的二分做法,我们需要4次才可以得到结果,但如果使用新办法,((num-arr[low])/(arr[upper]-arr[low])) = (16-1)/(99-1) 约等于0.153,即mid约等于1+0.153*(9-0)=2.377,取整得到mid=2,我们只需要二次就查找到结果了,显然大大提高了查找的效率。
插值查找算法代码如下:
public static int binarySearch(int[] arr,int num){
int low=0;
int upper=arr.length-1;
while(low<=upper){
int mid = low + ((num-arr[low])/(arr[upper]-arr[low]))*(upper-low);
if(arr[mid]<num){
low=mid+1;
} else if(arr[mid]>num){
upper=mid-1;
} else {
return mid;
}
}
return -1;
}
复制代码
从时间复杂度来看,它也是O(㏒n),但对于表长比较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比二分查找要好得多。
斐波那契查找
斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n] (如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。
斐波那契查找的时间复杂度还是O(logn ),但是与二分查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比二分查找小,但是还是得视具体情况而定。
对于斐波那契数列:1、1、2、3、5、8、13、21、34、55、89……(也可以从0开始),前后两个数字的比值随着数列的增加,越来越接近黄金比值0.618。比如这里的89,把它想象成整个有序表的元素个数,而89是由前面的两个斐波那契数34和55相加之后的和,也就是说把元素个数为89的有序表分成由前55个数据元素组成的前半段和由后34个数据元素组成的后半段,那么前半段元素个数和整个有序表长度的比值就接近黄金比值0.618,假如要查找的元素在前半段,那么继续按照斐波那契数列来看,55 = 34 + 21,所以继续把前半段分成前34个数据元素的前半段和后21个元素的后半段,继续查找,如此反复,直到查找成功或失败,这样就把斐波那契数列应用到查找算法中了。
斐波那契查找算法代码如下:
/**
* 斐波那契数列
*
* @return
*/
public static int[] fibonacci() {
int[] f = new int[20];
int i = 0;
f[0] = 1;
f[1] = 1;
for (i = 2; i < MAXSIZE; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
public static int fibonacciSearch(int[] data, int key) {
int low = 0;
int high = data.length - 1;
int mid = 0;
// 斐波那契分割数值下标
int k = 0;
// 序列元素个数
int i = 0;
// 获取斐波那契数列
int[] f = fibonacci();
// 获取斐波那契分割数值下标
while (data.length > f[k] - 1) {
k++;
}
// 创建临时数组
int[] temp = new int[f[k] - 1];
for (int j = 0; j < data.length; j++){
temp[j] = data[j];
}
// 序列补充至f[k]个元素
// 补充的元素值为最后一个元素的值
for (i = data.length; i < f[k] - 1; i++) {
temp[i] = temp[high];
}
while (low <= high) {
// low:起始位置
// 前半部分有f[k-1]个元素,由于下标从0开始
// 则-1 获取 黄金分割位置元素的下标
mid = low + f[k - 1] - 1;
if (temp[mid] > key) {
// 查找前半部分,高位指针移动
high = mid - 1;
// (全部元素) = (前半部分)+(后半部分)
// f[k] = f[k-1] + f[k-1]
// 因为前半部分有f[k-1]个元素,所以 k = k-1
k = k - 1;
} else if (temp[mid] < key) {
// 查找后半部分,高位指针移动
low = mid + 1;
// (全部元素) = (前半部分)+(后半部分)
// f[k] = f[k-1] + f[k-1]
// 因为后半部分有f[k-1]个元素,所以 k = k-2
k = k - 2;
} else {
// 如果为真则找到相应的位置
if (mid <= high) {
return mid;
} else {
// 出现这种情况是查找到补充的元素
// 而补充的元素与high位置的元素一样
return high;
}
}
}
return -1;
}
复制代码