目录在这里哦~
1 二分查找简介
二分查找算法的效率很高,而且它的思想也比较简单,但是由于内部的某些细节,导致二分查找不是很容易写对,有的时候也会陷入自我怀疑,我为何如此的蠢???
1.1 二分查找的核心思想
核心思想很简,单就一句: 每次尽可能多的排除掉无用元素
1.2 适用场景
二分查找只适合 有序集合
使用,
2 举个栗子 - 猜数字
在 0 - 100
的范围内猜一个目标值数字,每猜一次会反馈猜大了还是猜小了,使用最少的次数多少次才能猜对呢?
这里我们就能用上二分的思想,每次猜一半,最后的结果次数就能达到最小
每次都猜 有效范围
内的 中间值
,这样就能以最少的次数猜对
public class Guess_Num {
public static void main(String[] args) {
// 产生随机数
int num = new Random().nextInt(100);
Scanner sc = new Scanner(System.in);
// 循环判断
while (true) {
int guess = sc.nextInt();
if (guess > num) System.out.println("猜的数" + guess + "大了"); // 猜大
if (guess < num) System.out.println("猜的数" + guess + "小了"); // 猜小
if (num == guess) {
// 猜对
System.out.println("恭喜你猜中了");
break; // 结束循环
}
}
}
}
代码不是重点,重点是我们在猜数字时的思考过程,每次我们都会排除一半
图示栗子
分析栗子
下图所示,我们每次都会排除当前数字的个数的一半,n/21、n/22、n/23 . . . n/2k
计算得知,26 < 100,27 > 100,所以理论来说最少7次能得到最后的结果,以此类推,1000 需要 9 次,10000 需要 13 次,数据量是 指数
增长,而我们的查找次数是 线性
增长,这足以体现二分查找的优越性,也就不难发现二分查找的时间复杂度为 O(lgn)
3 二分查找中的几个关键部分
1、 关键指针:left
、right
、mid
2、 循环退出条件的判断
4 二分查找怎么写
4.1 查找数组中的目标值
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
例子
输入:nums = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], target = 3
输出:2
int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int l = 0, r = nums.length - 1;
while (l <= r) {
// 1
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
// 2
return mid;
} else if (nums[mid] < target) {
l = mid + 1; // 3
} else {
r = mid - 1; // 3
}
}
return -1;
}
这是最基础的二分查找了,这里的几个重要的部分
1 、r = nums.length - 1
: nums[l]
和 nums[r]
都会被遍历到,l 和 r 都在 nums.length
之内移动,如果 l
超出边界,那肯定达成了 l > r
,然后就退出了循环,所以从 nums.length - 1
开始即可
2、 while (l <= r)
: 为啥是 <=
,因为 在 l = r
时的值也需要判断,循环的退出条件时 l > r
3、 l = mid + 1
和 r = mid - 1
: 因为 mid
判断过,所以要向左或者向右移动一位
4.2 查找数组中的第一个目标值
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,搜索 nums
中的 target
,如果目标值存在返回 target
第一次出现的下标,否则返回 -1
例子
输入:nums = [ 1, 2, 2, 2, 3, 4, 5, 6, 7, 8, 9 ], target = 2
输出:1
public int binarySearch(int[] nums, int target) {
int len = nums.length;
int l = 0;
int r = len - 1; // 1
while (l <= r) {
// 2
int mid = l + ((r - l) / 2);
if (nums[mid] == target) {
if ((mid == 0) || (nums[mid - 1] != target)) return mid; // 4
else r = mid - 1; // 5
} else if (nums[mid] > target) {
r = mid - 1; // 3
} else if (nums[mid] < target) {
l = mid + 1; // 3
}
}
return -1;
}
1、 int r = len - 1
: nums[l]
和 nums[r]
都会被遍历到,l 和 r 都在 nums.length
之内移动,如果 l
超出边界,那肯定达成了 l > r
,然后就退出了循环,所以从 nums.length - 1
开始即可
2、 while (l <= r)
: 为啥是 <=
,因为 在 l = r
时的值也需要判断,循环的退出条件时 l > r
3、 r = mid - 1
和 l = mid + 1
: 因为 mid
判断过,所以要向左或者向右移动一位
4、 if ((mid == 0) || (nums[mid - 1] != target))
: 这是 nums[mid] = target
的情况,如果此时 mid = 0
,是第一个元素,条件就直接为true了,如果 mid != 0
,那还要再判断 nums[mid]
的前一个元素 nums[mid - 1]
是不是等于 target
,如果不是的话就满足了条件返回 mid
。
这里要注意的一点是,这里的两个判断条件关系是或的关系,第一条件不满足时,才会进行第二个条件的判断,所以不用担心 mid = 0
时 nums[mid - 1]
会出现索引溢出的问题
5、 r = mid - 1
: 这是 nums[mid] = target
的情况,如果不满足条件,那么就要把右边界向前移动一位,因为这个时候 nums[mid - 1] = nums[mid]
5 关于二分查找的边界问题
这是二分查找的两种形式,对比一下这两者的代码,它们只有三处不一样,解释在下方
int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1;
}
int binarySearch2(int[] nums, int target) {
if (nums == null || nums.length == 0) return -1;
int l = 0, r = nums.length;
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid;
}
}
return -1;
}
对于 r
的取值,取 nums.length - 1
还是 nums.length
似乎是个很难抉择的问题,但是事实上来说,这两个值都是可以的,只不过这和 跳出循环
与 r 的移动
有着密切的关系,也常常是在这种地方陷入疯狂挠头的处境,也就是这种情况,感觉一定不是自己的问题,头发也日渐减少…
如果取值是 l = 0, r = nums.length - 1
1、 取值范围是什么?
我们的取值区间的两个端点都是闭区间,也就是 [ l, r ]
,左闭右闭,l
和 r
的值都可以取得到
2、 l
能不能等于 r
?
这个时候如果出现 l = r
的情况是没有任何问题的,因为 [ r, r ]
是有意义的
3、 什么时候退出循环?
闭区间 [ l, r ]
,如果 l = r
,这个时候是一个临界值,当 l > r
时,我们应该退出循环
4、 关于 r = mid - 1
第一次的查询范围是[ l, r],判断mid之后,应该变为[ l, mid -1 ]或者[ mid + 1, r ],r总会指向需要进行判断的元素。我们当前判断的是mid,mid判断完成下一步要找的就是下一个要判断的区间。
如果取值是 l = 0, r = nums.length
1、 取值范围是什么?
这和前面得那种情况不一样,r得取值是 nums.length
,这个值我们是取不到的,所以这个时候区间是左闭右开得,也就是 [ l, r )
2、 l
能不能等于 r
?
这个时候如果出现 l = r
的情况,[ r, r )
,既包含 r
,又不包含 r
,这个时候是没有意义的,所以 l
不能等于 r
3、 什么时候退出循环?
左闭右开区间 [ l, r )
,如果 l = r
,这个时候是没有意义的,这个时候的临界值是 l = r - 1
,所以,当 l = r
时,我们应该退出循环
4、 关于 r = mid
第一次的查询范围是 [ l, r)
,判断 mid
之后,应该变为 [ l, mid )
或者 [ mid + 1, r )
,关于为什么是 [ l, mid )
,我们这个时候的 mid
位置的元素是不会访问的,r指向的是 最后一个元素的后一个位置
,其实 [ l, mid )
就相当于 [ l, mid - 1 ]
6 写在最后
文章可能略有粗糙,有些内容也许表达的不是很清楚,如果有争议的地方可以提出来一起讨论一下~