概述
##查找算法
引言
常见的七种查找算法有:顺序查找、二分查找、插值查找、斐波那契查找、树表查找、分块查找、哈希查找。其中二分查找、插值查找、斐波那契查找可以归为一类——插值查找,插值查找、斐波那契查找是在二分查找的基础上经过优化的查找算法。
###定义
查找是在大量的信息中寻找一个特定的信息元素,用关键字标识一个数据元素,查找时根据给定的某个值,在表中确定一个关键字的值等于给定值的记录或数据元素。
分类
-
静态查找和动态查找
注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
-
无序查找和有序查找。
无序查找:被查找数列有序无序均可;
有序查找:被查找数列必须为有序数列。
注:平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci : 找到第i个数据元素时已经比较过的次数。
顺序查找
-
说明:顺序查找适用于存储结构为顺序存储或链接存储的线性表。
-
基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
-
复杂度分析:查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
当查找不成功时,需要n+1次比较,时间复杂度为O(n);
所以,顺序查找的时间复杂度为O(n)。 -
优点:算法简单,对表的结果无任何要求,无论是用向量还是用链表来存放结点,也无论结点之间是否按关键字有序,它都同样适用。
-
缺点: 查找效率低,因此,当n较大时不宜采用顺序查找。
public static int seqSearch(int[] arrs,int num){ // 无序查找 for (int i = 0;i<arrs.length;i++){ if (arrs[i] == num){ return i; } } return -1; // 有序查找 for (int i = 0;i<arrs.length;i++){ if (arrs[i] == num){ return i; }else if (arrs[i] > num){ break; } System.out.println(arrs[i]); } return -1; }
###二分查找
-
说明:元素必须是有序的,如果是无序的则要先进行排序操作。
-
基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
-
复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
-
优点:比较次数少,查找速度快,平均性能好
-
缺点:待查表为有序数组,插入删除困难
-
注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。
// 二分查找非递归算法 public static int binarySearch(int[] arr,int key){ // 最小范围下标 int minIndex = 0; // 最大范围下标 int maxIndex = arr.length-1; // 中间范围下标 int midIndex = (minIndex+maxIndex)/2; while (minIndex <= maxIndex){ // 判断要查找的key与中间下标值的大小关系 if (arr[midIndex] > key){ // 中间下标数据值大于key maxIndex = midIndex - 1; }else if (arr[midIndex] < key){ // 中间下标数据值小于key minIndex = midIndex + 1; }else{ // 找到该数据,结束查找 return midIndex; } // 当边界发生变化时,需要更新中间下标 midIndex = (minIndex+maxIndex)/2; } return -1; } // 二分查找递归算法 public static int search2(int key,int[] arr,int minIndex,int maxIndex) { if(minIndex > maxIndex){ return -1; } int midIndex = (minIndex + maxIndex)/2; if(key<arr[midIndex]){ return search2(key,arr,minIndex,midIndex-1); }else if(key>arr[midIndex]){ return search2(key,arr,midIndex+1,maxIndex); }else{ return midIndex; } }
###插值查找
-
说明:插值查找与二分查找一样,只适用于有序表的查找。插值查找是在二分查找的基础上做了一些优化的查找算法
-
在进行二分查找时,我们一开始是选择了从查找表的中间开始查找,而在实际应用中,比如说在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
-
折半查找这种查找方式,不是自适应的,因此我们可以对二分查找进行一些优化:
二分查找中的查找点为:
midIndex = (minIndex+maxIndex)/2;
通过类比,我们可以将查找的点改进为如下:
midIndex=minIndex+(key-arr[minIndex])/(arr[maxIndex]-arr[minIndex])*(maxIndex -minIndex);
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
// 插值查找 public static int interpolationLookup(int[] arr,int key){ // 最小范围下标 int minIndex = 0; // 最大范围下标 int maxIndex = arr.length-1; // 中间范围下标 int midIndex = minIndex+(key-arr[minIndex])/(arr[maxIndex]-arr[minIndex])*(minIndex+maxIndex); while (minIndex <= maxIndex){ // 判断要查找的key与中间下标值的大小关系 if (arr[midIndex] > key){ // 中间下标数据值大于key maxIndex = midIndex - 1; }else if (arr[midIndex] < key){ // 中间下标数据值小于key minIndex = midIndex + 1; }else{ // 找到该数据,结束查找 return midIndex; } // 当边界发生变化时,需要更新中间下标 midIndex = minIndex+(key-arr[minIndex])/(arr[maxIndex]-arr[minIndex])*(minIndex+maxIndex); } return -1; }
斐波那契查找
-
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
-
黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。
-
说明:斐波那契查找也是插值查找的一种,它每次的查找点都是未查找部分的黄金分割点,可以提高查找效率,同时,它也属于一种有序查找算法。
-
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种
1)相等,mid位置的元素即为所求
2)>,low=mid+1,k-=2;
说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个。
3)<,high=mid-1,k-=1。
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个。
// 斐波那契查找 public static int fibonacciLookup(int[] arr,int key){ // 初始化一个斐波那契数列 ArrayList<Integer> fib = new ArrayList<>(); fib.add(1); int i =1; while (arr.length > fib.get(i-1)) { if (i < 2){ fib.add(1); }else { int result = fib.get(i-1)+fib.get(i-2); fib.add(result); } i++; } // 查找过程 int minIndex = 0; int maxIndex = arr.length-1; // 第一个黄金分割点 int k = fib.size()-2; int midIndex; int index; while (minIndex <= maxIndex) { if (k >= 0){ midIndex = minIndex+fib.get(k)-1; }else { break; } index = midIndex>=arr.length?arr.length-1:midIndex; // 判断要查找的key与中间下标值的大小关系 if (arr[index] > key) { // 中间下标数据值大于key maxIndex = midIndex - 1; k -= 1; } else if (arr[index] < key) { // 中间下标数据值小于key minIndex = index + 1; k -= 2; } else { // 找到该数据,结束查找 if (midIndex<arr.length){ return midIndex; }else { return arr.length-1; } } } return -1; }
树表查找
-
最简单的树表查找算法——二叉树查找算法。
基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3)任意节点的左、右子树也分别为二叉查找树。
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
-
基于二叉查找树进行优化,进而可以得到其他的树表查找算法,如平衡树、红黑树等高效算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4eR8BpeE-1588753316893)(C:UserszlAppDataRoamingTyporatypora-user-imagesimage-20200503145920646.png)]
分块查找
-
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
step1 先选取各块中的最大关键字构成一个索引表;
step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。 -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XXg0vzyo-1588753316896)(C:UserszlAppDataRoamingTyporatypora-user-imagesimage-20200503153438341.png)]
/* * 索引表类 * */ public class IndexItem { public int max; //值比较的索引 public int start; //开始位置 public int length;//块元素长度(非空) public IndexItem(int max, int start, int length) { this.max = max; this.start = start; this.length = length; } } /* * 分块查找 * */ public class Demo3 { // 索引表 static IndexItem[] indexItemList; public static void main(String[] args) { int[] arr = {14,31,8,22,18,43,62,49,35,52,88,78,71,83}; // 分块 indexItemList = new IndexItem[]{ new IndexItem(31, 0, 5), new IndexItem(62,5,5), new IndexItem(88,10,4) }; System.out.println(blockSearch(arr,49)); } public static int blockSearch(int[] arr,int key){ IndexItem indexItem = null; // 遍历索引表 for (int i = 0;i<indexItemList.length;i++){ if (indexItemList[i].max > key){ indexItem = indexItemList[i]; break; } } if (indexItem == null){ return -1; } // 遍历当前块 for (int i =indexItem.start;i<indexItem.start+indexItem.length;i++){ if (arr[i] == key){ return i; } } return -1; } }
哈希查找
-
什么是哈希表(Hash)?
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应(即key -> f(key)),于是用这个数组单元来存储这个元素。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。 -
什么是哈希函数?
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
散列因子:(默认为0.75),散列因子越大,查找的时间复杂度越大,散列因子越小,查找的时间复杂度越小。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
-
六种哈希函数构造方法:
-
直接定值法:
哈希地址:f(key) = a*key+b (a,b为常数)
这种方法的优点是:简单,均匀,不会产生冲突。但是需要事先知道 key 的分布情况,适合查找表较小并且连续的情况。
-
数字分析法
比如我们的11位手机号码“136xxxx5889”,其中前三位是接入号,一般对应不同运营公司的子品牌,如130是联通如意通,136是移动神州行等等。中间四位表示归属地。最后四位才是用户号。
若我们现在要存储某家公司员工登记表,如果用手机号码作为 key,那么极有可能前7位都是相同的,所以我们选择最后四位作为 f(key) 就是不错的选择。
-
平方取中法
故名思义,比如 key 是1234,那么它的平方就是1522756,再抽取中间的3位就是227作为 f(key) 。
-
折叠法
折叠法是将 key 从左到右分割成位数相等的几个部分(最后一部分位数不够可以短些),然后将这几部分叠加求和,并按哈希表的表长,取后几位作为 f(key) 。
比如我们的 key 是 9876543210,哈希表的表长为3位,我们将 key 分为4组,987|654|321|0 ,然后将它们叠加求和 987+654+321+0=1962,再取后3位即得到 f(key) = 962 。
-
除留余数法
哈希地址:f(key) = key mod p (p<=m) m为哈希表表长。
-
随机数法
哈希地址:f(key) = random(key)
这里 random 是随机函数,当 key 的长度不等时,采用这种方法比较合适。
-
-
解决冲突的方法:
-
开放地址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总是能找到,然后将记录插入。这种方法是最常用的解决冲突的方法。
-
链地址法
-
-
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
/* 哈希查找 */ public class Demo4 { // 初始化哈希表 static int hashLength = 7; static int[] hashTable = new int[hashLength]; // 原始数据 static int[] list = new int[]{13, 29, 27, 28, 26, 30, 38}; public static void main(String[] args){ // 创建哈希表 for (int i = 0; i < list.length; i++) { insert(hashTable, list[i]); } while (true) { // 哈希表查找 System.out.print("请输入要查找的数据:"); int result = search(hashTable,22); if (result == -1) { System.out.println("对不起,没有找到!"); } else { System.out.println("数据的位置是:" + result); } } } /** * 方法:哈希表插入 */ public static void insert(int[] hashTable, int data) { // 哈希函数,除留余数法 int hashAddress = hash(hashTable, data); // 如果不为0,则说明发生冲突 while (hashTable[hashAddress] != 0) { // 利用 开放定址法 解决冲突 hashAddress = (++hashAddress) % hashTable.length; } // 将待插入值存入字典中 hashTable[hashAddress] = data; } /** * 方法:哈希表查找 */ public static int search(int[] hashTable, int data) { // 哈希函数,除留余数法 int hashAddress = hash(hashTable, data); while (hashTable[hashAddress] != data) { // 利用 开放定址法 解决冲突 hashAddress = (++hashAddress) % hashTable.length; // 查找到开放单元 或者 循环回到原点,表示查找失败 if (hashTable[hashAddress] == 0 || hashAddress == hash(hashTable, data)) { return -1; } } // 查找成功,返回下标 return hashAddress; } /** * 方法:构建哈希函数(除留余数法) * * @param hashTable * @param data * @return */ public static int hash(int[] hashTable, int data) { return data % hashTable.length; } }
ss] == 0 || hashAddress == hash(hashTable, data)) {
return -1;
}
}
// 查找成功,返回下标
return hashAddress;
}
/**
* 方法:构建哈希函数(除留余数法)
*
* @param hashTable
* @param data
* @return
*/
public static int hash(int[] hashTable, int data) {
return data % hashTable.length;
}
}
```
最后
以上就是友好酸奶为你收集整理的查找算法的全部内容,希望文章能够帮你解决查找算法所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复