Java实现二分查找算法
本文主要演示通过Java实现二分查找算法的各种版本,包括:基础版、改进版、平衡版、Java版、Leftmost、Ringhtmost
1. 基础版
public static int binarySearchBasics(int[] arr, int target) {
int i = 0;
int j = arr.length - 1;
//思考1:为什么是 i<=j
while (i <= j) {
int m = (i + j) / 2;
//思考2:m = (i + j) / 2会不会出现问题?
if(target < arr[m]){
j = m - 1;
} else if (target > arr[m]) {
i = m + 1;
} else {
//找到了,返回下标m
return m;
}
}
//没找到,返回-1
return -1;
}
思考:
1.1 循环条件为什么是 i <= j , 而不是 i < j ?
i <= j 可以推到成 j - i >= 1,表示数组arr中还有一个元素没有跟目标值target进行比较,如果循环条件设置为 i < j,就不会比较这个元素,如果这个元素恰巧是要寻找的元素,则会出现错误。
1.2 m = (i + j) / 2会不会出现问题?该如何改进?
会。
m = (i + j) / 2 中,如果 i 和 j 的值非常大,i + j 的结果可能会超出数据类型的表示范围,从而导致溢出。例如,在 32 位整数中,如果 i 和 j 都接近 Integer.MAX_VALUE,那么 i + j 可能会超出 int 类型的范围,导致计算错误。
解决方法:
为了避免溢出问题,通常会使用 m = (i + j) >>> 1 来计算中间值,其中 >>> 是无符号右移操作符。这个操作符有以下特点:
“>>>” 是无符号右移,即无符号地将二进制数向右移动,并且填充 0。
m = (i +j) >>> 1 相当于 (i + j) /2,但它避免了溢出问题。
2. 改进版
public static int binarySearchAlternative(int[] arr, int target){
int i = 0;
int j = arr.length;
while (i < j) {
int m = (i + j) >>> 1; //等价于 int m = (i + j) / 2;
if(target < arr[m]){
j = m;
} else if (arr[m] < target) {
i = m + 1;
} else {
//找到了,返回下标m
return m;
}
}
//没找到,返回-1
return -1;
}
改进点:
1. 将 j = arr.length - 1改成 j = arr.length
好处: 下标 j 指向的元素 一定不是 查找目标,i == j的情况不可能是目标值,循环条件变成了i < j。如果某次要缩小右边界,那么 j = m,因为此时的 m m m 已经不是查找目标了
2. 将 m = (i + j) / 2 改成 m = (i + j) >>> 1
好处: 避免了因 i ,j 的值过大而引起的溢出问题
3.比较大小符号统一用<
好处: 与数组大小顺序保持一致(从小到大),提高代码可读性
3. 平衡版
解决: 基础版目标值在最左边,查找L次;在最右边,查找2*L次,左右不平衡
public static int binarySearchBalance(int[] arr, int target){
int i = 0;
int j = arr.length;
while (1 < j - i){//要查找的范围 > 1 时
int m = (i + j) >>> 1;
if(target < arr[m]){
j = m;
} else {
i = m;
}
}
return (target == arr[i]) ? i : -1;
}
4. Java版
直接调用Arrays.binarySearch( )方法即可。底层用基础版二分查找实现。
public static int binarySearchJava(int[] arr, int target){
return Arrays.binarySearch(arr, target);
}
5. Leftmost
有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找
-
对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素4,结果是索引3
-
对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素4,结果也是索引3,并不是最左侧的元素
所以,二分查找Leftmost版本来了
public static int binarySearchLeftmost1(int[] arr, int target){
//arr[] = 1,2,3,4,4,4,4,5,5,5,6,7,8
int i = 0;
int j = arr.length - 1;
int candidate = -1;
while (i <= j){
int m = (i + j) >>> 1;
if(target < arr[m]){
j = m - 1;
} else if (arr[m] < target) {
i = m + 1;
} else {
candidate = m;
j = m - 1;
}
}
return candidate;
}
调用该方法,
-
对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素4,结果是索引3
-
对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素4,结果是索引2
符合预期
6. Rightmost
同理,希望返回的是最右侧的重复元素,则有Rightmost版本
public static int binarySearchRightmost1(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m; // 记录候选位置
i = m + 1; // 继续向右
}
}
return candidate;
}
7. Leftmost(改进版)
对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值
public static int binarySearchLeftmost(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target <= a[m]) {
j = m - 1;
} else {
i = m + 1;
}
}
return i;
}
Leftmost 返回值的另一层含义: < t a r g e t \lt target <target 的元素个数
8. Rightmost(改进版)
同理,Rightmost版本
public static int binarySearchRightmost(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else {
i = m + 1;
}
}
return i - 1;
}
Rightmost 返回值的另一层含义:> t a r g e t \ target target 的元素个数
应用:
范围查询:
- 查询 x < 4 x \lt 4 x<4, 0.. l e f t m o s t ( 4 ) − 1 0 .. leftmost(4) - 1 0..leftmost(4)−1
- 查询 x ≤ 4 x \leq 4 x≤4, 0.. r i g h t m o s t ( 4 ) 0 .. rightmost(4) 0..rightmost(4)
- 查询 4 < x 4 \lt x 4<x, r i g h t m o s t ( 4 ) + 1.. ∞ rightmost(4) + 1 .. \infty rightmost(4)+1..∞
- 查询 4 ≤ x 4 \leq x 4≤x, l e f t m o s t ( 4 ) . . ∞ leftmost(4) .. \infty leftmost(4)..∞
- 查询 4 ≤ x ≤ 7 4 \leq x \leq 7 4≤x≤7, l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 ) leftmost(4) .. rightmost(7) leftmost(4)..rightmost(7)
- 查询 4 < x < 7 4 \lt x \lt 7 4<x<7, r i g h t m o s t ( 4 ) + 1.. l e f t m o s t ( 7 ) − 1 rightmost(4)+1 .. leftmost(7)-1 rightmost(4)+1..leftmost(7)−1
求排名: l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 leftmost(target)+1
- t a r g e t target target 可以不存在,如: l e f t m o s t ( 5 ) + 1 = 6 leftmost(5)+1 = 6 leftmost(5)+1=6
- t a r g e t target target 也可以存在,如: l e f t m o s t ( 4 ) + 1 = 3 leftmost(4)+1 = 3 leftmost(4)+1=3
求前任(predecessor): l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 leftmost(target)−1
- l e f t m o s t ( 3 ) − 1 = 1 leftmost(3) - 1 = 1 leftmost(3)−1=1,前任 a 1 = 2 a_1 = 2 a1=2
- l e f t m o s t ( 4 ) − 1 = 1 leftmost(4) - 1 = 1 leftmost(4)−1=1,前任 a 1 = 2 a_1 = 2 a1=2
求后任(successor): r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 rightmost(target)+1
- r i g h t m o s t ( 5 ) + 1 = 5 rightmost(5) + 1 = 5 rightmost(5)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
- r i g h t m o s t ( 4 ) + 1 = 5 rightmost(4) + 1 = 5 rightmost(4)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
求最近邻居:
- 前任和后任距离更近者
E03. 搜索开始结束位置-Leetcode 34
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题
例如:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
输入:nums = [], target = 0 输出:[-1,-1]
参考答案:
public static int left(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
j = m - 1;
}
}
return candidate;
}
public static int right(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
i = m + 1;
}
}
return candidate;
}
public static int[] searchRange(int[] nums, int target) {
int x = left(nums, target);
if(x == -1) {
return new int[] {-1, -1};
} else {
return new int[] {x, right(nums, target)};
}
}