二分查找
刷题时,对二分查找的各种应用情况不太熟悉,严重影响了做题速度,特此总结下。在知乎上有一篇关于较全面的二叉查找,参考链接如下:【二分查找有几种写法?它们的区别是什么?】
分类
- 取整方式(2种)
- 向下取整
- 向上取整
- 区间开闭(4种)
- 左闭右闭
- 左闭右开
- 左开右闭
- 左开右开
- 问题类型(8种)
- 对于不下降序列a,求最小的i,使得
a[i] = key
- 对于不下降序列a,求最大的i,使得
a[i] = key
- 对于不下降序列a,求最小的i,使得
a[i] > key
- 对于不下降序列a,求最大的i,使得
a[i] < key
- 对于不上升序列a,求最小的i,使得
a[i] = key
- 对于不上升序列a,求最大的i,使得
a[i] = key
- 对于不上升序列a,求最小的i,使得
a[i] < key
- 对于不上升序列a,求最大的i,使得
a[i] > key
- 对于不下降序列a,求最小的i,使得
综上所述,二分查找共有 2×4×8=64 种写法。接下来,我们就分别实现下,上述各种版本。
取整方式为向下取整
向下取整的核心代码为int mid = lf + (rt - lf) /2;
,简单列一个表格,说明向下取整。
当数组长度为奇数的情况时:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
len | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
int lf = 0, rt = len - 1; // lf = 0, rt = 10
int mid = lf + (rt - lf) / 2; // mid = 5 整除
当数组长度为偶数的情况时:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
len | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
int lf = 0, rt = len - 1; // lf = 0, rt = 11
int mid = lf + (rt - lf) / 2; // mid = 5 向下取整
对于不下降序列a,求最小的i,使得a[i] = key
/** * 对于不下降序列a,求最小的i,使得a[i] = key * @param nums * @param key * @return */
public int binarySearch_1(int[] nums,int key){
int lf = 0, rt = nums.length-1; //闭区间 [0,len-1]
while (lf < rt){
int mid = lf + (rt-lf) / 2;
if(nums[mid] < key){
lf = mid + 1;
}
else{
rt = mid;
}
}
if (nums[rt] == key) return rt;
return -1;
}
测试结果:
非重复元素
int[] nums = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < nums.length; i++){
System.out.print(bs.binarySearch_1(nums,nums[i]) + " ");
}
int NONE = -1;
System.out.println(bs.binarySearch_1(nums,NONE));
输出:
0 1 2 3 4 5 6 7 8 9 -1
重复元素
// 重复元素
int[] nums_repeated = {1,2,3,4,5,5,6,6,7,8,9,10};
for (int i = 0; i < nums_repeated.length; i++){
System.out.print(bs.binarySearch_1(nums_repeated,nums_repeated[i]) + " ");
}
System.out.println(bs.binarySearch_1(nums,NONE));
输出:
0 1 2 3 4 4 6 6 8 9 10 11 -1
为什么是
lf < rt
,能不能lf <= rt
?
不能,注意while循环中的判断语句,砍掉的元素一定是小于key的,如果元素不重复好办,当小于key的元素坎完后,就可以接着坎key的右半部分,由向下取整决定,每当坎一次右,rt向左移动一格,直到lf == rt
,此时必须跳出循环,如果改成lf <= rt
,那么必然会进入死循环。while循环外部为什么还需要判断一次?
小于key的左半部分一定是被砍掉的,但while循环中被砍掉最后一个元素跳出循环后,它可能有两种情况,key 和比key大的值,所以必须进行一次判断。
对于不下降序列a,求最小的i,使得a[i] > key
/** * 对于不下降序列a,求最小的i,使得a[i] > key * @param nums * @param key * @return */
public int binarySearch_2(int[] nums, int key){
int lf = 0, rt = nums.length-1; //闭区间
while (lf < rt){
int mid = lf + (rt - lf) / 2; //向下取整
if (nums[mid] <= key){
lf = mid + 1;
}else{
rt = mid;
}
}
if (nums[rt] > key) return rt;
return -1;
}
测试结果:
// 对于不下降序列a,求最小的i,使得a[i] > key
int[] nums_2 = {1,2,3,4,5,6,7,8,9,10};
for(int i = 0; i < nums_2.length; i++){
System.out.print(bs.binarySearch_2(nums_2, nums_2[i]) + " ");
}
输出:
1 2 3 4 5 6 7 8 9 -1
- 循环外的if语句是否可以去掉,直接返回
rt
?
你可以试试,用我的测试用例重新跑一下,你就发现问题了。这里就是为了防止边界条件而进行的约束,假设lf
在不断更新,导致的一个结果就是它将不断靠近rt
而rt始终没有变化,此处如果遍历到数组末端那个元素,它会同样跳出循环,而让它跳出的是while循环中的if语句,而非else,所以while循环的判断语句是为了规避这种特殊情况。
取整方式为向上取整
向下取整的核心代码为int mid = lf + (rt + 1 - lf) /2;
,简单列一个表格,说明向上取整。
当数组长度为奇数的情况时:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
len | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
int lf = 0, rt = len - 1; // lf = 0, rt = 10
int mid = lf + (rt + 1 - lf) / 2; // mid = 5 不影响
当数组长度为偶数的情况时:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
len | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
int lf = 0, rt = len - 1; // lf = 0, rt = 11
int mid = lf + (rt + 1 - lf) / 2; // mid = 6 向上取整
对于不下降序列a,求最大的i,使得a[i] = key
此处和向下取整,求最小的i是一个完美对偶关系,显然用向上取整的方式更容易实现,代码如下。
/** * 对于不下降序列a,求最大的i,使得a[i] = key * @param nums * @param key * @return */
public int binarySearch_3(int[] nums, int key){
int lf = 0, rt = nums.length-1;
while (lf < rt){
int mid = lf + (rt + 1 - lf) / 2;
if (nums[mid] > key){
rt = mid -1;
}else{
lf = mid;
}
}
if(nums[lf] == key) return lf;
return -1;
}
测试结果:
非重复元素
// 非重复元素
for (int i = 0; i < nums.length; i++){
System.out.print(bs.binarySearch_3(nums,nums[i]) + " ");
}
System.out.println(bs.binarySearch_3(nums,NONE));
输出:
0 1 2 3 4 5 6 7 8 9 -1
重复元素
// 重复元素
for (int i = 0; i < nums_repeated.length; i++){
System.out.print(bs.binarySearch_3(nums_repeated,nums_repeated[i]) + " ");
}
System.out.println(bs.binarySearch_3(nums,NONE));
输出:
0 1 2 3 5 5 7 7 8 9 10 11 -1
是不是很有爱,向上取整能够取重复元素的最大下标!而且代码形式和向下取整的最小i完美对称。
同样的,因为while循环中的主要变动在于rt
指针,把所有大于key的元素全部砍掉,而一旦所有元素符合小于等于key的条件时,由于向上取整的好处,lf
指针也会慢慢向rt
靠近,所以针对重复元素它选取的一定是最大下标。
对于不下降序列a,求最大的i,使得a[i] < key
/** * 对于不下降序列a,求最大的i,使得a[i] < key * @param nums * @param key * @return */
public int binarySearch_4(int[] nums, int key){
int lf = 0, rt = nums.length-1;
while (lf < rt){
int mid = lf + (rt + 1 - lf) / 2;
if (nums[mid] >= key){
rt = mid - 1;
}else{
lf = mid;
}
}
if (nums[lf] < key) return lf;
return -1;
}
测试结果:
// 对于不下降序列a,求最大的i,使得a[i] < key
for(int i = 0; i < nums_2.length; i++){
System.out.print(bs.binarySearch_4(nums_2, nums_2[i]) + " ");
}
输出:
-1 0 1 2 3 4 5 6 7 8
和向下取整求最小的i,一个道理,最坏情况rt
不断递减地靠近lf
,此时对于第一个元素会存在漏检的情况,所以最后要加一个边界条件。