二分查找 -- 获取目标元素的位置
问题描述
输入:数组 arr=[11, 12, 22, 25, 64, 90],目标元素 target=64
输出:若存在,返回目标元素在数组中的位置;否则,返回 -(left+1)。
示例1:
- 数组
arr=[11, 12, 22, 25, 64, 90],目标元素target=64 - 输出:4
示例2:
- 数组
arr=[11, 12, 22, 25, 64, 90],目标元素target=100 - 输出:-7
请思考:
- 返回值为什么不是
-left? - 返回值为什么不是-1?
- 当目标元素查找失败时,
left值的含义是什么?
状态定义
findRecursive(arr, target, left, right) 表示
- 目标元素
target在区间arr[left, right]中的位置,或者-(left+1)
基准情况:
- 当
left > right时,区间arr[left,right]为空,目标元素target显然不在其中,返回值为 -(left+1)。
推导递推关系
要计算 findRecursive(arr, target, left, right), 让我们考虑区间arr[left, right] 的中间元素arr[mid],其中mid=left+(right-left)/2。
有 3 种情况
target == arr[mid]:查找成功,返回 mid
target < arr[mid]:目标元素在左半边,等价于调用 findRecursive(arr, target, left, mid-1)
target > arr[mid]:目标元素在右半边,等价于调用 findRecursive(arr, target, mid + 1, right)
请总结下递推关系:
txt
findRecursive(arr, target, left, right) =
if left > right
-(left+1)
else if arr[mid] == target
mid
else if target < arr[mid]
findRecursive(arr, target, left, mid - 1)
else
findRecursive(arr, target, mid + 1, right)
其中 mid = left + (right - left) / 2
朴素递归解
java
public static int findRecursive(int[] arr, int target){
return findRecursiveHelper(arr, target, 0, arr.length - 1);
}
private static int findRecursiveHelper(int[] arr, int target, int left, int right) {
if (left > right) {
return -(left+1);
}
int mid = left + (right - left) / 2;
if (target == arr[mid]) {
return mid;
} else if (target < arr[mid]) {
return findRecursiveHelper(arr, target, left, mid - 1);
} else {
return findRecursiveHelper(arr, target, mid + 1, right);
}
}
迭代解
java
public static int findIterative(int[] arr, int target){
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (target == arr[mid]) {
return mid;
} else if (target < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -(left+1);
}
思考题
请思考:
- 当目标元素查找失败时,
left值的含义是什么? - 返回值为什么不是
-left? - 返回值为什么不是-1?
追踪执行流程
结论:当目标元素查找失败时,left值编码了插入点的位置信息。
txt
数组:[11, 12, 22, 25, 64, 90]
查找:22
mid = left + (right - left) / 2
Step 1: left=0, right=5, mid=2
data[2] = 22 == 22, 找到了,返回值:2
查找:11
mid = left + (right - left) / 2
Step 1: left=0, right=5, mid=2
data[2] = 22 > 11, 查找左半边
Step 2: left=0, right=1, mid=0
data[0] = 11 == 11, 找到了,返回值:0
查找:8
mid = left + (right - left) / 2
Step 1: left=0, right=5, mid=2
data[2] = 22 > 8, 查找左半边
Step 2: left=0, right=1, mid=0
data[0] = 11 > 8, 查找左半边
Step 3: left=0, right=-1 left > right, exit loop
返回值:?
查找:90
mid = left + (right - left) / 2
Step 1: left=0, right=5, mid=2
data[2] = 22 < 90, 查找右半边
Step 2: left=3, right=5, mid=4
data[4] = 64 < 90, 查找右半边
Step 3: left=5, right=5, mid=5
data[5] = 90 == 90, 找到了,返回 5
查找:92
mid = left + (right - left) / 2
Step 1: left=0, right=5, mid=2
data[2] = 22 < 92, 查找右半边
Step 2: left=3, right=5, mid=4
data[4] = 64 < 92, 查找右半边
Step 3: left=5, right=5, mid=5
data[5] = 90 < 92, 查找右半边
Step 4: left=6, right=5, left > right, exit loop
data[5] = 90 < 92, 查找右半边
返回值:?
当目标元素查找失败时,left 的含义是什么?
当二分查找失败时,
left指向的是 "目标元素如果要插入数组,应该插入的位置(插入点)"
为什么是插入点?
在整个二分过程中,你始终维护着一个不变量:
区间
[left, right]是 target 可能存在的唯一区间
当算法终止时:
txt
left > right
这意味着什么?
[left, right]这个区间已经 为空- 所有
< left的位置,其元素 都 < target - 所有
>= left的位置,其元素 都 > target
👉 因此:
left恰好是数组中第一个 ≥ target 的位置。也就是:插入 target 后,数组仍然有序的位置
查找 8(比最小值还小)
txt
最终:left = 0, right = -1
- 插入点应为索引
0 left = 0✔️
查找 92(比最大值还大)
txt
最终:left = 6, right = 5
- 插入点应为数组末尾(index = 6)
left = 6✔️
无论 target 落在哪,失败时的 left 都精确编码了插入位置
二、返回值为什么不是 -left?
这是一个编码冲突问题。
核心要求
返回值必须同时表达两件事:
- 查找是否成功
- 如果失败,插入点在哪里
如果返回 -left 会发生什么?
冲突情况 1:left = 0
txt
返回 -0 = 0
但 0 是一个合法索引!
-
你无法区分:
- 「找到了,位置在 0」
- 「没找到,插入点在 0」
信息丢失
-(left + 1) 如何避免冲突?
txt
返回值 = -(left + 1)
| left | 返回值 | 是否可能与合法索引冲突 |
|---|---|---|
| 0 | -1 | ❌ 不可能 |
| 1 | -2 | ❌ |
| 4 | -5 | ❌ |
所有失败返回值都严格小于 0
反向解码也很优雅
txt
insertionPoint = -(returnValue + 1)
这就是 Java Arrays.binarySearch 的设计原因。
返回值为什么不是 -1?
因为:
-1只能表示失败,无法表示"失败发生在哪里"
查找 8 和 92
如果统一返回 -1:
txt
查找 8 -> -1
查找 92 -> -1
你完全不知道:
- 插入点是
0? - 还是
6?
而 -(left+1) 能区分:
| target | left | 返回值 |
|---|---|---|
| 8 | 0 | -1 |
| 92 | 6 | -7 |
信息完整、可逆、无歧义。
一句话总结
二分查找失败时,
left精确表示"插入点"。
-(left+1)是一种不与合法索引冲突、且可逆的信息编码方式