Tips: 采用java语言,关注博主,底部附有完整代码
工具:IDEA
本篇介绍查找算法:
- 线性查找
- 二分查找(寻找单个元素)
- 二分查找(寻找所有元素)
- 插值查找
- 斐波那契查找
名称 | 效果图 |
---|---|
线性查找 | ![]() |
二分查找 | ![]() |
插值查找 | ![]() |
斐波那契数列 | ![]() |
斐波那契查找 | ![]() |
线性查找算法
重点:
- 从头到尾一个一个循环
线性查找比较简单,就算刚入门也应该会,直接看代码了!
流程图
完整代码
# 线性查找
/*
* @param ints: 原始数据
* @param findValue: 查找元素
* return -1未找到
*/
public static int findIndex(int[] ints, int findValue) {
for (int i = 0; i < ints.length; i++) {
if (findValue == ints[i]) {
return i;
}
}
return -1;
}
使用:
int[] ints = new int[]{12, 4, 16, 1, 20, 21, 5, 8, 10, 3};
int index = findIndex(ints, 20);
if (index == -1) {
System.out.println("没有找到元素");
} else {
System.out.printf("找到元素下标为=%d\n", index);
}
优缺点分析
优点:
- 简单
- 无需排序,从头到位一个一个判断
缺点:
- 从头到位一个一个判断,效率低
二分查找(寻找单个元素)
重点:
-
寻找数组必须有序
-
一半一半的找
-
指针记录
指针记录
这里的指针和C中的指针不同
这里的指针可以理解为下标,就是找一个临时的数来记录当前位置
二分查找只需要3个指针
- 开始指针
- 结束指针
- 中间指针
每次值和中间位置
做比较
如果需要寻找的元素findValue
> 中间位置 ,说明 findValue
在中间指针右边
所以需要将 开始指针 = 中间指针 + 1
如果 findValue
< 中间位置 ,说明findValue
在中间指针左侧
所以需要将 结束指针 = 中间指针
最终需要找的元素和当前元素相同时候,说明找到了该元素,返回下标即可!
流程图
完整代码
方式一:
递归法:
/**
* @param ints[]: 需要查找的数组
* @param start: 开始位置
* @param end: 结束位置
* @param findValue:需要找的元素
* return -1:没有找到元素
*/
public static int findIndex(int[] ints, int start, int end, int findValue) {
System.out.println("方式一");
// 如果左侧指针 > 右侧指针,说明开始位置已经超出结束位置 说明没找到元素 返回-1即可
if (start > end) {
return -1;
}
// 中间位置
int middle = (end + start) / 2;
System.out.printf("开始位置=%d\t结束位置=%d\t居中元素位置=%d\n", ints[start], ints[end], ints[middle]);
// 查找元素 > 中间位置
if (findValue > ints[middle]) {
return findIndex(ints, middle + 1, end, findValue);
} else if (findValue < ints[middle]) {
// 查找元素 < 中间位置
return findIndex(ints, start, middle, findValue);
} else {
return middle;
}
}
方式二:
非递归法:
/**
* @param ints[]: 需要找的数组
* @param findValue: 需要找的元素
* return -1:没有找到
*/
public static int findIndex2(int[] ints, int findValue) {
// 开始指针
int start = 0;
// 结束指针
int end = ints.length - 1;
while (true) {
if (start > end) {
return -1;
}
// 中间元素
int middle = (end + start) / 2;
System.out.printf("开始位置=%d\t结束位置=%d\t居中元素位置=%d\n", ints[start], ints[end], ints[middle]);
// 当前元素 > middle 元素
if (findValue < ints[middle]) {
// 向左找元素
end = middle;
} else if (findValue > ints[middle]) {
start = middle + 1;
}
if (ints[middle] == findValue) {
return middle;
}
}
}
使用:
int[] ints = new int[]{2, 6, 17, 23, 42, 44, 51, 55, 75, 242, 253, 332, 432, 542, 638, 761};
// 方式一 递归法
int index = findIndex(ints, 0, ints.length - 1, 253);
// 方式二 非递归法
// int index = findIndex2(ints, 332);
if (index == -1) {
System.out.println("没有找到元素");
} else {
System.out.printf("找到元素下标为:%d\t值为=%d\n", index, ints[index]);
}
这两种方式思想都是一样的!
优缺点分析
优点:
- 正常情况(99%)下比线性查找找的快,特别是数据量非常大的情况下,效果会更加明显
缺点:
- 需要数组是有序的
- 中间指针(mid)不够灵性,没次都是分一半来找
中间指针不够灵性,那么就引出了接下来要介绍的插值查找 和 斐波那契查找了!
二分查找(寻找所有元素)
技巧:
根据普通二分查找,先找到单个元素,然后向前遍历,和向后遍历,吧元素相同的元素都记录起来即可
因为二分查找的数组是有序的,所以需要找的元素,在数组中一定是连续的!
例如:
完整代码
public static ArrayList<Integer> findIndex(int[] ints, int findValue) {
// 用来存储找到的元素
ArrayList<Integer> temp = new ArrayList<>();
int start = 0;
int end = ints.length - 1;
while (true) {
if (start > end) {
return temp;
}
// 中间元素
int middle = (end + start) / 2;
System.out.printf("开始位置=%d\t结束位置=%d\t居中元素位置=%d\n", ints[start], ints[end], ints[middle]);
// 当前元素 > middle 元素
if (findValue < ints[middle]) {
// 向左找元素
end = middle;
} else if (findValue > ints[middle]) {
start = middle + 1;
}
// 找到元素
if (ints[middle] == findValue) {
int tempIndex = middle;
System.out.println("率先找到的位置为" + middle);
// 循环之前的元素
while (true) {
// 保证tempIndex在数组中 并且当前元素 == 寻找的元素
if (tempIndex >= 0 && ints[tempIndex] == findValue) {
temp.add(tempIndex);
System.out.println("之前的元素 = " + tempIndex);
tempIndex--;
} else {
// 如果上一个元素 和findValue不想等,那么就退出循环
break;
}
}
tempIndex = middle;
// 循环之后的元素
while (true) {
tempIndex++;
// 保证在数组中 并且 当前元素 == 寻找的元素
if (tempIndex <= ints.length - 1 && ints[tempIndex] == findValue) {
temp.add(tempIndex);
System.out.println("之后的元素 = " + tempIndex);
} else {
// 如果下一个元素 和findValue不想等,那么就退出循环
break;
}
}
return temp;
}
}
}
调用:
int[] ints = new int[]{1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5};
int findValue = 4;
ArrayList<Integer> temp = findIndex(ints, findValue);
if (temp.size() == 0) {
System.out.println("没有找到元素");
} else {
System.out.printf("需要找的元素为%d\t找到元素下标集合为:%s\n", findValue, temp);
}
调用结果:
开始位置=1 结束位置=5 居中元素位置=4
率先找到的位置为7
之前的元素 = 7
之前的元素 = 6
之后的元素 = 8
之后的元素 = 9
需要找的元素为4 找到元素下标集合为:[7, 6, 8, 9]
这段代码也比较简单,只是在找到元素中向前遍历,和向后遍历,找到相同的元素返回即可!
插值查找
重点:
- 步长几乎一样
上面二分查找也说到了他的缺点, 中间指针(mid)不够灵性,每次都切一半来找位置
插值查找是对二分查找的一次小升级,升级了中间指针的变化,不过只有在特定场景能用,并不适合所有的场景!
步长几乎一样
在插值查找中,不仅需要查找数组有序,而且需要每个元素的步长几乎一样使用起来才比较快
什么叫步长几乎一样? 什么叫步长?
步长:
假设当前寻找数组为 [1,2,3,4,5,6,7,8,9,10] 可以看出每个元素仅隔1位,所以说他的步长为1
假设当前寻找数组为 [6, 12, 18, 19, 22, 24, 30, 36, 42, 43, 44, 46, 48, 54, 60]每个元素隔6位,那么他的步长为6
理想状态
先来看步长相同的理想状态:
首先可以获取到48在整个数组中的占比:
- A = findValue - ints[start]
- B = ints[end] - ints[start]
48在整个数组中的占比为 = A / B
数组的长度为 end - start
所以需要找的元素的下标就可以得到公式(end - start) * A / B
带入完整公式:
mid = (end - start) * (findValue - ints[start]) / (ints[end] - ints[start])
带入数据:
mid = (9 - 0) * (48 - 6) / (60 - 6)
= 9 * 42 / 54
= 7
完整效果图:
代码和二分查找代码完全类似
只需要吧之前 mid = (end - start) / 2 改为
mid = (end - start) * (findValue - ints[start]) / (ints[end] - ints[start]) 就是插值排序!
这样一来,只需要一次寻找,就能快速找到想要的下标
但是,但是,但是这是理想状态,俗话说理想很丰满,现实很骨感
不理想状态
假如数据没有这么完整,数据是长这样呢:
可以看出,这样一来,步长完全不规律,再次带入刚才的公式试试能否找到具体值
刚才公式 mid = (end - start) * (findValue - ints[start]) / (ints[end] - ints[start])
带入数据:
mid = (14 - 0) * (48 - 6) / (60 - 6)
= 14 * 42 / 54
= 10
可以看出,虽然没有一次性找到想要的值,找到的是下标10,但是已经和预想结果很相近了
此时状态变成了这样:
此时 start = mid + 1
再次带入刚才的公式:
mid = (end - start) * (findValue - ints[start]) / (ints[end] - ints[start])
mid = (14 - 11) * (48 - 46) / (60 - 46)
= 3 * 2 / 14
= 0
可以看出,现在已经出错了,mid已经不在start 和 end之间了,这样一来就会导致无限的循环下去…
再次优化一下插值排序
优化前:
mid = (end - start) * (findValue - ints[start]) / (ints[end] - ints[start])
优化后:
mid =start + (end - start) * (findValue - ints[start]) / (ints[end] - ints[start])
这样一来,就不会一直无限循环下去了
来看看完整不理想状态
流程图:
优化后理想状态流程图:
完整代码
public static int findValue(int[] ints, int value) {
// 开始位置
int start = 0;
// 结束位置
int end = ints.length - 1;
// 插值位置
int mid = 0;
while (true) {
// 如果开始位置 = 结束位置 那么就结束循环
if (start > end) {
break;
}
// (value - ints[start]) / (ints[end] - ints[start]) 找到值在数组中的大致位置
// (end - start) 获取到查找的长度
mid = start + (end - start) * (value - ints[start]) / (ints[end] - ints[start]);
System.out.printf("start = %d\tend = %d\tmid = %d\n", start, end, mid);
// value < 当前中间位置
if (value < ints[mid]) {
end = mid - 1;
} else if (value > ints[mid]) {
start = mid + 1;
} else {
// 找到元素
return mid;
}
// 如果开始位置 和找的元素相等 那么就返回开始位置
if (value == ints[start]) {
return start;
}
// 如果结束位置 和找的元素相等 那么就返回结束位置
if (value == ints[end]) {
return end;
}
}
return -1;
}
调用代码:
int[] ints = new int[]{6, 12, 18, 19, 22, 24, 30, 36, 42, 43, 44, 46, 48, 54, 60};
int index = findValue(ints, 48);
if (index != -1)
System.out.printf("当前找到的下标为%d\t元素为%d\n", index, ints[index]);
优缺点分析:
优点:
- 如果在数组步长完全一致的情况下可以通过一次循环就找到想要的下标
缺点:
- 自认为没啥用… 现实情况绝对比这个复杂,而且不可能这么完美的步长相同
- 感觉不如普通二分查找…
- 如果第一次没有找到,那么mid只能龟速移动.
- 不建议使用.
斐波那契查找
前沿: 斐波那契算法也是对二分查找的升级版本
- 二分查找: 每隔一半开始找
- 斐波那契: 找到完美分割点来进行查找
斐波那契数组:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, …]
特性:
-
fib[0] = 1
-
fib[1] = 1
-
fib[2] = fib[1] + fib[0]
-
fib[3] = fib[2] + fib[1]
-
fib[4] = fib[3] + fib[2]
-
…
来看看效果图:
得出公式: fib[n] = fib[n-1] + fib[n - 2]
由此可以得出: 假设当前数组长度为13, 在斐波那契数列中对应的是下标7
那么当前就可以通过 fib[n] = fib[n-1] + fib[n - 2]
分割为
- fib[n - 1] = 8
- fib[n - 2] = 5
fib[n - 1] 就是 0-7的元素
fib[n - 2] 就是8-12的元素
所以mid 就是
mid = fib[n-1] - 1
= 8 - 1
= 7
思路
斐波那契数组为: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, …]
-
首先创建一个斐波那契数列,用来寻找完美分割点
-
找到需要查找数组长度对应的斐波那契值
例如当前数组长度为9,那么对应的斐波那契值就是13
例如当前数组长度为22, 那么对应的斐波那契子就是34
-
创建一个临时数组,将当前数组的长度,转变为斐波那契找对应值的长度,临时数组不足位补最后一位
假设当前数组值为[2, 6, 17, 23, 42, 44, 51, 55, 75] length = 9
那么转变后的数组为:[2, 6, 17, 23, 42, 44, 51, 55, 75, 75, 75, 75] length = 12
这样做的目的是为了可以吧数组分为2部分
- start 至 mid (0至7)
- mid 至 end (8至11)
所以由得出
start = 0 end = 11 mid = fib[n - 1] - 1 = 8 - 1 = 7
-
**如果查找的值在mid左侧,**并且没有找到,那么指向斐波那契的指针就 减1
这里为什么要减1?
还是按照上面的例子,当前数组长度为[2, 6, 17, 23, 42, 44, 51, 55, 75, 75, 75, 75] length = 12, 需要找的元素为 23
-
start = 0 value = 2
-
end = 11 value = 75
-
mid = 7 value = 55
那么第一次找值的时候,23 < mid (55),第二次的时候就变成了
- start = 0 value = 2
- end = 6 value = 51
此时范围缩小到了0-7,对应的斐波那契至为8,下标为5
斐波那契数值8和下标为5,又可以通过
fib[k] = fib[k-1] + fib[k-2]
得知- fib[k-1] = 5
- fib[k - 2] = 3
所以第二次的黄金分割点就是fib[k-1] -1,就是 5-1 = 4
-
-
如果查找的值在mid右侧,并且没有找到,那么只想斐波那契的指针就 减2
这里减2和上面减1是相同的道理,就不过多赘述
效果图:
斐波那契数组为: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, …]
小结:
斐波那契数组中的每个元素就是一个长度,以34来举例,假设当前查找的数组ints长度, 21 < ints.length < 34
那么就让他的长度转变为34
那么第一次找值的mid(黄金分割点)就是21 - 1,这里减1是因为从0开始计算
如果没有找到
- findValue > ints[mid] 向右查找 并且记录斐波那契的指针 减2
- findValue < ints[mid] 向左查找 并且记录的斐波那契指针 减1
依次递归,如果确实数组中有需要寻找的值,那么就可以找到!
本质就是二分查找的逻辑,只是采用斐波那契数列的特性,改变了mid的值!
完整代码
# 斐波那契
/**
* @param ints 查找的数组
* @param value 需要查找的元素
*/
public static int findValue(int[] ints, int value) {
// 开始位置
int start = 0;
// 结束位置
int end = ints.length - 1;
// 黄金分割点位置
int mid = 0;
// 斐波那契数列
int[] fib = fib();
System.out.println("获取到的数据为:" + Arrays.toString(fib()));
// 表示斐波那契分隔下标
int fibIndex = 0;
// end 数组长度
// fib[fibIndex] -1 = 斐波那契具体值
// 因为是从0 开始计算 所以 fib[fibIndex] -1
while (end > fib[fibIndex] - 1) {
fibIndex++;
}
// ints = 当前数组
// fib[fibIndex] = 拷贝长度
// 给ints 扩容出一个新的数组 用来进行二分查找
int[] temp = Arrays.copyOf(ints, fib[fibIndex] - 1);
for (int i = end; i < temp.length; i++) {
// 将int最后一个值 赋值给temp
temp[i] = ints[ints.length - 1];
}
System.out.printf("temp[] = %s\tlength = %d\n", Arrays.toString(temp), temp.length);
// 如果开始位置 <= 结束位置
while (start <= end) {
// mid = 中间位置
if (fibIndex >= 1) {
mid = start + fib[fibIndex - 1] - 1;
} else {
mid = (end + start) / 2;
}
System.out.printf("此时start = %d\tend = %d\tmid = %d\tfibIndex = %d\tmidValue = %d\n", start, end, mid, fibIndex, temp[mid]);
// 当前值 < mid 说明 在左侧
if (value < temp[mid]) {
end = mid;
fibIndex -= 1;
// System.out.println("fibIndex减1 = " + fibIndex);
} else if (value > temp[mid]) {
// 当前值 > mid 说明在右侧
start = mid + 1;
fibIndex -= 2;
// System.out.println("fibIndex减2 = " + fibIndex);
} else {
// 防止下标越界
if (mid > ints.length - 1) {
return ints.length - 1;
}
return mid;
}
// 如果搜索值 = 当前结束位置 那么就直接返回结束位置
if (value == temp[end]) {
return end;
}
// 如果搜索值 = 开始位置 那么就直接返回开始位置
if (value == temp[start]) {
return start;
}
}
return -1;
}
// 斐波那契数列
public static int[] fib() {
int[] temp = new int[mMaxSize];
temp[0] = 1;
temp[1] = 1;
for (int i = 2; i < mMaxSize; i++) {
temp[i] = temp[i - 1] + temp[i - 2];
}
return temp;
}
调用:
# 斐波那契
System.out.println("斐波那契查找");
int[] ints = new int[]{2, 6, 17, 23, 42, 44, 51, 55, 75};
int index = findValue(ints, 23);
if (index != -1)
System.out.printf("查询的结果下标为:%d\t具体值 = %d\n", index, ints[index]);
else
System.out.println("没找到该元素");
猜你喜欢:
原创不易,您的点赞就是对我最大的支持!