【算法日积月累】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)

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

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

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

二分查找的 个变形问题

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:代码文件夹。

(本节完)

你可能感兴趣的:(【算法日积月累】12-二分查找法)