leetcode 33 & 81 二分查找(四)—— 搜索旋转排序数组

176 阅读3分钟

在旋转排序数组中搜索某个值”相关题目在leetcode上有多个,这组题是“寻找旋转排序数组中的最小值”相关题目的扩展。

解题核心思路是:二分查找

第一题 搜索旋转排序数组

该题关键字:

  • 已按升序排序
  • 元素互不相同
  • 没找到返回-1

代码中,我依然考虑了重复元素的情况。其一,不会给代码运行增加额外负担。其二,将本题解法和下一题解法统一在一起了。

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

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

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

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

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

func search(nums []int, target int) int {
    l := len(nums)
    if l == 0 {
        return -1
    }
    if l == 1 {
        if nums[0] == target {
            return 0
        }
        return -1
    }
    var (
        low int
        high = l-1
    )
OuterLoop:
    for low <= high {
        // 中间位置
        mid := low + (high-low)>>1
        v := nums[mid]
        // 找到了
        if v == target {
            return mid
        }
        
        // 区间左端元素小于右端元素,从小到大有序,直接应用二分查找。
        // 如 [...,1,2,3,4,5,...]
        if nums[low] < nums[high] {
            if v < target {
                low = mid+1
            } else {
                high = mid-1
            }
            continue
        }
        
        // 区间左端元素大于中间元素,如 [...,5,6,7,1,2,3,4,...],5 > 1。
        if nums[low] > v {
            // target可能存在于索引区间 (mid,high] 
            if target < nums[low] && v < target {
                low = mid+1
            } else { // target可能存在于索引区间 [low, mid)
                high = mid-1
            }
            continue
        }
        
        // 区间中间元素大于右端元素,如 [...,2,3,4,5,6,7,1,...]
        if v > nums[high] {
            // target可能存在于索引区间 [low,mid)
            if target < v && nums[high] < target {
                high = mid-1
            } else { // target可能存在于索引区间 (mid, high]
                low = mid+1
            }
            continue
        }
        
        // 到此处,nums[low] == nums[mid] == nums[high]
        for i := low; i < high; i++ {
            // 遇到一个不同的值
            // 可能是下面某一种情况
            // [...,9,5,6,9,9,9,9,9,9,...]
            // [...,9,9,9,9,9,9,5,6,9,...]
            // [...,9,10,11,9,9,9,9,9,9,...]
            // [...,9,9,9,9,9,9,10,11,9,...]
            // [...,9,10,2,9,9,9,9,9,9,...]
            // [...,9,9,9,9,9,9,11,2,9,...]
            if nums[i] != nums[i+1] {
                low = i+1
                if i+1 < mid {
                    // 索引区间 [low, i] 和 [mid,high] 的元素都相等,且不等于target。
                    // 只需要在索引区间 [i+1, mid-1] 中继续搜索。
                    high = mid-1
                } else { // i+1 > mid。不可能出现 i+1 == mid
                    // 索引区间[low, i]和索引high的元素都相等,且不等于target。
                    // 只需要在索引区间 [i+1, high-1] 中继续搜索。
                    high--
                }
                continue OuterLoop
            }
        }
        // 上面for循环中元素都相等
        // 没找到
        break
    }
    return -1
}

第二题 搜索旋转排序数组 II

本题关键字:

  • 非降序排列
  • 元素值不必互不相同
  • 找到返回true,否则返回false

该题一定要考虑重复元素的情况了。代码中,除了返回值不同,其他逻辑和上一题相同,因为上一题的解法已经考虑重复元素了。

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

func search(nums []int, target int) bool {
    l := len(nums)
    if l == 0 {
        return false
    }
    var (
        low int
        high = l-1
    )
OuterLoop:
    for low <= high {
        mid := low + (high-low)>>1
        v := nums[mid]
        if v == target {
            return true
        }
        if nums[low] < nums[high] {
            if v < target {
                low = mid+1
            } else {
                high = mid-1
            }
            continue
        }
        if nums[low] > v {
            if target < nums[low] && v < target {
                low = mid+1
            } else {
                high = mid-1
            }
            continue
        }
        if v > nums[high] {
            if target < v && nums[high] < target {
                high = mid-1
            } else {
                low = mid+1
            }
            continue
        }
        for i := low; i < high; i++ {
            if nums[i] != nums[i+1] {
                low = i+1
                if i+1 < mid {
                    high = mid-1
                } else { // i+1 > mid。不可能出现 i+1 == mid
                    high--
                }
                continue OuterLoop
            }
        }
        break
    }
    return false
}

第三题 搜索旋转数组

本题关键字:

  • 已按升序排序
  • 存在重复元素
  • 要返回索引。若有多个相同元素,返回索引最小的一个。还有一个隐含细节,没找到时返回-1

解法逻辑和第一题类似。只是针对该题做了一些调整。

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

func search(arr []int, target int) int {
    l := len(arr)
    if l == 0 {
        return -1
    }
    var (
        low int
        high = l-1
        k = -1
    )
OuterLoop:
    for low <= high {
        mid := low + (high-low)>>1
        v := arr[mid]
        if v == target {
            k = mid // 记录target的索引
        }
        if arr[low] < arr[high] {
            // v == target时,尝试找更小的索引
            if v >= target {
                high = mid-1
            } else { // v < target
                low = mid+1
            }
            continue
        }
        if arr[low] > v {
            if target < arr[low] && v < target {
                low = mid+1
            } else {
                high = mid-1
            }
            continue
        }
        if v > arr[high] {
            if target < v && arr[high] < target {
                high = mid-1
            } else {
                low = mid+1
            }
            continue
        }
        if arr[low] == target {
            // low就是最小索引,不需要再找了
            k = low
            break
        }
        for i := low; i < high; i++ {
            if arr[i] != arr[i+1] {
                low = i+1
                if i+1 < mid {
                    high = mid-1
                } else { // i+1 > mid。不存在 i+1 == mid
                    high--
                }
                continue OuterLoop
            }
        }
        // 不需要再找了
        break
    }
    return k
}