本章节说的所有查找算法如没有特殊说明都是基于有序数据为前提的,否则算法无效
什么是有序表
有序表就是把数据按照从大到小或者从小到大顺序排列起来的一组数据。这组数据不一定是数字,可以是字母、字符串或者数据对象,只要找到关键码(可以比较大小的标识)按照顺序排列起来即可。
顺序查找
基本思想:
按照从大到小或者从小到大顺序依次比较,直到和给定值相等;否则查找失败
代码:
/** * 顺序查找算法 * @param arr * @param k * @return */ private static int sequentSearch(int[] arr, int k) { for (int i = 0; i < arr.length; i++) { if (arr[i] == k) { return i; } } return -1; }
顺序查找算法和思路都很简单,按照一定顺序依次比较就可以了。这种算法不但适应有序表,在无序表中同样适用。因为关键字在任何位置出现的概率相同,所以时间复杂度是O(n)。
但顺序查找有个比较大的缺点,当数据量很大时,效率变得异常低下,该算法就不适用了。
折半查找
基本思想:
在给定的有序表中,取中间记录作为比较对象,如果中间记录和给定值相等则查找成功;如果给定值小于中间记录则在中间记录的左侧继续查找;如果给定值大于中间记录则在右侧查找。不断重复上面过程,直到查找成为为止,否则查找失败。
代码:
private static int search(int[] arr, int k) { int start = 0; // 查找开坐标 int end = arr.length - 1; // 结束坐标 while (start <= end) { int m = (start + end) / 2; // 计算中间坐标 if (arr[m] == k) { return m; } else if (arr[m] > k) { end = m - 1; } else { start = m + 1; } } return -1; }
查找过程:
比如从数组[1, 5, 7, 8, 9, 10, 13, 17, 20, 30]中查找7。
1、第一次循环:m = (0+9)/2 = 4,数组arr[4] =9,因为7小于9,所以比较左面end=3
2、第二次循环:m=(0+3)/2=1,数组arr[1]=5,因为5 < 7,比较右面start=2
3、第三次循环:m=(2+3)/2=2,数组arr[2]=7,和给定值相等,查找成功。
此时可以看到折半查找效率非常高,他的时间复杂度是O(logn)。不过由于折半查找的前提条件是有序表,对于静态数据一次排序后不再变化,这样的算法效率很高。但对于频繁变动的数据集合来说排序维护的工作量很大,该算法就不一定适用了。
插值查找
插值查找是对折半查找的补充,在一定场景下大大提高性能。比如在英文字典查单词‘apple’,我们从头开始查找,查找‘zip’就从后面开始找都会比折半查找从中间字母m开始效率高很多。这个查找开始位置就是插值查找改进的算法。
基本思想:
基本思想和折半查找一致,只是在获取中间坐标时通过给定值和开始值比较,计算出大概位置。
折半查找的中间坐标计算公式:
mid = (low + high) / 2 = low + (high - low) / 2
插值查找把1/2进行改进,替换为 1/2 => (key - a[start]) / (a[end] - a[start])
插值查找坐标公式:
mid = low + (high - low) * (key - a[start]) / (a[end] - a[start])
代码:
/** * 插值查找算法 * @param arr * @param k * @return */ private static int interSearch(int[] arr, int k) { int start = 0; // 查找开坐标 int end = arr.length - 1; // 结束坐标 while (start <= end) { // 插值查找根据和开始值比较获得大概位置 // m = (start + end) / 2 = start + (ent-start)/2 // 把 1/2替换为 (k - arr[start]) / (arr[end] - arr[start]) int m = start + (end - start) * (k - arr[start]) / (arr[end] - arr[start]); if (arr[m] == k) { return m; } else if (arr[m] > k) { end = m - 1; } else { start = m + 1; } } return -1; }
插值查找的核心点在于插值的计算公式(key - a[start]) / (a[end] - a[start])。因为和折半查找算法一样,所以时间复杂度也是O(logn) 。对于数据关键字分布比较均匀的查找表来说,插值查找算法比折半查找好的多,但对于分布很不均匀的数据来说,不一定是最优选择。
斐波那契查找
什么是斐波那契数列
斐波那契又称黄金分割数列,如:1、1、2、3、5、8、13、……,在数学上满足如下定义:F(1)=1,F(2)=1,F(n) = F(n-1) + F(n-2) (n >= 2) 。该数列相邻的两个数越往后越满足黄金分割比例,即:F(n-2) :F(n-1) = 0.618
基本思想
斐波那契查找思想和折半查找算法类似,只不过获取坐标mid不再是中间点,而变成了黄金分割点附近mid = low + F(k-1) -1,F代表斐波那契数列。
由F(n)=F(n-1) + F(n-2)可以得到F(n)-1 = [F(n-1)-1] + [F(n-2)-1] + 1,这个式子说明可以把长度为F(n)-1的数列分为[F(n-1)-1]和[F(n-2)-1]两段,另外一个1就是mid位置的元素。
因此,只需要把给定的数列补全满足F(n)-1条件后,然后根据折半查找算法查找即可。
代码
/** * 斐波那契查找算法 * @param arr * @param key * @return */ private static int fibonaSearch(int[] arr, int key) { // 斐波那契数列定义,实际应用时可以缓存起来 int[] fibArr = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; // 查找数组长度在斐波那契数列中的位置 int n = arr.length; int k = 0; while (n > fibArr[k] - 1) { k++; } // 创建新的符合完整斐波那契数列的数组, 用最大值填充 int[] tempArr = null; if (arr.length < fibArr[k] - 1) { tempArr = Arrays.copyOf(arr, fibArr[k] - 1); for (int i = arr.length; i < fibArr[k] - 1; i++) { tempArr[i] = arr[arr.length - 1]; } } else { tempArr = arr; } int start = 0; // 查找开坐标 int end = tempArr.length - 1; // 结束坐标 while (start <= end) { int mid = start + fibArr[k - 1] - 1; // 找到斐波那契分割的下标 if (key < tempArr[mid]) { end = mid - 1; k--; } else if (key > tempArr[mid]) { start = mid + 1; k -= 2; } else { if (mid <= arr.length) { return mid; } else { return arr.length; } } } return -1; }
通过上面逻辑过程可以看到,斐波那契查找的时间复杂度也是O(logn),但就平均性能来说斐波那契查找要优于折半查找,因为折半查找计算mid值使用四则运算而斐波那契使用简单加减法。这种细微差别在海量数据面前尤为明显。
上面三种算法各有特点,没有绝对优劣之分,运用时还要根据实际情况而定 。
ps:《大话数据结构》读书笔记