栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JavaSE 二分查找

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JavaSE 二分查找

文章目录
  • 第八章 有序数组查找
    • 8.1 二分查找
    • 8.2 插值查找
    • 8.3 斐波那契查找
      • 8.3.1 斐波那契数列
      • 8.3.2 斐波那契查找
    • 8.x 总结回顾
    • 8.y 脑海练习
  • 参考答案
    • 第八章答案

第八章 有序数组查找

内容导视:

  • 二分查找
  • 插值查找
  • 斐波那契查找
8.1 二分查找

前提:数组中的元素必须是顺序排列

Binary Search

玩过猜数游戏吗?如给定一个数,让你在 [1,100] 去猜,猜错了,会提示你猜的数是小了还是大了,最多只能猜 8 次。

如果使用前面的顺序查找,是不是有可能猜 100 次?

比较有效率的一种做法就是对半猜,每次都取范围内的中间数:猜 50,大了,范围被限定在 [1,50);向左猜 25,小了,答案在 (25,50)区间;向右猜 37,小了,此时的答案就被限制在 (37,50)中…

那么查找元素时也可以使用这种方法,但是要求数组中元素必须是顺序。

思想:取中间元素与给定值比较,若相等,查找成功;若给定值大于中间元素,则在中间元素的右半部分查找,否则在左半部分查找。

找数 num,取中间数 arr[mid],若 num > arr[mid],就在右边 [mid+1, n - 1] 范围内找;若 num < arr[mid],在左边 [0, mid - 1] 找;重复以上过程,直到 num = arr[mid] 或查找范围内无元素为止;记查找范围为 [start, end]。

public static int binarySearch(int[] arr, int num) {
    int start = 0;
    int end = arr.length - 1;

    // 若 start > end,则范围内无元素,查找失败,返回 -1
    while (start <= end) {
        // 取范围内的中间值
        int mid = start + (end - start) / 2;
        int midVal = arr[mid];

    	
        if (num < midVal) {
            end = mid - 1;
        } else if (num > midVal) {
         	
            start = mid + 1;
        } else {
        	
            return mid;
        }
    }
    // 到这里说明没有找到 num,返回 -1
    return -1;
}

二分查找每次都将查找范围缩小一半,到一个元素为止,平均时间复杂度为 O(logkn)。

8.2 插值查找

Interpolation Search

当元素分布均匀时,可以通过控制权重的方法,使得 mid 更加接近要查找的元素 num 所在位置。

举例:{2、5、… 、1000000} 近百万长度的数组中,寻找元素 8 的下标,如果使用二分查找,不偏不倚从 50 万、25 万、12.5 万… 开始慢慢向左找,权重为 1/2,还是慢了。

我们可以观察到,8 相比 100 万而言十分地小,应该从更靠左的地方开始找,而不是从中间。

只需改动一点代码:

int weight = (num - arr[start]) / (arr[end] - arr[start]);
int mid = start + (end - start) * weight;

当要查找的数 num 特别靠近范围的左端 arr[start] 时,分子值、权重(weight)趋向于 0,那么 mid 趋向 start,舍弃了右大半部分的查找范围;

当 num 处于中间时,weight 趋向 1/2,mid 也会在中间附近开始查找。

当 num 特别靠近 arr[end] 时,weight 趋向 1,mid 会更加接近 end。

可以验证,比如 {0,1,2,3,4,…,10000000} 中寻找 1 的下标:

初始值:start = 0,end = 10000000;

weight = (1 - 0) / (10000000 - 0) = 0;
mid = 0 + (10000000 - 0) * 0 = 0;

1 > arr[0],向右找,start = 0 + 1 = 1;
下一次循环,weight = (1 - 1) / (1000000 - 1) = 0;
mid = 1 + (1000000 - 1) * 0 = 1;

1 = arr[1],找到了,只需两次。

要是使用二分法,需要 ⌊log210000000⌋ - 1 = 22 次。

但是需要注意由于这种方式依赖 num 得出下标,所以 num 的值要在 arr[start]、arr[end] 之间,否则会计算出负数,导致下标越界。

可以在循环之前加入:

if (num < arr[start] || num > arr[end]) {
    return -1;
}
8.3 斐波那契查找

除了对半分,还有黄金分割;不知道你们听说过斐波那契数列没有,随着 n 的增大,相邻两个斐波那契数的比值 f(n)/ f(n+1)会逐渐趋向黄金分割比 0.61803398…,下面我们就利用斐波那契数列完成分割。

8.3.1 斐波那契数列

Fibonacci sequence:{1,1,2,3,5,8,13,21,34 …}

性质:数组前两项的和等于后一项,用递归描述:f(n)= f(n-1)+ f(n-2),n > 2 且是整数;

