数据结构与算法|第十一章:二分查找-上

数据结构与算法|第十一章:二分查找-上

前言

每次看到或者听到二分查找,都会想起一个故事,给博主留下了深刻的印象,今天分享给大家,故事源自于吴军博士的《谷歌方法论》。

这是一个关于二分查找的游戏,比如游戏者让大家在心目中想一个1-1000的数字,然后你问他问题,对方回答“yes(是)”或者“no(不是)”,你用10次一定能够猜出他心目中想的数字。

这个花招其实很简单,你第一次只要问他是否小于500,如果他给出了肯定的答案,说明数字在1-499里面,第二次折半问他这个问题即可。类似地,如果他的回答是否定的,说明对方心目中的数字是在500-1000之间,第二次往大了折半问即可。然后你不断缩小范围,每次减少一半,这样问10次即可,因为 2 的十次方等于1024,大于1000。这就是二分查找。

如果你用这个办法找到数据库中某个身份证号的位置(以及相应的个人信息),大约只要31次,不需要把13亿中国人的身份证扫一遍,31次和13亿次,这个差距还是巨大的。

1.项目环境

2.图解原理

假设我们需要从数组 [1,11,19,21,25,27,33,38,55,87],查找元素 19

我们用三个标示来分别标示

  • low 待查找区间的第一位元素下标
  • high 待查找区间的最后一位元素下标
  • mid 待查找区间的中间元素下标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7oy4NnnU-1591842607894)(G:\workspace\csdn\learn-document\data-structure-algorithm\csdn\image-20200611090130580.png)]
我们只需要进行三次查询,就可以找到 元素19。

3.O(logn) 惊人的查找速度

从前言中的例子就可以知道,即使在13亿人中查找某个人,也只需要31次查找就可以找到,这样的查找速度在我第一次接触二分查找的时候带来了深深的震撼,我们来分析一下它的时间复杂度。

假设数据大小是 n,每次查找会缩小为原来的一半,最坏情况下,直到区间缩小为空,才停止。

n − > n / 2 − > n / 4 − > n / 8 − > . . . − > n / 2 k − > . . . n->n/2->n/4->n/8->...->n/2^k->... n>n/2>n/4>n/8>...>n/2k>...

n / 2 k = 1 n/2^k=1 n/2k=1 时,k 的值就是总共缩小的次数,每次缩小只需要比较 mid位置值 和 查找元素 两个值的大小,所以经过 k 次缩小的时间复杂度为 O ( k ) O(k) O(k)。通过 n / 2 k = 1 n/2^k=1 n/2k=1 可以得到 k = l o g n k = logn k=logn,所以二分查找的时间复杂度为 O ( l o g n ) O(logn) O(logn)

O ( l o g n ) O(logn) O(logn) 这种对数时间复杂度是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级 O ( 1 ) O(1) O(1) 的算法还要高效。为什么这么说呢?

我们前面讲过,用大O标记法表示时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1) 有可能表示的是一个非常大的常量值,比如O(1000)、O(10000)。所以,常量级时间复杂度的算法有时候可能还没有 O ( l o g n ) O(logn) O(logn) 的算法执行效率高。

4.代码实现

    private static int binarySearch(int[] numbers, int value) {
        int index = -1;
        int low = 0;
        int high = numbers.length - 1;
        while (low <= high) {
            int mid = (low + high) / 2;
            if (numbers[mid] == value) {
                return mid;
            } else if (numbers[mid] < value) {
                low = mid + 1;
            } else if (numbers[mid] > value) {
                high = mid - 1;
            }
        }
        return index;
    }

测试

    public static void main(String[] args) {
        int[] numbers = new int[]{1, 11, 19, 21, 25, 27, 33, 38, 55, 87};

        int index = binarySearch(numbers, 19);

        System.out.printf("元素[%d]下标:[%d]", 19, index);
    }

执行结果:

元素[19]下标:[2]

上述的代码只是最简单的实现,我们可以对照 Java 中的 Arrays 中的实现来看看。

  • java.util.Arrays#binarySearch0(int[], int, int, int)
    // Like public version, but without range checks.
    private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                     int key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            int midVal = a[mid];

            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }

区别点1:

int mid = (low + high) >>> 1, >>> 这种位移操作性能更高。

区别点2:

int midVal = a[mid]; 获取到中间元素值后,进行比较,而不是每次比较都去取一次,虽然是一个常数的时间

区别3:

返回值 -(low + 1),可能比 -1 更有意义吧,这个仁者见仁了

问题点:

其实我们的取中间位置的写法其实是有些问题的,如果 low 和 high 两个值比较大的话,有可能会超过 int 的大小限制,我们可以改写成

int mid = low+((high-low)>>1)

5.递归实现

这里我们来复习了一下之前章节的递归算法

    private static int binarySearchInternally(int[] numbers, int low, int high, int key) {
        if (low > high || low >= numbers.length) {
            return -(low + 1);
        }
        int mid = low + ((high - low) >> 1);
        int midVal = numbers[mid];
        if (midVal == key) {
            return mid;
        } else if (midVal < key) {
            return binarySearchInternally(numbers, mid + 1, high, key);
        } else {
            return binarySearchInternally(numbers, low, mid - 1, key);
        }
    }

执行结果:

普通实现-元素[19]下标:[2]
递归实现-元素[19]下标:[2]

6.二分查找限制多

二分查找的速度非常快,不过它的应用场景也非常有局限性,并不是什么情况都可以用二分查找。

二分查找的限制:

  • 二分查找依赖的是顺序表结构,简单说就是数组
  • 二次查找的数组必须是有序的,如果一个数组没有进行排序,我们可以先使用快排进行排序,再进行二分查找,对于一个数组没有频繁的插入和删除操作,我们可以进行一次排序,多次二分查找,这样对排序成本进行均摊,可以降低二分查找的成本
  • 数据量太小的不适合二分查找,小的话直接遍历即可
  • 数据量太大也不适合二分查找,这个是由于数组的限制,数组的数据结构要求必须是连续的空间,如果数据量非常大比如 1GB,如果我们的服务器上的内存没有连续的 1GB 的空间(即使剩余空间 2GB 也不一定有 1GB 的连续空间),就无法进行查找

7.小结

二分查找是一种针对有序数据的高效查找算法,时间复杂度是 O ( l o g n ) O(logn) O(logn) 。但应用场景也比较有限。底层必须依赖数组,并且还要求数据是有序的。对于较小规模的数据查找,我们直接使用顺序遍历就可以了,二分查找的优势并不明显。二分查找更适合处理静态数据,也就是没有频繁插入和删除的数据。

8.参考

  • 极客时间 -《数据结构与算法之美》王争
  • 得到 -《谷歌方法论》吴军
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>