二分查找的「左右为难」:如何优雅地找到数组中元素的首尾位置

二分查找的「左右为难」:如何优雅地找到数组中元素的首尾位置

当你在人群中寻找某个人时,你会怎么做?从头到尾挨个找?还是站在中间,问左边的人「他在你们那边吗」?聪明的你一定选择后者。今天我们就来聊聊二分查找中的一个经典问题------如何找到排序数组中目标元素的第一个和最后一个位置。

题目回顾

LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums ,和一个目标值 target 。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target ,返回 [-1, -1] 。

关键要求:你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

ini 复制代码
示例 1:
输入:nums = [5,7,7,8,8,10], 
target = 8
输出:[3,4]

示例 2:
输入:nums = [5,7,7,8,8,10], 
target = 6
输出:[-1,-1]

初学者的「暴力美学」

让我们先看看最直观的解法:

ini 复制代码
var searchRange = function(nums, target) {
    let begin = -1, end = -1
    for (var i = 0; i < nums.length; i++) {
        if(begin === -1) {
            if (nums[i] === target) {
                begin = i
                end = i
            }
        } else {
            if (nums[i] === target) {
                end = i
            }
        }
    }
    return [begin, end]
};

这个解法就像是在图书馆里从第一本书开始翻,直到找到你要的那本书的所有副本。虽然逻辑清晰,但是...

时间复杂度:O(n) 😱

面试官:「同学,你这是在逗我吗?题目明明要求 O(log n)!」

二分查找的「分身术」

既然要 O(log n),那就必须祭出二分查找这个神器。但这里有个巧妙之处:我们需要找到 两个边界 ------左边界和右边界。

核心思想

  1. 找左边界 :当找到目标值时,不要停下,继续向左搜索,看看还有没有更左的目标值
  2. 找右边界 :当找到目标值时,不要停下,继续向右搜索,看看还有没有更右的目标值 这就像是在拥挤的地铁里找座位,找到一个空座位后,你还要看看旁边是不是还有连续的空座位。

正确的实现

ini 复制代码
var searchRange = function(nums, target) {
    // 查找左边界(最左边的目标值)
    const findLeft = (nums, target) => {
        let left = 0, right = nums.length - 1, result = -1;
        while (left <= right) {
            let mid = Math.floor((left + right) / 2);
            if (nums[mid] === target) {
                result = mid;
                right = mid - 1; 
                // 关键:继续向左搜索
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    };
    
    // 查找右边界(最右边的目标值)
    const findRight = (nums, target) => {
        let left = 0, right = nums.length - 1, result = -1;
        while (left <= right) {
            let mid = Math.floor((left + right) / 2);
            if (nums[mid] === target) {
                result = mid;
                left = mid + 1;
                // 关键:继续向右搜索
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    };
    
    return [findLeft(nums, target), findRight(nums, target)];
};

代码中的「坑」与「巧思」

坑点1:边界处理

在你提供的第二个实现中,有个小bug:

ini 复制代码
// 错误的写法
if (nums[mid] === target) {
    result = mid
    left = mid - 1  // 应该
    是 mid + 1
}

找右边界时,应该是 left = mid + 1 ,而不是 left = mid - 1 。这就像是你在找队伍的最后一个人,找到一个目标后,应该继续往后看,而不是往前看。

巧思1:为什么要用两次二分查找?

有人可能会问:「为什么不能一次二分查找解决?」

答案是: 贪心不足蛇吞象 。一次二分查找只能找到一个位置,而我们需要两个边界。就像你不能指望一次投篮就能同时投进两个篮筐一样。

巧思2:时间复杂度分析

  • 两次二分查找:2 × O(log n) = O(log n)
  • 空间复杂度:O(1) 完美符合题目要求!

实战演练

让我们用 nums = [5,7,7,8,8,10], target = 8 来走一遍:

查找左边界:

  1. left=0, right=5, mid=2, nums[2]=7 < 8 → left=3

  2. left=3, right=5, mid=4, nums[4]=8 = 8 → result=4, right=3

  3. left=3, right=3, mid=3, nums[3]=8 = 8 → result=3, right=2

  4. left=3, right=2 → 结束,返回 result=3 查找右边界:

  5. left=0, right=5, mid=2, nums[2]=7 < 8 → left=3

  6. left=3, right=5, mid=4, nums[4]=8 = 8 → result=4, left=5

  7. left=5, right=5, mid=5, nums[5]=10 > 8 → right=4

  8. left=5, right=4 → 结束,返回 result=4 最终结果: [3, 4] ✅

总结

这道题的精髓在于:

  1. 不要被表面的简单迷惑 :看似简单的查找,实则需要精妙的边界处理
  2. 分而治之的智慧 :将复杂问题拆解为两个简单的子问题
  3. 细节决定成败 :一个 +1 和 -1 的差别,就是 AC 和 WA 的区别 记住,编程就像是在走钢丝,每一步都要小心翼翼,但掌握了技巧后,你就能在代码的世界里翩翩起舞。

「代码如诗,算法如画。愿你在二分查找的世界里,找到属于自己的那份优雅。」

相关题目推荐

  • LeetCode 704. 二分查找(基础版)
  • LeetCode 35. 搜索插入位置
  • LeetCode 153. 寻找旋转排序数组中的最小值

如果这篇文章对你有帮助,别忘了点赞收藏哦!有问题欢迎在评论区讨论~ 🚀

相关推荐
MessiGo12 分钟前
Javascript 编程基础(5)面向对象 | 5.1、构造函数实例化对象
开发语言·javascript·原型模式
曦月逸霜17 分钟前
第34次CCF-CSP认证真题解析(目标300分做法)
数据结构·c++·算法
前端小白从0开始28 分钟前
Vue3项目实现WPS文件预览和内容回填功能
前端·javascript·vue.js·html5·wps·文档回填·文档在线预览
JohnYan1 小时前
Bun技术评估 - 03 HTTP Server
javascript·后端·bun
开开心心就好1 小时前
高效Excel合并拆分软件
开发语言·javascript·c#·ocr·排序算法·excel·最小二乘法
難釋懷1 小时前
Vue解决开发环境 Ajax 跨域问题
前端·vue.js·ajax
特立独行的猫a1 小时前
Nuxt.js 中的路由配置详解
开发语言·前端·javascript·路由·nuxt·nuxtjs
中微子1 小时前
小白也能懂:JavaScript 原型链和隐藏类的奇妙世界
javascript
咸虾米1 小时前
在uniCloud云对象中定义dbJQL的便捷方法
前端·javascript