08_查找算法

查找算法概述

在Java中,常用的查找算法有四种:

1、顺序(线性)查找

2、二分查找/折半查找

3、插值查找

4、斐波那契查找

线程查找

线性查找概述

线性查找十分简单。

现在有一个数列,可以是有序的,可以是无序的。

我们从头到尾依次寻找,如果找到了那么提示找到并返回下标。

代码实现

/**
 * 实现一个比较简答的线性查找,也就是说只查找一个值,这里就不写查找多个值的了,因为比较简单
 *
 * @param arr 要查找的数组
 * @param val 要查找的值
 * @return 查找到的值的下标,没有查到则返回-1
 */
public static int seqSearch(int[] arr, int val) {

    int index = -1;

    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == val) {
            return i;
        }
    }

    return index;
}

二分查找

线性查找概述

二分查找必须要在一个有序的数组中进行查找

有序的意思是:从大到小,或者从小到大都可以。

二分查找有两种

  • 递归查找
  • 非递归查找

二分查找的思想

1、确定好数组元素的中间下标:mid = (left + right) / 2

2、让需要查找的数findVal和arr[mid]比较

  • 假如findVal > arr[mid],那么说明findVal的下标在mid之后,那么向右边进行递归查找
  • 假如findVal < arr[mid],那么说明findVal的下标在mid之前,那么向左边进行递归查找
  • 假如findVal == arr[mid],说明查找到了内容

注意点

结束递归的条件

1、找到了对应的值,我们退出递归

2、所有的数据已经遍历完成,但是还没有找到数据,那么结束递归(这种情况也就是left指针 > right指针)

代码实现

  • 基本的代码实现
/**
 * 二分法查找,必须是一个有序数组才可以进行查找
 *
 * @param arr     要查找的数组
 * @param left    左边的索引
 * @param right   右边的索引
 * @param findVal 要查找的元素
 * @return
 */
public static int binarySeach(int[] arr, int left, int right, int findVal) {
    /*
        假如左边的指针比右边的指针大,那么说明没有找到最终的值,返回-1即可
     */
    if (left > right) {
        return -1;
    }

    /*
        假如还在寻找的过程中,那么有如下三种情况:
     */

    //中间索引
    int mid = (left + right) / 2;
    //中间值
    int midVal = arr[mid];

    //1、假如要查找的值>中间值,那么进行向右递归
    if (findVal > midVal) {
        return binarySeach(arr, mid + 1, right, findVal);
    }
    //2、假如要查找的治<中间值,那么进行向左递归
    else if (findVal < midVal) {
        return binarySeach(arr, left, mid - 1, findVal);
    }
    //3、剩下的一种情况就说明已经找到了对应的值,结束递归,返回对应的索引
    else {
        return mid;
    }
}
  • 二分查找的升级处理

需求:当我们的数组中存在多个相同的值时,如何将这多个相同的值都查到,而不是只返回一个

/**
 * 二分查找,这里要进行一次升级,也就是将多个相同的值的索引放到List里面一块返回
 *
 * @param arr     要查找的数组
 * @param left    左边的下标
 * @param right   右边的下标
 * @param findVal 要寻找的元素
 * @return 要寻找的值的索引的集合,没有则返回null
 */
public static List<Integer> binarySearchUpgrade(int[] arr, int left, int right, int findVal) {
    if (left > right) {
        return null;
    }
    int mid = (left + right) / 2;
    int midVal = arr[mid];

    if (findVal > midVal) {
        return binarySearchUpgrade(arr, mid + 1, right, findVal);
    } else if (findVal < midVal) {
        return binarySearchUpgrade(arr, left, mid - 1, findVal);
    }
    /*
        前两个都没有什么区别,重点是这里
        假如我们找到了一个和findVal相同的值,同时也获得了它的下标
        那么根据有序排列的规则,这个下标的左右两边很可能是相同的个数字,所以要分别对左右两边进行遍历,直到找到下标不是为止
        而且注意,根据有序排列的规则,二分查找的肯定是有序的,不会存在1,1000,200这种情况
     */
    else {
        //0、创建一个List集合,加入已经确认的下标
        List list = new ArrayList();
        list.add(mid);
        //1、获取数组元素对应的下标
        int temp = mid - 1;
        //2、向左进行遍历,寻找对应值的下标,加入到集合中
        while (temp >= 0 && arr[temp] == midVal) {
            list.add(temp--);
        }
        //3、重置下标
        temp = mid + 1;
        //4、向右进行遍历,寻找对应值的下标,加入到集合中
        while (temp <= arr.length - 1 && arr[temp] == midVal) {
            list.add(temp++);
        }
        //5、集合进行排序
        Collections.sort(list);
        return list;
    }
}

