我是靠谱客的博主 友好酸奶,最近开发中收集的这篇文章主要介绍查找算法,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

##查找算法

引言

常见的七种查找算法有:顺序查找、二分查找、插值查找、斐波那契查找、树表查找、分块查找、哈希查找。其中二分查找、插值查找、斐波那契查找可以归为一类——插值查找,插值查找、斐波那契查找是在二分查找的基础上经过优化的查找算法。

###定义

查找是在大量的信息中寻找一个特定的信息元素,用关键字标识一个数据元素,查找时根据给定的某个值,在表中确定一个关键字的值等于给定值的记录或数据元素。

分类

  1. 静态查找和动态查找

    注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。

  2. 无序查找和有序查找。

    无序查找:被查找数列有序无序均可;
    有序查找:被查找数列必须为有序数列。

注:平均查找长度(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。

  • img

  • 说明:斐波那契查找也是插值查找的一种,它每次的查找点都是未查找部分的黄金分割点,可以提高查找效率,同时,它也属于一种有序查找算法。

  • 斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。

    开始将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),散列因子越大,查找的时间复杂度越大,散列因子越小,查找的时间复杂度越小。

    算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

  • 六种哈希函数构造方法:

    1. 直接定值法:

      哈希地址:f(key) = a*key+b (a,b为常数)

      这种方法的优点是:简单,均匀,不会产生冲突。但是需要事先知道 key 的分布情况,适合查找表较小并且连续的情况。

    2. 数字分析法

      比如我们的11位手机号码“136xxxx5889”,其中前三位是接入号,一般对应不同运营公司的子品牌,如130是联通如意通,136是移动神州行等等。中间四位表示归属地。最后四位才是用户号。

      若我们现在要存储某家公司员工登记表,如果用手机号码作为 key,那么极有可能前7位都是相同的,所以我们选择最后四位作为 f(key) 就是不错的选择。

    3. 平方取中法

      故名思义,比如 key 是1234,那么它的平方就是1522756,再抽取中间的3位就是227作为 f(key) 。

    4. 折叠法

      折叠法是将 key 从左到右分割成位数相等的几个部分(最后一部分位数不够可以短些),然后将这几部分叠加求和,并按哈希表的表长,取后几位作为 f(key) 。

      比如我们的 key 是 9876543210,哈希表的表长为3位,我们将 key 分为4组,987|654|321|0 ,然后将它们叠加求和 987+654+321+0=1962,再取后3位即得到 f(key) = 962 。

    5. 除留余数法

      哈希地址:f(key) = key mod p (p<=m) m为哈希表表长。

    6. 随机数法

      哈希地址:f(key) = random(key)

      这里 random 是随机函数,当 key 的长度不等时,采用这种方法比较合适。

  • 解决冲突的方法:

    1. 开放地址法

      开放定址法就是一旦发生了冲突,就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总是能找到,然后将记录插入。这种方法是最常用的解决冲突的方法。

    2. 链地址法

  • 哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为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;
}
}
```

最后

以上就是友好酸奶为你收集整理的查找算法的全部内容,希望文章能够帮你解决查找算法所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部