前言
在这个信息爆炸的时代,我们每天都需要处理大量的数据。如何高效地查找数据是编程中的基本挑战之一。常见的查找算法有线性查找和二分查找,其中二分查找以其O(log n) 的时间复杂度,成为了查找算法中的高效代表。本文将深入探讨如何在Java中实现二分查找,文章有误不足之处还请大佬们指正!
二分查找介绍
概述:二分查找是一种针对有序数组(或其他数据结构)进行查找的算法。它通过反复将查找范围分成两半来缩小查找的范围,从而大大提高了查找效率。与线性查找相比,二分查找每次都可以将问题规模缩小一半,因此时间复杂度为O(log n),而线性查找的时间复杂度为O(n)。这种显著的性能差异使得二分查找在大规模数据集中的应用非常广泛。
基本思想:二分查找最重要的一个条件就是有序(可以是全部有序或者部分有序),那样才能进行查找,不断查找中间数与目标值进行判断,相等则直接返回,目标值在左侧就对左侧继续进行二分查找,目标值在右侧就对右侧继续进行二分查找。这样不断折半查找,最终能实现时间复杂度为O(log n)。
当题目出现有序,时间复杂度要求O(log n)时我们都可以联想到使用二分查找。
二分模板
下面会介绍几种模板,分别时标准模板,左边界模板,右边界模板
标准模板
标准模板适用的题目一般是在一个有序数组中查找一个元素,找到就返回其索引,未找到返回-1,注释已经写的很详细了,大家可以自己看看。
//最基本模板:左闭右闭查找一个数并返回索引,不存在返回-1
public static int t1(int nums[],int target) {
//左闭右闭的意思就是索引能取到数组的第一个索引和最后一个索引,索引[0,nums.length-1]
//左闭右开的意思是左边能取到第一个索引,右边取不到,索引[0,nums.length)
//左开右闭,左开右开等同理,其实这些的思路都差不多,就是判断条件有差别
//怕记太多会乱掉,下面都使用左闭右闭来编写
int left = 0;
int right = nums.length-1;
//左闭右闭的终止条件是left<=right,即left+1=right时结束遍历
while(left<=right) {
//取到中间数的时候最好像我这样写,能防止数据溢出同时使用位运算代替除法提高性能
//相当于是int mid = (left+right)/2;
//注意这里位运算外面一定一定一定要加个括号保证先算,不然后面会陷入死循环
int mid = left+((right-left)>>1);
if(nums[mid]==target) {//当中间数等于目标值时,直接返回其索引
return mid;
}else if(nums[mid]<target){//当中间值小于目标值,说明目标值在右侧,左索引向右移
left = mid+1;
}else {//当中间值大于目标值,说明目标值在左侧,右索引向左移
right = mid-1;
}
}
return -1;
}
左边界模板
这个模板适用于找目标值的最左侧索引,题目可以进行变化,比如找第一个出现的索引等
找一个数组中目标数左边界其实与基本模板差不多,就是更改了终止条件和判断条件,当中间值与目标值对应时继续向左查找
//找左区间模板:数组有序,包含重复元素
public static int t2(int nums[],int target) {
int left = 0;
int right = nums.length -1;
//注意这里的终止条件,是小于,当数组遍历到left+1 = right时结束遍历,此时left和right相邻
//我们可以设想一下,如果这里是小于等于的话,最终数组会遍历到left=right,此时left,mid,right相同,就会一直遍历下去,进入死循环!
while(left<right) {
int mid = left+((right-left)>>1);
//找左边界就先移动左索引
if(nums[mid]<target) {
left = mid+1;
}else {//这里因为要查找左边界,所以当nums[mid]=target后还要继续向左找
right = mid;
}
}
//最后一直查找,当退出循环时,就已经找到最左侧的边界,此时left就是我们需要的索引,判断后返回即可
return nums[left]==target?left:-1;
}
右边界模板
理解完左侧模板,右侧模板同理
public static int t3(int nums[],int target) {
int left = 0;
int right = nums.length-1;
while(left<right) {
//注意:这里要多加一,因为上面的模板中中间值其实都是偏左的,这里因为向右查找,所以要多加一
int mid = left+((right-left)>>1)+1;
//移动右索引
if(nums[mid]>target) {
right = mid-1;;
}else {
left = mid;
}
}
return nums[right]==target?right:-1;
}
例题使用
下面是力扣热题100中的几道,可以拿来练练手
package LeetCode_Hot_100.binary_search;
/**
* @ClassDescription:35. 搜索插入位置
* @Author:小菜
* @Create:2024/11/5 11:27
* 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
*
* 请必须使用时间复杂度为 O(log n) 的算法。
**/
public class t35 {
public static void main(String[] args) {
int nums[] = {1,2,3,5,6};
System.out.println(searchInsert(nums,4));
}
//二分查找模板:在这个二分查找模板中我们查找一个元素,并返回其索引,未找到返回-1
public static int searchInsert(int[] nums, int target) {
//因为这里定义索引是0~nums.length-1,左闭右闭
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left+((right-left)>>1);
if(nums[mid]==target){//如果中间值等于目标值,直接返回其索引
return mid;
} else if (nums[mid]<target) {//如果中间值小于目标值,说明目标值在右侧,左侧索引右移
left = mid+1;
}else{//最后一种情况是中间值大于目标值,说明目标值在左侧,右侧索引左移
right = mid-1;
}
}
return -1;
}
//1.思路:直接使用二分模板,查找到数就返回,查找不到数返回最终的left,此时这个left = right+1,插入目标值
public static int searchInsert2(int[] nums, int target) {
//因为这里定义索引是0~nums.length-1,左闭右闭
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left+((right-left)>>1);
if(nums[mid]==target){//如果中间值等于目标值,直接返回其索引
return mid;
} else if (nums[mid]<target) {//如果中间值小于目标值,说明目标值在右侧,左侧索引右移
left = mid+1;
}else{//最后一种情况是中间值大于目标值,说明目标值在左侧,右侧索引左移
right = mid-1;
}
}
return left;
}
}
package LeetCode_Hot_100.binary_search;
/**
* @ClassDescription:74. 搜索二维矩阵
* @Author:小菜
* @Create:2024/11/6 8:37
* 给你一个满足下述两条属性的 m x n 整数矩阵:
* 每行中的整数从左到右按非严格递增顺序排列。
* 每行的第一个整数大于前一行的最后一个整数。
* 给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
**/
public class t74 {
public static void main(String[] args) {
int ans[][] = {{1,3,5,7},{10,11,16,20},{23,30,34,60}};
int target=3;
System.out.println(searchMatrix2(ans,target));
}
//1.自己思路:直接进行普通遍历,不再赘述
//时间:O(m*n) 空间:O(1)
//2.自己思路:因为现在正在学习使用二分查找,同时这道题满足逐渐递增的条件,所以可以将每一行拼接起来,然后进行二分查找
//但是普通拼接也是二次遍历,时间还是O(m*n)
//时间:O(log mn) 空间:O(m*n)
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
int[] ans = new int[m * n];
int t = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
ans[t++] = matrix[i][j];
}
}
//接下来进行二分查找
return true;
}
//3.答案思路:也是先拼接再使用二分查找,但是其实并不是要真的拼接,我们可以发现ans[i] = matrix[i/n][i%n],n=matrix[0].length,就当数组假装拼接,实际上还是使用原先的二维数组。
public static boolean searchMatrix2(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
//左闭右闭
int left = 0;
int right = m*n-1;
while(left<=right){
int mid = left + ((right-left)>>1);//进行位运算要加括号,不然会超时
if(target==matrix[mid/n][mid%n]){
return true;
} else if (target > matrix[mid / n][mid % n]) {
left = mid +1;
}else{
right = mid -1;
}
}
return false;
}
//4.答案思路:其实这道题也可以使用t240题的思路,从角落开始遍历,进行一排/列的查询
public boolean searchMatrix3(int[][] matrix, int target) {
int i = 0;
int j = matrix[0].length-1;
while(i<matrix.length&&j>0){
if(matrix[i][j]==target){
return true;
}else if(matrix[i][j]>target){
j--;
}else{
i++;
}
}
return false;
}
}
package LeetCode_Hot_100.binary_search;
/**
* @ClassDescription:34. 在排序数组中查找元素的第一个和最后一个位置
* @Author:小菜
* @Create:2024/11/7 8:57
* 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
* 如果数组中不存在目标值 target,返回 [-1, -1]。
* 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
**/
public class t34 {
public static void main(String[] args) {
int nums[] ={5,7,7,8,8,10};
System.out.println(searchRange(nums,8));
}
//1.自己思路:非递减,整数数组,时间复杂度O(logn),所以使用二分查找
//题目要求找到起始位置和终止位置,其实就是查找左边界和右边界,套模板即可
public static int[] searchRange(int[] nums, int target) {
//定义答案数据用来记录左右边界
int ans[] = new int[]{-1,-1};
//我们可以先找出左边界,再找出有边界即可
int left = 0;
int right = nums.length-1;
while(left<right){//注意这里是小于
int mid = left+((right-left)>>1);
//找左边界先移动左边界
if(nums[mid]<target){
left = mid+1;
}else{
right=mid;
}
}
ans[0] = nums[left]==target?left:-1;
int left2 = 0;
int right2 = nums.length-1;
while(left2<right2){
int mid2 = left2+((right2-left2)>>1)+1;//右边界的mid要偏右
//找右边界先移动右边界
if(nums[mid2]>target){
right2 = mid2 -1;
}else{
left2=mid2;
}
}
ans[1] = nums[right2]==target?right2:-1;
return ans;
}
}
package LeetCode_Hot_100.binary_search;
/**
* @ClassDescription:33. 搜索旋转排序数组
* @Author:小菜
* @Create:2024/11/9 11:43
* 整数数组 nums 按升序排列,数组中的值 互不相同 。
*
* 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
*
* 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
*
* 你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
**/
public class t33 {
public static void main(String[] args) {
}
//1.答案思路:将数组从中间分为两半后,肯定有一部分是有序的,我们可以对有序的进行判断,因为有序我们可以判断出目标值是否在其中,使用(nums[mid]<>nums[left]能判断出哪边有序)
//若左侧[left,mid-1]有序并且目标值在其中(nums[left]<=target<=nums[mid-1]),就在左侧找(令right=mid-1),不然就去右侧找(令left=mid+1)
//若右侧[mid+1,right]有序并且目标值在其中(nums[mid+1]<=target<=nums[right]),就在右侧找(令left=mid+1),不然就去左侧找(令right=mid-1)
public static int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = left+((right-left)>>1);
if(nums[mid]==target){//找到目标值直接返回
return mid;
}
if(nums[mid]>=nums[left]){//说明左侧有序
if(nums[left]<=target&&target<nums[mid]){//说明要找的元素在左侧
right=mid-1;
}else{//目标在右侧
left=mid+1;
}
}else{//右侧有序,这里不能写nums[mid+1]<=target会越界,写成nums[mid]<target
if(nums[mid]<target&&target<=nums[right]){//目标在右侧
left=mid+1;
}else{//左侧
right=mid-1;
}
}
}
return -1;
}
}
总结
二分查找是一种高效的查找算法,特别适用于有序数据结构。相比线性查找的O(n)时间复杂度,二分查找通过将查找范围不断折半,使得查找的时间复杂度降低到O(log n),大大提高了查找效率。
本文介绍了几种常见的二分查找模板,包括标准模板、左边界模板和右边界模板,帮助解决不同类型的查找问题。二分查找不仅仅适用于普通的有序数组,还可以通过巧妙的变形应用到一些更复杂的场景,如旋转排序数组和二维矩阵的查找。
- 标准模板:适用于查找目标元素的索引,能够高效地在有序数组中进行查找。
- 左边界模板:用于查找目标值在数组中的最左侧位置,适合处理重复元素的情况。
- 右边界模板:用于查找目标值在数组中的最右侧位置,适用于某些变体的查找问题。
在实际应用中,二分查找不仅能显著提高查找效率,还能在一些看似复杂的问题中简化计算,例如旋转排序数组的查找和二维矩阵的查找。
通过掌握这些模板和技巧,我们可以灵活地应用二分查找来解决各类查找问题,提升算法的执行效率。如果这篇文章有帮到你的话,还请多多支持,你的支持就是我的最大动力!!!