【算法日积月累】12-二分查找法

基于有序数组的二分查找法

查找问题是计算机中非常重要的一类基础问题,也是我们生活中常见的问题。

在我们的生活中,要想方便地“找东西”,就要在“存东西”的时候多花点心思。例如:我们在“存东西”的时候,会有意识地“分门别类”放好。在我们的计算机中,文件夹呈现的树形结构,就是一种分类存放的策略,我们想要找张三的资料,肯定不会去李四的文件夹下查找。我们去图书馆要找一本《算法》的书,我们肯定不会去标明“文学”的那个书架上找。

学习过 Java 语言的朋友们一定知道 hash 表,我们可以给放进集合中的对象计算一个 hash 值,以后取出来的时候通过计算 hash 值得到之前放进去的对象,这也是一种存放数据和读取数组的策略的策略。

那么“二分查找法”要求数据是怎么存的呢?很简单,只要数据是顺序摆放的,我们就可以用“二分查找算法”。即二分查找法的使用前提:对于有序的数列才能使用二分查找法,如果数组无序,就不能使用二分查找法。

“二分查找法”能够根据数组的索引大大减少每次查找所需的比较次数。因为每次查找都能缩短一半的查找区间。

二分查找法的思想在 1946 年就被提出来了。但是第 1 个没有 bug 的二分查找法在 1962 年才出现。这个 bug 就在于查找两个数的中间数,如何避免为了求中间数而导致的整形溢出,避免这个 bug 的方法我们在介绍归并排序时介绍过。

二分查找算法要求数组是顺序数组,但是在我们的生活中,还有这样的场景:我们购买的物品是一件一件买回来的,有时,我们还会丢弃一些物品,那么是不是我们每增加或者丢弃一些物品的时候,都要将他们重新整理一下呢?

二分查找问题在 LeetCode 上的模板问题是第 704 题。

LeetCode 第 704 题:二分查找

传送门:704. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

下面是标准的二分查找代码:

Python 代码:

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        size = len(nums)

        if size == 0:
            return -1

        l = 0
        r = size - 1

        while l <= r:
            mid = l + (r - l) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] > target:
                # [1,2,3,4,5,6,7,8]
                r = mid - 1
            else:
                l = mid + 1
        return -1

二分查找法的模板,可以参考二分查找算法模板。自从我看到了这个模板以后,二分问题都这样思考,不会错。

Python 代码1:

class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        size = len(nums)

        if size == 0:
            return -1

        l = 0
        r = size - 1

        while l < r:
            mid = l + (r - l + 1) // 2
            if nums[mid] <= target:
                # mid 在,因此,不能排除 mid
                l = mid
            else:
                assert nums[mid] > target
                # mid 不在,所以可以排除 mid
                r = mid - 1
        # 为了避免不存在的情况出现,最后要判断一下
        if nums[l] == target:
            return l
        return -1

Python 代码2:

 class Solution:
    def search(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        size = len(nums)

        if size == 0:
            return -1

        l = 0
        r = size - 1

        while l < r:
            mid = l + (r - l) // 2
            if nums[mid] < target:
                # mid 可以排除,所以要 + 1
                l = mid + 1
            else:
                assert nums[mid] >= target
                # mid 不能排除,所以至少是 mid 这个位置
                r = mid
        # 为了避免不存在的情况出现,最后要判断一下
        if nums[l] == target:
            return l
        return -1

如何写好二分查找法的

在理解的基础上记住模板。

1、无脑地先写上 while l < r 作为循环可以继续下去的条件;
说明:这样写的好处是,我们就不必纠结应该返回 l 还是 r 了,此时返回 l 或者 r 都行 。 因为使用 l < r ,退出循环的时候,就一定满足 l == rtrue。不过这里要特别强调的一点是:如果你能确定给出的数组中一定包含给定的元素,那么直接返回 l 就可以了。否则,还要单独做判断

2、无脑地先写上 mid = l + ((r - l) >> 1) ,根据循环体的情况,判断是否要修改成 mid = l + ((r - l + 1) >> 1)

说明:

Python 代码:下面取的是靠近左边界的中点

mid = l + ((r - l) >> 1)

Python 代码:下面取的是靠近右边界的中点

mid = l + ((r - l + 1) >> 1)

你可以拿数组只有 2 个元素的情况来理解这个事实,这一点一定要非常清楚,否则我们写出来的代码有可能陷入死循环。

3、逻辑上真正实现“二分”,我们不用写 3 个分支作判断,根据写好的分支判断第 2 步取中点是靠左还是靠右。

这一步的关键是要判断清楚中点是不是你要找的元素,如果是,就不能排除它,如果不是就可以排除它

二分查找的 4 个变形问题

1、查找第一个值等于给定值的元素

Python 代码:

# 查找第一个值等于给定值的元素
def binary_search_1(nums, target):
    size = len(nums)
    l = 0
    r = size - 1
    while l < r:
        mid = l + ((r - l) >> 1)
        # 1,2,3,3,3,3,4,5
        if nums[mid] < target:
            l = mid + 1
        else:
            assert nums[mid] >= target
            # mid 有可能是最优解
            r = mid
    if nums[l] != target:
        return -1
    return l


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3, 4, 5]
    target = 6
    result = binary_search_1(nums, target)
    print(result)

