【字节跳动】数据挖掘面试题0005:在旋转有序数组中查找是否存在元素key

文章大纲

      • 方法思路
      • 代码解释
      • 问题场景:在“打乱”的有序数组里找数
      • 核心思路:每次排除一半可能性
      • 分步骤找数(以数组[7,8,9,10,1,2,3]为例,找数字10)
      • 再举个反例:找数字5(数组中没有)
      • 用“左右有序”的逻辑来总结
      • 代码的“人话”翻译
      • 为什么时间复杂度是O(log n)?

要在旋转后的有序数组中以 O (log n) 时间复杂度查找元素,可利用二分查找的变体。关键在于确定哪一半数组仍然有序,并判断目标值是否在该范围内。

方法思路

    1. 二分查找框架:初始化左右指针,每次取中间值,判断是否为目标值。
    1. 判断有序半区:通过比较中间值与左端点值的大小,确定左半区或右半区是否有序。
    1. 缩小搜索范围:根据目标值是否在有序半区内,调整左右指针。
    def search(nums, target):
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            # 判断左半区是否有序
            if nums[left] <= nums[mid]:
                # 目标在左半区的有序部分
                if nums[left] <= target < nums[mid]:
                    right = mid - 1
                else:
                    left = mid + 1
            # 右半区有序
            else:
                # 目标在右半区的有序部分
                if nums[mid] < target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid - 1
        return -1  # 未找到目标值
    
    # 示例测试
    nums = [7, 8, 9, 10, 1, 2, 3]
    print(search(nums, 10))  # 输出: 3
    print(search(nums, 3))   # 输出: 6
    print(search(nums, 5))   # 输出: -1
    

代码解释

    1. 二分查找初始化:设置左右指针分别指向数组首尾。
    1. 中间值判断:每次计算中间索引 mid,若中间值等于目标值,直接返回索引。
    1. 左半区有序判断:若 nums[left] <= nums[mid],说明左半区有序。此时检查目标值是否在左半区范围内,若是则缩小右边界;否则缩小左边界。
    1. 右半区有序判断:若左半区无序,则右半区必定有序。检查目标值是否在右半区范围内,若是则缩小左边界;否则缩小右边界。
    1. 循环结束条件:若指针交叉仍未找到目标值,返回 -1。

该方法通过每次迭代将搜索范围缩小一半,确保时间复杂度为 O (log n),适用于无重复元素的旋转有序数组。


我用“翻书找页码”的例子来通俗解释这个问题,这样你会更容易理解~

问题场景:在“打乱”的有序数组里找数

比如有一本字典,原本按字母顺序排好(如1,2,3,7,8,9,10),但被人从中间撕开后倒着拼起来(变成7,8,9,10,1,2,3)。
现在要在这本“打乱”的字典里快速找到某个字(比如找“10”或“3”),怎么找最快?

核心思路:每次排除一半可能性

就像正常翻字典时,我们会先翻中间页判断目标在左边还是右边,这里的关键是:即使数组被旋转,总有一半是“正常有序”的

分步骤找数(以数组[7,8,9,10,1,2,3]为例,找数字10)

    1. 第1步:翻“中间页”
      • 数组长度7,中间位置是第3个元素(索引3,值为10)。
      • 刚好是目标值,直接找到!(如果不是,继续下面的判断)
    1. 假设要找数字3(中间值是10,不是目标)
    • 判断左边是否“正常”
      • 左边元素是7,中间值是10,7≤10,说明左边7,8,9,10是正常递增的。
      • 目标3不在左边(左边最小是7,3比7小),所以去右边找(右边是1,2,3)。
    1. 右边范围是索引4到6(1,2,3)
    • 再翻中间页:索引5,值为2。
    • 现在左边是1,中间值是2,1≤2,说明右边的1,2是正常递增的。
    • 目标3比中间值2大,且右边最大是3,所以去右边的右边(索引6),找到3。

再举个反例:找数字5(数组中没有)

    1. 中间值是10,左边7-10有序,5不在左边(5<7),去右边1-3。
    1. 右边中间值是2,右边1-2有序,5比3大,超出右边范围,所以不存在。

用“左右有序”的逻辑来总结

每次看中间值时:

  • 如果左边头≤中间值:左边是正常递增的,比如[7,8,9,10]。
    • 目标在左边范围内(≥左边头且<中间值):去左边找;
    • 否则:去右边找。
  • 如果左边头>中间值:右边是正常递增的,比如[1,2,3]。
    • 目标在右边范围内(>中间值且≤右边尾):去右边找;
    • 否则:去左边找。

代码的“人话”翻译

def search(数组, 目标):
    左指针, 右指针 = 0, 数组长度-1
    while 左指针<=右指针:
        中间位置 = 左指针和右指针的中间点
        if 数组[中间位置] == 目标:
            return 中间位置
        
        # 左边是正常递增的(比如7,8,9,10)
        if 数组[左指针] <= 数组[中间位置]:
            if 数组[左指针] <= 目标 < 数组[中间位置]:
                # 目标在左边,去左边找
                右指针 = 中间位置 - 1
            else:
                # 目标在右边,去右边找
                左指针 = 中间位置 + 1
        # 右边是正常递增的(比如1,2,3)
        else:
            if 数组[中间位置] < 目标 <= 数组[右指针]:
                # 目标在右边,去右边找
                左指针 = 中间位置 + 1
            else:
                # 目标在左边,去左边找
                右指针 = 中间位置 - 1
    return -1  # 没找到

为什么时间复杂度是O(log n)?

  • 每次判断都把数组砍掉一半,比如7个数→3个→1个,最多找3次(2³=8>7)
  • n个数最多找log₂n次,所以是O(log n),和正常二分查找一样快~

你可能感兴趣的:(数据挖掘常见面试题,算法面试题,数据挖掘,二分查找法)