嚎羸的博客

因为Hexo是静态博客,部署多有不便,建议查看我的语雀文档

0%

08、查找算法

查找算法概述

在Java中,常用的查找算法有四种:

1、顺序(线性)查找

2、二分查找/折半查找

3、插值查找

4、斐波那契查找

线程查找

线性查找概述

线性查找十分简单。

现在有一个数列,可以是有序的,可以是无序的。

我们从头到尾依次寻找,如果找到了那么提示找到并返回下标。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 实现一个比较简答的线性查找,也就是说只查找一个值,这里就不写查找多个值的了,因为比较简单
*
* @param arr 要查找的数组
* @param val 要查找的值
* @return 查找到的值的下标,没有查到则返回-1
*/
public static int seqSearch(int[] arr, int val) {

int index = -1;

for (int i = 0; i < arr.length; i++) {
if (arr[i] == val) {
return i;
}
}

return index;
}

二分查找

线性查找概述

二分查找必须要在一个有序的数组中进行查找

有序的意思是:从大到小,或者从小到大都可以。

二分查找有两种

  • 递归查找
  • 非递归查找

二分查找的思想

1、确定好数组元素的中间下标:mid = (left + right) / 2

2、让需要查找的数findVal和arr[mid]比较

  • 假如findVal > arr[mid],那么说明findVal的下标在mid之后,那么向右边进行递归查找
  • 假如findVal < arr[mid],那么说明findVal的下标在mid之前,那么向左边进行递归查找
  • 假如findVal == arr[mid],说明查找到了内容

注意点

结束递归的条件

1、找到了对应的值,我们退出递归

2、所有的数据已经遍历完成,但是还没有找到数据,那么结束递归(这种情况也就是left指针 > right指针)

代码实现

  • 基本的代码实现
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
/**
* 二分法查找,必须是一个有序数组才可以进行查找
*
* @param arr 要查找的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的元素
* @return
*/
public static int binarySeach(int[] arr, int left, int right, int findVal) {
/*
假如左边的指针比右边的指针大,那么说明没有找到最终的值,返回-1即可
*/
if (left > right) {
return -1;
}

/*
假如还在寻找的过程中,那么有如下三种情况:
*/

//中间索引
int mid = (left + right) / 2;
//中间值
int midVal = arr[mid];

//1、假如要查找的值>中间值,那么进行向右递归
if (findVal > midVal) {
return binarySeach(arr, mid + 1, right, findVal);
}
//2、假如要查找的治<中间值,那么进行向左递归
else if (findVal < midVal) {
return binarySeach(arr, left, mid - 1, findVal);
}
//3、剩下的一种情况就说明已经找到了对应的值,结束递归,返回对应的索引
else {
return mid;
}
}
  • 二分查找的升级处理

