来源:黑马基础数据结构-012-二分查找-平衡版_哔哩哔哩_bilibili
二分查找法
算法基础
二分查找(Binary Search)是一种在有序数组中快速查找目标值的算法。它的核心思想是"折半查找"——每次将搜索范围缩小一半,直到找到目标或确定不存在。
二分查找必须满足两个条件:
- 数组必须有序(升序或降序)
- 数组必须是线性结构(支持随机访问)
算法描述
- 初始化:设定初始搜索范围为整个数组(升序排序)。左边界 left为 0(第一个元素的索引),右边界 right为 数组长度 - 1(最后一个元素的索引)。
- 循环条件:只要 left <= right,就继续查找。
- 计算中间点:取中间索引 mid,计算公式为 mid = (left + right) / 2。
从向量的角度理解为什么mid = (left + right) / 2 :
从起点 left指向终点 right的方向向量是 (right - left),将这个方向向量除以2,得到 (right - left) / 2,最后,将这个“半向量”的起点加到初始位置 left上,就得到了中点的精确位置:mid = left + (right - left) / 2 = (left + right) / 2
-
比较与判断:
- 如果 arr[mid] == target,查找成功,返回 mid。
- 如果 arr[mid] < target,说明目标在右侧,调整左边界:left = mid + 1。
- 如果 arr[mid] > target,说明目标在左侧,调整右边界:right = mid - 1。
-
终止:如果循环结束仍未找到目标值,返回 -1 表示查找失败
算法时间复杂度:O(logn): 最好情况就是找的值就在中间(O(1)) 最坏的情况就是两边(O(logn))
public class BinarySearch {
/**
*
* @param a 升序的数组
* @param target 要找的目标值
* @return 目标值的在a中的索引。如果a中没有目标值,则返回-1
*/
public static int binarySearch(int[] a, int target){
// 1.设i = 0 ; j= a.length-1 ,表示两端的索引
int i = 0 ;
int j = a.length-1;
// 2.若 i>j 则返回-1
for ( ; i <= j ; ){
// 3.求 m = (i+j)/2,中间的索引
// 4.1 若a[m] = target ,返回m
// 4.2 若 a[m] > target , j = m-1 ,返回第2步
// 4.3 若 a[m] < target, i = m+1 , 返回第2步
int m = (i+j)/2;
if (a[m] == target) return m;
else if (a[m] > target) {
j = m-1;
}else {
i = m +1;
}
}
return -1;
}
}
平衡版
已知上述代码中的二分查找要循环L次,如果target在最左边,那么就只进入了if和else if 里,比较执行了2L次,如果target在最右边,那么就会执行3L次,这个就是不平衡的。下面是一个平衡的算法,不管目标值是最左还是最右都是O(logn)
public static int binarySearchBalance(int[] a, int target){
int i = 0 ;
int j = a.length-1;
for ( ; j-i >1 ; ){
int mid = (i+j)/2;
if (target < a[mid]){
j = mid - 1;
}else {
i = mid;
}
}
if (a[i] == target){
return i;
}else if (a[j] == target){
return j;
}else return -1;
}
相对于基础版
- 将target > a[mid] 和target = a[mid]的两种情况合并到了一个else里;
target > a[mid]: i = mid+1; target = a[mid] return mid
-> i = mid
这两种情况合并意味着什么?意味着
- 要么 i 是目标元素,然后固定 i ,然后j一直左移(这个时候一直进入if判断里),直到i ,j 紧邻
- 要么i + 1 是目标元素,也是固定i,然后j一直左移
Java源码版本
Java.utils.Arrays.binarySearch
数组是升序排序的,如果找到了目标值,返回数组中目标值所在的索引,如果没有找到目标值,则返回(插入点+1)的负数,插入点的意思是它本该在这个数组的索引(数组无重复要素)。
/**
* Searches the specified array of ints for the specified value using the
* binary search algorithm. The array must be sorted (as
* by the {@link #sort(int[])} method) prior to making this call. If it
* is not sorted, the results are undefined. If the array contains
* multiple elements with the specified value, there is no guarantee which
* one will be found.
*
* @param a the array to be searched
* @param key the value to be searched for
* @return index of the search key, if it is contained in the array;
* otherwise, <code>(-(<i>insertion point</i>) - 1)</code>. The
* <i>insertion point</i> is defined as the point at which the
* key would be inserted into the array: the index of the first
* element greater than the key, or {@code a.length} if all
* elements in the array are less than the specified key. Note
* that this guarantees that the return value will be >= 0 if
* and only if the key is found.
*/
public static int binarySearch(int[] a, int key) {
return binarySearch0(a, 0, a.length, key);
}
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int 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.
}
插入点的应用:把缺失的元素插入到数组中,保持有序
public class InsertionPoint {
public static void main(String[] args) {
int[] src = {1, 2, 3, 4, 5};
int target = 0;
int targetPoint = Arrays.binarySearch(src, target);
if (targetPoint <= -1){
// 初始化目标数组
int[] dest = new int[6];
// 获取相对于原数组插入点的索引
int insertionPoint = Math.abs(targetPoint + 1);
// 将原数组中插入点之前的部分复制到目标数组中:将原数组从索引0开始复制insertionPoint个数的元素到目标数组的索引0
System.arraycopy(src,0,dest,0,insertionPoint);
// 目标元素的插入到目标数组的插入点
dest[insertionPoint] = target;
// 将原数组中插入点之后的部分复制到目标数组中:将原数组从索引insertionPoint开始复制src.length-insertionPoint个数的元素到目标数组的索引insertionPoint+1
System.arraycopy(src,insertionPoint,dest,insertionPoint+1,src.length-insertionPoint);
System.out.println(Arrays.toString(dest));
}
}
}
二分查找法寻找最左和最右侧的值
找最右侧的目标值:在基础版本上找到了目标值之后不直接返回,而是左边的i继续右移进行二分查找
public static int binarySearchRightMost(int[] a, int target) {
int i = 0;
int j = a.length - 1;
int result = -1;
while (i <= j) {
int mid = (i + j) / 2;
if (target < a[mid]) {
j = mid - 1;
} else if (target > a[mid]) {
i = mid + 1;
} else {
result = mid;
i = mid +1;
}
}
return result;
}
@Test
public void testBinarySearchRightMost() {
int[] a = {1, 2, 2, 2, 3, 4, 4};
Assert.assertEquals(3, binarySearchRightMost(a, 2));
Assert.assertEquals(-1, binarySearchRightMost(a, 5));
}
找最左侧的目标值:在基础版本上找到了目标值之后不直接返回,而是右边的j继续左移进行二分查找
public static int binarySearchLeftMost(int[] a, int target) {
int i = 0;
int j = a.length - 1;
int result = -1;
while (i <= j) {
int mid = (i + j) / 2;
if (target < a[mid]) {
j = mid - 1;
} else if (target > a[mid]) {
i = mid + 1;
} else {
// 如果找到了目标值,记录下索引,然后继续找
result = mid;
j = mid - 1;
}
}
return result;
}
@Test
public void testBinarySearchLeftMost() {
int[] a = {1, 2, 2, 2, 3, 3, 4};
Assert.assertEquals(1, binarySearchLeftMost(a, 2));
Assert.assertEquals(0, binarySearchLeftMost(a, 1));
Assert.assertEquals(4, binarySearchLeftMost(a, 3));
}
找到最后一个小于等于目标值元素的索引(≤target的最大的索引):rightMost
在基础版的基础上,找到目标值后不要直接返回,而是left指针继续右移,直到两个指针重合之后,这个时候的左指针之前所在的位置就是最终结果,如果返回了-1,则表示左指针就没移动过,也就是目标值一直在左指针的左边,所以目标值是在数组的左边界之外的
public static int findLastLessOrEqual(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
int right = nums.length - 1;
int result = -1; // 用于记录满足条件的索引
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
result = mid; // 记录候选位置
left = mid + 1; // 继续向右查找
} else if (nums[mid] > target) {
right = mid - 1; // 向左调整搜索区间
}else {
result = mid; // 记录候选位置
left = mid + 1; // 继续向右查找
}
}
// 最终检查result是否满足条件
return (result != -1 && nums[result] <= target) ? result : -1;
}
找到第一个大于等于目标值元素的索引(>=target的最左侧的索引):leftMost
在基础版本的基础上,找到目标值后不要直接返回,而是右指针一直左移,直到两个指针重合之后,这个时候的右指针之前所在的位置就是最终结果,如果返回了-1,则表示右指针就没移动过,所以目标元素在数组的右边界之外。
public static int findLastLessOrEqual2(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0;
int right = nums.length - 1; // 闭区间:[left, right]
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止整数溢出
if (nums[mid] < target) {
left = mid + 1; // 目标在右半部分
} else if (nums[mid] > target) {
result = mid;
right = mid - 1; // 目标在左半部分(包含等于情况)
} else {
result = mid ;
right = mid -1;
}
}
// 循环结束时,left 是第一个 >= target 的位置
// 需检查 left 是否在有效范围内
return result;
}
应用
求排名
@Test
public void testFindRank(){
int[] a = {1, 2, 4, 4, 4, 7, 7};
int target = 4;
System.out.println(findFirstMoreThanTargetIndex(a, target)+1);
target = 5;
System.out.println(findFirstMoreThanTargetIndex(a, target)+1);
}