leetcode 153 & 154 二分查找(三)—— 寻找旋转排序数组中的最小值

201 阅读3分钟

寻找旋转排序数组中的最小值”相关题目,leetcode上有多个。

解题核心思路是:二分查找。然后,区分旋转后的不同情况做具体调整。

第一题 寻找旋转排序数组中的最小值

这道题的关键点是

  • 升序排列
  • 元素互不相同
  • 返回最小元素

代码中,我依然考虑了有重复元素的情况。其一,考虑重复元素不会对代码执行带来额外负担,因为本题输入元素互不相同。其二,考虑重复元素,将本题和下一题的解法统一在一起了。

请通过注释了解思路。读者要多思考注释中的例子,通过反证法排除部分区间。

总结一下。对某个区间做分析,有四种可能:

  • 区间左端元素小于右端元素
  • 区间左端元素大于中间元素
  • 区间中间元素大于右端元素
  • 区间左端、中间、右端元素相等

充分利用已知条件,尽量缩小搜索区间

解法如下(代码提交通过)。

func findMin(nums []int) int {
    l := len(nums)
    if l == 0 {
        panic("nums is empty")
    }
    var (
        low int
        high = l-1
    )
OuterLoop:
    for low <= high {
        // 如果当前区间左端元素小于右端元素,
        // 那么该区间就是从小到大有序的,
        // 如[...,1,2,3,4,5,...],1 < 5。
        if nums[low] < nums[high] {
            // 最小值就是左端元素。
            return nums[low]
        }
        
        // 中间位置
        mid := low + (high-low)>>1
        v := nums[mid]
        
        // 如果当前区间的左端元素大于中间元素,
        // 如[...,6,7,1,2,3,4,5,...], 6 > 2。
        // 那么最小值就在索引区间[low+1,mid]中。
        // 为什么最小值不会出现在索引区间(mid,high]中呢?
        // 假如最小值出现在索引区间(mid,high]中,
        // 会是这样的:[...,6,7,1,2,3,-1,5],
        // 它旋转之前不是有序的,
        // 所以最小值不会出现在索引区间(mid,high]中。
        if nums[low] > v {
            // nums[low]比某个元素大,它肯定不是最小元素,所以不包含。
            low++
            // nums[mid]较小,可能是最小元素,要包含。
            high = mid
            continue
        }
        
        // 如果当前区间的中间元素大于右端元素,
        // 如[...,4,5,6,7,1,2,3,...],7 > 3。
        // 那么最小元素就在索引区间(mid,high]中。
        // 为什么最小值不会出现在索引区间[low,mid]中,
        // 根据前面的解释可同理反证出来。
        if v > nums[high] {
            // nums[mid]大于某个元素,它肯定不是最小值,所以不包含。
            low = mid+1
            // nums[high]较小,可能是最小值,所以high不变。
            continue
        }
        
        // 到这里,只能是 nums[low] == nums[mid] == nums[high]。
        // 为什么不写 nums[low] <= nums[mid] <= nums[high]?
        // 如果存在小于的情况,就必然会在循环最开始的判断中处理,
        // 即 nums[low] < nums[high] 的分支。
        // 遍历该区间,尽量利用可利用的条件。
        for i := low; i < high; i++ {
            // 发现了一个较小的值,它就是最小值。
            // 如[...,9,9,1,2,9,9,9,9,9,...]
            // 或[...,9,9,9,9,9,9,1,2,9,...]
            if nums[i] > nums[i+1] {
                return nums[i+1]
            }
            // 发现一个较大的值。
            // 如[...,2,3,6,1,2,2,2,2,2,...]
            // 或[...,2,2,2,2,2,3,6,1,2,...]
            if nums[i] < nums[i+1] {
                if i+2 == mid {
                    // 如[...,2,2,2,5,2,2,2,2,2,...]
                    return v
                }
                // nums[i+1]肯定不是最小值,所以i+2索引作为新的low。
                // 如[...,2,5,0,1,2,2,2,2,2,...],i+2 < mid。
                // 如[...,2,2,2,2,2,5,0,1,2,...],i+2 > mid。
                low = i+2
                if i+2 < mid {
                    // 索引区间(mid,high]不可能出现比nums[mid]小的值了。
                    // 所以最小值就在索引区间[i+2,mid]中。
                    // nums[mid]可能是最小值,要包含它。
                    high = mid
                }
                // 注意:跳转到外层for循环
                continue OuterLoop
            }
        }
        // 全部相等,每个都是最小值
        break
    }
    return nums[low]
}

第二题 寻找旋转排序数组中的最小值 II

这道题的关键点是

  • 升序排列
  • 存在重复元素
  • 返回最小元素

直接使用上一道题的代码就可以解这道题,因为上一题的解法代码已经考虑重复元素了。

第三题 旋转数组的最小数字

这道题和上一题相同。