递归

// 获取第 n 项的斐波那契数
public static int f(int n) {
    // 前一项
    if (n < 2) {
        // 如果 n 是第 1 项,返回 1,否则返回 0
        return n == 1 ? 1 : 0;
    }
    return f(n - 1) + f(n - 2);
}

循环

if (n < 2) {
    return n == 1 ? 1 : 0;
}
int n0 = 0;
int n1 = 1;
for (int i = 2; i <= n; i++) {
    int n2 = n0 + n1;
	// 后移一位
    n0 = n1;
	n1 = n2;
}
return n2;

循环比递归效率高些。

8.3.2 斐波那契查找

Fibonacci Search:前提是有序数组。

{0,1,1,2,3,5,8,13,21,34 …},0 是第 0 项。

引子:数组长度为 n,下标从 0 到 n - 1,总范围:[0, n - 1];从 start 开始取出 8 个元素,则它们的下标分别为 start、start + 1、start + 2、… start + 7 即 [start, start + 7]。

引子 2:记第 k 项的斐波那契数为 f(k),则 k > 2 时,有 f(k)= f(k - 1)+ f(k - 2)

设 k 为正整数,从个数为 f(k)- 1 的元素集合中寻找 num 的下标,记最左边的元素下标 start,则查找范围为 [start, start + f(k)- 2],取 mid = start + f(k - 1)- 1,将其分割成两个部分,左边范围:[start, mid - 1] 即 [start, start + f(k - 1)- 2];右边范围:[mid + 1, f(k)- 2] 即 [start + f(k - 1), start + f(k)- 2];

左边部分的元素个数为 (start + f(k - 1)- 2)- start + 1 = f(k - 1)- 1;

右边部分的元素个数为 (start + f(k)- 2)- (start + f(k - 1))+ 1 = f(k)- f(k - 1)- 1 = f(k - 2)- 1;

无论是左部分还是右部分,都可以重复上述过程,如从元素个数为 f(m)- 1 的集合中寻找 num 的下标,记最左边的元素下标为 start … (左半部分 m = k - 1,右半部分 m = k - 2)

直到 arr[mid] = num 为止。

需要注意的是数组长度 n 并不一定等于 f(k)- 1,这需要将其扩容至 f(k)- 1(保证 f(k)- 1 要大于等于 n);多出来的部分补最大值 arr[n - 1],以保证是有序数组,所以如果查到的 num 的下标大于等于 n,则 arr[num] = arr[n - 1],返回 n - 1 即可。

public static int fibonacciSearch(int[] arr, int num) {
    int n = arr.length;
    int start = 0;
    int end = n - 1;

    // 如果 f(k) - 1 < n,则 k++,一直到 f(k) - 1 >= n 为止
    int k = 0;
    while (f(k) - 1 < n) {
        k++;
    }
    
    int[] temp = new int[f(k) - 1];
    // 前 n 个元素与 arr 一致
    for (int i = 0; i < n; i++) {
        temp[i] = arr[i];
    }
    // 多出来的部分补 arr[n - 1](下标大于等于 n 的元素都为 arr[n - 1])
    for (int i = n; i < f(k) - 1; i++) {
        temp[i] = arr[n - 1];
    }
    while (start <= end) {
        int mid = start + f(k - 1) - 1;
        int midVal = temp[mid];
        
        // num 在左半部分,k 应减一
        if (num < midVal) {
            end = mid - 1;
            k--;
        // num 在右半部分,k 应减二    
        } else if (num > midVal) {
            start = mid + 1;
            k -= 2;
        // arr[mid] = num    
        } else {
            // mid < n
            if (mid < n) {
                return mid;
            // mid >= n    
                
            } else {
                return n - 1;
            }
        }
    }
    return -1;
}

f(k)可以使用备好的斐波那契数组 f[k] 取代。

这种六四分的方式,如果要查找的元素偏右边一些,当然会比二分查找效率高;否则还不如二分查找。

8.x 总结回顾

一片朦胧选二分,分布均匀选插值,偏右选黄金分割。

8.y 脑海练习

8.1 编写方法返回长度为 n 的斐波那契数组。(包括 0)

参考答案 第八章答案

8.1 编写方法返回长度为 n 的斐波那契数组。(包括 0)

public static int[] fibonacciArr(int n) {
    
    if (n < 2) {
        return new int[]{0};
    } else if (n == 2) {
        return new int[]{0, 1};
    }
    
    int[] f = new int[n];
    f[0] = 0;
    f[1] = 1;
    for (int i = 2; i < n; i++) {
        f[i] = f[i - 1] + f[i - 2];
    }
    return f;
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/953288.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号