二分查找-6.4 循环有序数组的最小值

题目:

对于一个有序循环数组nums,返回nums中的最小值。有序循环数组是指,有序数组左边任意长度的部分放到右边去,右边的部分拿到左边来。比如数组[1,2,3,3,4],是有序循环数组,[4,1,2,3,3]也是。

给定数组nums及它的大小n,请返回最小值。

这题在leetcode上也有类似的题目,而且还分为了数组中无重复值

153. 寻找旋转排序数组中的最小值

和数组中有重复值

154. 寻找旋转排序数组中的最小值 II

思路(对于无重复值):

对于数组中无重复值的情况,整个数组的数值要么是升序的,要么分为前后两个部分,每一部分都是升序,但是前一部分的最小值一定比最后一个部分的最大值大(见下面a,b两图)。对于任意一个不是右端点的值(图中绿色的点),它和右端点(图中橙色的点)的比较情况可以分为:比右端点小;比右端点大。

如果某一点比右端点小,有以下两种可能:

那么对于二分法,可以发现最小值一定是在绿点左边,因此去左边寻找。

如果某一点比右端点大,a中找不出这样的点,b中可能为:

 

对于二分法,最小值一定在绿点和右端点之间,因此去这一区间寻找

代码:

class Solution:
    def findMin(self, nums: List[int]) -> int:
        n = len(nums)
        left, right = 0, n-1 #右端点不动,相当于一个哨兵,因此开区间从n-1处开始二分查找
        while left < right:
            mid = left + (right-left)//2
            if nums[mid] < nums[-1]:
                right = mid
            else:
                left = mid+1
        return nums[left]

note:

这种做法似乎把搜索区间从nums[0:n]变为了nums[0:n-1],那么这么做如果nums[n-1]本身就是最小值的话,会不会被跳过呢?可以想象这种极端情况,即nums[0:n-1]的值都比nums[n-1]大,那么在循环中,left一直会+1,直到left = n-2,right = n-1,此时取中值仍然取到mid = n-2,nums[mid]<nums[n-1],left继续+1,然后left == right == n-1,跳出循环,取到最小值。

以上思路是把nums[n-1]看作哨兵元素,从而在区间[0:n-1)内寻找最小值。但是,循环内的if分支其实可以改成:if nums[mid] < nums[right]。代码如下:

class Solution:
    def findMin(self, nums: List[int]) -> int:
        n = len(nums)
        left, right = 0, n-1
        while left < right:
            # 循环的最后,left+1 = right
            # 如果nums[left] < nums[right], mid = left, right -= 1, 退出循环时是取left最小值
            # 如果nums[left] > nums[right], mid = left, left = mid+1, 退出循环时依然取最小值 
            mid = left + (right-left)//2
            if nums[mid] < nums[right]:
                right = mid-1
            else:
                left = mid
        return nums[left]

 这种做法和普通二分法的区别就是:普通二分法是查找一个数,这个数是否存在于数组中我们并不知道,所以最后left == right(以取开区间为例) 退出循环后,我们还要判断最后查找到的数是否是目标值;而查找一个数组中的最小值,一定可以找到一个答案,所以left == right时我们可以直接返回这个最小值。并且我们的区间取的是[0:n-1)而非[0:n),因为即使答案就是nums[n-1],最后退出时left == n-1,也可以找到正确答案。如果我们的区间取的是[0:n),那么在循环中不好比较nums[mid]和nums[right](因为right可能越界)。 

思路(对于有重复值):

对于有重复值的情况,数组会变得复杂一些,主要是因为存在相同的元素。那么在上一节中的2个if分支其实就应该改为3个。对于nums[mid] == nums[right]的情况,我们同样可以分为mid在循环数组前半部分或mid在循环数组后半部分

 此时,我们可以不断收缩区间,将right向左移动。如果nums[right]本身就是最小值,那么因为nums[mid] == nums[right],答案仍在查找区间内;如果nums[right]是错误答案,那么它被就排除了。

代码:

class Solution:
    def findMin(self, nums: List[int]) -> int:    
        low, high = 0, len(nums) - 1
        while low < high:
            pivot = low + (high - low) // 2
            if nums[pivot] < nums[high]:
                high = pivot
            elif nums[pivot] > nums[high]:
                low = pivot + 1
            else:
                high -= 1
        return nums[low]

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值