二分查找法,也被称为折半查找法,是一种在有序数组中查找某一特定元素的搜索算法,它是一种速率较高的查找算法。
算法思想:搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。
时间复杂度:
-
最好情况下:只需要进行一次比较就能找到目标元素 O(1);
-
最坏时间复杂度:O(log2^n)
-
平均时间复杂度:O(log2^n)
下面是如何用文字描述二分查找法的步骤:
初始化两个指针,一个指向数组的开始(low),一个指向数组的结束(high)。
计算中间元素的索引 mid = (low + high) >>> 2(使用整数除法)。
检查中间元素是否是要查找的元素。如果是,返回 mid。
如果要查找的元素小于中间元素,更新 high = mid - 1,
并在数组的左半部分重复步骤 2 和 3。
如果要查找的元素大于中间元素,更新 low = mid + 1,
并在数组的右半部分重复步骤 2 和 3。
如果 low > high,说明元素不在数组中,返回 -1 或其他表示未找到的值。
注:图中的最后的25弄错了,应该是35;
代码实现:
public class BinarySearch01 {
public static void main(String[] args) {
int[] array = {1,3,4,6,8,10,24,26,27,30,35};
int target = 26;
int result = myBinarySearch(array, target);
System.out.println(result);
}
private static int myBinarySearch(int[] array, int target) {
//计算数组长度
int len = array.length;
//左边指针
int low= 0;
//右边指针
int high= len - 1;
while (low <= high) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (low+ high) >>> 1;
//目标值小于中间值
if (target < array[mid]) {
//右指针赋值为中间值减一
high= mid - 1;
} else if (target > array[mid]) {
//左指针赋值为中间值加一
low= mid + 1;
} else {
return mid;
}
}
return -1;
}
}
的到这里来,看看java为我们提供的是如何实现的吧(Arrays中):
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.
}
看着是不是和上面的差不多呀,不同点在于查找不到元素的返回值不一样,我们的是直接返回-1,而官方返回的是-(low + 1)。
让我们看看官方的解释:
大概意思就是:使用二分查找算法在指定的数组中搜索指定的值。在进行此调用之前,必须对数组进行排序(如 sort(short[]) 方法)。如果未对其进行排序,则结果未定义。如果数组包含多个具有指定值的元素,则无法保证会找到哪一个元素。
形参:a – 要搜索的数组 key – 要搜索的值 返回值:搜索键的索引,如果它包含在数组中;否则,(-(插入点) - 1)。插入点定义为将键插入到数组中的点:第一个元素的索引大于键,如果数组中的所有元素都小于指定的键,则为 a. 长度。请注意,这保证了当且仅当找到键时,返回值将为 >= 0。
脑袋是不是还没有反应回来,简单点说,和我们想的一样,就是返回值不一样,它表示“负的插入点减一”,至于为什么是负的,感兴趣的小伙伴可以去查查看。
注:(1)循环条件一定是low>=high,千万不要写错了;
问题:(1)要是循环条件写成low>high,会如何?
-
low和high同时指向的元素不会被参与比较。
这个比较简单,这里就不详细解释了,小伙伴们可以画图试试看;
(2)计算中间值为什么用“>>>”,而不是“/”?大家看看下面的代码就知道了。
public class test {
public static void main(String[] args) {
int low = 0;
int high = Integer.MAX_VALUE - 1;//int类型的最大值减一
int mid = (low+high)/2;
System.out.println(mid);
low = mid +1;
mid = (low+high)/2;
System.out.println(mid);
}
}
大家发现问题了吗,但在现实中,一般遇不到这么大的数。
看到这里有的小伙伴应该会问,我看到的有些和你的不一样呀???
为了加深大家对二分查找法的印象,给大家提供改动版本;
public class BinarySearch02 {
public static void main(String[] args) {
int[] array = {1,3,4,6,8,10,24,26,27,30,35};
int target = 26;
int result = myBinarySearch02(array, target);
System.out.println(result);
}
private static int myBinarySearch02(int[] array, int target) {
//计算数组长度
//第一处不一样
int len = array.length;
//左边指针
int low= 0;
//右边指针
int high= len;
//第二处不一样
while (low< high) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (low+ high) >>> 1;
//目标值小于中间值
if (target < array[mid]) {
//第三处不一样
high= mid;
} else if (target > array[mid]) {
//左指针赋值为中间值加一
low= mid + 1;
} else {
return mid;
}
}
return -1;
}
}
总体来说,和基础版的没有什么区别,就三处不同而已,
看看图解:
总的来说,和第一种没有多大的区别!
放大招了,有的小伙伴看见了这两种方法肯定发现了,里面的if语句有点繁琐,以你们的聪明才智肯定找到了解决方案,看看我的,和你们的一样不:
public class BinarySearch03 {
public static void main(String[] args) {
int[] array = {1,3,4,6,8,10,24,26,27,30,35};
int target = 26;
int result = myBinarySearch03(array, target);
System.out.println(result);
}
private static int myBinarySearch03(int[] array, int target) {
//左边指针
int left = 0;
//右边指针
int right = array.length;
//循环条件为:左指针和右指针的元素为零
//思想:不在while循环里面找出,循环的作用为不断缩小范围直到只剩left指向的元素时结束循环,
// 然后再循环外面比较array[left] 是否等于 target ;是就返回 left ,不是就返回-1;
while (right - left > 1) {
int mid = (left + right) >>> 1;
if (target >= array[mid]) {
left = mid;
} else {
right = mid;
}
}
if (array[left] == target) {
return left;
} else {
return -1;
}
}
}
算法思路:不在while循环里面找出,循环的作用为不断缩小范围直到只剩low指向的元素时结束循环,然后再循环外面比较array[left] 是否等于 target ;是就返回 low,不是就返回-1;
小伙伴们肯定遇到过一哥有序数组里面有重复的元素,要求找最左边或者最右边的那个元素。下面Diudiu给大家展示一下吧!
核心思想:
1. 先找到第一个等于给定值的元素
2. 然后向左遍历,直到找到第一个等于给定值的元素
时间复杂度:O(logn)
空间复杂度:O(1)
其他的和基础版的一样,无非就是比基础版多了一点记录的步骤!
下面是代码是实现:
是不是发现if语句里面有的是重复的,看起来有点繁琐,强迫症的小伙伴肯定受不了了,下面是合并过的代码:
public class BinarySearchLeftMost02 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 6, 7, 9, 9, 9, 9, 9, 11, 24, 56, 66, 77, 88, 99, 121, 234, 345};
int target = 346;
int result = myBinarySearchLeftMost02(array, target);
System.out.println(result);
}
private static int myBinarySearchLeftMost02(int[] array, int target) {
//计算数组长度
int len = array.length - 1;
//左边指针
int low= 0;
//右边指针
int high= len;
//零时变量记录最开始查找到的值
while (low<= high) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (low+ high) >>> 1;
//目标值小于中间值
if (target <= array[mid]) {
//右指针赋值为中间值减一
high= mid - 1;
} else {
//左指针赋值为中间值加一
low= mid + 1;
}
}
//找到就返回该元素索引,找不到就返回目标元素插入点
return low;
}
}
有的小伙伴可能想不通为什么找不到元素要返回low,看注释就说找不到就返回该元素的插入点,这是怎么来的?看看下面的图,大家就懂了!
看完这张图之后,大家发现没有,low和mid(也就是temp)位置是一样的,要是以为是巧合的或者想看找不到的情况的,再看看下面这张:
看到这里,大家明白了吗,同样的道理,右边的也是这样:
第一种方法,找到就返回,找不到就返回-1:
第二种方法:找到就返回,找不到就返回所在位置:
public class BinarySearchRightMost02 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 6, 7, 9, 9, 9, 9, 9, 11, 24, 56, 66, 77, 88, 99, 121, 234, 345};
int target = 346;
int result = myBinarySearchRightMost02(array, target);
System.out.println(result);
}
private static int myBinarySearchRightMost02(int[] array, int target) {
//计算数组长度
int len = array.length - 1;
//左边指针
int low= 0;
//右边指针
int high= len;
while (low<= high) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (low+ high) >>> 1;
//目标值小于中间值
if (target < array[mid]) {
//右指针赋值为中间值减一
high= mid - 1;
} else {
//左指针赋值为中间值加一
low= mid + 1;
}
}
//找到就返回该元素索引,找不到就返回目标元素插入点
return high;
}
}
好了,到这里二分查找法的基本方法就写完了,小伙伴们做做下面的题目巩固一下吧!
温馨提示:上看完上面的再看题目哟,先自己完成,想不通就在看一遍文章,然后再看看我这个吧,最后去力扣找到对应的题目完成。
例题1(力扣704):
public class BinarySearch01 {
public static void main(String[] args) {
int[] array = {-1,0,3,5,9,12};
int target = 9;
int result = myBinarySearch01(array, target);
System.out.println(result);
}
private static int myBinarySearch01(int[] array, int target) {
//计算数组长度
int len = array.length;
//左边指针
int left = 0;
//右边指针
int right = len - 1;
while (left <= right) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (left + right) >>> 1;
//目标值小于中间值
if (target < array[mid]) {
//右指针赋值为中间值减一
right = mid - 1;
} else if (target > array[mid]) {
//左指针赋值为中间值加一
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
}
例题2(力扣35):看见这个题目是不是很熟悉呢
public class BinarySearchLeftMost02 {
public static void main(String[] args) {
int[] array = {1,3,5,6};
int target = 5;
int result = myBinarySearchLeftMost02(array, target);
System.out.println(result);
}
private static int myBinarySearchLeftMost02(int[] array, int target) {
//计算数组长度
int len = array.length - 1;
//左边指针
int left = 0;
//右边指针
int right = len;
//零时变量记录最开始查找到的值
while (left <= right) {
//中间变量
//注:>>>无符号右移,忽略符号位,空位都以0补齐,避免用 / 造成失误
int mid = (left + right) >>> 1;
//目标值小于中间值
if (target <= array[mid]) {
//右指针赋值为中间值减一
right = mid - 1;
} else {
//左指针赋值为中间值加一
left = mid + 1;
}
}
//找到就返回该元素索引,找不到就返回目标元素插入点
return left;
}
}
看完之后是不是觉得和上面的一样呢?
例题3(力扣34):
由于题目是力扣里面的,我们不能直接运行,就以示例1展示吧:
class Solution {
public static void main(String[] args) {
// 定义一个整数数组array,并赋值
int[] array = {5,7,7,8,8,10};
// 定义一个整数target,并赋值
int target = 8;
// 调用myBinarySearch方法,传入array和target,并将结果赋值给result
int[] result = myBinarySearch(array, target);
// 遍历result,并输出结果
System.out.print("[");
for (int i = 0; i < result.length; i++) {
if (i != result.length - 1) {
System.out.print(result[i] +",");
}else {
System.out.println(result[i] +"]");
}
}
}
// 定义一个方法myBinarySearch,传入一个整数数组array和一个整数target,返回一个整数数组result
public static int[] myBinarySearch(int[] array, int target) {
// 定义一个整数数组result,长度为2
int[] result = new int[2];
// 定义一个整数low,初始值为0
int low = 0;
// 定义一个整数high,初始值为array的长度减1
int high = array.length - 1;
// 定义一个整数tempLeft,初始值为-1
int tempLeft = -1;
// 使用while循环,条件为low小于等于high
while(low<=high) {
// 计算mid的值,mid为(low+high)除以2
int mid = (low + high) >>> 1;
// 如果target小于array[mid],则high等于mid减1
if (target < array[mid]) {
high = mid - 1;
// 如果target大于array[mid],则low等于mid加1
} else if (target > array[mid]) {
low = mid + 1;
// 如果target等于array[mid],则tempLeft等于mid,high等于mid减1
} else {
tempLeft = mid;
high = mid - 1;
}
// 将tempLeft的值赋值给result[0]
result[0] = tempLeft;
}
// 定义一个整数left,初始值为0
int left = 0;
// 定义一个整数right,初始值为array的长度减1
int right = array.length - 1;
// 定义一个整数tempRight,初始值为-1
int tempRight = -1;
// 使用while循环,条件为left小于等于right
while(left<=right) {
// 计算mid的值,mid为(left+right)除以2
int mid = (left + right) >>> 1;
// 如果target小于array[mid],则right等于mid减1
if (target < array[mid]) {
right = mid - 1;
// 如果target大于array[mid],则left等于mid加1
} else if (target > array[mid]) {
left = mid + 1;
// 如果target等于array[mid],则tempRight等于mid,left等于mid加1
} else {
tempRight = mid;
left = mid + 1;
}
// 将tempRight的值赋值给result[1]
result[1] = tempRight;
}
// 返回result
return result;
}
}
注:上述代码由本人完成,有错误的勿怪!!!想看相关视频解说,请打开哔哩哔哩大学,搜索CodeDiudiu( https://b23.tv/W3S9VRk)!!!
总结:
-
二分查找是一个时间效率极高的算法,时间复杂度是log (n)。
-
二分法的套路包括确定一个区间和找到一个满足二段性的性质。
-
二分查找的应用包括查找目标值区域的左边界、右边界,查找最小元素等。
更多算法相关问题请关注