首页 > Java > java教程 > 正文

在山脉数组中查找峰值索引:O(log N) 二分查找详解

心靈之曲
发布: 2025-09-20 10:13:00
原创
263人浏览过

在山脉数组中查找峰值索引:O(log N) 二分查找详解

本文详细介绍了如何在满足O(log N)时间复杂度的要求下,利用二分查找算法在山脉数组中找到其峰值索引。文章首先定义了山脉数组及其特性,随后剖析了二分查找的核心逻辑,并通过Java代码示例展示了如何高效地实现这一算法,同时提供了注意事项和两种解法的对比。

1. 理解山脉数组与峰值索引

一个数组 arr 被定义为山脉数组,需满足以下条件:

  • arr.length >= 3
  • 存在一个索引 i(0 < i < arr.length - 1),使得:
    • arr[0] < arr[1] < ... < arr[i-1] < arr[i] (严格递增)
    • arr[i] > arr[i+1] > ... > arr[arr.length-1] (严格递减)

我们的目标是找到这个峰值 arr[i] 对应的索引 i。例如,对于数组 [0, 2, 1, 0],峰值是 2,其索引为 1。问题的核心挑战在于,要求算法的时间复杂度必须为 O(log(arr.length))。

2. O(log N) 复杂度要求下的二分查找

由于问题明确要求 O(log N) 的时间复杂度,这意味着我们需要采用二分查找(Binary Search)策略。二分查找适用于在有序或具有某种单调性的数据结构中进行搜索。山脉数组虽然整体无序,但在峰值两侧分别呈现严格的单调性(递增和递减),这为二分查找提供了可能。

2.1 初始尝试的常见问题分析

在实现二分查找时,常见的错误在于对边界条件和搜索方向的判断。例如,一个常见的错误实现可能如下:

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int low = 0;
        int high = arr.length - 1;
        int mid = 0;
        while (low <= high) {
            mid = (low + high) / 2;
            // 错误的判断逻辑和边界处理
            if (mid == 0 || (arr[mid] >= arr[mid - 1]) && (mid == high || arr[mid] >= arr[mid + 1]))
                return mid;
            else if (mid > 0 || arr[mid - 1] > arr[mid]) { // 这里的条件和逻辑更新是错误的
                low = mid + 1;
            }
            high = mid - 1; // 这里的 high 更新不应是无条件的
        }
        return mid;
    }
}
登录后复制

上述代码的问题在于:

  1. 条件判断不准确:mid==0 或 mid==high 时的边界处理复杂且容易出错。
  2. 逻辑运算符误用:| (位或) 在这里应使用 || (逻辑或)。
  3. 搜索空间更新错误:else if 和 high = mid - 1 的更新逻辑会导致搜索方向混乱,无法正确收敛到峰值。特别是在 arr[mid-1] > arr[mid] 时,意味着当前在递减坡上,峰值应在左侧或当前位置,但却将 low 移到 mid+1,导致峰值被跳过。

2.2 正确的二分查找策略

实现 O(log N) 的二分查找,关键在于根据 arr[mid] 与其相邻元素 arr[mid+1] 的关系来判断峰值可能存在的区间:

  1. 初始化搜索区间

    • low = 0
    • high = arr.length - 1
    • 由于峰值 i 满足 0 < i < arr.length - 1,我们可以将 high 初始设置为 arr.length - 1。
  2. 循环条件

    纳米搜索
    纳米搜索

    纳米搜索:360推出的新一代AI搜索引擎

    纳米搜索 30
    查看详情 纳米搜索
    • while (low < high):当 low 等于 high 时,表示找到了唯一的峰值索引。
  3. 计算中间索引

    • mid = low + (high - low) / 2:这种写法可以有效避免 (low + high) 溢出。
  4. 判断峰值位置并更新搜索区间

    • 情况一:arr[mid] < arr[mid+1]
      • 这意味着 mid 位于山脉的上升坡上。峰值一定在 mid 的右侧(包括 mid+1)。
      • 因此,将搜索区间的下限 low 更新为 mid + 1。
    • 情况二:arr[mid] > arr[mid+1]
      • 这意味着 mid 位于山脉的下降坡上,或者 mid 本身就是峰值。峰值可能在 mid 处或 mid 的左侧。
      • 因此,将搜索区间的上限 high 更新为 mid。
  5. 返回结果

    • 当循环结束时 (low == high),low (或 high) 所指向的索引就是峰值索引。

3. Java 代码实现 (O(log N))

