Bootstrap

二分查找题目:山脉数组中查找目标值

题目

标题和出处

标题:山脉数组中查找目标值

出处:1095. 山脉数组中查找目标值

难度

8 级

题目描述

要求

这是一个交互式问题

当且仅当以下条件满足时,数组 arr \texttt{arr} arr山脉数组

  • arr.length ≥ 3 \texttt{arr.length} \ge \texttt{3} arr.length3
  • 存在下标 i \texttt{i} i,其范围是 0 < i < arr.length   -   1 \texttt{0} < \texttt{i} < \texttt{arr.length - 1} 0<i<arr.length - 1,使得:
    • arr[0] < arr[1] < … < arr[i   -   1] < arr[i] \texttt{arr[0]} < \texttt{arr[1]} < \ldots < \texttt{arr[i - 1]} < \texttt{arr[i]} arr[0]<arr[1]<<arr[i - 1]<arr[i]
    • arr[i] > arr[i   +   1] > … > arr[arr.length   -   1] \texttt{arr[i]} > \texttt{arr[i + 1]} > \ldots > \texttt{arr[arr.length - 1]} arr[i]>arr[i + 1]>>arr[arr.length - 1]

给定一个山脉数组 mountainArr \texttt{mountainArr} mountainArr,返回使得 mountainArr.get(index) = target \texttt{mountainArr.get(index)} = \texttt{target} mountainArr.get(index)=target最小下标 index \texttt{index} index。如果不存在这样的下标 index \texttt{index} index,返回 -1 \texttt{-1} -1

不能直接访问该山脉数组,必须通过 MountainArray \texttt{MountainArray} MountainArray 接口访问数组:

  • MountainArray.get(k) \texttt{MountainArray.get(k)} MountainArray.get(k) 返回数组下标 k \texttt{k} k 处的元素(下标从 0 \texttt{0} 0 开始)。
  • MountainArray.length() \texttt{MountainArray.length()} MountainArray.length() 返回数组的长度。

MountainArray.get \texttt{MountainArray.get} MountainArray.get 调用超过 100 \texttt{100} 100 次的提交将被视为错误答案。此外,任何试图规避判题系统的解法都是不允许的。

示例

示例 1:

输入: array   =   [1,2,3,4,5,3,1],   target   =   3 \texttt{array = [1,2,3,4,5,3,1], target = 3} array = [1,2,3,4,5,3,1], target = 3
输出: 2 \texttt{2} 2
解释: 3 \texttt{3} 3 在数组中出现了两次,下标分别为 2 \texttt{2} 2 5 \texttt{5} 5。返回最小的下标 2 \texttt{2} 2

示例 2:

输入: array   =   [0,1,2,4,2,1],   target   =   3 \texttt{array = [0,1,2,4,2,1], target = 3} array = [0,1,2,4,2,1], target = 3
输出: -1 \texttt{-1} -1
解释: 3 \texttt{3} 3 在数组中没有出现,返回 -1 \texttt{-1} -1

数据范围

  • 3 ≤ mountainArr.length() ≤ 10 4 \texttt{3} \le \texttt{mountainArr.length()} \le \texttt{10}^\texttt{4} 3mountainArr.length()104
  • 0 ≤ target ≤ 10 9 \texttt{0} \le \texttt{target} \le \texttt{10}^\texttt{9} 0target109
  • 0 ≤ mountainArr.get(index) ≤ 10 9 \texttt{0} \le \texttt{mountainArr.get(index)} \le \texttt{10}^\texttt{9} 0mountainArr.get(index)109

解法

思路和算法

根据山脉数组的定义,山脉数组中存在唯一的最大值,该最大值为峰顶元素,峰顶元素所在下标为峰顶下标,峰顶左侧和右侧为两个非空子数组。峰顶左侧的子数组为严格单调递增的数组,峰顶右侧的子数组为严格单调递减的数组,因此峰顶同一侧的子数组中的元素各不相同。如果一个元素在山脉数组中出现且该元素不是峰顶元素,则该元素最多在山脉数组中出现两次,即在峰顶左侧的子数组和峰顶右侧的子数组中最多各出现一次。

为了判断目标值在山脉数组中是否存在,需要首先得到山脉数组的峰顶下标,将山脉数组分成三个部分:峰顶、左侧子数组和右侧子数组,然后分别在三个部分寻找目标值。由于 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作的调用次数有上限,因此寻找峰顶下标和在每个部分寻找目标值都需要使用二分查找实现。

为了方便表述,用 array \textit{array} array 表示数组,用 n n n 表示山脉数组的长度,用 peakIndex \textit{peakIndex} peakIndex 表示峰顶下标,则 1 ≤ peakIndex ≤ n − 2 1 \le \textit{peakIndex} \le n - 2 1peakIndexn2,峰顶下标为下标范围 [ 1 , n − 2 ] [1, n - 2] [1,n2] 内使得 array [ peakIndex ] > array [ peakIndex + 1 ] \textit{array}[\textit{peakIndex}] > \textit{array}[\textit{peakIndex} + 1] array[peakIndex]>array[peakIndex+1] 的最小下标 peakIndex \textit{peakIndex} peakIndex

