vector 查找_怎么写出无bug的二分查找算法代码

94f229b92d1be5cbb83f9bdda832e0a4.png

封面图来自 geeksforgeeks

1 简介

二分查找算法是一类比较基础的算法。然而想要短时间内,写出二分查找的无 bug 版本,也不是很容易的。为此我查找了一些资料,终于弄清了二分查找算法的套路,在此分享给大家,也算是对自己学习知识的一个总结。

2 算法介绍

还是来简单介绍一下二分查找算法,了解算法的目的、思路、套路,才能让我们更好地写出对应的代码。

2.1 算法目的

顾名思义,这是一类查找算法,目的是在一个容器(比如 vector)中查找某个符合要求的元素。在我看来,下面三种情景能解决大部分的二分查找问题(以 vector 为例):

  1. 假设 vector 中没有重复元素,查找 vector 中是否存在某个元素。如果存在,返回它的索引,否则返回 -1。
  2. 假设 vector 中存在重复元素,查找 vector 中第一个符合某条件的元素。如果存在,返回它的索引,否则返回 -1。
  3. 假设 vector 中存在重复元素,查找 vector 中最后一个符合某条件的元素。如果存在,返回它的索引,否则返回 -1。

2.2 算法思路

顾名思义,算法是使用“二分”的方法来进行查找的,这里的“二分”,其实是“减而治之”的意思。怎么理解这里的“减而治之”呢,我自己是这样理解的:每次我们将 vector 分成两半,根据分界点的情况,决定我们往哪一边进行查找,直到最终找到符合条件的元素。这里有几个注意点:

  1. 首先要求 vector 中的元素是有序的。因为如果元素无序,那么我们没有办法根据分界点的情况来判断可行解的范围,换句话说,它不能提供给我们额外的,关于 vector 前半部分或者后半部分的信息。
  2. 在每一次判断分界点的情况后,我们都需要缩小可行解的范围,不然的话,算法就可能陷入死循环跳不出来。
  3. 我们需要保证,在每次缩小范围时,不会影响解的情况。换句话说,每次我们排除的,都是非可行解的那一部分。
  4. 我们需要仔细思考边界情况。上面都是在说,我们怎么缩小搜索范围,那么当搜索范围足够小之后(比如搜索范围为空,或者搜索范围里只有一个元素),我们需要返回什么。

上面说的几点保证了算法的正确性。而我们写代码时,经常出错的地方,就是没有处理好上面这几点,导致数组越界,或者陷入死循环等。

3 算法实现

OK,在知道算法的作用原理之后,其实写代码也不难了,只需要对上面说的几点加以注意就好了。

3.1 情况一

假设 vector 中没有重复元素,查找 vector 中是否存在某个元素。如果存在,返回它的索引,否则返回 -1。

int binary_search(vector<int>& nums, int target) {
    int lo = 0, hi = nums.size() - 1; // 在闭区间 [lo, hi] 中查找
    // 退出查找的条件是:查找范围为空
    while (lo <= hi) {
        int mi = lo + (hi - lo) / 2;
        if (nums[mi] < target) {
            lo = mi + 1; // mi 不是可行解,范围变成 [mi+1, hi]
        } else if (nums[mi] > target) {
            hi = mi - 1; // mi 不是可行解,范围变成 [lo, mi-1]
        } else {
            return mi;
        }
    }
    return -1;
}

看代码中的 if 判断语句,三个分支中,要么是退出循环,要么是缩小了范围。因此,查找范围会不断减小,直到为空,对应了 while 语句中的判断。

3.2 情况二

假设 vector 中存在重复元素,查找 vector 中第一个符合某条件的元素。如果存在,返回它的索引,否则返回 -1。

我们将这种情况换成一个更具体的例子:假设 vector 是一个 0 和 1 组成有序序列,前面都是0,后面都是1,求第一个 1 元素的索引。我们把这种情况称为:0011 模式下查找第一个1。

int binary_search(vector<int>& nums) {
    int lo = 0, hi = nums.size() - 1; // 在闭区间 [lo, hi] 中查找
    // 退出查找的条件是:查找范围为1个元素
    while (lo < hi) {
        int mi = lo + (hi - lo) / 2;
        if (nums[mi] == 1) {
            hi = mi; // mi 在可行解区间中,可行解区间范围缩小为 [lo, mi]
        } else {
            lo = mi + 1; // mi 不在可行解区间中,可行解区间范围缩小为 [mi+1, hi]
        }
    }
    return lo;
}


当 nums[mi] == 1 时,mi 可能是最终的解,因此需要保留,范围缩小为 [lo, mi];当 nums[mi] != 1 时,mi 不可能是最终的解,因此不需保留,范围缩小为 [mi+1, hi]。