public class Solution {
    /**
     * 在山脉数组中查找峰值索引,时间复杂度为 O(log N)。
     *
     * @param arr 山脉数组
     * @return 峰值索引
     */
    public int peakIndexInMountainArray(int[] arr) {
        int low = 0;
        // 搜索区间可以设定为 [0, arr.length - 1]
        // 也可以更精确地设定为 [1, arr.length - 2],因为峰值不可能在两端
        int high = arr.length - 1; 

        // 当 low == high 时,循环结束,此时 low (或 high) 就是峰值索引
        while (low < high) {
            int mid = low + (high - low) / 2;

            // 如果 arr[mid] 小于 arr[mid+1],说明 mid 在上升坡
            // 峰值在 mid 的右侧,更新 low
            if (arr[mid] < arr[mid + 1]) {
                low = mid + 1;
            } 
            // 如果 arr[mid] 大于 arr[mid+1],说明 mid 在下降坡或 mid 就是峰值
            // 峰值在 mid 或 mid 的左侧,更新 high
            else { // arr[mid] > arr[mid+1]
                high = mid;
            }
        }
        // 循环结束时,low 和 high 相等,指向峰值索引
        return low;
    }

    public static void main(String[] args) {
        Solution solver = new Solution();
        System.out.println("Set 1: [0,1,2] -> Peak Index: " + solver.peakIndexInMountainArray(new int[]{0,1,2})); // 2
        System.out.println("Set 2: [0,1,0] -> Peak Index: " + solver.peakIndexInMountainArray(new int[]{0,1,0})); // 1
        System.out.println("Set 3: [0,2,1,0] -> Peak Index: " + solver.peakIndexInMountainArray(new int[]{0,2,1,0})); // 1
        System.out.println("Set 4: [0,10,5,2] -> Peak Index: " + solver.peakIndexInMountainArray(new int[]{0,10,5,2})); // 1
        System.out.println("Set 5: [0,100,500,2] -> Peak Index: " + solver.peakIndexInMountainArray(new int[]{0,100,500,2})); // 2
    }
}
登录后复制

4. O(N) 线性扫描解法(作为对比)

尽管问题要求 O(log N),但如果忽略时间复杂度要求,最直观且简单的解法是进行一次线性扫描。这种方法通过遍历数组,维护当前遇到的最大值及其索引,最终找到峰值。

public class LinearScanSolution {
    /**
     * 在山脉数组中查找峰值索引,时间复杂度为 O(N)。
     *
     * @param arr 山脉数组
     * @return 峰值索引
     */
    public static int peakIndexInMountainArray(int[] arr) {
        int peakValue = arr[0]; // 假设第一个元素是初始峰值
        int peakIndex = 0;
        for (int i = 1; i < arr.length; i++) { // 从第二个元素开始遍历
            int value = arr[i];
            if (value > peakValue) {
                peakValue = value;
                peakIndex = i;
            } else {
                // 如果当前值不大于peakValue,且之前是递增,现在开始递减,
                // 那么当前的peakValue就是峰值。
                // 由于山脉数组的特性,一旦开始下降,就不会再上升,
                // 所以可以直接返回当前的 peakIndex。
                // 优化:对于山脉数组,一旦遇到下降趋势,就可以确定峰值已过
                return peakIndex; 
            }
        }
        return peakIndex; // 适用于数组只有递增部分(不严格的山脉)或递增到末尾的情况
    }

    public static void main(String[] args) {
        System.out.println("Set 1: " + peakIndexInMountainArray(new int[]{0,1,2})); // 2
        System.out.println("Set 2: " + peakIndexInMountainArray(new int[]{0,1,0})); // 1
        System.out.println("Set 3: " + peakIndexInMountainArray(new int[]{0,2,1,0})); // 1
        System.out.println("Set 4: " + peakIndexInMountainArray(new int[]{0,10,5,2})); // 1
        System.out.println("Set 5: " + peakIndexInMountainArray(new int[]{0,100,500,2})); // 2
    }
}
登录后复制

注意事项:

  • 上述线性扫描代码中的优化 return peakIndex; 仅适用于严格的山脉数组定义。如果 arr[i] 不大于 peakValue,且 arr[i-1] 已经小于 arr[i] (即在递增段),这说明 arr[i-1] 才是峰值。但由于 peakValue 已经记录了 arr[i-1],所以直接返回 peakIndex 是正确的。
  • 虽然此方法功能上正确,但其时间复杂度为 O(N),不符合原问题 O(log N) 的要求。因此,在有严格复杂度要求的场景下,应优先选择二分查找。

5. 总结与注意事项

  • 理解问题定义:在解决任何算法问题之前,务必仔细阅读并理解所有约束条件,特别是时间复杂度要求。
  • 二分查找的核心:在于每次迭代都能将搜索空间减半。对于山脉数组,通过比较 arr[mid] 和 arr[mid+1],可以确定峰值位于 mid 的左侧或右侧,从而正确收缩搜索区间。
  • 边界处理:在二分查找中,low < high 的循环条件和 low = mid + 1 / high = mid 的更新方式是确保算法正确性和避免死循环的关键。当 low = high 时,循环终止,此时 low 即为所求。
  • 溢出风险:计算 mid 时使用 low + (high - low) / 2 优于 (low + high) / 2,以防止 low + high 发生整数溢出。
  • O(N) 与 O(log N):虽然线性扫描更直观易懂,但在大数据量下,O(log N) 的二分查找具有显著的性能优势。对于本问题,应优先采用二分查找。

以上就是在山脉数组中查找峰值索引:O(log N) 二分查找详解的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号