程序员面试金典 - 面试题 10.03. 搜索旋转数组

题目难度: 中等

原题链接

今天继续更新程序员面试金典系列, 大家在公众号 算法精选 里回复 面试金典 就能看到该系列当前连载的所有文章了, 记得关注哦~

题目描述

搜索旋转数组。给定一个排序后的数组,包含 n 个整数,但这个数组已被旋转过很多次了,次数不详。请编写代码找出数组中的某个元素,假设数组元素原先是按升序排列的。若有多个相同元素,返回索引值最小的一个。

示例 1:

  • 输入: arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 5
  • 输出: 8(元素 5 在该数组中的索引)

示例 2:

  • 输入:arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 11
  • 输出:-1 (没有找到)

提示:

  • arr 长度范围在[1, 1000000]之间

题目思考

  1. 直接查找好找吗, 如何对问题进行简化?

解决方案

思路
  • 根据题目描述, 这个数组虽然原始是排序的, 但现在经过了旋转, 而且其中还可能有重复元素
  • 这时候如果仍然直接应用传统的二分查找的话, 就需要大量的条件判断, 比如先判断哪一侧有序, 再判断有序侧的区间起点终点和 target 的关系, 判断逻辑较为复杂
  • 我们可以换一个角度分析, 如果能够找到旋转点, 这样就可以将当前数组划分成左右两个有序部分了, 然后就可以转换成经典的二分查找, 即依次对左右部分进行二分, 从而大大降低代码的复杂程度
  • 那么如果找到旋转点呢? 很显然旋转点位于整个数组的无序侧, 所以我们也可以使用一个二分查找, 只需要找到无序侧, 然后继续对那一侧进行二分查找即可; 这里唯一需要注意的是, 如果无法判断是否无序, 需要额外判断当前终点是否是旋转点, 是的话需要直接跳出循环
  • 找到旋转点后, 我们就可以对划分成的左右两部分依次应用传统二分查找了, 由于题目要求返回目标值的最小下标, 所以只有当左侧没找到的话才继续查找右侧, 如果都没找到的话就返回-1
  • 下面的代码就对应了上面的整个过程, 并且有详细的注释, 方便大家理解
复杂度
  • 时间复杂度 O(N): 最差情况(所有数字都相等)下, 需要遍历每个数字一遍
  • 空间复杂度 O(1): 只使用了几个常数空间的变量
代码
class Solution:
    def search(self, arr: List[int], target: int) -> int:
        # 问题转换+找旋转点+两次二分
        # 第一步, 先找旋转点, 将数组划分成左右两个有序部分
        s, e = 0, len(arr) - 1
        while s < e:
            m = (s + e) >> 1
            if arr[m] < arr[e]:
                # 右侧有序, 旋转点必在[s,m]区间
                e = m
            elif arr[m] == arr[e]:
                # 无法判断右侧是否有序, 将e减1后再次循环判断
                if arr[e - 1] > arr[e]:
                    # 注意这里可能出现旋转点就是e的情况, 例如11121, 此时如果直接-1就错了, 需要额外判断, e是旋转点的话直接跳出循环
                    break
                e -= 1
            else:
                # 右侧无序, 旋转点必在[m+1,e]区间
                s = m + 1
        # 此时旋转点即为e (注意不是s, 因为有可能是因为上面的break直接跳出的循环)
        # 注意也有可能arr本身就是有序的, 此时得到的e就是下标0
        # 也即原数组划分成了[0,e-1]和[e,len(arr)-1]两个有序区间
        def bisearch(s, e):
            # 常规有序数组二分找等于target的最小下标
            res = float("inf")
            while s <= e:
                m = (s + e) >> 1
                if arr[m] >= target:
                    if arr[m] == target:
                        res = min(res, m)
                    e = m - 1
                else:
                    s = m + 1
            return -1 if res == float("inf") else res

        # 第二步, 再对左右两个有序部分应用常规二分找target, 如果左侧部分找到后就不再继续
        l = bisearch(0, e - 1)
        if l != -1:
            return l
        r = bisearch(e, len(arr) - 1)
        if r != -1:
            return r
        return -1

大家可以在下面这些地方找到我~😊

我的 GitHub

我的 Leetcode

我的 CSDN

我的知乎专栏

我的头条号

我的牛客网博客

我的公众号: 算法精选, 欢迎大家扫码关注~😊

算法精选 - 微信扫一扫关注我

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值