• 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;
    }
    

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


  • 相关阅读:
    处理不同方向的文本1.0
    CSS盒模型
    费德曼学习法
    [转]Photoshop中的高斯模糊、高反差保留和Halcon中的rft频域分析研究
    [转]仿射变换及其变换矩阵的理解
    [转]Scintilla开源库使用指南(一
    [转]Scintilla开源库使用指南(二
    [转]C#中WinForm窗体事件的执行次序
    [转]透过IL看C#:switch语句(转)
    [转]程序员必读书单(转)
  • 原文地址:https://www.cnblogs.com/howling/p/14243235.html
Copyright © 2020-2023  润新知