二分查找
基础版
重点记录:
将 int m = (i + j) / 2
写成 int m = (i + j) >>> 1
>>> 1
二进制右移一位
假设 j 为 int 可以储存的最大数值, 那么在加上i之后,就会二进制溢出,结果不准确,可以通过>>> 1
二进制右移一位准确的计算数量除以2
/**
* 二分查找 基础版
*
* @param a 待查找的数组
* @param target 带查找的参数,找不到返回-1
* @return
*/
public static int binarySearchBasic(int[] a, int target) {
// 左指针
int i = 0;
// 右指针
int j = a.length - 1;
// todo 为什么是(i <= j)而不是(i < j)
// 如果 i < j 的话 那么当 i = j 时 左右指针一起指向一个元素,不在进行一次判断会检索不到这个元素。
while (i <= j) {
// 查找中间值
int m = (i + j) / 2;
if (target < a[m] ) {
j = m - 1;
} else if (a[m] < target ) {
i = m + 1;
} else {
return a[m];
}
}
return -1;
}
改动版
/**
* 二分查找 改动版
*
* @param a 待查找的数组
* @param target 带查找的参数,找不到返回-1
* @return
*/
public static int binarySearchAlternative(int[] a, int target) {
// 左指针
int i = 0;
// 右指针
// 改动1
int j = a.length;
// 改动2
while (i < j) {
// 查找中间值
int m = (i + j) / 2;
if (target < a[m]) {
// 改动3
j = m;
} else if (a[m] < target) {
i = m + 1;
} else {
return a[m];
}
}
return -1;
}
与基础版区别
- 基础版:i 和 j 分别指向了一个元素
- 改动版:i 指向一个元素,j 含义变为边界,不参与运算,代码为
int j = a.length;
平衡版
/**
* 二分查找稳定版
*
* @param a 待查找的数组
* @param target 带查找的参数,找不到返回-1
* @return
*/
public static int binarySearchBalance(int[] a, int target) {
int i = 0, j = a.length;
while (1 < j - i) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m;
} else {
i = m;
}
}
return (a[i] == target) ? i : -1;
}
与改动版区别
-
改动版:
- 代码中如果查找元素在最左侧,循环N次就会走N次来查找元素
- 如果元素在最右侧,循环N次会走N*2次 if (
if target < a[m]
条件不满足后走else if a[m] < target
) - 所以根据查找的元素位置不同,判断的次数不稳定
-
平衡版:
-
循环中判断的次数稳定
-
i 指的可能是元素,j 依然指边界
-
不在循环中去比较数据
-
根据
while (1 < j - i)
判断剩最后一个元素时结束循环,并判断这个元素的值是否等于被查找的值。
-
JAVA版
/**
* 二分查找 JAVA版
* @param a
* @param fromIndex
* @param toIndex
* @param key
* @return
*/
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
- 插入点取负是为了与找到情况区分
- -1 是为了把索引 0 位置的插入点与找到的情况进行区分 例如 待插入的下标为0 则返回 -1
Leftmost 与 Rightmost
有时我们希望返回的是最左侧的重复元素,如果用 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,并不是最左侧的元素
public static int binarySearchLeftmost1(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 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;
}
应用
对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值
Leftmost 改为
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 的元素个数
- 小于等于中间值,都要向左找
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;
}
- 大于等于中间值,都要向右找
几个名词
范围查询:
- 查询 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,$rightmost(4) + 1 … \infty $
- 查询 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
求最近邻居:
- 前任和后任距离更近者