查找算法
- 前言
- 顺序查找
- 原理
- 流程分析
- 代码实现
- 对分查找
- 原理
- 流程分析
- 代码实现
- 插值查找
- 原理
- 流程分析
- 代码实现
- 斐波那契查找
- 原理
- 流程分析
- 代码实现
前言
查找在实际的工作中不可谓不重要,从大量的数据中找到自己需要的数据肯定不能是数数一样一个一个来,有效的查找算法可以在实际运用中大大加快工作效率。
本人知识水平有限,文章表述可能存在些许不足,欢迎评论区指出相互讨论学习。
查找算法按照表的功能分为静态查找和动态查找(查找的表是否存在插入和删除),按照表内元素的顺序分为有序查找和无序查找。
文章开头先贴出项目的代码结构:
public class MySeek {
public static void main(String[] args) {
int[] list1 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] list2 = { 6, 3, 2, 8, 5, 9, 0, 1, 7, 4 };
writeSeek(list1, 9, true);
writeSeek(list2, 3, false);
}
//打印排序结果
public static void writeSeek(int[] li, int count, boolean isOrder) {
System.out.print("原序列:[ ");
for (int i : li) {
System.out.print(i + " ");
}
System.out.println("]");
order(li, count);
//有序序列可使用有序查找
if (isOrder) {
binary(li, count);
insert(li, count);
fibonacci(li, count);
}
}
}
顺序查找
原理
顺序查找是最简单的查找算法,直接从头到尾找到结束,也不用管动态静态有序无序的。
流程分析
查找数 | 查找表 |
3 | 0 1 2 3 4 |
3 | 0 1 2 3 4 |
3 | 0 1 2 3 4 |
3 | 0 1 2 3 4 |
代码实现
/*
* 顺序查找
* 时间复杂度:O(n)
*/
public static void order(int[] li, int count) {
for (int i = 0; i < li.length; i++) {
if (li[i] == count) {
System.out.println("顺序查找:已找到" + count + ",位置是" + (i + 1));
return;
}
}
System.out.println("顺序查找:查找失败,序列中没有" + count);
}
对分查找
原理
通俗易懂的顺序查找在大数量级的数据中显得有些力不从心,于是就有了对分查找——在有序序列中找一个标记点,根据和标记点的大小比较每次就可以减少一半的比较量,从而得到了O(logn)的时间复杂度。
对分查找的标记点是左指针和右指针之和的一半,即mid=(left+right)/2。
若查找数count>list[mid],那么就是在mid的右边,把left=mid+1即选中右半段;
若count<list[mid],那么就是在mid的左边,把right=mid-1即选中左半段。
以此类推最后得到查找结果。
流程分析
查找数 | 左指针 | 右指针 | 标记点 | 查找表 |
3 | 0 | 4 | 2 | /0 1 2 3 4/ |
3 | 3 | 4 | 3 | 0 1 2 /3 4/ |
代码实现
/*
* 对分查找
* 时间复杂度:O(logn)
*/
public static void binary(int[] li, int count) {
//左指针
int low = 0;
//右指针
int high = li.length - 1;
int middle;
while (low <= high) {
middle = (low + high) / 2;
if (count == li[middle]) {
System.out.println("对分查找:已找到" + count + ",位置是" + (middle + 1));
return;
} else if (count < li[middle]) {
high = middle - 1;
} else {
low = middle + 1;
}
}
System.out.println("对分查找:查找失败,序列中没有" + count);
}
插值查找
原理
出于算法无止境的初心,已经足够优秀的对分查找也需要得到优化。
分析对分查找的过程中发现,如果查找的数恰好贴近left/mid/right,那么每次都找中间值就显得冗余,如何更快锁定查找数所在区间就成了差值查找所要解决的问题。
关于mid的取值,对分查找中使用的是mid=(left+right)/2=left+(right-left)/2,显得有些许死板,如果查找表中元素值分布均匀,那么就可以根据count和list[mid]的差值动态调整mid的值。
插值查找用的是mid=low+(count-li[low])/(li[high]-li[low])*(high-low)。这种动态的调整可以在O(logn)的时间复杂度不变的情况下,使这个数尽量地小。
流程分析
查找数 | 左指针 | 右指针 | 标记点 | 查找表 |
2 | 0 | 9 | 2 | /0 1 2 3 4 5 6 7 8 9/ |
这就是在完全均衡情况下的插值查找,可以对比一下对分查找:
查找数 | 左指针 | 右指针 | 标记点 | 查找表 |
2 | 0 | 9 | 4 | /0 1 2 3 4 5 6 7 8 9/ |
2 | 0 | 3 | 1 | /0 1 2 3/ 4 5 6 7 8 9 |
2 | 2 | 3 | 2 | 0 1 /2 3/ 4 5 6 7 8 9 |
代码实现
/*
* 插值查找 时间复杂度:O(log(logn))
*/
public static void insert(int[] li, int count) {
//左指针
int low = 0;
//右指针
int high = li.length - 1;
int middle;
while (low <= high) {
middle = low + (count - li[low]) / (li[high] - li[low]) * (high - low);
if (count == li[middle]) {
System.out.println("插值查找:已找到" + count + ",位置是" + (middle + 1));
return;
} else if (count < li[middle]) {
high = middle - 1;
} else {
low = middle + 1;
}
}
System.out.println("插值查找:查找失败,序列中没有" + count);
}
斐波那契查找
原理
顾名思义,斐波那契查找是基于斐波那契数列对对分查找的优化。长度为fibonacci[i]的序列,标记位为fibonacci[i-1],比较后序列就分为fibonacci[i-1]和fibonacci[i-2]两段。
查找算法 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … |
斐波那契查找 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 44 | 63 | … |
对分查找 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | … |
说是优化,但我在理解这个算法的时候却越来越疑惑,对于50个数据的排序,对分查找只需6轮就可以完成排序,而斐波那契查找却需要9轮。
随着数据的增多,斐波那契需要多走的步数就更多,那谈何优化呢?
根据查阅的相关博文以及一些我较难读懂的文章,结合我个人的理解和猜测,得到了两个比较能说服我自己的结论:
1、对于CPU而言,加减和移位都是耗时极短的操作,而乘除会带来相对于加减2~3倍的耗时,在不考虑除法被优化为移位操作的情况下,斐波那契查找确实是可以以较小的额外执行轮次得到较大的效率提升。
2、直观上斐波那契查找会比对分查找多一些执行轮次,但是不均匀的序列分配也给了斐波那契查找“跨步走”的可能,综合最好情况和最坏情况,斐波那契查找的执行轮次还是可以和对分查找的执行轮次画上约等号。
随着编译器的优化和发展,高耗时的乘除运算也被编译成低耗时的移位操作。综合来看,不显著的效率优化和较长的代码段使得斐波那契查找仅仅作为了一种解决方案和优化方向。
流程分析
首先是要将被查找序列扩充为斐波那契数列长度的序列,扩充的位置全部填上最后一个元素。
查找数 | 左指针 | 右指针 | 标记点 | 查找表 |
3 | 0 | 12 | 7 | /0 1 2 3 4 5 6 7 8 9 9 9/ |
3 | 0 | 6 | 4 | /0 1 2 3 4 5 6 7/ 8 9 9 9 |
3 | 0 | 3 | 2 | /0 1 2 3/ 4 5 6 7 8 9 9 9 |
3 | 3 | 3 | 3 | 0 1 2 /3/ 4 5 6 7 8 9 9 9 |
代码实现
/*
* 斐波那契查找 时间复杂度:O(logn)
*/
public static void fibonacci(int[] li, int count) {
int length = 0;
int[] fibonacci = makeFibonacci();
while (li.length > fibonacci[length]) {
length++;
}
// 新建序列并存入
int[] newList = new int[fibonacci[length] - 1];
for (int i = 0; i < li.length; i++) {
newList[i] = li[i];
}
// 剩余填入最后一数
for (int i = newList.length - 1; i >= li.length; i--) {
newList[i] = li[li.length - 1];
}
// 开始查找
int low = 0;
int high = newList.length - 1;
int mid;
while (low <= high) {
mid = low + fibonacci[length - 1] - 1;
if (newList[mid] == count) {
System.out.println("斐波那契查找:已找到" + count + ",位置是" + (Math.min(mid, li.length - 1) + 1));
return;
} else if (newList[mid] > count) {
length = length - 1;
high = mid - 1;
} else {
length = length - 2;
low = mid + 1;
}
}
System.out.println("斐波那契查找:查找失败,序列中没有" + count);
}
// 构建斐波那契数列
public static int[] makeFibonacci() {
int[] list = new int[20];
list[0] = 1;
list[1] = 1;
for (int i = 2; i < 20; i++) {
list[i] = list[i - 1] + list[i - 2];
}
return list;
}
个人整理算法:
【算法】Java实现常用排序算法一(冒泡排序、选择排序、插入排序、堆排序、快速排序)【算法】Java实现常用排序算法二(希尔排序、归并排序、计数排序、桶排序、基数排序)