LeetCode 35 Search Insert Position

题目-搜索插入位置

【英文版】https://leetcode.com/problems/search-insert-position/
【中文版】https://leetcode-cn.com/problems/search-insert-position/

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例

输入: [1,3,5,6], 5  输出: 2   输入: [1,3,5,6], 2  输出: 1
输入: [1,3,5,6], 7  输出: 4   输入: [1,3,5,6], 0  输出: 0

暴力求解

由于数组以排好序,只要从头到尾遍历一次,只要nums[i]>=target就返回i
★ 时间复杂度:\(O(n)\)
★ 空间复杂度:\(O(1)\)

Brute Force-python-1

class Solution(object):
    def searchInsert(self, nums, target):
        for i in range(len(nums)):
            if nums[i] < target:
                i+=1
            else:
                return i
        return i

Runtime: 32 ms, faster than 86.48% of Python online submissions for Search Insert Position.
Memory Usage: 12.3 MB, less than 24.56% of Python online submissions for Search Insert Position.

代码可精简部分:
用列表生成器复制nums中所有小于target的元素,最后返回生成列表的长度---可一行代码完成

Brute Force-python-2

class Solution(object):
    def searchInsert(self, nums, target):
        return len([element for element in nums if element < target])

Runtime: 36 ms, faster than 63.09% of Python online submissions for Search Insert Position.
Memory Usage: 12.4 MB, less than 10.53% of Python online submissions for Search Insert Position.

二分查找

该部分由整理liweiwei1419的LeetCode 25解答而来,推荐有空的小伙伴可以看下原作更详细的回答。

