查找算法
查找( Search)是指从一批记录中找出满足指定条件的某一记录的过程,查找又称为检索。查找算法广泛应用于各类应用程序中。因此,一个有效的查找算法往往可以大大提高程序的执行效率。在实际应用中,数据的类型千变万化,每条数据项往往包含多个数据域。但是,在执行查找操作时,往往只是指定一个或几个域的值,这些作为查找条件的域称为关键字(Key),关键字分为两类。
在实际应用中,针对不同的情况往往可以选择不同的查找算法。对于无顺序的数据,只有逐个比较数据,才能找到需要的内容,这种方法称为顺序查找。对于有顺序的数据,也可以采用顺序查找法逐个比较,但也可以采取其他更快速的方法找到所需数据。另外,对于一些特殊的数据结构,例如链表、树结构和图结构等,也都有相对应的合适的查找算法。
常见七大查找算法:
-
顺序查找 2. 二分查找 3. 插值查找 4. 斐波那契查找 5. 分块查找
-
哈希查找(涉及哈希表结构) 7. 树表查找(涉及树表结构)
1. 顺序查找
线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的K值相等,则查找成功;若比较结果与文件中n个记录的关键字都不等,则查找失败。
代码实现
/** * 顺序查找 * @param array 查找的数组 * @param value 查找的值 * @return 数组内有和查找值匹配的值则返回数组内匹配值的下标,反之则返回-1 */ public static int orderSearch(int[] array, int value){ //遍历数组中的所有数据,当有能和value匹配的数据时,返回该数据的数组下标 for (int i=0; i<array.length; i++){ if (array[i] == value){ return i; } } //当数组中没有能和value匹配的数据时,返回-1 return -1; }
2. 二分查找
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
二分查找的操作就像它的名字一样,根据中间值将一个存储结构一分为二,分为前段和后段,因为规定了储存结构必须按关键字有序排序,所以只需将传入的查找值与中间值进行比较,根据比较结果再判断是往前段查找还是往后段查找,反复执行前面的操作,直到查找到传入的值或查找完整个储存结构。
/** * 二分查找(查找的数组必须是有序数组) * @param array 查找的数组 * @param left 中间值的左标(该次递归操作数组段的最左端下标) * @param right 中间值的右标(该次递归操作数组段的最右端下标) * @param findValue 查找的值 * @return 数组内有和查找值匹配的值则返回数组内匹配值的下标,反之则返回-1 */ public static int binarySearch(int[] array, int left, int right, int findValue){ if (left > right){//判断是否已经查找到尽头 return -1; } int mid = (left + right)/2;//取中间值下标 if (findValue > array[mid]){//如果findValue>array[mid] 则从中间值向右递归 return binarySearch(array, findValue, mid+1, right); }else if (findValue < array[mid]){//如果findValue<array[mid] 则从中间值向左递归 return binarySearch(array, findValue, left, mid); }else {//findValue==array[mid]时,则匹配成功,返回mid下标 return mid; } }
插值查找,有序表的一种查找方式。插值查找是根据查找关键字与查找表中最大最小记录关键字比较后的查找方法。插值查找基于二分查找,将查找点的选择改进为自适应选择,提高查找效率。插值类似于平常查英文字典的方法,在查一个以字母C开头的英文单词时,决不会用二分查找,从字典的中间一页开始,因为知道它的大概位置是在字典的较前面的部分,因此可以从前面的某处查起,这就是插值查找的基本思想。
插值查找除要求查找表是顺序存储的有序表外,还要求数据元素的关键字在查找表中均匀分布,这样,就可以按比例插值。
mid:查找数组段内的中间值
key:查找的值
low:查找数组段内最小值的下标,即查找数组段内最左下标left
high:查找数组段内最大值的下标,即查找数组段内最右下标right
代码实现
/** * 插值查找(查找的数组必须是有序数组) * @param array 查找的数组 * @param left 查找段的最小值索引下标,即查找段内最左下标 * @param right 查找段的最大值索引下标,即查找段内最右下标 * @param findValue 查找的值 * @return 数组内有和查找值匹配的值则返回数组内匹配值的下标,反之则返回-1 */ public static int insertSearch(int array[], int left, int right, int findValue){ //当左标left大于右标right时,即查找已经遍历到数组的尽头,则返回-1 //当查找值findValue小于数组内最小值或大于数组内最大值时,即数组内没有能匹配的值,则返回-1 if (left>right || findValue<array[0] || findValue>array[array.length-1]){ return -1; } //使用插值公式求出中间值位置下标 int mid = left + (right-left) * (findValue-array[left]) / (array[right]-array[left]); if (findValue < array[mid]){//当查找值小于中间中间值时,向左递归查找 return insertSearch(array, left, mid-1, findValue); }else if (findValue > array[mid]){//当查找值大于中间值时,向右递归查找 return insertSearch(array, mid+1, right, findValue); }else {//当查找值等于中间值时,则返回中间值下标 return mid; } }
斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数f[n],将原查找表扩展为长度为f[n],完成后进行斐波那契分割,即f[n]个元素分割为前半部分f[n-1]个元素,后半部分f[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。
黄金分割:在介绍斐波那契查找算法之前,先了解根它紧密相连的一个概念——黄金分割。黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
代码实现
/** * 斐波那契查找(查找数组必须是有序数组) * @param array 查找的数组 * @param findValue 查找的值 * @return 数组内有和查找值匹配的值则返回数组内匹配值的下标,反之则返回-1 */ public static int fibonacciSearch(int array[], int findValue){ int low = 0;//查找段的最小值索引下标,即查找段内最左下标 int high = array.length-1;//查找段的最大值索引下标,即查找段内最右下标 int mid = 0;//储存中间值的下标 int k = 0;//斐波那契数组的下标索引 int fib[] = fib();//斐波那契数列数组 /* 获取和查找数组最接近且大于查找数组最右下标的斐波那契数组的值,以作为新数组的长度 因为斐波那契的值fib[k]对应的是数组的长度,而右标high对应的是数组下标, 所以比较时,fib[k]的值需要-1 */ while (high > fib[k]-1){ k++; } //将查找的数组复制到新的数组,长度为前面求出的斐波那契数组值fib[k] int temp[] = Arrays.copyOf(array, fib[k]); /* 因为fib[k]不一定等于查找数组的长度,所以在最右下标high右边, 即超出查找数组的部分,把它们的值都设为最右下标的值temp[high] */ for (int i=high+1; i<temp.length-1; i++){ temp[i] = temp[high]; } /* 在斐波那契查找中是按照斐波那契数列的黄金分割点来划分查找数组的, 而在斐波那契数列中 fib[k] = fib[k-1]+fib[k-2], 所以可以把查找数组分成俩部分fib[k-1]和fib[k-2],因此 黄金分割点,即中间值的下标mid = low+fib[k-1]-1, 向左递归部分 = fib[k-1],向右递归部 = fib[k-2] */ while (low <= high){//判断是否查找到尽头 /* low:随着向右递归而发生变化,使得中间值可以跟着适应 fib[k-1]-1:因为斐波那契数列数组中的值对应的是数组长度,所以需-1 */ mid = low + fib[k-1] -1; if (findValue < temp[mid]){//查找值小于中间值,向左递归 high = mid-1;//重新设置查找数组右标,设置新的查找段 k -=1; //因为是向左递归,对应的是fib[k-1],所以斐波那契数组的下标k要-1 }else if (findValue > temp[mid]){//查找值大于于中间值,向右递归 low = mid+1;//重新设置查找数组的左标,设置新的查找段 k -=2;//因为是向右递归,对应的是fib[k-2],所以斐波那契数组的下标k要-2 }else {//当查找值等于中间值时,即匹配成功 /* 因为临时数组temp的长度可能超过原先的查找数组,而超出部分的值都等于右标位置的值, 所以当匹配成功值的下标大于右标时,要返回右标high,反之则之间返回中间值下标mid即可 */ if (mid <= high){ return mid; }else { return high; } } } //数组内无匹配成功的值,则返回-1 return -1; } //获取斐波那契数列 public static int[] fib(){ int fib[] = new int[20]; fib[0] = 1; fib[1] = 1; for (int i=2; i<fib.length; i++){ fib[i] = fib[i-1] + fib[i-2]; } return fib; }
分块查找是二分查找和顺序查找的一种改进方法,二分查找虽然具有很好的性能,但其前提条件时线性表顺序存储而且按照关键码排序,这一前提条件在结点树很大且表元素动态变化时是难以满足的。而顺序查找可以解决表元素动态变化的要求,但查找效率很低。如果既要保持对线性表的查找具有较快的速度,又要能够满足表元素动态变化的要求,则可采用分块查找的方法。
分块查找的速度虽然不如折半查找算法,但比顺序查找算法快得多,同时又不需要对全部节点进行排序。当节点很多且块数很大时,对索引表可以采用折半查找,这样能够进一步提高查找的速度。
分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况。当增加或减少节以及节点的关键码改变时,只需将该节点调整到所在的块即可。在空间复杂性上,分块查找的主要代价是增加了一个辅助数组。
分块查找操作:分块,即把一个存储结构分成多块,每一块都有一个关键字——该块数据池中的最大值,和一个块在原始存储结构中的起始位置。将分后的各块组合成一个索引表,储存各块的最大值和起始位置。通过查找值和块中的最大值进行比较,从而缩短需要查找的原始储存结构长度。
代码实现
public class BlockSearch { public static void main(String[] args) { int[] array = new int[50]; for (int i=0; i<array.length; i++){ array[i] = (int)(Math.random() * 100); } System.out.println(Arrays.toString(array)); List<Block> blockTable = createBlockTable(array); System.out.println(blockSearch(array, 10, blockTable)); } /** * 将数组分成多块,并创建块索引表储存各块的最大值和起始位置 * @param array 查找的数组 * @return 返回块索引表 */ public static List<Block> createBlockTable(int[] array){ int nums = 10;//每块储存的数据数量 int start = 0;//块的起始位置,初始为0 int maxValue = 0;//块中的最大值 List<Block> blockTable = new ArrayList<>();//块索引表 while (start < array.length){//判断起始起始位置是否大于查找数组的长度 maxValue = array[start];//假定块中的最大值为起始位置的值 //判断剩余的数组长度是否小于每块储存的初始数据量, //如果大于则的块的长度为初始数据量,如果大于则最后一块的长度就是剩余的数组长度 int maxLength = (start+nums) < array.length ? (start+nums) : array.length; //遍历块中的元素,选出块中的最大值 for (int i=start; i<maxLength; i++){ if (maxValue < array[i]){ maxValue = array[i]; } } //将块中的最大值和起始位置添加进块索引表 //注意:块中的数据与块的索引对于查找数组来说是有序的,即是按照查找数组的原始顺序来排序的 blockTable.add(new Block(start, maxValue)); //移动到下一块的起始位置 start += nums; } return blockTable; } /** * 分块查找 * @param array 查找数组 * @param findValue 查找的值 * @param blockTable 块索引表 * @return 数组内有和查找值匹配的值则返回数组内匹配值的下标,反之则返回-1 */ public static int blockSearch(int[] array, int findValue, List<Block> blockTable){ int blockIndex = 0;//块索引表中块的索引,按顺序遍历,初始值为0 //判断查找值是否大于块中的最大值,如果大于则移动到下一块 while (blockIndex < blockTable.size() && findValue > blockTable.get(blockIndex).maxValue){ blockIndex++; } //当块的索引大于块索引表的大小时,即说明查找值大于查找数组中的最大值,数组中没有查找值 if (blockIndex > blockTable.size()-1){ return -1; } //从块的起始位置开始在查找数组中进行查找,这时查找长度比顺序查找的长度缩短了start+1 for (int i=blockTable.get(blockIndex).start; i<array.length; i++){ //当从数组中匹配到查找值时,返回该匹配值在数组中的下标 if (array[i] == findValue){ return i; } } //当查找数组中无与查找值匹配的值时,返回-1 return -1; } } //块 class Block{ int start;//起始位置 int maxValue;//块中的最大值 public Block(int start, int maxValue) { this.start = start; this.maxValue = maxValue; } }