low \textit{low} low high \textit{high} high 分别表示二分查找的下标范围的下界和上界,初始时 low = 1 \textit{low} = 1 low=1 high = n − 2 \textit{high} = n - 2 high=n2。每次查找时,取 mid \textit{mid} mid low \textit{low} low high \textit{high} high 的平均数向下取整,比较 array [ mid ] \textit{array}[\textit{mid}] array[mid] array [ mid + 1 ] \textit{array}[\textit{mid} + 1] array[mid+1] 的大小关系,调整查找的下标范围。

  • 如果 array [ mid ] > array [ mid + 1 ] \textit{array}[\textit{mid}] > \textit{array}[\textit{mid} + 1] array[mid]>array[mid+1],则 peakIndex ≤ mid \textit{peakIndex} \le \textit{mid} peakIndexmid,因此在下标范围 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 中继续查找。

  • 如果 array [ mid ] < array [ mid + 1 ] \textit{array}[\textit{mid}] < \textit{array}[\textit{mid} + 1] array[mid]<array[mid+1],则 peakIndex > mid \textit{peakIndex} > \textit{mid} peakIndex>mid,因此在下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 中继续查找。

low = high \textit{low} = \textit{high} low=high 时,查找结束,此时 low \textit{low} low 即为峰顶下标。

得到峰顶下标 peakIndex \textit{peakIndex} peakIndex 之后, array [ peakIndex ] \textit{array}[\textit{peakIndex}] array[peakIndex] 即为峰顶元素。比较峰顶元素与目标值的大小关系,以下两种情况可直接返回结果。

  • 如果峰顶元素等于目标值,则由于峰顶元素在山脉数组中只出现一次,因此峰顶下标即为元素值等于目标值的最小下标,返回峰顶下标。

  • 如果峰顶元素小于目标值,则由于峰顶元素是山脉数组中的最大值,因此山脉数组中的所有元素都小于目标值,返回 − 1 -1 1

如果峰顶元素大于目标值,则为了得到目标值在山脉数组中的最小下标,需要依次在左侧子数组和右侧子数组中寻找目标值。由于左侧子数组严格单调递增,右侧子数组严格单调递减,因此在左侧子数组和右侧子数组中使用二分查找的方式寻找目标值。在左侧子数组和右侧子数组中寻找目标值的顺序如下。

  1. 在左侧子数组中寻找目标值,如果左侧子数组中找到目标值则返回目标值的下标。

  2. 如果左侧子数组中没有找到目标值,则在右侧子数组中寻找目标值,如果右侧子数组中找到目标值则返回目标值的下标。

  3. 如果右侧子数组中也没有找到目标值,则返回 − 1 -1 1

由于二分查找的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),因此该解法的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)。已知山脉数组的长度 n n n 最大为 1 0 4 10^4 104,考虑一个问题: MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作最多会被调用多少次?

  1. 寻找峰顶下标需要在整个山脉数组中二分查找,查找的次数是 ⌈ log ⁡ n ⌉ = 14 \lceil \log n \rceil = 14 logn=14。每次查找时,由于需要获得当前下标处的元素值与后一个下标处的元素值,因此每次查找需要调用 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作 2 2 2 次,一共调用 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作 2 × 14 = 28 2 \times 14 = 28 2×14=28 次。

  2. 得到峰顶下标之后,为了得到峰顶元素,需要对峰顶下标调用 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作 1 1 1 次。

  3. n 1 n_1 n1 n 2 n_2 n2 分别表示左侧子数组和右侧子数组的长度, n 1 n_1 n1 n 2 n_2 n2 都是正整数且 n 1 + n 2 + 1 = n n_1 + n_2 + 1 = n n1+n2+1=n,则左侧子数组和右侧子数组的二分查找次数分别是 ⌈ log ⁡ n 1 ⌉ \lceil \log n_1 \rceil logn1 ⌈ log ⁡ n 2 ⌉ \lceil \log n_2 \rceil logn2,每次二分查找都需要调用 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作 1 1 1 次。当 n 1 n_1 n1 n 2 n_2 n2 最接近时, ⌈ log ⁡ n 1 ⌉ + ⌈ log ⁡ n 2 ⌉ \lceil \log n_1 \rceil + \lceil \log n_2 \rceil logn1+logn2 的值最大,为 13 + 13 = 26 13 + 13 = 26 13+13=26

因此 MountainArray.get \texttt{MountainArray.get} MountainArray.get 操作最多会被调用 28 + 1 + 26 = 55 28 + 1 + 26 = 55 28+1+26=55 次,少于题目要求的 100 100 100 次调用上限。

代码

class Solution {
    public int findInMountainArray(int target, MountainArray mountainArr) {
        int n = mountainArr.length();
        int peakIndex = getPeakIndex(mountainArr, n);
        int peakElement = mountainArr.get(peakIndex);
        if (peakElement == target) {
            return peakIndex;
        }
        if (peakElement < target) {
            return -1;
        }
        int index = findInAscending(target, mountainArr, 0, peakIndex - 1);
        if (index >= 0) {
            return index;
        }
        index = findInDescending(target, mountainArr, peakIndex + 1, n - 1);
        return index;
    }

    public int getPeakIndex(MountainArray mountainArr, int n) {
        int low = 1, high = n - 2;
        while (low < high) {
            int mid = low + (high - low) / 2;
            int curr = mountainArr.get(mid), next = mountainArr.get(mid + 1);
            if (curr > next) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }

    public int findInAscending(int target, MountainArray mountainArr, int low, int high) {
        while (low <= high) {
            int mid = low + (high - low) / 2;
            int num = mountainArr.get(mid);
            if (num == target) {
                return mid;
            } else if (num > target) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return -1;
    }

    public int findInDescending(int target, MountainArray mountainArr, int low, int high) {
        while (low <= high) {
            int mid = low + (high - low) / 2;
            int num = mountainArr.get(mid);
            if (num == target) {
                return mid;
            } else if (num < target) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return -1;
    }
}

复杂度分析

  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn),其中 n n n 是山脉数组的长度。二分查找的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)

  • 空间复杂度: O ( 1 ) O(1) O(1)

;