利用了数组有序的特点,在每一次的搜索过程中,都可以排除将近一半的数,使得搜索区间越来越小,直到区间成为一个数。[1]
思想
“排除法”即:在每一轮循环中排除一半以上的元素,于是在对数级别的时间复杂度内,就可以把区间“夹逼” 只剩下 1 个数,而这个数是不是我们要找的数,单独做一次判断就可以了。
[2]
★ 时间复杂度:\(O(logn)\)
★ 空间复杂度:\(O(1)\)
注意

  1. 中位数的取法
    ① 整型溢出
    • int mid = (left + right) / 2
    在 left 和 right 都比较大的时候,left + right 很有可能超过 int 类型能表示的最大值,即整型溢出
    • mid = left + (right - left) // 2
    在 right 很大,且 left 是负数且很小的时候会溢出;只不过一般情况下 left 和 right 表示的是数组索引值,left 是非负数,因此 right - left 溢出的可能性很小。因此,它是正确的写法。
    • 更推荐的写法-无符号右移(Java)
    int mid = (left + right) >>> 1;
    如果这样写, left + right 在发生整型溢出以后,会变成负数,此时如果除以 2 ,mid 是一个负数,但是经过无符号右移,可以得到在不溢出的情况下正确的结果。
    解释“无符号右移”:
    在 Java 中,无符号右移运算符 >>> 和右移运算符 >> 的区别如下:
    右移运算符 >> 在右移时,丢弃右边指定位数,左边补上符号位;
    无符号右移运算符 >>> 在右移时,丢弃右边指定位数,左边补上 00,也就是说,对于正数来说,二者一样,而负数通过 >>> 后能变成正数。

    ② 元素个数

    当数组元素个数为偶数的时候,中位数有左中位数与右中位数之分。
    当元素个数为偶数:
     mid=left+(right-left)//2, 得到左中位数的索引
     mid=left+(right-left+1)//2,得到右中位数的索引
    当元素个数为奇数:
     两种取法都可以取得中位数

  2. 循环内只写两个分支。
    一个分支排除中位数,另一个分支不排除中位数,循环中不单独对中位数作判断。
    既然是“夹逼”法,没有必要在每一轮循环开始前单独判断当前中位数是否是目标元素,因此分支数少了一支,代码执行效率更高。
    每次循环开始的时候都单独做一次判断,在统计意义上看,二分时候的中位数恰好是目标元素的概率并不高,并且即使要这么做,也不是普适性的,不能解决绝大部分的问题。
  3. 二分的逻辑一定要写对!
    否则会出现死循环或者数组下标越界。即便后面加入考虑特殊情况的语句,代码也不能通过所有测试。如下面的代码:
    Binary Search (wrong)-python
    
    class Solution(object):
        def searchInsert(self, nums, target):
            if len(nums)==1 and nums[0] < target:
                return 1
            elif len(nums)==1:
                return 0
    
            left=0
            right=len(nums)-1
    
            while left < right:
                mid = int((left + right) / 2)
                if nums[mid]  target:
                    left=mid
                elif nums[mid]==target:
                    return mid
                else:
                    right=mid
            return left
    

    代码可修改部分:
    ① 开头的特殊情况判断,可以改为更加适合多数测试案例的,直接判断数组最后一位是否小于target
    ② 修改中位数索引的取法
    ③ 二分的逻辑不对!排除后应该是不包括中位数,而不是把中位数又赋值给左右索引。
    ④ 没有必要单独判断当前中位数是否为目标元素。

    每次判断应该至少有一个区间收缩,排除后应该是不包括中位数,而不是把中位数又赋值给左右索引。以下是“排除中位数的逻辑”思考清楚以后,可能出现的两个模板代码
  4. 根据分支逻辑选择中位数的类型
    可能是左中位数,也可能是右位数,选择的标准是避免死循环 死循环容易发生在区间只有 22 个元素时候,此时中位数的选择尤为关键。选择中位数的依据是:避免出现死循环。我们需要确保:
      ① 如果分支的逻辑,在选择左边界的时候,不能排除中位数,那么中位数就选“右中位数”,只有这样区间才会收缩,否则进入死循环;
    因为在区间中的元素只剩下 2个元素时候,例如:left = 3,right = 4。此时左中位数就是左边界,如果你的逻辑执行到 left = mid 这个分支,且你选择的中位数是左中位数,此时左边界就不会得到更新,区间就不会再收缩(理解这句话是关键),从而进入死循环;
    为了避免出现死循环,你需要选择中位数是右中位数,当逻辑执行到 left = mid 这个分支的时候,因为你选择了右中位数,让逻辑可以转而执行到 right = mid - 1 让区间收缩,最终成为 1 个数,退出 while 循环。
    choose left-python
    
    while left < right:
          # 不妨先写左中位数,看看你的分支会不会让你代码出现死循环,从而调整
        mid = left + (right - left) // 2
        # 业务逻辑代码
        if (check(mid)):
            # 选择右边界的时候,可以排除中位数
            right = mid - 1
        else:
            # 选择左边界的时候,不能排除中位数
            left = mid
    
      ② 同理,如果分支的逻辑,在选择右边界的时候,不能排除中位数,那么中位数就选“左中位数”,只有这样区间才会收缩,否则进入死循环。
    choose right-python
    
    while left < right:
          # 取右中位数
        mid = left + (right - left + 1) // 2
        # 业务逻辑代码
        if (check(mid)):
            # 选择左边界的时候,可以排除中位数
            left = mid + 1
        else:
            # 选择右边界的时候,不能排除中位数
            right = mid
    
  5. 后处理 退出循环的时候,可能需要对“夹逼”剩下的那个数单独做一次判断,这一步称之为“后处理”。
    “区间左右边界相等(即收缩成 1 个数)时,这个数是否会漏掉”:
      ① 如果你的业务逻辑保证了你要找的数一定在左边界和右边界所表示的区间里出现,那么可以放心地返回 left 或者 right,无需再做判断
       ② 如果你的业务逻辑不能保证你要找的数一定在左边界和右边界所表示的区间里出现,那么只要在退出循环以后,再针对 nums[left] 或者 nums[right] (此时 nums[left] == nums[right])单独作一次判断,看它是不是你要找的数即可,这一步操作常常叫做“后处理”。
Binary Search-python

class Solution(object):
    def searchInsert(self, nums, target):
        if nums[len(nums) - 1] < target:
            return len(nums)

        left = 0
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return left

Runtime: 32 ms, faster than 86.48% of Python online submissions for Search Insert Position.
Memory Usage: 12.3 MB, less than 56.14% of Python online submissions for Search Insert Position.

 

总结

同类型题目

  1. LeetCode 69 Sqrt(x)

  2. LeetCode 704 Binary Search

参考


  1. 超级优秀的binary search详解 ↩︎

  2. 画解算法by灵魂画师牧码 ↩︎

posted @ 2019-08-25 15:39  维夏十四  阅读(255)  评论(0编辑  收藏  举报