[ 数据结构 ] 查找算法--------线性、二分、插值、斐波那契查找

0 前言

查找算法有4种:

  • 线性查找

  • 二分查找/折半查找

  • 插值查找

  • 斐波那契查找

1 线性查找

思路:线性遍历数组元素,与目标值比较,相同则返回下标

/**
     *
     * @param arr  给定数组
     * @param value 目标元素值
     * @return 返回目标元素的下标,没找到返回-1
     */
    public static int search(int[] arr, int value) {
        int index = -1;

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

        return index;
    }

2 二分查找

思路:

  1. 在第一版search01中编写递归方法:

    递归三要素:①传参为目标数组(有序),查找区间的上下界,目标值,返回值为找到的下标 + ②终止条件为区间终止就是目标值(下界>上界表示找不到,则目标值为-1) + ③递归调用为改变区间上界或下界,即向查找区间的左半部分或右半部分查找

  2. 在第二版search02中:

    对search01方法优化:解决了存在多个相同查找目标的问题,当找到目标后并未立即返回,而是向两侧搜索相同值并存放在结果集合中,最后返回结果集

  3. 在第三版search03中:

    采用暴力搜索,将所有目标值的下标收集到集合中

  4. 对比总结:

    第二版和第三版一样是递归但是原理不同,第二版要求数组本身有序所以可以采用if-else结构来递归调用(可以理解为剪枝,即优化),而第三版就是简单的二叉树模型,遍历所有节点去查找目标值

//返回下标,没找到返回-1,前提是数组本身有序
    public static int search01(int[] arr,int lo,int hi, int value) {
        int index = -1;

        //这里为什么不能是大于或等于,因为最后区间大小就是1,即lo==mid==hi
        //而任一树枝上(本题只有一只树枝)一定先满足lo==hi(如果能找到此层就该返回正确值),而后才是lo>hi,
        if (lo > hi) {
            return index;
        }

        int mid = (lo + hi) / 2;
        int midVal = arr[mid];

        if (value < midVal) {
            index=search01(arr, lo, mid - 1, value);
        } else if (value > midVal) {
            index=search01(arr, mid + 1, hi, value);
        } else {
            index=mid;
        }

        return index;
    }

    //返回多个相同值的索引集合,前提是数组本身有序
    //原理:找到一个正确值后没有立即返回,而是向两侧搜索相同值
    public static List<Integer> search02(int[] arr,int lo,int hi, int value) {
        ArrayList<Integer> result = new ArrayList<>();

        //这里为什么不能是大于或等于,因为最后区间大小就是1,即lo==mid==hi
        //而任一树枝上(本题只有一只树枝)一定先满足lo==hi(如果能找到此层就该返回正确值),而后才是lo>hi,
        if (lo > hi) {
            return result;
        }

        int mid = (lo + hi) / 2;
        int midVal = arr[mid];

        if (value < midVal) {
            return search02(arr, lo, mid - 1, value);
        } else if (value > midVal) {
            return search02(arr, mid + 1, hi, value);
        } else {
            result.add(mid);
            int l = mid;
            int r = mid;
            //注意这里的或不能用短路或,否则后面的r自增不会执行,导致mid重复收集
            while (arr[--l] == midVal | arr[++r] == midVal) {
                if (arr[l] == midVal) {
                    result.add(l);
                }
                if (arr[r] == midVal) {
                    result.add(r);
                }
            }
        }

        return result;
    }


    public static List<Integer> list = new ArrayList<>();

    //基于search01优化:返回若干个相同值
    //不同于search01,方法1的原理是树只有一个树枝,遇到正确结果立即层层返回
    //search03原理,没有返回值,暴力搜索,遍历多个树枝的所有节点,收集所有满足的结果放入全局变量list集合
    //当然本方法不要求数组本身有序
    public static void search03(int[] arr,int lo,int hi, int value) {

        //这里为什么不能是大于或等于,因为最后区间大小就是1,即lo==mid==hi
        //而任一树枝上(本题只有一只树枝)一定先满足lo==hi(如果能找到此层就该返回正确值),而后才是lo>hi,
        if (lo > hi) {
            return;
        }

        int mid = (lo + hi) / 2;
        int midVal = arr[mid];

        if (value == midVal) {
            list.add(mid);
        }

        search03(arr, lo, mid - 1, value);
        search03(arr, mid + 1, hi, value);
    }

3 插值查找

