题目描述

✅ 剑指 Offer 53 - I. 在排序数组中查找数字 I

image-20261001230752578

给定一个排序数组 nums 和一个目标值 target,请你在数组中查找目标值 target 的出现次数。如果 target 不存在于数组中,则返回 0。


示例 1:

输入: nums = [5, 7, 7, 8, 8, 10], target = 8
输出: 2
解释: 8 在数组中出现了两次。


示例 2:

输入: nums = [5, 7, 7, 8, 8, 10], target = 6
输出: 0
解释: 6 在数组中不存在。


提示:

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9

题意分析

统计非递减数组中 target 出现了多少次,不存在时返回 0。数组允许为空,相同值在排序后必然形成一段连续区间,因此次数等于这一段的长度。

找到任意一个匹配值后再向两侧逐个扩展,最坏仍需扫描整个数组。可以直接用两次边界二分找到目标区间的开始与结束,在对数时间内计算数量,不需要逐个访问重复值。

解法:二分查找左右边界

核心思路

[!blue]

lowerBound 找到第一个大于等于目标的位置 first,upperBound 找到第一个严格大于目标的位置 after。有序性保证 [first, after) 内的元素既不小于目标、又不大于目标,因此恰好全部等于目标,数量就是 after - first。

两次查找都使用 [left, right),初始化为 [0, n)。对相应判据,左边界之前的元素已经确定不满足,右边界及其之后的元素已经确定满足;未确定的部分留在中间继续二分。边界可以等于 n,表示直到数组末尾都没有满足判据的元素。

中点满足判据时,它可能已经是第一个满足的位置,也可能左侧还有更早的位置,因此令 right = mid;不满足时,依据有序性,中点及其左侧都可排除,令 left = mid + 1。两端相遇时,不满足部分与满足部分的分界就已经确定。

两次查找的差别只在相等情况:下界遇到等于目标的中点,应继续往左找;上界遇到等于目标的中点,还没有满足严格大于的要求,应继续往右找。因此两者分别使用 >= 和 >,不能找到相等值就提前返回。

如果目标不存在,第一个大于等于目标的位置也就是第一个严格大于目标的位置,两边界相等,差值自然为 0。空数组也直接返回两个 0;目标位于数组两侧之外时,边界同样能用 0 或 n 表示,无需额外访问边界处的元素。

解题步骤

  1. 对条件 nums[mid] >= target 做边界二分,得到 first。
  2. 对条件 nums[mid] > target 做同样的边界二分,得到 after。
  3. 每次都初始化 left = 0、right = n,只在 left < right 时继续。
  4. 满足本次判据则令 right = mid,否则令 left = mid + 1;最终返回相遇位置。
  5. 两个查找结果相减,返回 after - first。

代码实现

class Solution {
    public int search(int[] nums, int target) {
        // 目标出现范围左闭右开,两端之差就是数量。
        return upperBound(nums, target) - lowerBound(nums, target);
    }

    private int lowerBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length;

        while (left < right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }

    private int upperBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length;

        while (left < right) {
            int mid = left + (right - left) / 2;

            // upperBound 找第一个严格大于 target 的位置。
            if (nums[mid] > target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }
}
func search(nums []int, target int) int {
    // 目标出现范围左闭右开,两端之差就是数量。
    return upperBound(nums, target) - lowerBound(nums, target)
}

func lowerBound(nums []int, target int) int {
    left := 0
    right := len(nums)
    for left < right {
        mid := left + (right-left)/2
        if nums[mid] >= target {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return left
}

func upperBound(nums []int, target int) int {
    left := 0
    right := len(nums)
    for left < right {
        mid := left + (right-left)/2
        if nums[mid] > target {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return left
}

复杂度分析

  • 时间复杂度:$O(\log(n+1))$,两次二分只增加常数倍。
  • 空间复杂度:$O(1)$,只使用常数个下标变量。

关键点总结

[!green]

  • 不要找到一个目标后向两侧线性扩展;全为目标值时会退化为 $O(n)$。
  • lowerBound 与 upperBound 的唯一关键差异是 >= 和 >。
  • 半开区间允许返回 nums.length,因此「目标比所有元素大」也能自然处理。

易错点总结

[!yellow]

  • 半开区间右端写成 n - 1:会漏掉末尾位置,也无法自然表示整个数组之后的边界。
  • 满足判据时写 right = mid - 1:中点可能就是第一个符合的位置,不能把这个边界候选排除。
  • 两次都使用同一个比较符:得到的是同一个边界,无法计算目标覆盖的区间长度。
  • 返回差值时额外加一:after 已经位于目标区间之后,半开区间长度直接相减即可。
  • 把返回边界当成有效下标读取:它可能等于 n,本解法只用它们计算距离,不访问对应元素。

相似题目

题目 难度 关联与区别
35. 搜索插入位置 简单 下界查询可找第一次出现,本题还需求大于目标的第一个位置以得到右边界。
704. 二分查找 简单 普通二分找到任意相等位置即可,本题必须继续沿边界方向收缩。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/54309961
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!