二分查找
一句话介绍
在查找时,我们先将被查找的键和子数组的中间键比较。 如果被查找的键小于中间键,我们就在左子数组中继续查找,如果大于我们就在右子数组中继续查找,否则中间键就是我们要找的键。
一般而言,当一个题目出现以下特性时,你就应该立即联想到它可能需要使用二分查找:
- 待查找的数组有序或者部分有序
- 要求时间复杂度低于O(n),或者直接要求时间复杂度为O(log n)
使用时需要注意查找条件,判断条件和左右边界的更新方式,三者配合不好就很容易出现死循环或者遗漏区域。
标准二分查找模板
基于有序数组的二分查找(迭代)
class BinarySearch {
public int search(int key, int[] array) {
int left = 0, right = array.length - 1;
while (left <= right) {
int mid = left + ((right - left) >>1);
if (key == array[mid]) return mid;
if (key < array[mid]) right = mid - 1;
else left = mid + 1;
}
return -1
}
}
- 循环条件:
left <= right
- 中间位置计算:
mid = left + ((right -left) >> 1)
- 左边界更新:
left = mid + 1
- 右边界更新:
right = mid - 1
- 返回值:
mid 或者 -1
实现时需要注意以下细节:
- 在计算 mid 时不能使用
(left + right) / 2
这种方式,因为 (left + right) / 2 可能会导致加法溢出。解决办法:
- 用 mid = l + (h - l) / 2
- (推荐) 用
left + ((right -left) >> 1)
代替(left + right) / 2
。这样写的目的除了防止 (left + right)出现溢出,还可以用右移操作替代除法来提升性能。
-
对 right 的赋值和循环条件有关,当循环条件为 left <= right 时,right = mid - 1;当循环条件为 left < right 时,h = mid。
-
left 的赋值一般都为 left = mid + 1。
1的解释:关于右移,这篇文章写的挺详细的:Java中的左移、右移详细分析 总结来说就是,<<
左移 不分正负 低位补0,>>
右移 正数高位补0 负数高位补1,所以左移一位相当于乘以2的一次方,右移一位相当于除以2。
2的解释
Leetcode题目
1.求开方
Leetcode : 69. Sqrt(x) (Easy)
思路
当 x≥2时,它的整数平方根一定小于 x/2且大于 0,即
0<a<x/2。由于 a 一定是整数,此问题可以转换成在有序整数集中寻找一个特定值,因此可以使用二分查找
。
算法
如果 x < 2,返回 x。
令左边界为 2,右边界为 x / 2。
当 left <= right 时:
- 令 num = (left + right) / 2,比较 num * num 与 x:
- 如果 num * num > x,更新右边界为 right = pivot -1。
- 如果 num * num < x,更新左边界为 left = pivot + 1。
- 如果 num * num == x,即整数平方根为 num,返回 num。
返回 right
注意,为了防止 num * num结果溢出,所以比较num 与 x/num的大小
代码实现
class Solution {
public int mySqrt(int x) {
if (x<2) return x;
int left = 2;
int right = x>>1;
int mid;
while( left<=right ){
mid = left + ((right-left)>>1);
if (mid == x / mid ) return mid;
else if ( mid < x / mid) left=mid+1;
else right = mid-1;
}
return right;
}
}
时间复杂度: O(log N)
空间复杂度: O(1)
2. 排列硬币
Leetcode : 441. Arranging Coins (Easy)
class Solution {
public int arrangeCoins(int n) {
if (n <= 1) {
return n;
}
int left = 1;
int right = (n>>1)+1;
int mid;
while(left<=right){
mid = left + ((right - left)>>1);
long sum = mid*(mid+1L)/2;
if (n == sum) return mid;
if (n < sum) right = mid-1;
else left = mid+1;
}
return right;
}
}
3. 有序数组中的单一元素
Leetcode : 540. 有序数组中的单一元素(Medium)
题目描述
方法一:暴力求解
求解这道题搜先可以想到线性搜索
,逐个检查元素是否是单一元素。
算法:
- 从第一个元素开始,我们检查每个第二个元素是否与当前元素相同。 如果不同,说明该元素是单个元素。
- 如果我们到达最后一个元素,则它为单一元素。
但是线性搜索的时间复杂度是 O(N)。不符合题目要求
方法二:二分搜索
题目要求时间复杂度为O(log N),很容易联想到二分查找
,所以我们尝试把线性搜索转化成二分搜索。想一下二分查找的核心思想,通俗的说就是:找到中间位置,然后判断目标位置在左边还是右边。所以问题关键就是,如何判断这个“单身狗”在哪。嘿,现在我们再一次观察例子。
好了,让我们默念:单一元素所在的子数组一定是奇数数组!!!
下面我们就可以用伪代码来讨论了
\\ 这是一个伪代码哟~
\\ 当中间元素与右边元素相同时(是cp)
if (n[mid] == n[mid+1]) :
if (right - (left+1))%2==0 : right = mid-1 \\ 数组是偶数,目标元素在右边
else: left = mid +2 \\ 否则数组是奇数,目标元素在左边
\\ 当中间元素与左边元素相同时
else if (n[mid] == n[mid-1]) :
if (right - left)%2==0 : right = mid-2 \\ 数组是偶数
else: left = mid +1 \\ 数组是奇数
\\ 中间元素与两边都不相同
else:return n[mid]
经过上面分析,我们已经把左右边界更新值
确定了(不理解的话,画个图就很好理解啦~),下面我们来确定循环条件
已知,确定循环条件最重要的就是1. 不能漏判 2. 不能陷入死循环
啊,我终于说到这了,可以愉快的贴代码啦~
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0;
int right = nums.length-1;
int mid;
while(left < right){
mid = left + ((right-left)>>1);
// 判断中间元素是否与其右边元素相同
if (nums[mid] == nums[mid+1]) {
if ((right - mid +1)%2 == 0){ // 当右子数组为偶数时,目标元素在左子数组
right = mid-1;
}
else left = mid+2; // 否则,目标元素在右边
} // 判断中间元素是否与其左边元素相同
else if (nums[mid] == nums[mid-1]){
if ((right - mid)%2 == 0){
right = mid-2; // 当右子数组为偶数时,目标元素在左子数组
}
else left = mid+1; // 否则,目标元素在右边
}
// 中间元素与两边均不相同
else return nums[mid];
}
return nums[left];
}
}
参考笔记:
https://segmentfault.com/a/1190000016825704?utm_source=tag-newest