引出:二分查找简单说就是看中间,不停地对半砍同时看正中间是不是我要的,那么问题就来了,如果我要的就是最左边,那得切多少下才能切到最左边,简单!不要对半切就行了,改下权重就解决

思路:将权重由1/2改为自适应,在数据量较大且分布均匀时明显效率更优,不再对半切了,你要的值和两边中哪边差不多就往那边切,找起来快多了

//基于二分查找的改良,将权重由1/2改为自适应,在数据量较大且分布均匀时明显效率更优
    public static int search01(int[] arr,int lo,int hi, int value) {
        int index = -1;

        //优化:查找值超过极值则直接返回-1
        if (lo > hi||value<arr[0]||value>arr[arr.length-1]) {
            return index;
        }

        //(value-arr[lo])/(arr[hi]-arr[lo])就是自适应的权重,二分的权重是1/2
        int mid = lo+(hi-lo)*( (value-arr[lo])/(arr[hi]-arr[lo]) );
        int midVal = arr[mid];

        if (value < midVal) {
            index=search01(arr, lo, mid - 1, value);
        } else if (value > midVal) {
            index=search01(arr, mid + 1, hi, value);
        } else {
            index=mid;
        }

        return index;
    }

4 斐波那契查找

引出:如果说二分找中间快,插值找两边快(中间也快),那么斐波那契查找就是鸡肋中的鸡肋,华而不实的lj东西

思路:首先你得拿到著名的斐波那契数列(1,1,2,3,5,8…),假定给定的待排序数组arr大小为6,那么给它扩到斐波那契数列的下一级,6和5一级,下一级就是8,扩充的元素就是当前的最后一个元素,完事就开始砍,不是对半砍,你得找到可以砍的地方,满足砍完左右两部分的数组大小刚好在斐波那契数列中,比如8个元素,对着arr[4]砍一刀,左边包含arr[0]到arr[4]即大小为5的数组,右边就剩下标为567了,砍的同时看下刀点arr[4]是不是你要的,整体就是这样一直砍一直看下刀点

总结:且不说这种砍法没有实际意义,说白了就是二分查找的权重改为无限接近黄金分割比0.618(而且数组越大越接近0.618,数组越小越接近1),那你干嘛不直接用0.618呢,况且你砍完之后,下刀点那个元素还是归到了左半部分,这就太sb了,完全就是为了凑数组大小为斐波那契数列中的元素值了,造成重复判断

image-20230107192331090.png

//获取斐波那契数组方法
    public static int[] fib() {
        int[] f = new int[20];
        f[0] = 1;
        f[1] = 1;
        for (int i = 2; i < 20; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }

    //查找方法,不使用递归实现
    //斐波那契查找说到底只是让查找的区间大小一直在斐波那契数列中
    //,说白了就是切割比接近0.618,既然如此为什么不将二分查找的逻辑改为权重0.618
    //,纯粹就是装杯吧,说是感受数学之美还比不上插值查找的自适应权重有用
    public static int search(int[] arr, int value) {
        int lo = 0;
        int hi = arr.length - 1;
        int key = 0;//指针指向斐波那契数列元素,用以控制区间大小
        int mid = 0;//切割点
        int[] f = fib();//斐波那契数列

        //临时数组扩容到原数组大小的下一级,比如6扩容到5的下一级8
        while (hi > f[++key]-1) {
        }
        int[] temp = Arrays.copyOf(arr, f[key]);
        for (int i = hi+1; i < temp.length; i++) {
            temp[i] = temp[hi];
        }

        //此时key==5,f[key]==8,f[key-1]==5,f[key-2]==3,lo==0,hi==5,temp.length==8,mid==4
        //主要就是保证lo和hi的差值就是斐波那契数列的某个值-1,即查找的区间大小就是斐波那契数列值
        //缩小查找区间只需使key自减就行,但是
        //上下界的变迁需要注意,hi变迁只需赋为mid,lo却赋为mid+1,这是因为mid和hi间距是f[key-2]需要-1
        while (lo < hi) {
            mid = lo+ f[key - 1] - 1;
            if (value < temp[mid]) {
                hi = mid;
                key--;
            } else if (value > temp[mid]) {
                lo = mid + 1;
                key -= 2;
            } else {
                return mid;
            }
        }

        //hi==lo+f[k]-1,f[k]>=1
        if (lo == hi) {
            return lo;
        }


        return -1;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值