四、查找算法
1、顺序(线性)查找
(1)什么是 线性查找?
最简单直接的一种查找方式,基本思想是 对于待查找数据 key, 从数据的第一个记录开始,逐个 与 key 比较,若存在与 key 相同的值则查找成功,若不存在则查找失败。
(2)代码实现
【代码实现:】
package com.lyh.search;
public class LinearSearch {
public static void main(String[] args) {
int[] arrays = new int[]{100, 40, 78, 24, 10, 16};
int key = 10;
int index = linearSearch(arrays, key);
if (index != -1) {
System.out.println("查找成功,下标为: " + index);
} else {
System.out.println("查找失败");
}
}
/**
* 顺序查找,返回元素下标
* @param arrays 待查找数组
* @param key 待查找数据
* @return 查找失败返回 -1,查找成功返回 0 ~ n-1
*/
public static int linearSearch(int[] arrays, int key) {
// 遍历数组,挨个匹配
for (int i = 0; i < arrays.length; i++) {
if (arrays[i] == key) {
return i;
}
}
return -1;
}
}
【输出结果:】
查找成功,下标为: 4
(3)分析
顺序查找效率是比较低的,n 个数据最坏情况下需要比较 n 次,即时间复杂度为 O(n)。
2、二分(折半)查找
(1)什么是 折半查找?
是一个效率较高的查找方法。其要求必须采用 顺序存储结构 且 存储数据有序。
【基本实现思路:】
Step1:确定数组的中间下标。 middle = (left + right) / 2。将数据 分为左右两部分。
Step2:将待查找数据 key 与 中间元素 arrays[middle] 比较。
Step2.1:如果 key > arrays[middle],则说明要查找数据在 middle 下标右侧,需要在右侧数据进行查找(递归)。
Step2.2:如果 key < arrays[middle],则说明要查找数据在 middle 下标左侧,需要在左侧数据进行查找(递归)。
Step2.3:如果 key == arrays[middle],则说明查找成功。
上面递归结束条件:
查找成功,结束递归。
查找失败,即 left > right 时,退出递归。
【举例:】
在 {13, 27, 38, 49, 65, 76, 97} 中查找 key = 27。
第一次折半:
left = 0, right = 6, middle = 3
即 a[left] = 13, a[right] = 97, a[middle] = 49。
由于待查找数据 key < a[middle],则从左侧剩余数据 {13, 27, 38, 49} 开始查找。
第二次折半:
left = 0, right = 2, middle = 1
即 a[left] = 0, a[right] = 38, a[middle] = 27。
由于待查找数据 key == a[middle],则查找成功。
(2)代码实现
【代码实现:】
package com.lyh.search;
public class BinarySearch {
public static void main(String[] args) {
int[] arrays = new int[]{13, 27, 38, 49, 65, 76, 97};
int key = 27;
int index = binarySearch(arrays, 0, arrays.length - 1, key);
if (index != -1) {
System.out.println("查找成功,下标为: " + index);
} else {
System.out.println("查找失败");
}
}
/**
* 折半查找,返回元素下标
* @param arrays 待查找数组
* @param left 最左侧下标
* @param right 最右侧下标
* @param key 待查找数据
* @return 查找失败返回 -1,查找成功返回元素下标 0 ~ n
*/
public static int binarySearch(int[] arrays, int left, int right, int key) {
// 若 left > right,则表示查找失败
if (left <= right) {
// 获取中间下标
int middle = (left + right) / 2;
if (key == arrays[middle]) {
return middle;
} else if (key > arrays[middle]) {
return binarySearch(arrays, middle + 1, right, key);
} else {
return binarySearch(arrays, left, middle - 1, key);
}
}
return -1;
}
}
【输出结果:】
查找成功,下标为: 1
(3)分析:
每次查找数据均折半,设折半次数为 x,则 2^x = n,即折半次数为 x = logn,时间复杂度为 O(logn)。效率比顺序查找高。
3、插值查找
(1)什么是插值查找?
插值查找类似于 折半查找,其区别在于 中间节点 是自适应的。
采用自适应节点是为了 使 middle 值更靠近 key,从而 减少 key 比较次数。
【插值查找、折半查找区别:】
折半查找 求 middle:
middle = (left + right) / 2 = left + (right - left) / 2.
插值查找 求 middle:
middle = left + (right - left) * (key - a[left]) / (a[right] - a[left]).
即 使用 (key - a[left]) / (a[right] - a[left]) 去替换 1 / 2,可以在某种情况下提高查找效率。
对于数据量较大、且数据分布较均匀的 数据来说,使用 插值查找 速度较快(较少比较次数)。
注:
除法可能会遇到异常(java.lang.ArithmeticException: / by zero)。
【举例:】
对于 0 ~ 99 的数,查找 27,
若采用 折半查找,需要折半 5 次。
若采用 插值查找,需要折半 1 次。
(2)代码实现
【代码实现:】
package com.lyh.search;
public class InsertionSearch {
public static void main(String[] args) {
int[] arrays = new int[100];
for(int i = 0; i < arrays.length; i++) {
arrays[i] = i;
}
int key = 27;
int index = insertionSearch(arrays, 0, arrays.length - 1, key);
if (index != -1) {
System.out.println("查找成功,下标为: " + index);
} else {
System.out.println("查找失败");
}
}
/**
* 插值查找,返回元素下标
* @param arrays 待查找数组
* @param left 最左侧下标
* @param right 最右侧下标
* @param key 待查找数据
* @return 查找失败返回 -1,查找成功返回元素下标 0 ~ n
*/
public static int insertionSearch(int[] arrays, int left, int right, int key) {
// 数据不符合时,退出递归
if (left > right || key > arrays[right] || key < arrays[left]) {
return -1;
}
// 自适应节点
int middle = left + (right - left) * (key - arrays[left]) / (arrays[right] - arrays[left]);
if (key == arrays[middle]) {
return middle;
} else if (key > arrays[middle]) {
return insertionSearch(arrays, middle + 1, right, key);
} else {
return insertionSearch(arrays, left, middle + 1, key);
}
}
}
【输出结果:】
查找成功,下标为: 27
4、斐波那契(黄金分割)查找
(1)什么是 斐波那契 查找?
斐波那契查找 与 折半查找、插值查找 类似,都是改变中间节点的位置。
此时的 中间节点 位于 黄金分割点 附近。
【黄金分割比例:】
黄金分割比例指 将一个整体分为两个部分,其中 较小部分 : 较大部分 = 较大部分 : 整体,且值约为 0.618,此比例称为黄金分割比例。
比如:
1 米长绳子,分为 0.618 与 0.382 两部分,则 0.382 :0.618 == 0.618 :1。
【斐波那契数列:】
斐波那契公式:
f(1) = 1;
f(2) = 1;
f(n) = f(n - 1) + f(n - 2); n > 2
即:数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...}
斐波那契数列两个相邻数的比例,近似于 0.618。
比如:
21 : 34 == 0.6176470588235294 : 1
34 : 55 == 0.6181818181818182 : 1
【斐波那契查找算法原理:】
如何求黄金分割点:
由于斐波那契数列公式为 f(n) = f(n - 1) + f(n - 2), 且 f(n -1 ) : f(n - 2) == 0.618。
想要使用 斐波那契处理 数据,即将数据按照 f(n-1) 与 f(n-2) 分成两部分即可。
比如:f(n) - 1 = (f(n - 1) - 1) + (f(n - 2) - 1 + 1); 分为 (f(n - 1) - 1) 与 (f(n - 2) - 1 + 1) 两部分。
此时 left + (f(n - 1) - 1) 即为黄金分割点 middle。
斐波那契查找:
其将一组数据长度 看成是 斐波那契数列 进行处理,若当前数据长度 不满足 斐波那契数列,则使用 最后一个元素将其补齐。
长度符合后,记新数组为 temp,根据 middle 计算出中间节点,并进行判断。
若 key > temp[middle],则需要在右侧进行递归判断,而此时右侧属于 f(n - 2) 部分,即 k = k - 2;
若 key < temp[middle],则需要在左侧进行递归判断,而此时左侧属于 f(n - 1) 部分,即 k = k - 1;
若 key == temp[middle],则查找成功。
【举例:】
在 {13, 27, 38, 49, 65, 76, 97} 中查找 key = 27。
Step1:补齐数据。
当前数据 arrays 长度为 7,而与之相近的斐波那契数列值为 8(f(n) = 8, n = 5),需要将其补齐。
即数据变为 temp = {13, 27, 38, 49, 65, 76, 97, 97}.
Step2:开始第一次查找操作,中间节点 middle = left + (f(n - 1) - 1)
left = 0,right = 7,n = 5,middle = 4,
key < temp[middle],即下次在左侧 {13, 27, 38, 49} 进行查找(right = middle - 1 = 3)。
左侧部分等同于 f(n - 1) 区,所以 n 减 1, 即 n = 4。
Step3:开始第二次查找,
left = 0, right = 3,n = 4, middle = 2
key < temp[middle],即下次在左侧 {13, 27} 进行查找(right = middle - 1 = 1)。
左侧部分等同于 f(n - 1) 区,所以 n 减 1, 即 n = 3。
Step3:开始第三次查找,
left = 0, right = 1,n = 3, middle = 1
key == temp[middle],查找成功。
(2)代码实现
【代码实现:】
package com.lyh.search;
import java.util.Arrays;
public class FibonacciSearch {
public static void main(String[] args) {
int[] arrays = new int[]{13, 27, 38, 49, 65, 76, 97};
int key = 27;
int index = fibonacciSearch(arrays, key);
if (index != -1) {
System.out.println("查找成功,当前下标为: " + index);
} else {
System.out.println("查找失败");
}
}
/**
* 返回斐波那契数组(使用 迭代 实现)
* @return 斐波那契数组
*/
public static int[] fibonacci(int length) {
if (length < 0) {
return null;
}
int[] fib = new int[length];
if (length >= 1) {
fib[0] = 1;
}
if (length >= 2) {
fib[1] = 1;
}
for (int i = 2; i < length; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
/**
* 斐波那契查找,返回对应数据下标
* 将数据长度看成 斐波那契数列,将数据分为 f(n - 1)、 f(n - 2) 两部分
* @param arrays 待查找数组
* @param key 待查找数据
* @return 查找失败返回 -1,查找成功返回相应的下标
*/
public static int fibonacciSearch(int[] arrays, int key) {
int n = 0; // 用于记录当前 分隔点 下标
int[] fibs = fibonacci(arrays.length); // 用于记录斐波那契数列
// 获取第一次分割点下标
while (arrays.length > fibs[n]) {
n++;
}
// 若当前数组长度 不满足 斐波那契数列,则使用最后一个元素 去填充新数组,使新长度满足 斐波那契数列
int[] temp = Arrays.copyOf(arrays, fibs[n]);
for (int i = arrays.length; i < fibs[n]; i++) {
temp[i] = arrays[arrays.length - 1];
}
// 开始查找
int left = 0;
int right = temp.length - 1;
while(left <= right) {
// 获取中间节点,将数据区域分为 f(n - 1), f(n - 2) 两部分
int middle = left + fibs[n - 1] - 1;
if (key == temp[middle]) {
// 查找成功
return middle;
} else if (key > temp[middle]) {
// 当前查找失败,下次在右侧 f(n - 2) 区域进行查找
left = middle + 1;
n -= 2;
} else {
// 当前查找失败,下次在左侧 f(n - 1) 区域进行查找
right = middle - 1;
n -= 1;
}
}
// 查找失败,即 left > right,返回 -1
return -1;
}
}
【输出结果:】
查找成功,当前下标为: 1