3.3 情况三

假设 vector 中存在重复元素,查找 vector 中最后一个符合某条件的元素。如果存在,返回它的索引,否则返回 -1。

我们将这种情况换成一个更具体的例子:假设 vector 是一个 0 和 1 组成有序序列,前面都是1,后面都是0,求最后一个 1 元素的索引。我们把这种情况称为:1100 模式下查找最后一个1。

int binary_search(vector<int>& nums) {
    int lo = 0, hi = nums.size() - 1; // 在闭区间 [lo, hi] 中查找
    // 退出查找的条件是:查找范围为1个元素
    while (lo < hi) {
        int mi = lo + (hi - lo + 1) / 2; // +1 保证可行解区间范围缩小
        if (nums[mi] == 1) {
            lo = mi; // mi 在可行解区间中,可行解区间范围缩小为 [mi, hi]
        } else {
            hi = mi - 1; // mi 不在可行解区间中,可行解区间范围缩小为 [lo, mi-1]
        }
    }
    return lo;
}

当 nums[mi] == 1 时,mi 可能是最终的解,因此需要保留,范围缩小为 [mi, hi];当 nums[mi] != 1 时,mi 不可能是最终的解,因此不需保留,范围缩小为 [lo, mi-1]。需要注意的是,在这种情况下,mi 的取值是 lo +(hi - lo +1)/2,在这里 +1 的目的是保证可行解区间范围缩小(想象 lo = hi-1 的情况)。

4 实战

上面这三种情况足以应对绝大部分的二分查找的情景,只需要确定原始序列中的 “0” 和 “1” 分别表示什么含义,这就是二分查找算法的套路。

我们找 leetcode 上几道典型的二分查找的题目练练手看看。

4.1 Find First and Last Position of Element in Sorted Array

Given an array of integers nums sorted in ascending order, find the starting and ending position of a given target value.
Your algorithm's runtime complexity must be in the order of O(log n).
If the target is not found in the array, return [-1, -1].

题目让我们在数组中查找某个元素出现的第一个位置以及最后一个位置。在这里

  • 第一个位置就对应了算法实现中的“情况二”,在这个情况下,我们要查找第一个 “1” ,这里的 “1” 表示 “大于等于 target”。
  • 最后一个位置就对应了算法实现中的“情况三”,在这个情况下,我们要查找最后一个 “1” ,这里的 “1” 表示 “小于等于 target”。

于是直接套上面的模版就可以轻松写出代码。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if (nums.size() == 0) return {-1, -1};
        int lo = 0, hi = nums.size() - 1;
        int left, right;
        
        // 0011 模式中第一个 1 其中 1 表示 “>= target”
        while (lo < hi) {
            int mi = lo + (hi - lo) / 2;
            if (nums[mi] >= target) hi = mi;
            else lo = mi + 1;
        }
        left = (nums[lo] == target) ? lo : -1;
        
        // 1100 模式最后一个 1,其中 1 表示 “<= target”
        lo = 0, hi = nums.size() - 1;
        while (lo < hi) {
            int mi = lo + (hi - lo + 1) / 2;
            if (nums[mi] <= target) lo = mi;
            else hi = mi - 1;
        }
        right = (nums[lo] == target) ? lo : -1;
        
        return {left, right};
    }
};

4.2 https://leetcode.com/problems/search-insert-position/

Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.
You may assume no duplicates in the array.
  • 这到题对应了算法实现中的“情况二”,查找第一个大于等于目标值的位置。
  • 注意特殊情况,当目标值比数组中所有数都大时,需要特殊处理。
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        if (nums.back() < target) return nums.size(); // 特殊情况
        // 0011 模式下的第一个 1,1 表示大于等于 target
        int lo = 0, hi = nums.size() - 1;
        while (lo < hi) {
            int mi = lo + (hi - lo) / 2;
            if (nums[mi] >= target) {
                hi = mi;
            } else {
                lo = mi + 1;
            }
        }
        return lo;
    }
};

5 总结

总结一下,二分查找算法是一种“减而治之”的查找算法。

算法正确性的保证:序列有序、每次搜索范围都会减小、每次减小搜索范围不影响解的情况。

三种情景下对应的算法:无重复元素下的查找、0011 模式下查找第一个1、1100 模式下查找最后一个1。

大部分二分查找都可以转化为上面的三种模式,于是可以比较方便地写出代码。

6 参考资料

在写作的过程中,参考了以下一些资料,在此表示感谢

漫谈二分查找-Binary Search http:// duanple.blog.163.com/bl og/static/709717672009049528185/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值