先记住这个答案
用两个指针维护搜索区间,当中间值等于目标时,先把结果记为当前下标,再令右指针移到该位置左侧继续寻找更靠前的相等元素;循环结束后,记录的下标就是第一个出现位置。若从未命中,则返回 -1。时间 O(log n),空间 O(1)。
- 命中目标后继续向左收缩区间
- 用候选下标记录当前最左命中
- 未命中返回 -1,不混淆插入位
让相等分支也收缩是关键
标准二分查找在 arr[mid] == target 时立即返回 mid,但这只能保证找到某个目标,不能保证是第一个。因为左侧可能还有相等元素。解决办法是:记录当前命中位置 ans = mid,然后强制把右边界移到 mid - 1,继续在左半段寻找。即使左边没有更多目标,收缩过程也会在区间为空后自然结束,此时 ans 就是最左边的命中点。
另一种思路是先求第一个大于等于目标的下标 lower_bound,再检查该下标对应元素是否等于目标。两种方式等价,但直接实现相等收缩更直观,也便于理解为什么方向选择至关重要。每次迭代丢弃一半不可能含首个目标的区间,因此时间复杂度稳定为 O(log n)。
function firstOccurrence(arr, target) {
let left = 0, right = arr.length - 1, ans = -1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (arr[mid] === target) {
ans = mid;
right = mid - 1;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return ans;
}
console.log(firstOccurrence([1,2,2,2,3], 2));
console.log(firstOccurrence([5,7,7,8,8,10], 8));
console.log(firstOccurrence([5,7,7,8,8,10], 6));
console.log(firstOccurrence([], 1));查看输出与解释
1
3
-1
-1每次把区间折半,只保留可能含有更靠左相同元素的部分。数组为空或目标不存在时 ans 保持 -1。运行时间 O(log n),空间 O(1)。
成绩单查找首个满分的位置
假设考试系统里有一份升序排列的成绩数组,例如 [60, 60, 72, 72, 72, 90],需要找出第一个 72 分所在的下标,以便确定第一个达到该分数的学生。若直接用普通二分,可能命中中间的某个 72 就返回,从而漏掉更早出现的位置。系统要求返回最早出现的位置,因此可用本题算法。
采用上述算法,若第一次命中 index = 3,记录该位置并把右边界移到 mid - 1(即收缩到左半段),继续寻找更靠前的等于 72 的元素;随后可能命中 index = 2,再次记录并移动右边界;当左区间 [0,1] 的元素都小于 72 时,循环结束,返回 2。该过程能稳定得到最左侧的相等元素下标。
空数组与全不命中时的返回语义
若数组为空,初始 left=0, right=-1,循环条件 left <= right 不成立,函数直接返回 -1,表示没有目标。当目标小于所有元素,如 [2,3,5] 找 1,循环中所有中间值都大于 1,right 持续左移直到 -1,同样返回 -1。
当目标大于所有元素,如 [2,3,5] 找 7,left 最终越过数组末尾 right,但 ans 从未被更新,依然返回 -1。这里需要明确:本函数语义是“目标是否存在并返回其首现”,并非返回“应插入位置”。因此它与 lower_bound 的语义不同,后者在找不到时返回可插入的下标。若面试者混淆二者,会导致结果不一致,必须在出口处澄清。
容易答错的地方
- 命中后马上返回
- 错误。
arr[mid] == target时直接返回只能得到任意一个命中点,左侧可能还有相同值。应记录mid后令right = mid - 1继续搜索,直到区间为空,ans才必然是第一个位置。 - 把返回值当成 lower_bound
- 部分实现会返回第一个大于等于目标的下标,判断
arr[pos]==target后再决定。但本题要求“找第一个出现”,若目标不存在应返回 -1,而不是 pos 本身。两种语义容易混用,需要按题目要求调整出口。
面试官还会怎么问?
如何通过该函数得到最后一个出现位置?
对称地改动相等分支:命中时记录并令 left = mid + 1 向右搜索,最终得到最右命中点。也可先求最后一个 <= target 的位置,再验证等于目标,从而复用现有代码。
为什么 `mid` 取左中位且 `left <= right` 也能正确工作?
循环结束时 left > right,所有可能的相等元素区间都已检查。取左中位在偶数长度下偏左,不影响收缩方向,只是避免死循环。关键在相等分支必须改变边界,使区间严格缩小。
若数组未排序,这方法还适用吗?
不适用。二分依赖有序性来排除区间,未排序时无法确定目标可能在左还是右。必须先排序,但排序后原索引会丢失,若需保留原序则另寻他法,如线性扫描。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。