需求:当我们的数组中存在多个相同的值时,如何将这多个相同的值都查到,而不是只返回一个

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
/**
* 二分查找,这里要进行一次升级,也就是将多个相同的值的索引放到List里面一块返回
*
* @param arr 要查找的数组
* @param left 左边的下标
* @param right 右边的下标
* @param findVal 要寻找的元素
* @return 要寻找的值的索引的集合,没有则返回null
*/
public static List<Integer> binarySearchUpgrade(int[] arr, int left, int right, int findVal) {
if (left > right) {
return null;
}
int mid = (left + right) / 2;
int midVal = arr[mid];

if (findVal > midVal) {
return binarySearchUpgrade(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {
return binarySearchUpgrade(arr, left, mid - 1, findVal);
}
/*
前两个都没有什么区别,重点是这里
假如我们找到了一个和findVal相同的值,同时也获得了它的下标
那么根据有序排列的规则,这个下标的左右两边很可能是相同的个数字,所以要分别对左右两边进行遍历,直到找到下标不是为止
而且注意,根据有序排列的规则,二分查找的肯定是有序的,不会存在1,1000,200这种情况
*/
else {
//0、创建一个List集合,加入已经确认的下标
List list = new ArrayList();
list.add(mid);
//1、获取数组元素对应的下标
int temp = mid - 1;
//2、向左进行遍历,寻找对应值的下标,加入到集合中
while (temp >= 0 && arr[temp] == midVal) {
list.add(temp--);
}
//3、重置下标
temp = mid + 1;
//4、向右进行遍历,寻找对应值的下标,加入到集合中
while (temp <= arr.length - 1 && arr[temp] == midVal) {
list.add(temp++);
}
//5、集合进行排序
Collections.sort(list);
return list;
}
}

插值查找

插值查找概述

插值查找是二分查找的一种更进一步的优化方案,与二分查找不同的是:插值查找的中间点mid是自适应的

我们在二分查找的时候,中间的点是*(left + right)/2*得到的

但是假如使用插值查找,那么中间的点是 left + (right - left) x (findVal - arr[left]) / (arr[right] - arr[left])

这个公式也就是拉格朗日插值(注意,数字必须是有序的,才能使用这个公式)


那么其实插值算法最重要的思想就是这个mid的公式,但是这方面确实涉及到了一些数学的知识

还是记忆下:

left:在数组中,就代表0

right:数组的长度-1

findVal:要查找的值

left + (right - left) x (findVal - arr[left]) / (arr[right] - arr[left])


比如现在有一个数组:int[] arr = {1,2,3,4,….,100};

那么假如我们要查询1的下标,那么按照插值查找的公式应当是:0 + (99 - 0) x (1 - 0) / (99 - 0) = 0

一下子就找到了


所以,插值算法最重要的就是它的自适应,在数字有序排列并且数据量十分多时,插值查找的优势巨大

代码实现

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
/**
* 插值算法
*
* @param arr 要去寻找的数组
* @param left 左边的索引
* @param right 右边的索引
* @param findVal 要查找的值
* @return
*/
public static int interpolationSearch(int[] arr, int left, int right, int findVal) {
/*
假如左边的指针比右边的指针大,那么说明没有找到最终的值,返回-1即可
*/
if (left > right) {
return -1;
}

/*
假如还在寻找的过程中,那么有如下三种情况:
*/

//中间索引a
int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
//中间值
int midVal = arr[mid];

//1、假如要查找的值>中间值,那么进行向右递归
if (findVal > midVal) {
return binarySeach(arr, mid + 1, right, findVal);
}
//2、假如要查找的治<中间值,那么进行向左递归
if (findVal < midVal) {
return binarySeach(arr, left, mid - 1, findVal);
}

//3、剩下的一种情况就说明已经找到了对应的值,结束递归,返回对应的索引
return mid;
}

基本上和二分查找差不多,唯一的区别就是mid的计算


斐波那契(黄金分割)查找

斐波那契查找概述

概念介绍

1、黄金分割点

把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比,取其三位数字的近似值为0.618,这个数值就是黄金分割点。

由于按此比例设计的造型十分美丽,因此成为黄金分割,也称为中外比。这是一个神奇的数字,会带来意想不到的效果。

2、斐波那契数列

斐波那契数列是这样的:{1,1,2,3,5,8,13,21,34,55}

其中除了前两位之外,后面的每一个值都是前面两个值相加得到的结果

斐波那契数列到了最后的数值,两个相邻相邻的数值的比值,其实就无限接近于黄金分割点0.618

3、在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2)(n>=2),……

image-20201219205338238

斐波那契查找的原理

我们刚才讲到,在数学中的定义F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2)(n>=2)

但是我们知道,在Java中数组的下标是以0开始的,所以根据 F(k) = F(k-1) + F(k-2) 得到:

F(k)-1 = (F(k-1)-1) + (F(k-2)-1) + 1,这样所有的下标都减去了1,符合了程序的定义

image-20201219205702147

其实mid在黄金分割段附近,但是注意,它的值不是F(k-1)-1,我们将它定义为low + F(k-1)-1

也就是说,mid = low + F(k-1)-1,其中low为最左端的值

但是现在有一个问题:假如我们的长度不够了怎么办?比如斐波那契需要长度为8的线段,然而我们只有7个或者更少,那么我们采取的解决措施是:使用最后一个的数值去填充到线段的后半部分,如下:

image-20201220103359018

斐波那契查找举例

假如我们现在有一个数组:*int[] arr = {1,2,3,4,5,6,7,8,9,10}*,假如我们要查询9,下面的步骤应当是这样的:

1、因为数组长度为10,但是斐波那契数列中没有10这个数字,所以补充到13

image-20201220104811808

2、阶段2,找到第一个mid节点,然后数值与9比较

image-20201220104735165

3、因为数值比9小,那么向右递归,寻找第二个mid节点

image-20201220110546276

4、同理,向左递归,直到找到最终数值

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 斐波那契数列的生成
*
* @param length 需要的斐波那契数列的长度
* @return 返回一个斐波那契数列
*/
public static int[] fibonacci(int length) {

int[] fibonacciArr = new int[length];

//斐波那契数列的前两位是1
fibonacciArr[0] = 1;
fibonacciArr[1] = 1;

//从第三位开始填充,直到填充到最后一位
for (int i = 2; i < fibonacciArr.length - 1; i++) {
fibonacciArr[i] = fibonacciArr[i - 1] + fibonacciArr[i - 2];
}

return fibonacciArr;
}

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
77
78
79
/**
* 斐波那契查找
*
* @param arr 要查询的数组
* @param fibonacci fibonacci 需要一个斐波那契数列,根据这个进行搜索
* @param val 要查询的值
* @return 返回的数组下标
*/
public static int fibonacciSearch(int[] arr, int[] fibonacci, int val) {


int low = 0;
int high = arr.length - 1;

//定义斐波那契数组的下标,暂定为0
int k = 0;

//表示mid节点
int mid = 0;

//因为我们的数组长度不一定符合斐波那契的规则,所以我们要获取能符合斐波那契规则的长度
while (arr.length > fibonacci[k]) {
k++;
}
//我们构造一个新的数组,让超出的部分的值为arr最后的值
int[] temp = Arrays.copyOf(arr, fibonacci[k]);
for (int i = arr.length; i < temp.length; i++) {
temp[i] = arr[high];
}

/*
使用while循环来处理数据
只要low<=high并且没有找到数据退出,就说明还有数据可以寻找
只要low>high,那么说明全部的数据都找完了,也没有找到对应的数据
*/
while (low <= high) {
//给mid赋值
mid = low + fibonacci[k - 1] - 1;

//假如val小于中间值,那么说明要寻找的数字在左边,那么向左寻找
if (val < temp[mid]) {
//令右边的界限到达mid的前一位,说明要从low-high去查找
high = mid - 1;
/*
我们之前说 F(k)-1 = (F(k-1)-1) + (F(k-2)-2) + 1
斐波那契数列是 1,1,2,3,5,8,13,...
现在界限改变了,长度从13改为了8,那么k肯定也需要改变一下了
比如以前F(13)对应的下标是6,那么现在换为了F(8)下标肯定是5了
*/
k--;
continue;
}
//假如val大于中间值,那么说明要寻找的数字在右边,那么向右寻找
if (val > temp[mid]) {
//左边的界限从刚才的界限到达了mid节点的下一位
low = mid + 1;
/*
k = k - 2其实也是一眼的,还是按照上面的13拆分开始讲
13拆分为8和5,左边是8,右边肯定就是5,那么就是k-2了
*/
k -= 2;
continue;
}

//假如val等于中间值,那么就说明要寻找的数字找到了,那么返回下标即可
if (val == temp[mid]) {
/*
但是不要着急,因为数组是经过补长的
因为我们的high就是数组的长度,所以可以根据high来判断mid是否超过了原数组的下标
假如mid超过了原数组的下标,那么因为mid下标的值等于high下标的值,直接返回high即可
假如mid没有超过原数组的下标,那么直接返回mid即可
*/
return mid < high ? mid : high;
}
}

//假如一次while循环全都找不到,那么说明这个数值根本不存在
return -1;
}

斐波那契查找的前提是,一个数组应该是一个有序数组