美文网首页
[LeetCode]34、在排序数组中查找元素的第一个和最后一个

[LeetCode]34、在排序数组中查找元素的第一个和最后一个

作者: 河海中最菜 | 来源:发表于2019-07-28 10:01 被阅读0次

    题目描述

    给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

    你的算法时间复杂度必须是 O(log n) 级别。

    如果数组中不存在目标值,返回 [-1, -1]。

    示例 1:

    输入: nums = [5,7,7,8,8,10], target = 8
    输出: [3,4]
    示例 2:

    输入: nums = [5,7,7,8,8,10], target = 6
    输出: [-1,-1]

    思路解析

    两个问题:查找相等的最左边和最右边
    二分查找:思路很简单,细节是魔鬼。深入细节,比如不等号是否应该带等号,mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。

    while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,因为没有数字既大于等于 3 又小于等于 2的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。

    while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [2, 2],这时候搜索区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。

    当然,如果你非要用 while(left < right)也可以,我们已经知道了出错的原因,就打个补丁好了

    1. 为什么 left = mid + 1,right = mid - 1?我看有的代码是 right = mid或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断?

    答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。

    刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢?

    当然是 [left, mid - 1] 或者 [mid + 1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除

    1. 此算法有什么缺陷?

    答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。

    比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 22,没错。但是如果我想得到 target 的左侧边界,即索引 11,或者我想得到 target 的右侧边界,即索引 33,这样的话此算法是无法处理的。

    这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
    本题:

    因为我们需找到 target 的最左侧索引
    所以当 nums[mid] == target 时不要立即返回
    而要收紧右侧边界以锁定左侧边界
    因为我们需找到 target 的最右侧索引
    所以当 nums[mid] == target 时不要立即返回
    而要收紧左侧边界以锁定右侧边界

    又因为收紧左侧边界时必须 left = mid + 1

    class Solution:
        def searchRange(self, nums, target):
            size = len(nums)
            # 特判,这一步很重要,否则执行到后序方法可能会出现数组下标越界
            # 同时后序两个方法也不用做特殊判断了
            if size == 0:
                return [-1, -1]
    
            num1 = self.__find_lower_bound(nums, target)
            # 细节:如果左边界都搜索不到,右边界也没有必要看了
            if num1 == -1:
                return [-1, -1]
    
            num2 = self.__find_up_bound(nums, target)
            return [num1, num2]
    
        def __find_lower_bound(self, nums, target):
            # 找大于等于 target 的第 1 个数的索引,小于一定不符合要求
            size = len(nums)
            left = 0
            right = size - 1
            while left < right:
                # 根据分支逻辑,这里选择左中位数
                # mid = left + (right - left) // 2
                mid = (left + right) >> 1
                # 因为找大于等于 target 的第 1 个数,因此小于一定不符合要求
                # 把它写在分支的前面
                if nums[mid] < target:
                    left = mid + 1
                else:
                    right = mid
            # 因为有可能不存在目标元素,最后一定要单独判断一下
            if nums[left] != target:
                return -1
            return left
    
        def __find_up_bound(self, nums, target):
            # 找小于等于 target 的最后 1 个数的索引,大于一定不符合要求
            # 因为有可能不存在,最后一定要单独判断一下
            size = len(nums)
            left = 0
            right = size - 1
            while left < right:
                # 根据分支逻辑,这里选择右中位数
                # mid = left + (right - left + 1) // 2
                mid = (left + right + 1) >> 1
                # 因为找小于等于 target 的最后 1 个数,因此大于一定不符合要求
                # 把它写在分支的前面
                if nums[mid] > target:
                    right = mid - 1
                else:
                    left = mid
            # 因为有可能不存在目标元素,最后一定要单独判断一下
            if nums[left] != target:
                return -1
            return left
    
    
    AC34

    相关文章

      网友评论

          本文标题:[LeetCode]34、在排序数组中查找元素的第一个和最后一个

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