算法<java>——查找(顺序、二分、插值、分块、斐波那契)
一、顺序查找
(1)基本思想:
属于线性查找和无序查找,从线性表的一端开始遍历,找到与目标值相等的元素输出下标,反之返回-1。
(2)代码:
//第一种:顺序查找public Type SequenceSearch(Type[] list,Type t){for(Type temp : list){if(temp.equals(t))return temp;}return null;}
//第一种:顺序查找public Type SequenceSearch(Type[] list,Type t){for(Type temp : list){if(temp.equals(t))return temp;}return null;}
二、分块查找
(1)基本思想:
属于顺序查找的进阶版,也可以叫做索引顺序查找。
主要是将n个元素分成m块,每个块中元素可以没有顺序,但是m个块之间是有序排列、所以特别适合节点动态变化的请情况。
分块查找的速度虽然不如二分查找,但是比顺序查找快的多,同时又不需要对全部节点进行排序。当节点很多且块数很大时,对索引表可以采用折半查找,这样能够进一步提高查找的速度。
那么索引表构成的就是每个块中的最大元素,查找方式是先对索引表进行二分或者是顺序查找,选出目标值应该所在的块,然后在块内进行顺序查找
(2)代码:
package day2;import java.util.Scanner;public class blockSearch {public static void main(String[] args) {int[] arr = {27, 22, 30, 40, 36,13, 19, 16, 20,7, 10,43, 50, 48};// 初始化4个块:参数(max, min, endIndex, startIndex)与原数组分块匹配Block b1 = new Block(40, 22, 4, 0); // 块1:arr[0-4]Block b2 = new Block(20, 13, 8, 5); // 块2:arr[5-8]Block b3 = new Block(10, 7, 10, 9); // 块3:arr[9-10]Block b4 = new Block(50, 43, 13, 11); // 块4:arr[11-13]Block[] blockArr = {b1, b2, b3, b4};// 1. 修复Scanner使用:用try-with-resources自动关闭资源(无需手动close)try (Scanner scanner = new Scanner(System.in)) {System.out.print("请输入要查找的数字:");int target = scanner.nextInt(); // 读取控制台输入的整数(目标值)// 2. 修复参数类型:传入int类型的target,而非Scanner对象int index = getIndex(blockArr, arr, target);// 输出结果:优化提示,让结果更直观if (index != -1) {System.out.printf("查找成功!数字%d在数组中的索引为:%d%n", target, index);} else {System.out.printf("查找失败!数字%d不在数组中%n", target);}}}// 3. 修复参数名:将number改为target,与“目标查找值”含义一致private static int getIndex(Block[] blockArr, int[] arr, int target) {// 先找到目标值所在的块int indexBlock = findIndexBlock(blockArr, target);if (indexBlock == -1) { // 目标值不在任何块的范围内return -1;}// 获取目标块的起始/结束索引int startIndex = blockArr[indexBlock].getStartIndex();int endIndex = blockArr[indexBlock].getEndIndex();// 在目标块内线性查找for (int i = startIndex; i <= endIndex; i++) {if (arr[i] == target) {return i; // 找到目标,返回原数组索引}}return -1; // 目标在块范围内,但块内无此值}// 4. 修复参数名:将number改为target,保持与getIndex一致public static int findIndexBlock(Block[] blockArr, int target) {for (int i = 0; i < blockArr.length; i++) {// 判断目标值是否在当前块的[min, max]范围内if (target >= blockArr[i].getMin() && target <= blockArr[i].getMax()) {return i; // 返回块的索引}}return -1; // 目标值不在任何块的范围内}
}// 块类:保持原封装逻辑不变
class Block {private int max; // 块内最大值private int min; // 块内最小值private int endIndex; // 块在原数组的结束索引(含)private int startIndex;// 块在原数组的起始索引(含)// 构造方法:初始化块的属性public Block(int max, int min, int endIndex, int startIndex) {this.max = max;this.min = min;this.endIndex = endIndex;this.startIndex = startIndex;}// Getter方法:保持原逻辑不变public int getMax() {return max;}public void setMax(int max) {this.max = max;}public int getMin() {return min;}public void setMin(int min) {this.min = min;}public int getEndIndex() {return endIndex;}public void setEndIndex(int endIndex) {this.endIndex = endIndex;}public int getStartIndex() {return startIndex;}public void setStartIndex(int startIndex) {this.startIndex = startIndex;}
}
三、二分查找
(1)基本思想:
属于有序查找算法,页脚折半查找。就是将数组每次选取一般进行查找,怎么选取一半就需要让中间值与value进行比较,因为有序,所以中间值小于目标值则选取后半部分,大于目标值则选取前半部分,以此类推,查找出与目标值相等的元素,否则返回-1
这种方法有效的缩减查找的次数和查找范围,适用于数据量比较大的有序表。
因为前提是有序表,所以对于插入删除等操作过多的数据集并不适用,以为排序算法上浪费的时间会比较多。
一般的时间复杂度是(log2n)
(2)注意点:(low+high)/2这种写法并不严谨,因为如果high和low过大时容易产生溢出,mid可以改写为mid=low+(high-low)/2,或者使用位运算mid=low+(high-low)>>1.
(3)代码:
//第二种:二分查找,这里用int类型数组举例,找到中间元素的索引//非递归public int BinarySearch(int[] list,int value){int low = 0,high = list.length-1,mid;while (low<=high){mid = (low+high)/2;if(list[mid] == value)//如果是寻找String,就用equalreturn mid;if(list[mid]>value)high = mid-1;if(list[mid]<value)low=mid+1;}return -1;}//递归public int BinarySearch(int[] list,int value,int low,int high){int mid = (low+high)/2;if(list[mid] == value)//如果是寻找String,就用equalreturn mid;if(list[mid]>value)return BinarySearch(list,value,low,mid-1);if(list[mid]<value)return BinarySearch(list,value,mid+1,high);elsereturn -1;}
四、插值查找
(1)基本思想:
属于二分查找的改进版,二分查找一直重复一半一半的操作,这种操作比较固定,并不会根据目标值的大小进行自适应分段和选择,而插值查找可以根据目标值value进行自适应。
下面是百度词条对插值的解释:插值类似于平常查英文字典的方法,在查一个以字母C开头的英文单词时,决不会用二分查找,从字典的中间一页开始,因为知道它的大概位置是在字典的较前面的部分,因此可以从前面的某处查起。
既然是二分查找的改进版,那么就要找关键点进行改进,二分是取1/2的有序表进行查找,那么mid就是关键点,二分中mid=(low+high)/2,可以转化成mid=low+(high-low)/2,所以相当于(high-low)/2中的1/2就是所分的比例,那么可以对mid进行改进,mid=low+low+(value-list[low])/(list[high]-list[low])*(high-low),(value-list[low])/(list[high]-list[low])就是所分的比例。
根据目标值在整个有序表中所处的位置,让mid的变化更靠近目标值value,这样也就间接地减少了比较次数。
这种方法适用于关键字分布均匀的有序表。
复杂度为O(log2(log2n))
(2)代码:
//第三种:插值查找,用int类型数组举例public int InsertSearch(int[] list,int value,int low,int high){int mid = low+(value-list[low])/(list[high]-list[low])*(high-low);if(list[mid] == value)//如果是寻找String,就用equalreturn mid;if(list[mid]>value)return InsertSearch(list,value,low,mid-1);if(list[mid]<value)return InsertSearch(list,value,mid+1,high);elsereturn -1;}
五、斐波那契查找
(1)基本思想:
斐波那契数列与0.618有着奇妙的关联,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,所以可以将黄金比例运用到查找中。
百度词条:
斐波那契搜索就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F(n),将原查找表扩展为长度为F(n)(如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。
(2)代码:
package Searching;import java.util.Arrays;public class Fibonaccialo {public static void main(String[] args) {int arr[] = {2,4,9,10,13,15,16,19,34};System.out.println(FibonacciSearch(arr,34)+1);}//创建斐波那契数列public static int[] Fibonacci(int n) {int[] FibonacciList = new int[n];FibonacciList[0] = 0;FibonacciList[1] = 1;for (int i = 2; i < n; i++) {FibonacciList[i] = FibonacciList[i - 1] + FibonacciList[i -2];}return FibonacciList;}//斐波那契查找,返回的是value在list中的索引值,所以在最后可以加一,为了看出效果,我在上面打印的时候再加一public static int FibonacciSearch(int[] list,int value){int low = 0;int high = list.length -1;int k = 0;//表示斐波那契分割的值int mid = 0;//存放黄金分割点的值int f[] = Fibonacci(20);//斐波那契数列//找k值while (high>f[k] - 1){k++;}//复制到新的数组int temp[] = Arrays.copyOf(list,f[k]);//得到f[k]的值可能会大于a的长度,将新数组的多余的部分用数组中的最后一个数字填充for (int i = high+1; i <temp.length ; i++) {temp[i] = list[high];}//使用while循环,找到value的位置while (low<=high){//满足这个条件就可以继续找mid = low+f[k-1]-1;//找到的黄金分割点if(value<temp[mid]){high = mid - 1;k--;}else if(value>temp[mid]){low = mid + 1;k-=2;}else{if(mid<=high){return mid;}else {return high;}}}return -1;}}
六、哈希(基本思想)
(1)什么是哈希表(Hash)?
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
(2)什么是哈希函数?
哈希函数的规则是:通过某个转换关系,使关键字适度的分散到指定大小的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高
(3)基本思想:
哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
流程:
1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
常见的解决冲突的方法:拉链法和线性探测法。
3)在哈希表的基础上执行哈希查找。
七、树表查找
1.二叉树:
基本思想:
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
2.查找树:
和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
1)要么为空,要么:
2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大
3.红黑树
基本思想:
红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。
4.B树和B+树
维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。
B树定义:
B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
根节点至少有两个子节点
每个节点有M-1个key,并且以升序排列
位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
其它节点至少有M/2个子节点
下图是一个M=4 阶的B树:
B+树定义:
B+树是对B树的一种变形树,它与B树的差异在于:
有k个子结点的结点必然有k个关键码;
非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
如下图,是一个B+树:
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
下面是B 树和B+树的区别图:
树表查找总结:
二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
除此之外,查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。