插值查找

插值查找概述

插值查找是二分查找的一种更进一步的优化方案,与二分查找不同的是:插值查找的中间点mid是自适应的

我们在二分查找的时候,中间的点是(left + right)/2得到的

但是假如使用插值查找,那么中间的点是 left + (right - left) x (findVal - arr[left]) / (arr[right] - arr[left])

这个公式也就是拉格朗日插值(注意,数字必须是有序的,才能使用这个公式)


那么其实插值算法最重要的思想就是这个mid的公式,但是这方面确实涉及到了一些数学的知识

还是记忆下:

left:在数组中,就代表0

right:数组的长度-1

findVal:要查找的值

left + (right - left) x (findVal - arr[left]) / (arr[right] - arr[left])


比如现在有一个数组:int[] arr = {1,2,3,4,....,100};

那么假如我们要查询1的下标,那么按照插值查找的公式应当是:0 + (99 - 0) x (1 - 0) / (99 - 0) = 0

一下子就找到了


所以,插值算法最重要的就是它的自适应,在数字有序排列并且数据量十分多时,插值查找的优势巨大

代码实现

/**
 * 插值算法
 *
 * @param arr     要去寻找的数组
 * @param left    左边的索引
 * @param right   右边的索引
 * @param findVal 要查找的值
 * @return
 */
public static int interpolationSearch(int[] arr, int left, int right, int findVal) {
/*
    假如左边的指针比右边的指针大,那么说明没有找到最终的值,返回-1即可
 */
    if (left > right) {
        return -1;
    }

    /*
        假如还在寻找的过程中,那么有如下三种情况:
     */

    //中间索引a
    int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);
    //中间值
    int midVal = arr[mid];

    //1、假如要查找的值>中间值,那么进行向右递归
    if (findVal > midVal) {
        return binarySeach(arr, mid + 1, right, findVal);
    }
    //2、假如要查找的治<中间值,那么进行向左递归
    if (findVal < midVal) {
        return binarySeach(arr, left, mid - 1, findVal);
    }

    //3、剩下的一种情况就说明已经找到了对应的值,结束递归,返回对应的索引
    return mid;
}

基本上和二分查找差不多,唯一的区别就是mid的计算


斐波那契(黄金分割)查找

斐波那契查找概述

概念介绍

1、黄金分割点

把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比,取其三位数字的近似值为0.618,这个数值就是黄金分割点。

由于按此比例设计的造型十分美丽,因此成为黄金分割,也称为中外比。这是一个神奇的数字,会带来意想不到的效果。

2、斐波那契数列

斐波那契数列是这样的:{1,1,2,3,5,8,13,21,34,55}

其中除了前两位之外,后面的每一个值都是前面两个值相加得到的结果

斐波那契数列到了最后的数值,两个相邻相邻的数值的比值,其实就无限接近于黄金分割点0.618

3、在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2)(n>=2),......

image-20201219205338238

斐波那契查找的原理

我们刚才讲到,在数学中的定义F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2)(n>=2)

但是我们知道,在Java中数组的下标是以0开始的,所以根据 F(k) = F(k-1) + F(k-2) 得到:

F(k)-1 = (F(k-1)-1) + (F(k-2)-1) + 1,这样所有的下标都减去了1,符合了程序的定义

image-20201219205702147

其实mid在黄金分割段附近,但是注意,它的值不是F(k-1)-1,我们将它定义为low + F(k-1)-1

也就是说,mid = low + F(k-1)-1,其中low为最左端的值

但是现在有一个问题:假如我们的长度不够了怎么办?比如斐波那契需要长度为8的线段,然而我们只有7个或者更少,那么我们采取的解决措施是:使用最后一个的数值去填充到线段的后半部分,如下:

image-20201220103359018

