【Leetcode刷题】旋转数组的最小数字

https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/

朴素遍历法

class Solution(object):
    def minArray(self, numbers):
        """
        :type numbers: List[int]
        :rtype: int
        """
        # 设最小值所处位置为i,数组中最小值的特性是:numbers[i-1] > numbers[i]
        # 而数组中处于其他位置j的数字都满足 numbers[j-1] <= numbers[j]
        n = len(numbers)
        for i in range(n):
            if numbers[i - 1] > numbers[i]:
                return numbers[i]
        # 所有元素都相等
        return numbers[0]

时间复杂度:O(n)

空间复杂度:O(1)

二分查找法

使用二分查找缩小搜索范围,降低时间复杂度

下面的解法是错误的

class Solution(object):
    def minArray(self, numbers):
        """
        :type numbers: List[int]
        :rtype: int
        """
        # 设最小值所处位置为i,数组中最小值的特性是:numbers[i-1] > numbers[i]
        # 而数组中处于其他位置j的数字都满足 numbers[j-1] < numbers[j]
        # 使用二分查找找到这个位置
        # 如何缩小搜索范围:
        # 若numbers[0] > numbers[j],说明最小值的位置在j左侧
        # 若numbers[0] < numbers[j],说明最小值的位置在j右侧
        # 若numbers[0] = numbers[j],说明numbers[0,j]的值都相同,则最小值的位置在j右侧
        left = 0
        right = len(numbers) - 1
        while left <= right:
            print(left, right)
            mid = (left + right) // 2
            # 当mid=0时,numbers[mid-1]表示numbers最后一个数字
            if numbers[mid] < numbers[mid - 1]:
                return numbers[mid]
            if numbers[left] <= numbers[mid]:
                left = mid + 1
            else:
                right = mid - 1
        # 所有元素都相等
        return numbers[right]

错误的原因是:不能与数组中第一个元素比较来缩小查找范围

因为当numbers[left] < numbers[mid]时,由于numbers[left]可能是最小值,因此不能直接舍弃掉mid左半边。这个问题导致了无法通过一次比较舍弃掉数组的一半

而与数组中最后一个元素比较则能很好地将数组分成两部分。

numbers[mid] < numbers[right]时,说明最小值在mid的左侧,且最小值有可能是numbers[mid],但不可能是numbers[-1],因此可以将numbers[mid+1:]这部分舍弃

numbers[mid] > numbers[right]时,说明最小值在mid的右侧,且最小值有可能是numbers[-1],但不可能是numbers[mid],因此可以将numbers[:mid+1]这部分舍弃

相等的情况在上面的解法中也没有考虑得很全面,下面我们着重分析

numbers[mid] == numbers[right]时,最小值可能在mid的右侧,即numbers[mid:]先递增后递减;也可能在mid的左侧,即numbers[mid:]的值全部相等。那么这怎么办?

根据官方题解:

我们唯一可以知道的是,由于它们的值相同,所以无论 numbers[-1] 是不是最小值,都有一个它的「替代品」numbers[mid] ,因此我们可以忽略二分查找区间的右端点。

由以上思路可得到正确的二分查找写法:

class Solution(object):
    def minArray(self, numbers):
        """
        :type numbers: List[int]
        :rtype: int
        """
        left = 0
        right = len(numbers) - 1
        while left <= right:
            mid = (left + right) // 2
            # 当mid=0时,numbers[mid-1]表示numbers最后一个数字
            if numbers[mid - 1] > numbers[mid]:
                return numbers[mid]
            if numbers[mid] < numbers[right]:
                # 前一个判断已经排除了最小值是numbers[mid]的可能
                right = mid - 1
            elif numbers[mid] > numbers[right]:
                left = mid + 1
            else:
                # 相等时,忽略右端点
                right -= 1
        # 所有元素都相等
        return numbers[0]

时间复杂度:O(logn)

空间复杂度:O(1)

优化

考虑想办法省去numbers[mid - 1] > numbers[mid]这个判断

最终,搜索区间收敛到left = right - 1,则mid = left,left左边是一个递增序列,right右边也是一个递增序列

numbers[mid] < numbers[right]时,即numbers[left] < numbers[right],又因为left左侧序列递增,则必然有numbers[left-1] > numbers[left],因此此时结束循环返回numbers[left]即可

numbers[mid] > numbers[right]时,很明显,numbers[right]就是要找的最小值

numbers[mid] == numbers[right]时,返回任一即可

最终优化的代码为

class Solution(object):
    def minArray(self, numbers):
        """
        :type numbers: List[int]
        :rtype: int
        """
        left = 0
        right = len(numbers) - 1
        while left < right:
            mid = (left + right) // 2
            if numbers[mid] < numbers[right]:
                # 此时mid有可能是最小值,应加入搜索区间
                right = mid
            elif numbers[mid] > numbers[right]:
                left = mid + 1
            else:
                # 相等时,忽略右端点
                right -= 1
        # 所有元素都相等
        return numbers[left]
posted @ 2020-07-22 14:17  luozx207  阅读(132)  评论(0)    收藏  举报