在我们的很多使用中,其实很多用的是线性查找,即遍历所有数据(通过一个for循环),当找到查找的数据就返回,这种其实也是一直算法,只是效率较低。
下面我介绍的是二分查找算法(递归实现)、插值查找算法(递归实现)、斐波那契查找算法(递归+非递归实现)
查找算法在我们不论是面试、工作中都十分重要,在海量数据中,如何提高查找效率,也是我们程序猿一直追求的目标。现在我就介绍其中最普遍的三种查找算法,希望对大家有所帮助。
首先,以上三种查找算法都基于同一个前提:就是有序
所以我们在进行查找之前,必须先对数组进行排序,而如何排序呢?我在之前的一篇文章有讲到对应的7大排序算法,用兴趣的朋友可以先去了解一下(链接附上),这里我就不再赘述了,直接进入正题。
目录
二分查找(递归):
二分查找的思路是,先判断要查找的数据是否与给定的数据组的中间值相等,若小于中间值,则继续向左递归(找左边数据的中间值再与给定的值比较);若大于中间值,则向右递归;若等于中间值,则返回该值对应的下标。
图解:
下面附上代码实现
package com.liu.search;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-11 20:36
*/
//二分查找,前提为有序列表
public class BinarySearchAlgorithm {
public static void main(String[] args) {
int[] arr = new int[8];
for (int i = 1; i <= arr.length; i++) {
arr[i - 1] = i;
}
System.out.println(Arrays.toString(arr));
int result = BinarySearchAlgorithm.search(arr, 10, 0, arr.length-1);
System.out.println(result);
}
/**
* 二分查找
*
* @param arr 查找的范围
* @param dest 要查找的数据
* @param left 第一个数据的下标
* @param right 最后一个数据的下标
* @return 若存在,则返回该数据的下标,如果不存在,则返回-1
*/
public static int search(int[] arr, int dest, int left, int right) {
int mid = (left + right) / 2;//记录中间值的下标
if (arr[mid] == dest) {//若与中间值相等,则返回下标
return mid;
}
if(left>=right){//若当left>=right时,此时已把所有数据遍历完,没有相等的数据,则返回-1
return -1;
}
if (arr[mid] < dest) {//当要找的数据在中间值的右边
//向右递归
return search(arr, dest, mid + 1, right);
} else {//要找的数据在中间值的左边
//向左递归
return search(arr, dest, left, mid-1);
}
}
}
插值查找算法(查找)
其实插值查找算法与二分查找算法无大差异,只是mid值的选择不同。插值查找算法的mid的取值为:
上面是二分查找算法的mid取值,下面是插值查找算法的mid取值。
值得注意的是:
1、在数据量大,数据大小分布均匀的数据组中,插值查找算法能够快速定位到要查找数据的对应下边。相比于二分查找算法效率提高很多!
2、但是!如果在数据分布不均匀的情况下,插值查找算法的效率不一定比二分查找算法高
由于插值查找算法与二分查找算法思路区别不大,我这里就不画图阐述了。里面有个注意的点:
就是我们在插值查找算法时需要先进行判断dest的范围,否则会出现数组角标越界的报错,具体的解释我在代码中的注释已然提及,大家可以在代码中了解。
代码:
package com.liu.search;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-11 22:29
*/
public class InsertValueSearchAlgorithm {
public static void main(String[] args) {
int[] arr = new int[8];
for (int i = 1; i <= arr.length; i++) {
arr[i - 1] = i;
}
System.out.println(Arrays.toString(arr));
int result = InsertValueSearchAlgorithm.search(arr, 0, arr.length - 1, 8);
System.out.println(result);
}
/**
* 插值查找算法的实现
*
* @param arr 查找的范围
* @param left 第一个数据的下标
* @param right 最后一个数据的下标
* @param dest 要查找的数据
* @return 若存在,则返回该数据的下标,如果不存在,则返回-1
*/
public static int search(int[] arr, int left, int right, int dest) {
//这里为什么要先判定呢?理由在于我们进行递归时,会把范围前移或者后移,如下面递归mid-1和mid+1
//若我们一开始定位到的mid是最左边或者最后边,在下面的递归中会产生数组角标越界.
if (dest < arr[0] || left > right || dest > arr[arr.length - 1]) {
//当要查找的数据超过给定范围的限定值或者已然遍历完仍找不到值,则返回-1
return -1;
}
int mid = left + ((dest - arr[left]) / (arr[right] - arr[left])) * (right - left);
if (arr[mid] == dest) {//找到该数据,则返回对应的下标
return mid;
}
if (arr[mid] > dest) {
//要找的值比中间值小,则向左递归
return search(arr, left, mid-1, dest);
} else {
//要找的值比中间值大,则向右递归
return search(arr, mid+1, right, dest);
}
}
}
斐波那契(黄金分割点)查找算法
首先我们先了解什么是斐波那契数列
1 1 2 3 5 8 13 21 34 55 89
上面的数列即为斐波那契数列,大家发现有什么特别的吗?
没错,就是该数列自第三项起,每一项的数据都是前面两项数据之和,这种情况即为斐波那契数列。
然后我们再来了解一个黄金分割点的定义:黄金分割点指的是将一条线段分割成两部分,是其中一部分的长度与全场之比等于另一部分的长度与这部分的长度之比,该比例大概在0.618。
那这两者有什么联系吗?有的!大家可以观察,自第三项起,每一项与后一项之比都接近与0.618。2/3=0.667,3/5=0.6,5/8=0.625,8/13=0.615,13/21=0.619......
数列最往后,其比值就约接近0.618
黄金分割点被称为一个完美的比例,而斐波那契数列又契合这个比例。
因此,我们将黄金分割点运用到我们的查找算法中。我们的斐波那契查找算法,就是基于这一个斐波那契数列来进行查询的,以期获得完美的查找效率。
思路:
斐波那契算法也与前面两种查找算法类似,只是mid的值发生了变化,此时mid=left+fibonacci(k-1)-1。fibonacci()为斐波那契数列。
这里的low和high我下面用left和right表示,会形象一点。
由斐波那契数列fibonacci [k]= fibonacci [k-1]+ fibonacci [k-2]的性质,可以得到(fibonacci [k]-1)=(fibonacci [k-1]-1)+(fibonacci [k-2]-1)+1。该式说明:只要顺序表的长度为fibonacci [k]-1,则可以将该表分成长度为fibonacci [k-1]-1 和 fibonacci [k-2]-1 的两段,即如上图所示。从而中间位置为 mid=low+ fibonacci [k-1]-1
因为我们的数据的最后一个数据的下标为arr.length()-1,所以如果我们的顺序表长度为fibonacci [k]-1的话,(fibonacci [k]-1)-1即为最后一个数据的下标,此时可以借助于黄金分割,将数据分割为(left到fibonacci[k-1]-1)这段和(fibonacci[k-1]-1到right)这一段这时候就是符合黄金分割,此时mid就等于left+fibonacci[k-1]-1。如果长度不满足,则将数组扩容。
本来尚硅谷韩老师的解法只提供了非递归的实现方式,然后我本来也想只提供非递归的实现方式,只是自己写着写着写到了递归的方式去,而且我查找了一些其他的博客,很少有递归式实现的介绍,即使有,也含糊不清,代码太过简洁,不便于阅读。因此我就写下了递归式实现的方法,供大家参考学习。
因为我在代码中加了较多的注释,所以大家可能看代码会更直观一点,我就把代码附上。
后面会附上完整代码(下面单独的非递归和递归的方法中,没有加上创建斐波那契数列的代码,所以可以看完整版代码)
非递归式实现斐波那契查找算法
下面是非递归的代码:
/**
* 非递归斐波那契查找算法的实现
*
* @param arr 查找的范围
* @param dest 要查找的数据
* @param left 数组的起始下标
* @param right 数组的终止下标
* @return 若存在,则返回该数据的下标,如果不存在,则返回-1
*/
public static int search(int[] arr, int dest, int left, int right) {
int[] fibonacci = getFibonacci();//获得一个斐波那契数列
int k = 0;//指向斐波那契数列的下标
while (arr.length > (fibonacci[k] - 1)) {
//当数组的长度大于斐波那契数列的数
//则继续循环,要找到与斐波那契数列相近的数
k++;
}
//循环结束得到一个与数组长度相近的斐波那契数
//但此时数组长度不一定是与斐波那契数一致的,如果不一致,我们需要借用一个临时数组,即扩容原数组长度后的数组
//扩容数组
int[] temp = new int[fibonacci[k] - 1];
//将原数据添加到临时数组中,借助临时数组排序,没有填到的数据用一个极大值来填补Integer.MAX_VALUE
//实际开发中会用数组的最后一个数据填补,我这里偷个懒用一个常数Integer.MAX_VALUE来填补
//这里可以借助一个Aarrys.copyOf()来进行复制数组,有兴趣的可以去查找一下相关api,我这里就有原始的方法去实现
for (int i = 0; i < temp.length; i++) {
if (i < arr.length) {//先填补为arr数组内的数据
temp[i] = arr[i];
} else {
//当填补完arr数组内的数据后,用Integer.MAX_VALUE填补剩下的位置
temp[i] = Integer.MAX_VALUE;
}
}
while (left <= right) {
int mid = left + fibonacci[k - 1] - 1;
if (dest < temp[mid]) {
//我们应该继续向左边查找
right = mid - 1;
//为什么是k-=1?
//因为fibonacci[k]=fibonacci[k-1]+fibonacci[k-2],此时前面剩下fibonacci[k-1]个数据
//fibonacci[k-1]继续拆分,拆分成fibonacci[k-2]+fibonacci[k-3],我们同样先取该部分的前一部分即fibonacci[k-2]进行查找,所以-1
k--;
} else if (dest > temp[mid]) {
//向右边查找
left = mid + 1;
//为什么是k-=2?
//因为fibonacci[k]=fibonacci[k-1]+fibonacci[k-2],此时后面剩下fibonacci[k-2]个数据
//fibonacci[k-2]继续拆分,拆分成fibonacci[k-3]+fibonacci[k-4],我们同样先取该部分的前一部分即fibonacci[k-3]进行查找,所以-2
k -= 2;
} else {
//找到了
//返回下标
return mid;
}
}
return -1;
}
递归式实现斐波那契查找算法
下面是代码实现:
/**
* 递归式实现斐波那契查找算法
*
* @param arr 查找的范围
* @param k 斐波那契数列对应下标
* @param left 数组的起始下标
* @param right 数组的终止下标
* @param dest 要查找的数
* @param flag 判断是否已扩容的标志位
* @return 如果找到,则返回下标;如果没找到,则返回-1
*/
public static int search1(int[] arr, int k, int left, int right, int dest, boolean flag) {
int[] fibonacci = getFibonacci();//获得斐波那契数列
if (arr == null || arr[left] > dest || arr[right] < dest || left > right) {
//如果数组为空 或者 查找的值小于数组最小值 或者 查找值大于最大值 或者 已遍历结束没找到,则返回-1
return -1;
} else {
int mid = left + fibonacci[k - 1] - 1;//获得中间值的下标
if (arr[mid] == dest) {//如果找到,则返回下标
return mid;
} else {
//判断是否需要扩容以及是否已发生扩容,若二者不能同时满足,则不需扩容
if (arr.length != (fibonacci[k] - 1)&&!flag) {
//扩容数组
int[] temp = new int[fibonacci[k] - 1];
//将原数据添加到临时数组中,借助临时数组排序,没有填到的数据用一个极大值来填补Integer.MAX_VALUE
//实际开发中会用数组的最后一个数据填补,我这里偷个懒用一个常数来填补
//这里可以借助一个Aarrys.copyOf()来进行复制数组,有兴趣的可以去查找一下相关api,我这里就有原始的方法去实现
for (int i = 0; i < temp.length; i++) {
if (i < arr.length) {//先填补为arr数组内的数据
temp[i] = arr[i];
} else {
//当填补完arr数组内的数据后,用Integer.MAX_VALUE填补剩下的位置
temp[i] = Integer.MAX_VALUE;
}
}
//发生完扩容
flag=true;
if (temp[mid] > dest) {
//向左递归
return search1(temp, k - 1, left, mid - 1, dest, flag);
} else {
//向右递归
return search1(temp, k - 2, mid + 1, right, dest, flag);
}
}
else {
//未发生扩容或不需要再扩容
if (arr[mid] > dest) {
//向左递归
return search1(arr, k - 1, left, mid - 1, dest, flag);
} else {
//向右递归
return search1(arr, k - 2, mid + 1, right, dest, flag);
}
}
}
}
}
完整斐波那契实现代码:
package com.liu.searchalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-11 22:51
*/
//斐波那契查找算法(非递归+递归)
public class FibonacciSearchAlgorithm {
private static int maxSize = 20;
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 1; i <= arr.length; i++) {
arr[i - 1] = i;
}
// System.out.println(Arrays.toString(arr));
// int result = FibonacciSearchAlgorithm.search(arr, 5, 0, arr.length - 1);
//要是觉得这样找k值麻烦的话,可以封装成一个方法,这里我就不封装了。
int k = 0;
while (arr.length > (getFibonacci()[k] - 1)) {
k++;
}
int result = FibonacciSearchAlgorithm.search1(arr, k, 0, arr.length - 1, 89,false);
System.out.println(result);
}
/**
* 非递归斐波那契查找算法的实现
*
* @param arr 查找的范围
* @param dest 要查找的数据
* @param left 数组的起始下标
* @param right 数组的终止下标
* @return 若存在,则返回该数据的下标,如果不存在,则返回-1
*/
public static int search(int[] arr, int dest, int left, int right) {
int[] fibonacci = getFibonacci();//获得一个斐波那契数列
int k = 0;//指向斐波那契数列的下标
while (arr.length > (fibonacci[k] - 1)) {
//当数组的长度大于斐波那契数列的数
//则继续循环,要找到与斐波那契数列相近的数
k++;
}
//循环结束得到一个与数组长度相近的斐波那契数
//但此时数组长度不一定是与斐波那契数一致的,如果不一致,我们需要借用一个临时数组,即扩容原数组长度后的数组
//扩容数组
int[] temp = new int[fibonacci[k] - 1];
//将原数据添加到临时数组中,借助临时数组排序,没有填到的数据用一个极大值来填补Integer.MAX_VALUE
//实际开发中会用数组的最后一个数据填补,我这里偷个懒用一个常数Integer.MAX_VALUE来填补
//这里可以借助一个Aarrys.copyOf()来进行复制数组,有兴趣的可以去查找一下相关api,我这里就有原始的方法去实现
for (int i = 0; i < temp.length; i++) {
if (i < arr.length) {//先填补为arr数组内的数据
temp[i] = arr[i];
} else {
//当填补完arr数组内的数据后,用Integer.MAX_VALUE填补剩下的位置
temp[i] = Integer.MAX_VALUE;
}
}
while (left <= right) {
int mid = left + fibonacci[k - 1] - 1;
if (dest < temp[mid]) {
//我们应该继续向左边查找
right = mid - 1;
//为什么是k-=1?
//因为fibonacci[k]=fibonacci[k-1]+fibonacci[k-2],此时前面剩下fibonacci[k-1]个数据
//fibonacci[k-1]继续拆分,拆分成fibonacci[k-2]+fibonacci[k-3],我们同样先取该部分的前一部分即fibonacci[k-2]进行查找,所以-1
k--;
} else if (dest > temp[mid]) {
//向右边查找
left = mid + 1;
//为什么是k-=2?
//因为fibonacci[k]=fibonacci[k-1]+fibonacci[k-2],此时后面剩下fibonacci[k-2]个数据
//fibonacci[k-2]继续拆分,拆分成fibonacci[k-3]+fibonacci[k-4],我们同样先取该部分的前一部分即fibonacci[k-3]进行查找,所以-2
k -= 2;
} else {
//找到了
//返回下标
return mid;
}
}
return -1;
}
/**
* 递归式实现斐波那契查找算法
*
* @param arr 查找的范围
* @param k 斐波那契数列对应下标
* @param left 数组的起始下标
* @param right 数组的终止下标
* @param dest 要查找的数
* @param flag 判断是否已扩容的标志位
* @return 如果找到,则返回下标;如果没找到,则返回-1
*/
public static int search1(int[] arr, int k, int left, int right, int dest, boolean flag) {
int[] fibonacci = getFibonacci();//获得斐波那契数列
if (arr == null || arr[left] > dest || arr[right] < dest || left > right) {
//如果数组为空 或者 查找的值小于数组最小值 或者 查找值大于最大值 或者 已遍历结束没找到,则返回-1
return -1;
} else {
int mid = left + fibonacci[k - 1] - 1;//获得中间值的下标
if (arr[mid] == dest) {//如果找到,则返回下标
return mid;
} else {
//判断是否需要扩容以及是否已发生扩容,若二者不能同时满足,则不需扩容
if (arr.length != (fibonacci[k] - 1)&&!flag) {
//扩容数组
int[] temp = new int[fibonacci[k] - 1];
//将原数据添加到临时数组中,借助临时数组排序,没有填到的数据用一个极大值来填补Integer.MAX_VALUE
//实际开发中会用数组的最后一个数据填补,我这里偷个懒用一个常数来填补
//这里可以借助一个Aarrys.copyOf()来进行复制数组,有兴趣的可以去查找一下相关api,我这里就有原始的方法去实现
for (int i = 0; i < temp.length; i++) {
if (i < arr.length) {//先填补为arr数组内的数据
temp[i] = arr[i];
} else {
//当填补完arr数组内的数据后,用Integer.MAX_VALUE填补剩下的位置
temp[i] = Integer.MAX_VALUE;
}
}
//发生完扩容
flag=true;
if (temp[mid] > dest) {
//向左递归
return search1(temp, k - 1, left, mid - 1, dest, flag);
} else {
//向右递归
return search1(temp, k - 2, mid + 1, right, dest, flag);
}
}
else {
//未发生扩容或不需要再扩容
if (arr[mid] > dest) {
//向左递归
return search1(arr, k - 1, left, mid - 1, dest, flag);
} else {
//向右递归
return search1(arr, k - 2, mid + 1, right, dest, flag);
}
}
}
}
}
/**
* 创建一个斐波那契数列
*
* @return 返回一个斐波那契数列
*/
//要进行斐波那契排序算法,我们需要首先创建一个斐波那契数列
public static int[] getFibonacci() {
int[] fibonacci = new int[maxSize];
//斐波那契数列的第一、第二个数据是1
fibonacci[0] = 1;
fibonacci[1] = 1;
for (int i = 2; i < fibonacci.length; i++) {
fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];//斐波那契数列的特征,等于前两个数的和
}
return fibonacci;
}
}