2、查找最后一个值等于给定值的元素

Python 代码:

# 查找最后一个值等于给定值的元素
def binary_search_2(nums, target):
    size = len(nums)
    l = 0
    r = size - 1
    while l < r:
        mid = l + ((r - l + 1) >> 1)
        # 1,2,3,3,3,3,4,5
        if nums[mid] > target:
            r = mid - 1
        else:
            assert nums[mid] <= target
            # mid 有可能是最优解
            l = mid
    if nums[l] != target:
        return -1
    return l


if __name__ == '__main__':
    nums = [1, 2, 3, 3, 3, 3, 4, 5]
    target = 3
    result = binary_search_2(nums, target)
    print(result)

3、查找第一个大于等于给定值的元素(floor

Python 代码:

# 查找第一个大于等于给定值的元素
def binary_search_3(nums, target):
    size = len(nums)
    l = 0
    # 注意,r 向右边加了 1 位,因为 target 可能比 nums[-1] 还要大
    r = size
    while l < r:
        mid = l + ((r - l) >> 1)
        # 1,3,3,3,3,4,5
        if nums[mid] >= target:
            r = mid
        else:
            assert nums[mid] < target
            # mid 有可能是最优解
            l = mid + 1
    # 特判
    # if l == size - 1 and nums[-1] < target:
    #     return size
    return l


def binary_search_3_1(nums, target):
    size = len(nums)
    l = 0
    r = size - 1
    while l < r:
        mid = l + ((r - l) >> 1)
        # 1,3,3,3,3,4,5
        if nums[mid] >= target:
            r = mid
        else:
            assert nums[mid] < target
            # mid 有可能是最优解
            l = mid + 1
    # 特判
    if l == size - 1 and nums[-1] < target:
        return size
    return l


if __name__ == '__main__':
    nums = [1, 3, 3, 3, 3, 4, 5]
    target = 3
    result = binary_search_3_1(nums, target)
    print(result)

4、查找最后一个小于等于给定值的元素( ceiling

Python 代码:

# 查找最后一个小于等于给定值的元素
def binary_search_4(nums, target):
    size = len(nums)
    # 注意,r 向左边减了 1 位,因为 target 可能比 nums[0] 还要小
    l = -1
    r = size - 1
    while l < r:
        mid = l + ((r - l + 1) >> 1)
        # 1,3,3,3,3,4,5
        if nums[mid] <= target:
            # 不能排除 mid,并且最终值可能比 mid 大
            l = mid
        else:
            assert nums[mid] > target
            # mid 有可能是最优解
            r = mid - 1
    # 特判
    # if l == size - 1 and nums[-1] < target:
    #     return size
    return l


if __name__ == '__main__':
    nums = [1, 3, 3, 3, 3, 4, 5]
    target = 9
    result = binary_search_4(nums, target)
    print(result)

本文源代码

Python:代码文件夹,Java:代码文件夹

(本节完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容