美文网首页
二分查找中容易忽略的细节

二分查找中容易忽略的细节

作者: 拔丝圣代 | 来源:发表于2021-09-29 00:14 被阅读0次

前言

二分查找这个算法相信大家都很熟悉,但真正在写代码的时候,对于边界条件却很容易出错,这篇文章带你分析二分查找的关键细节,看完以后不再出错。

题目

虽然都是二分查找,但其实题目可能会有细微的差别。
这里先统一一下题目:给定非严格递增的列表nums,给定x,找出其中最小值i,满足nums[i] >= x。
也就是说,如果存在多个x,则返回第一个的下标;如果不存在x,则返回大于x的下标。

代码

这里展示两段代码,看看两者的差别,你认为哪个正确,哪个错误呢:

代码1

func find(nums []int, x int) int {
    i, j := 0, len(nums)
    var m int
    for i < j {
        m = i + (j-i)/2
        if nums[m] >= x {
            j = m
        } else {
            i = m+1
        }
    }
    return i
}

代码2

func find(nums []int, x int) int {
    i, j := 0, len(nums)-1
    var m int
    for i <= j {
        m = i + (j-i)/2
        if nums[m] >= x {
            j = m-1
        } else {
            i = m+1
        }
    }
    return i
}

注意两者的差别:

  1. 初始赋值不同,一个是j = len(nums),另一个是j = len(nums)-1
  2. 循环条件不同,一个是i < j,另一个是 i <= j
  3. j每次的变化不同,一个是j = m,另一个是j = m-1

你觉得哪一种才是正确的呢?
实际上,两种都是正确的,只不过这两段代码中,j永远相差了1而已。这也导致结束后,i和j的状态不同。代码1中结束时,i == j;而代码2中结束时i = j+1,记得一定要返回i,不要返回j。

但两种不能混用,如果循环条件是i <= j,而j的迭代是j = m,则可能会造成死循环。

你可能有疑问,为什么j有两种迭代方式,而i只有一种?能不能每次迭代让i = m?这当然不行,因为i和j本来就不是对等的,原因就在于m:i <= m < j,所以迭代部分如果改成i = m可能会陷入死循环。

判断条件抽象

然后你可能要问了,如果nums数组不是递增而是递减的怎么办?如果题目改成要找的是第一个nums[m] > x 的下标,而不是nums[m] >= x怎么办?

其实我刚开始也会在边界条件纠结很久,条件稍一变化就又要重新思考,但这归根结底还是对问题思考的不够透彻,没有把问题抽象出来。

实际上,我们看代码里,只有两个地方用到了nums,其中一个是用到其长度,另一个就是判断条件。也就是说,其实二分查找根本不需要知道nums数组本身,只要知道其长度,并且能够判断是否满足条件即可。

于是我们的代码可以变成这样:

代码3

func Search(n int, f func(int) bool) int {
    i, j := 0, n
    for i < j {
        h := int(uint(i+j) >> 1)
        if !f(h) {
            i = h + 1
        } else {
            j = h
        }
    }
    return i
}

只需要传入长度和一个判断函数,就可以找到第一个满足条件的值的下标。当然,判断函数f是有约束的:如果f(i) == false, f(j) == true,那么必须满足 i < j。简单说就是左边是false,右边是true。

显然,对于文章开头描述的题目,可以这样去调用:

代码4

func SearchInts(a []int, x int) int {
    return Search(len(a), func(i int) bool { return a[i] >= x })
}

实际上,这两段代码,正是golang里面官方sort包下面的函数。
这样一抽象,问题也就看得更加透彻了,题目再怎么变化,只要修改这个判断函数f就可以了。

总结

这篇文章主要讲了关于二分查找的两个点:

  1. i每次迭代都是i = m + 1,而j在两种实现里分成两档,涉及到3个地方,别混用就行。
  2. 函数要返回i,不要返回j(j不一定错,但i一定对)。
  3. for循环中的if判断条件,根据题目进行修改即可,代码最终找到的都是“第一个满足该条件的下标”

好了,以后无论遇到二分查找的什么变种,都能顺利解决啦!

相关文章

  • 二分查找中容易忽略的细节

    前言 二分查找这个算法相信大家都很熟悉,但真正在写代码的时候,对于边界条件却很容易出错,这篇文章带你分析二分查找的...

  • 二分查找

    二分查找很基础也很常用,但是写起来细节上容易出问题,留存

  • 刷题笔记

    算法思想 一、二分查找 1. 算法思想 算法详解 算法细节 一定要看二分查找细节.md 实现时需要注意以下细节: ...

  • 二分查找法

    前言在笔试的时候,二分查找法容易写错一些细节导致最后的结果是错的;

  • python二分查找算法

    文章概述 二分查找法介绍 简单查找与二分查找对比 二分查找  二分查找算法主要思想:在有序列表中查找指定元素,先从...

  • 数据结构和算法之——二分查找下

    最简单的二分查找情况下,我们假设数组中没有重复元素,因此很容易实现。如果数组中存在重复元素,二分查找就没有想象中那...

  • 二分查找模式

    二分查找通用的模板int mid = (left + right) / 2容易溢出 二分查找的通用模板 使用“左边...

  • 二分查找

    二分查找查找是在有序列表中查找指定值的有效数算,思想很简单,但是细节和变化很多。这里主要记录最近学习到的模板,参考...

  • IOS查找算法之二分查找

    二分查找(Binary Search)算法,也叫折半查找算法。二分查找的思想非常简单,很多非计算机专业的同学很容易...

  • 容易忽略的细节

    比尔盖茨曾经说过这样一段话,“我们总是高估今后一两年内将要发生的变革,总是低估未来10年将要发生的变革,所以,不要...

网友评论

      本文标题:二分查找中容易忽略的细节

      本文链接:https://www.haomeiwen.com/subject/mesdnltx.html