斐波那契查找举例

假如我们现在有一个数组:int[] arr = {1,2,3,4,5,6,7,8,9,10},假如我们要查询9,下面的步骤应当是这样的:

1、因为数组长度为10,但是斐波那契数列中没有10这个数字,所以补充到13

image-20201220104811808

2、阶段2,找到第一个mid节点,然后数值与9比较

image-20201220104735165

3、因为数值比9小,那么向右递归,寻找第二个mid节点

image-20201220110546276

4、同理,向左递归,直到找到最终数值

代码实现

/**
 * 斐波那契数列的生成
 *
 * @param length 需要的斐波那契数列的长度
 * @return 返回一个斐波那契数列
 */
public static int[] fibonacci(int length) {

    int[] fibonacciArr = new int[length];

    //斐波那契数列的前两位是1
    fibonacciArr[0] = 1;
    fibonacciArr[1] = 1;

    //从第三位开始填充,直到填充到最后一位
    for (int i = 2; i < fibonacciArr.length - 1; i++) {
        fibonacciArr[i] = fibonacciArr[i - 1] + fibonacciArr[i - 2];
    }

    return fibonacciArr;
}

/**
 * 斐波那契查找
 *
 * @param arr       要查询的数组
 * @param fibonacci fibonacci 需要一个斐波那契数列,根据这个进行搜索
 * @param val       要查询的值
 * @return 返回的数组下标
 */
public static int fibonacciSearch(int[] arr, int[] fibonacci, int val) {


    int low = 0;
    int high = arr.length - 1;

    //定义斐波那契数组的下标,暂定为0
    int k = 0;

    //表示mid节点
    int mid = 0;

    //因为我们的数组长度不一定符合斐波那契的规则,所以我们要获取能符合斐波那契规则的长度
    while (arr.length > fibonacci[k]) {
        k++;
    }
    //我们构造一个新的数组,让超出的部分的值为arr最后的值
    int[] temp = Arrays.copyOf(arr, fibonacci[k]);
    for (int i = arr.length; i < temp.length; i++) {
        temp[i] = arr[high];
    }

    /*
        使用while循环来处理数据
        只要low<=high并且没有找到数据退出,就说明还有数据可以寻找
        只要low>high,那么说明全部的数据都找完了,也没有找到对应的数据
     */
    while (low <= high) {
        //给mid赋值
        mid = low + fibonacci[k - 1] - 1;

        //假如val小于中间值,那么说明要寻找的数字在左边,那么向左寻找
        if (val < temp[mid]) {
            //令右边的界限到达mid的前一位,说明要从low-high去查找
            high = mid - 1;
            /*
                我们之前说 F(k)-1 = (F(k-1)-1) + (F(k-2)-2) + 1
                斐波那契数列是 1,1,2,3,5,8,13,...
                现在界限改变了,长度从13改为了8,那么k肯定也需要改变一下了
                比如以前F(13)对应的下标是6,那么现在换为了F(8)下标肯定是5了
             */
            k--;
            continue;
        }
        //假如val大于中间值,那么说明要寻找的数字在右边,那么向右寻找
        if (val > temp[mid]) {
            //左边的界限从刚才的界限到达了mid节点的下一位
            low = mid + 1;
            /*
                k = k - 2其实也是一眼的,还是按照上面的13拆分开始讲
                13拆分为8和5,左边是8,右边肯定就是5,那么就是k-2了
             */
            k -= 2;
            continue;
        }

        //假如val等于中间值,那么就说明要寻找的数字找到了,那么返回下标即可
        if (val == temp[mid]) {
            /*
                但是不要着急,因为数组是经过补长的
                因为我们的high就是数组的长度,所以可以根据high来判断mid是否超过了原数组的下标
                假如mid超过了原数组的下标,那么因为mid下标的值等于high下标的值,直接返回high即可
                假如mid没有超过原数组的下标,那么直接返回mid即可
             */
            return mid < high ? mid : high;
        }
    }

    //假如一次while循环全都找不到,那么说明这个数值根本不存在
    return -1;
}

斐波那契查找的前提是,一个数组应该是一个有序数组


posted @ 2021-01-06 20:17  嚎羸  阅读(10)  评论(0编辑  收藏  举报