引言
如果待查找的数组是有序的,那么此时的查找就是有序表查找,这对于查找的帮助是很大的。属于有序表查找的有:折半查找(二分查找)、插值查找以及斐波那契查找。
1. 折半查找
折半查找又称为二分查找,是一种效率较高的查找算法。折半查找的先决条件是查找表中的数据元素排列必须是有序的。折半查找先以有序数列的中点位置为比较对象,如果要找的元素值小于中点位置元素,则将待查序列缩小为左半部分,否则为右半部分。通过一次比较,可以将查找的区间缩小一半,每次比较,都可以将当前查找范围缩小至一般,可以明显的减少比较的次数,提高查找效率。
时间复杂度:O(logn)
算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| interface Searcher {
int search(int[] array, int key); }
class BinarySearcher implements Searcher { @Override public int search(int[] array, int key) { int low, high, mid; low = 0; high = array.length - 1; while (low <= high) { mid = (low + high) / 2; if (array[mid] > key) { high = mid - 1; } else if (array[mid] < key) { low = mid + 1; } else { return mid; } } return -1; } }
|
2. 插值查找
插值查找是二分查找演化而来,相比于二分查找(折半),该算法考虑的是每次折的时候折多少,即不一定是1/2;如在一本字典中找”abstract”这个单词,我们自己来操作肯定是先翻到字典开始的那一小部分,而不是从字典的中间开始进行折半查找。
在二分查找中 mid = (low + high) / 2 = low + 1/2 * (high - low)
,插值查找就是对1/2(系数,或者说比例)进行改变,它将1/2变成 (key - array[low]) / (array[high] - array[low])
,其实就是计算线性比例。
时间复杂度:O(logn)
因为插值查找是依赖线性比例的,如果当前数组分布不是均匀的,那么该算法就不合适。
算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class InterpolateSearcher implements Searcher {
@Override public int search(int[] array, int key) { int low, high, mid; low = 0; high = array.length - 1; while (low <= high) { mid = low + (int) (1.0 * (key - array[low]) / (array[high] - array[low]) * (high - low)); if (array[mid] > key) { high = mid - 1; } else if (array[mid] < key) { low = mid + 1; } else { return mid; } } return -1; } }
|
3. 斐波那契查找
根据前面二分查找以及插值查找来看,有序表上的查找的关键就是如何分割当前查找的区域(二分查找对半分割,差值查找按线性比例分割),说到分割,还有一个著名的分割方式就是黄金分割。
斐波那契数列,又称黄金分割数列,指的是这样一个数列: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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| class FibonacciSearcher implements Searcher {
private static final int MAX_ARRAY_SIZE = 30;
private int[] fibonacci(int len) { if (len < 0) throw new IllegalArgumentException("length must bigger than 0"); int[] fibonacci = new int[len]; fibonacci[0] = 1; fibonacci[1] = 1; for (int i = 2; i < len; i++) { fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2]; } return fibonacci; }
@Override public int search(int[] array, int key) { int low = 0; int len = array.length; int high = len - 1; int mid; int k = 0; int[] fib = fibonacci(MAX_ARRAY_SIZE); while (len > fib[k] - 1) { k++; } int[] tmp = new int[fib[k] - 1]; System.arraycopy(array, 0, tmp, 0, len); for (int i = len; i < fib[k] - 1; i++) { tmp[i] = array[high]; }
while (low <= high) { mid = low + fib[k - 1] - 1; if (tmp[mid] > key) { high = mid - 1; k = k - 1; } else if (tmp[mid] < key) { low = mid + 1; k = k - 2; } else { if (mid <= high) return mid; else return high; } } return -1; } }
|
结束语
以上三种查找算法中,都依赖于顺序表,三者的区别本质上就是分割点选的不同。在分割点的选择中,折半查找 mid=(low+high)/2
是加法与除法运算;插值查找mid=low+(key-array[low])/(array[high]-array[low])*(high-low)
是复杂的四则运算;斐波那契查找mid=low+fib[k-1]-1
是简单的加减运算。在海量数据查找过程中细微的差别会影响最终的效率。
三种查找算法,各有优劣,实际开发可以根据数据的特点综合考虑再做出选择。