概念先行(统一记法)
-
left
,right
:区间端点。 -
mid = left + (right - left) / 2
或mid = (left + right) >>> 1
(避免溢出)。 -
n
:数组长度,合法下标是0 .. n-1
。 -
two styles:
- 半开区间
[left, right)
:右端点不包含(常用在查找插入位置 / lower_bound / upper_bound)。 - 闭区间
[left, right]
:左右端点都包含(常用在查找是否存在某值并返回下标)。
- 半开区间
一、半开区间([left, right)
)模板 ------ 推荐用于 lower_bound / 插入点
不变量(重要)
目标插入位置始终位于区间 [left, right)
中(右端点不包含)。
结构(Java)
ini
int left = 0, right = n; // right = n -> 表示 [0, n)
while (left < right) {
int mid = (left + right) >>> 1; // or left + (right-left)/2
if (A[mid] < target) {
left = mid + 1; // 插入位置在右半区: [mid+1, right)
} else {
right = mid; // 插入位置在左半区(包含 mid): [left, mid)
}
}
// loop 结束时 left == right,即插入位置
int pos = left;
常见变体
-
lower_bound(第一个 >= target) :上面模板正是 lower_bound。
-
upper_bound(第一个 > target) :
ini// 半开区间,right = n while (left < right) { int mid = (left + right) >>> 1; if (A[mid] <= target) left = mid + 1; else right = mid; } pos = left; // 第一个 > target 的下标
优点
- 简洁、不易错的模板(左闭右开避免 mid == right 情况)。
- 自然返回"插入位置",适合维护
tails
这类需要插入点的场景。
二、闭区间([left, right]
)模板 ------ 适合查找是否存在且返回下标
不变量(重要)
目标位置始终在 [left, right]
中,循环条件通常是 left <= right
。
结构(Java)------ 查找"是否存在并返回任意下标"
ini
int left = 0, right = n - 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (A[mid] == target) return mid;
else if (A[mid] < target) left = mid + 1;
else right = mid - 1;
}
// not found -> left is insertion point, right = left - 1
变体:用闭区间实现 lower_bound(第一个 >= target)
ini
int left = 0, right = n - 1;
int pos = n; // 默认插入到末尾
while (left <= right) {
int mid = (left + right) >>> 1;
if (A[mid] >= target) {
pos = mid; // candidate
right = mid - 1; // 缩到左半区
} else {
left = mid + 1;
}
}
// pos 是第一个 >= target(可能为 n 表示 append)
注意点(闭区间常见错误)
- 当要把
mid
作为"候选"(A[mid] >= target
)时,必须做right = mid - 1
,否则区间不会收缩(会死循环)。 - 初始化
right
要是n - 1
(不是n
)。否则访问A[mid]
会越界。
三、对比与选用建议(实用)
- 想要插入位置 / lower_bound / upper_bound -> 用半开区间模板(更简洁、易用)。
- 单纯判断是否存在并返回下标 -> 用闭区间模板(直观)。
- 半开区间:
left=0,right=n, while(left<right), right=mid
。 - 闭区间:
left=0,right=n-1, while(left<=right), right=mid-1
(当 mid 作为 candidate 时)。
四、常见错误 & 排查清单
- 越界 :
right
设为n
却用while(left<=right)
,会算出mid==n
→ 访问A[n]
越界。 - 死循环 :闭区间写
while(left<=right)
却在候选分支写right = mid
(而非mid-1
)会导致区间不收缩。 - off-by-one :不清楚
mid
属于哪半区,更新时要保证区间长度严格减小。 - mid 溢出 :用
(left+right)/2
在极端大整数下溢出,改用left + (right - left)/2
或>>>1
。 - 空数组 :在半开区间
right = 0
,循环不进,pos=0 正确;闭区间right = -1
,要注意while(left<=right)
不进并处理返回值。
五、快速模板汇总(Java)
半开区间 lower_bound(第一个 >= target):
ini
int left = 0, right = n; // [left, right)
while (left < right) {
int mid = (left + right) >>> 1;
if (A[mid] < target) left = mid + 1;
else right = mid;
}
int pos = left;
半开区间 upper_bound(第一个 > target):
ini
int left = 0, right = n;
while (left < right) {
int mid = (left + right) >>> 1;
if (A[mid] <= target) left = mid + 1;
else right = mid;
}
int pos = left;
闭区间查找 exact match(是否存在):
ini
int left = 0, right = n - 1;
while (left <= right) {
int mid = (left + right) >>> 1;
if (A[mid] == target) return mid;
else if (A[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
闭区间 lower_bound(第一个 >= target):
ini
int left = 0, right = n - 1;
int pos = n;
while (left <= right) {
int mid = (left + right) >>> 1;
if (A[mid] >= target) { pos = mid; right = mid - 1; }
else left = mid + 1;
}
// pos is first >= target, may be n (not found -> insert at end)
六、举例演练(半开区间找 lower_bound)
数组 A=[2,3,7,101]
, n=4
, target=18:
- left=0,right=4 -> mid=2(A[2]=7)<18 -> left=3
- left=3,right=4 -> mid=3(A[3]=101)>=18 -> right=3
- left==right==3 -> pos=3(正确,替换 101)