一点就透的二分查找算法
69 求开方
题目描述
给定一个非负整数,求它的开方,向下取整。
例子1
输入: 8
输出: 2
解释: 8 的开方结果是 2.82842...,向下取整即是 2。
思考 1
这个思路很简单,因为是求n的开方,可以查找1到n/2的数字,这里就可以使用二分查找,如果发现等于n,则返回,如果大于n,则可以在比较小的一半中查找,如果发现小于n,则可以在比较大的一半中查找
当然这个题目还有其他的数学解法,这里我们只是了解二分查找的思想,其他的数学解法,可以自己去了解。
实现1
/**
* @param{number}x
* @return{number}
*/
exportdefault (x) => {
consthalfX = Math.ceil(x / 2);
letlow = 1;
lethigh = halfX;
while(low <= high) {
letmid = Math.floor(low + (high -low) / 2);
if(mid * mid === x) {
returnmid;
} elseif (mid * mid < x) {
low =mid + 1;
} else{
high= mid - 1;
}
}
returnlow * low > x ? low - 1: low + 1;
};
算法时间复杂度 O(lgn/2), 空间复杂度 O(1)
34 查找区间
题目描述
给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。如果不存在该值,则两个返回值都设为-1
进阶使用O(lgn)时间复杂度解决。
例子1
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
解释: 数字 8 在第 3 位第一次出现,在第 4 位最后一次出现。
思考 1
这个思路很简单,因为数组是升序的,而且指明使用O(lgn)方法解决,很明显使用二分查找解决。
这个也比较简单,就是常用的二分查找,如果最后没有发现,返回[-1,-1]就可以了
这里需要注意的就是你要找到target在数组中第一出现的位置和target最后一次出现的位置。
实现1
/**
* @param{number[]}nums
* @param{number}target
* @return{number[]}
*/
exportdefault (nums, target) => {
if(nums.length === 0|| (nums.length === 1&& nums[0]!== target)) {
return[-1, -1];
}
if(nums.length === 1&& nums[0]=== target) {
return[0, 0];
}
constlen = nums.length;
letlow = 0;
lethigh = len - 1;
constres = [];
while(low <= high) {
// 为了防止数据溢出
letmid = Math.floor(low + (high -low) / 2);
if(nums[mid] === target) {
letminFlag = false;
letmaxTemp = mid;
letminTemp = mid;
while(minTemp >= 0 &&nums[minTemp] === target) {
minTemp--;
}
if(minTemp + 1 !== mid) {
res.push(minTemp + 1);
} else{
res.push(mid);
}
while(maxTemp < len && nums[maxTemp] === target) {
maxTemp++;
}
if(maxTemp === mid + 1){
res.push(mid);
} else{
res.push(maxTemp - 1);
}
returnres;
}
if(nums[mid] < target) {
low =mid + 1;
}
if(nums[mid] > target) {
high= mid - 1;
}
}
if(res.length === 2){
returnres.sort((a, b) => a - b);
} else{
return[-1, -1];
}
};
算法时间复杂度 O(lgn), 空间复杂度 O(1)
81 旋转数组查找数字
题目描述
一个原本增序的数组被首尾相连后按某个位置断开(如 [1,2,2,3,4,5] → [2,3,4,5,1,2],在第一位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个旋转数组中。如果存在返回true,如果不存在返回false
例子1
输入: nums = [2,5,6,0,0,1,2], target = 0
输出: true
思考 1
最简单的肯定直接遍历数组,不过这里显然不使用这种
可以看到数组是一部分升序,另外一部分也是升序,问题是如何找到在哪里分割开?
当然这里可以遍历数组,找到分割开的两个升序数组,但是这样那不如直接遍历,不用二分查找了。
那么是否可以换个角度,假设我们如果直接使用二分查找,会怎么样呢?
可以想一下
刚开始我是想根据mid和mid+1的关系来决定移动low和high或者根据mid和mid-1的关系来决定移动low和high,可是这样感觉逻辑很复杂
后来看了题解,才明白可以把mid和low和high的关系来移动指针,如果nums[mid] 小于nums[high],那肯定是升序,因为问的数组是升序的,如果target在这个升序数组中,那么就可以排除另一半了
当nums[mid] 大于nums[low]的时候,说明这也是一个派系数组,如果同时target在这个排序数组呢,同时也能排除另外一半数组了
这里还有另外一种情况,因为数组中存在重复的数字,如果发现nums[mid]等于num[low],此时我们可以把low++,重新计算mid,计算target存在的范围
当nums[mid]等于num[high],此时我们可以把high,重新计算mid,计算target存在的范围
但是运行之后,可以发现这里的二分查找和遍历数组使用时间基本差不多。
实现1
/**
* @param{number[]}nums
* @param{number}target
* @return{boolean}
*/
exportdefault (nums, target) => {
if(nums.length === 0|| !nums) {
returnfalse;
}
if(nums.length === 1)return nums[0]=== target;
letlow = 0;
lethigh = nums.length - 1;
while(low <= high) {
letmid = Math.floor(low + (high -low) / 2);
if(nums[mid] === target) {
returntrue;
}
if(nums[mid] < nums[high]) {
// 判断target是否在这个范围内
if(target > nums[mid] && target <= nums[high]) {
low= mid + 1;
} else{
high = mid - 1;
}
} elseif (nums[mid] > nums[low]) {
if(target >= nums[low] && target < nums[mid]) {
high = mid - 1;
} else{
low= mid + 1;
}
} elseif (nums[low] === nums[mid]) {
low++;
} else{
high--;
}
}
returnfalse;
};
算法时间复杂度 O(n),因为最坏的情况下,二分查找就变成了遍历数组了。空间复杂度 O(1)
其他的待续。。。。。。
高品质全面课程升级,2020最新SVIP豪华版