一、顺序查找(线性查找)
顺序查找,也叫线性查找。遍历数组中的所有元素,与要查找的元素比对,不要求数组有序。该算法的时间复杂度为O(n)。代码如下:
public class SequentialSearch {
public static int search(int[] array, int key){
for(int i = 0; i < array.length; i++) {
if(array[i] == key)
return i;
}
return -1;
}
public static void main(String[] args) {
int[] array = {12, 45, 67, 44, 16};
System.out.println(search(array, 16));
System.out.println(search(array, 24));
}
}
测试结果:
4
-1
二、有序表查找
1、二分查找(折半查找)
(1)算法思想:
有序的序列,每次都以序列的中间位置的数来与待查找的关键字进行比较,每次缩小一半的查找范围,直到匹配成功。
例如:将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
(2)测试代码:
public class BinarySearch {
public static int search(int[] array, int key){
int low = 0;
int high = array.length - 1;
int middle = 0;
if(key < array[low] || key > array[high] || low > high){
return -1;
}
while(low <= high){
middle = (low + high) / 2;// 有bug
// 优化1:加法变减法
//middle = low + (high - low) / 2;
// 优化2:无符号右移一位
//middle = (low + high) >>> 1;
if(key == array[middle]){
return middle;
}else if(key < array[middle]){
high = middle - 1;
}else{
low = middle + 1;
}
}
return -1;
}
public static void main(String[] args) {
int[] array = {12, 16, 44, 45, 67};
System.out.println(search(array, 45));
}
}
上述程序存在一个bug,出现在 middle = (low + high) / 2,当数组极大时,low + high 会发生溢出。一般的改进方法:将加法变成减法。如下:
middle = low + (high - low)/ 2
还有一种更高逼格的写法,使用位运算,无符号右移一位,相当于除以 2:
middle = (low + high)>>> 1
二分查找算法的时间复杂度为O(logn)。
2、斐波那契查找
(1)斐波那契数列:
又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契用递归方法定义如下:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
(2)斐波那契查找
在二分查找的基础上根据斐波那契数列进行分割,在斐波那契查找算法中,middle 的位置位于黄金分割点附近。算法思路:
- 在斐波那契数列中找一个等于或略大于查找表元素个数的数 F[k],将原查找表扩展至长度 F[k](如果需要补充元素,则重复补充查找表的最后一个元素,直到满足 F[k] 个元素);
- 对扩展后的查找表进行斐波那契分割,即将 F[k] 个元素分割为前半部分 F[k-1] 个元素,后半部分 F[k-2] 个元素,找出要查找的元素在哪一部分并递归,直到找到为止。
注意几点:
(1)斐波那契数列的性质:F[k ]= F[k-1] + F[k-2] ,由此可以得到,F[k] - 1 =(F[k-1] - 1)+(F[k-2] - 1)+1该表达式的意思是:顺序表的长度为 F[k] - 1 时(由于数组下标是从 0 开始的,所以给 F[k] 减 1),可将该顺序表分成长度分别为 F[k-1] - 1 和 F[k-2] - 1 的两段,从而黄金分割位置 middle = low + F[k-1] - 1;
(2)同样,分割的两段也可以用同样的方式再分割;
(3)顺序表的长度不一定恰好等于 F[k-1],所以需要将原顺序表长度 n 增加至 F[k-1]。因此,这里的 k 值只要使得 F[k-1] 等于或略大于 n 即可。顺序表加长后,新增位置都赋为原顺序表 high 位置的值。
代码如下:
public class FibonacciSearch {
public static final int NUM = 20;
// 生成斐波那契数列
public static int[] fibonacci(){
int[] fib = new int[NUM];
fib[0] = fib[1] = 1;
for (int i = 2; i < NUM; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
public static int search(int[] array, int key){
int low = 0;
int high = array.length - 1;
int middle = 0;
int k = 0;// 斐波那契分割数下标
int[] fib = fibonacci();// 斐波那契数列
// 获取斐波那契分割数值下标:找到数组元素个数在斐波那契数列中的位置
while(array.length > fib[k] - 1){
k++;
}
// 如果顺序表中元素个数小于fib[k],需要补充元素
int[] temp = Arrays.copyOf(array, fib[k]);
if(fib[k] > high){
for (int i = high + 1; i < fib[k]; i++) {
temp[i] = array[high];
}
}
// 开始查找
while(low <= high){
middle = low + fib[k] - 1;
if(key > temp[middle]){// 查找后半部分
low = middle + 1;
// f[k] = f[k-1] + f[k-2],查找后半部分,所以 k = k - 2,即k -= 2
k -= 2;
}else if(key < temp[middle]){
high = middle - 1;
// f[k] = f[k-1] + f[k-2],查找前半部分,所以 k = k - 1,即k--
k--;
}else{
if(middle <= high){// 找到key在顺序表中的位置,直接返回middle
return middle;
}else{// 在补充的元素中找到key,则返回high
return high;
}
}
}
return -1;
}
public static void main(String[] args) {
int[] array = {12, 16, 44, 45, 67};
System.out.println(search(array, 67));
}
}
斐波那契查找的时间复杂度还是 O(logn ),但是与二分查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比折半查找小,但是还是得视具体情况而定。
3、插值查找
插值查找与二分查找原理相似,只是对于 middle 的选择不同。插值查找根据关键字 key 与查找表中最大最小记录的关键字比较后的确定出自适应的 middle,从而缩小查找范围,其核心在于插值公式
代码如下:
public class InterpolationSearch {
// 递归实现
public static int interpolationSearch(int[] array, int key, int low, int high){
int middle = 0;
if(low > high || key < array[low] || key > array[high]){
return -1;
}
middle = low + (key - array[low])*(high - low) / (array[high] - array[low]);
if(key < array[middle]){
return interpolationSearch(array, key, low, middle - 1);
}else if(key > array[middle]){
return interpolationSearch(array, key, middle + 1, high);
}
else{
return middle;
}
}
// 非递归实现,相比二分查找,只改了给middle赋值的那一行
public static int interpolationSearch1(int[] array, int key){
int low = 0;
int high = array.length - 1;
int middle = 0;
if(key < array[low] || key > array[high] || low > high){
return -1;
}
while(low <= high){
middle = low + (key - array[low]) * (high - low) /
(array[high] - array[low]);
if(key > array[middle]){
low = middle + 1;
}else if(key < array[middle]){
high = middle - 1;
}else{
return middle;
}
}
return -1;
}
public static void main(String[] args) {
int[] array = {12, 16, 44, 45, 67};
System.out.println(interpolationSearch(array, 16, 0, 4));
System.out.println(interpolationSearch1(array, 16));
}
}
三、线性索引查找
对于海量的无序数据,为了提高查找速度,一般会为其构造索引表。索引就是把一个关键字与它相对应的记录进行关联的过程。一个索引由若干个索引项构成,每个索引项至少包含关键字和其对应的记录在存储器中的位置等信息。
数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引按照结构可以分为:线性索引、树形索引和多级索引。
线性索引:将索引项的集合通过线性结构来组织,也叫索引表,可分为稠密索引、分块索引和倒排索引。下面介绍分块索引:
1、分块索引
给大量的无序数据集合进行分块处理,使得块内无序,块与块之间有序。这其实是有序查找和无序查找的一种中间状态。因为数据量过大,建立完整的稠密索引耗时耗力,占用资源过多;但如果不做任何排序或者索引,那么遍历的查找也无法接受,只能折中,做一定程度的排序或索引。分块索引的效率比遍历查找的 O(n) 要高一些,但与二分查找的 O(logn) 还是要差不少。
原理:
分块有序,是把数据集合的记录分成了若干份,并且这些块需要满足 2 个条件:
(1)块内无序,即每一块内的记录不要求有序。(有序更好,但需要付出大量时间和空间代价)
(2)块间有序,例如要求第二块所有记录的关键字均要大于第一块所有记录的关键字,第三块的所有记录要大于第二块的所有记录关键字,因为只有块间有序,才能提高查找效率。
举个例子,学生成绩就算是分块索引。0 ~ 60 为一块,60 ~ 70 为一块,70 ~ 80 为一块,80 ~ 90 为一块,90 ~ 100 为一块,共 5 块。
测试代码:
class Block{
private int max;// 以块内最大值为索引
private List<Integer> list = new ArrayList<>();
public int getMax() {
return max;
}
public void setMax(int max) {
this.max = max;
}
public List<Integer> getList() {
return list;
}
public void setList(List<Integer> list) {
this.list = list;
}
public String toString(){
return "{块最大值:" + max + ";块内元素:" + list.toString() + "}";
}
}
public class BlockSearch {
// 分块,对一组数据分块(学生成绩为例)
public static List<Block> block(int[] array){
List<Block> bList = new ArrayList<Block>();// 存储分块后的结果
// 分五个块
for (int i = 6; i <= 10; i++) {
Block block1 = new Block();
block1.setMax(i * 10);
List<Integer> list1 = new ArrayList<Integer>();
block1.setList(list1);
bList.add(block1);
}
// 将数据放入对应的块中
for (int i = 0; i < array.length; i++) {
if(array[i] < 60){
bList.get(0).getList().add(array[i]);
}else if(array[i] < 70){
bList.get(1).getList().add(array[i]);
}else if(array[i] < 80){
bList.get(2).getList().add(array[i]);
}else if(array[i] < 90){
bList.get(3).getList().add(array[i]);
}else if(array[i] <= 100){
bList.get(4).getList().add(array[i]);
}
}
return bList;
}
public static void blockSearch(int[] array, int key){
List<Block> bList = block(array);
int indexOfBlock = -1;
if(key < 0 || key > 100){
System.out.println("输入成绩有误!");
return;
}
// 1.先确定要查找数据所在的块
for (int i = 0; i < bList.size(); i++) {
if(key < bList.get(i).getMax()){
indexOfBlock = i;
System.out.println("所查成绩可能在块" + i + "中!");
break;
}
}
// 2.再确定要查找数据在块中的位置
List<Integer> list = bList.get(indexOfBlock).getList();
for (int i = 0; i < list.size(); i++) {
if(list.get(i) == key){
System.out.println("所查成绩在块" + indexOfBlock + "中,下标为" + i);
return;
}
}
System.out.println("块" + indexOfBlock + "中不存在所查成绩!");
}
public static void main(String[] args) {
int[] array = {54, 65, 88, 66, 98, 70, 79, 83, 93, 75, 100};
List<Block> blockList = block(array);
System.out.println(blockList);
blockSearch(array, 88);
System.out.println("==============");
blockSearch(array, -6);
System.out.println("==============");
blockSearch(array, 97);
}
}
测试结果:
[{块最大值:60;块内元素:[54]}, {块最大值:70;块内元素:[65, 66]}, {块最大值:80;块内元素:[70, 79, 75]}, {块最大值:90;块内元素:[88, 83]}, {块最大值:100;块内元素:[98, 93, 100]}]
所查成绩可能在块3中!
所查成绩在块3中,下标为0
==============
输入成绩有误!
==============
所查成绩可能在块4中!
块4中不存在所查成绩!
四、二叉树查找
参考二叉搜索树:
https://blog.csdn.net/weixin_45594025/article/details/104477793
五、哈希表查找
参考哈希表:
https://blog.csdn.net/weixin_45594025/article/details/104358434