LeetCode 数组专题 1:二分查找

二分查找法

说明:二分查找法在代码实现上有模板方法,一定要掌握。

1、二分查找法的使用前提:数组一定要是排好序的,如果数组不是排好序的,二分查找法便不能使用。

2、实现二分查找法的关键之处:维护循环不变量的定义。如果修改了区间边界的定义,算法得相应改变。

技巧:可以使用小数据量进行测试。

下面给出的问题不是标准的二分查找问题,但是可以用二分查找的思想来解决,稍微要绕一点弯子。其中,第 300 题要用到第 35 题。

LeetCode 第 34 题:在排序数组中查找元素的第一个和最后一个位置

传送门:34. 在排序数组中查找元素的第一个和最后一个位置。

给定一个按照升序排列的整数数组 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]

思路:使用二分查找,我使用的是二分查找法我认为最好用的模板写法。

1、循环可以继续的条件是 l < r,这样退出循环的时候 l == r 成立,因此就不用纠结返回 l 还是 r 了;

不过要特别注意一点:我们是通过夹逼的方式把搜索的范围逼到一个点,那么这个点是不是符合要求还要单独做判断。

2、循环体比较简单,真正地做到了“二分”,即“写两个分支”作判断,只要分支条件写正确,其中一个分支一定可以排除掉中点,而另一个分支则保留了中点;

3、取“中点”的操作有 2 种,根据循环体的收缩情况,采用合适的中点方法,这一点很重要,否则会出现死循环。

(1)mid = l + (r - l) // 2,特点:在只有两个数的时候,靠近左边。

(2)mid = l + (r - l + 1) // 2,特点:在只有两个数的时候,靠近右边。

例如:循环体是 l = mid + 1r = mid 的时候,表示左端点不断右移,则选择(1),否则会出现死循环;

循环体是 l = midr = mid - 1 的时候,表示右端点不断左移,则选择(2),否则会出现死循环。

Python 代码:

class Solution:
    def searchRange(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        left = self.__find_lower_bound(nums, target)
        if left == -1:
            return [-1, -1]

        right = self.__find_up_bound(nums, target)
        return [left, right]

    def __find_lower_bound(self, nums, target):
        # 找到小于等于 target 的第 1 个元素的索引
        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:
                l = mid + 1
            else:
                assert nums[mid] >= target
                r = mid
        # 最后还要单独判断一下
        if nums[l] != target:
            return -1
        return l

    def __find_up_bound(self, nums, target):
        # 找到大于等于 target 的最后 1 个元素的索引
        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:
                r = mid - 1
            else:
                assert nums[mid] <= target
                l = mid
        # 最后还要单独判断一下
        if nums[l] != target:
            return -1
        return l


if __name__ == '__main__':
    solution = Solution()
    nums = [5, 7, 7, 8, 8, 10]
    target = 8
    result = solution.searchRange(nums, target)
    print(result)

LeetCode 第 35 题:搜索插入位置

传送门:35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:

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

示例 2:

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

示例 3:

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

示例 4:

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

分析:仔细分析题意,返回的是大于等于 target 的索引,有可能是最后一个。

关键之一:如果 target 比 nums 所有的数都大,则最后一个数的索引 + 1 就是候选值,因此,右边界应该是数组的长度。

关键之二:二分的逻辑一定要写对,否则会出现死循环或者数组下标越界。

Python 代码:

class Solution:

    # 返回的是大于等于 target 的索引,有可能是最后一个

    def searchInsert(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """

        size = len(nums)
        if size == 0:
            return 0
        l = 0
        r = size

        while l < r:
            mid = l + (r - l) // 2
            if nums[mid] >= target:
                r = mid
            else:
                assert nums[mid] < target
                l = mid + 1
        return l


if __name__ == '__main__':
    nums = [1, 3, 5, 6]
    target = 5
    solution = Solution()
    result = solution.searchInsert(nums, target)
    print(result)

LeetCode 第 300 题:最长上升子序列

传送门:300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路:自己写一个辅助数组,用二分查找完成数组的覆盖或者插入,遍历完整个输入数组,辅助数组的长度就是所求。其实这道题的一个子过程就是 LeetCode 第 35 题:搜索插入位置。这个思路用到的策略是贪心算法,技巧和二分查找

关键:找大于等于“当前遍历的那个数”的第 1 个索引,将它替换成“当前遍历的那个数”,这样使得这个数变小,后面才有可能接一个更大的数。

注意:辅助数组不一定是一个最长上升子序列,但辅助数组的长度一定是最长上升子序列的长度。

LeetCode 第 300 题:最长上升子序列-1
LeetCode 第 300 题:最长上升子序列-2
LeetCode 第 300 题:最长上升子序列-3

Python 代码:

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

        size = len(nums)
        if size < 2:
            return size
        # 最长上升子序列
        lis = []
        for num in nums:
            # 找到大于等于 target 的第 1 个数
            l = 0
            r = len(lis)
            while l < r:
                mid = l + (r - l) // 2
                if lis[mid] >= num:
                    r = mid
                else:
                    l = mid + 1
            if l == len(lis):
                lis.append(num)
            else:
                lis[l] = num
        return len(lis)


if __name__ == '__main__':
    nums = [10, 9, 2, 5, 3, 7, 101, 18]
    solution = Solution()
    result = solution.lengthOfLIS(nums)
    print(result)

说明:这道题还可以用动态规划来完成。

(本节完)

你可能感兴趣的:(LeetCode 数组专题 1:二分查找)