二分查找法

力扣题目:704.二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

思路

这道题目的前提是==数组为有序数组==,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。

大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

写法(左闭右闭写法):

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)

区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

代码如下:(详细注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int search(int* nums, int numsSize, int target){
int left = 0;
int right = numsSize-1;
int middle = 0;
//若left小于等于right,说明区间中元素不为0
while(left<=right) {
//更新查找下标middle的值
middle = (left+right)/2;
//此时target可能会在[left,middle-1]区间中
if(nums[middle] > target) {
right = middle-1;
}
//此时target可能会在[middle+1,right]区间中
else if(nums[middle] < target) {
left = middle+1;
}
//当前下标元素等于target值时,返回middle
else if(nums[middle] == target){
return middle;
}
}
//若未找到target元素,返回-1
return -1;
}

相关题目推荐

力扣题目:35.搜索插入位置

题目描述:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

1
2
输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

1
2
输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

1
2
输入: nums = [1,3,5,6], target = 7
输出: 4

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int searchInsert(int* nums, int numsSize, int target){
int L=0;
int R=numsSize-1;
int mid;
if(target<=nums[0]) return 0;
else if(target>nums[numsSize-1]) return numsSize;
else{
while(L<=R){
mid=(L+R)/2;
if(target<nums[mid]) R=mid-1;
else if(target>nums[mid]) L=mid+1;
else return mid;
}
}
return L;//确保在没有找到目标值的情况下返回插入位置
}

力扣题目:34.在排序数组中查找元素的第一个和最后一个位置

题目描述:

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

1
2
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

1
2
输入:nums = [], target = 0
输出:[-1,-1]

题解:

解法一(自己写的,虽然与二分查找法没什么关系,但感觉好想一些,但这个方法的时间复杂度为 O(n),没有利用到数组升序排列的条件.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
int* result = (int*)malloc(2 * sizeof(int));


result[0] = -1;
result[1] = -1;

for (int i = 0; i < numsSize; i++) {
if (nums[i] == target) {
result[0] = i;
while (i < numsSize && nums[i] == target) {
i++;
}
result[1] = i - 1;
break;
}
}


*returnSize = 2;

return result;
}

解法二(二分查找法)

思路:

直观的思路肯定是从前往后遍历一遍。用两个变量记录第一次和最后一次遇见 target\textit{target}target 的下标,但这个方法的时间复杂度为 O(n)O(n)O(n),没有利用到数组升序排列的条件。

由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。

考虑 target\textit{target}target 开始和结束位置,其实我们要找的就是数组中「第一个等于 target\textit{target}target 的位置」(记为 leftIdx\textit{leftIdx}leftIdx)和「第一个大于 target\textit{target}target 的位置减一」(记为 rightIdx\textit{rightIdx}rightIdx)。

二分查找中,寻找 leftIdx\textit{leftIdx}leftIdx 即为在数组中寻找第一个大于等于 target\textit{target}target 的下标,寻找 rightIdx\textit{rightIdx}rightIdx 即为在数组中寻找第一个大于 target\textit{target}target 的下标,然后将下标减一。两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower) 表示在 nums\textit{nums}nums 数组中二分查找 target\textit{target}target 的位置,如果 lower\textit{lower}lower 为 true\rm truetrue,则查找第一个大于等于 target\textit{target}target 的下标,否则查找第一个大于 target\textit{target}target 的下标。

最后,因为 target\textit{target}target 可能不存在数组中,因此我们需要重新校验我们得到的两个下标 leftIdx\textit{leftIdx}leftIdx 和 rightIdx\textit{rightIdx}rightIdx,看是否符合条件,如果符合条件就返回 [leftIdx,rightIdx][\textit{leftIdx},\textit{rightIdx}][leftIdx,rightIdx],不符合就返回 [−1,−1][-1,-1][−1,−1]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int binarySearch(int* nums, int numsSize, int target, bool lower) {
int left = 0, right = numsSize - 1, ans = numsSize;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}

int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
int leftIdx = binarySearch(nums, numsSize, target, true);
int rightIdx = binarySearch(nums, numsSize, target, false) - 1;
int* ret = malloc(sizeof(int) * 2);
*returnSize = 2;
if (leftIdx <= rightIdx && rightIdx < numsSize && nums[leftIdx] == target && nums[rightIdx] == target) {
ret[0] = leftIdx, ret[1] = rightIdx;
return ret;
}
ret[0] = -1, ret[1] = -1;
return ret;
}


力扣题目:69.X的平方根

题目描述:

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

示例 1:

1
2
输入:x = 4
输出:2

示例 2:

1
2
3
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

题解:

方法一(暴力求解):

1
2
3
4
5
6
7
8
int mySqrt(int x){
long long i=1;
while(i*i<=x){
i++;
}
return i-1;
}

方法二(二分查找):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int mySqrt(int x){
long long left,right;//使用 long long 类型来存储 left、right 和 mid,以防止整数溢出。
left=1;right=x/2; //right 被初始化为 x / 2,因为 x 的平方根一定小于等于 x / 2。(right可以为任何小于等于x且大于1的数,不一定让right=x/2)
long long mid;
if(x==1){
return 1;
}
while(left<=right){
mid=(left+right)/2;
if(mid*mid==x){
return mid;
}
else if(mid*mid<x){
left=mid+1;
}
else{
right=mid-1;
}
}
return left-1;
}

力扣题目:367.有效的完全平方数

题目描述:

给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false

完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。

不能使用任何内置的库函数,如 sqrt

示例 1:

1
2
3
输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 16 且 4 是一个整数。

示例 2:

1
2
3
输入:num = 14
输出:false
解释:返回 false ,因为 3.742 * 3.742 = 14 但 3.742 不是一个整数。

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool isPerfectSquare(int num){
int left=0,right=num;
if(num==1){
return true;
}
while(left<=right){
int mid=(left+right)/2;
if(mid==num/mid&&num%mid==0){
return true;
}
else if(mid<=num/mid){
left=mid+1;
}
else{
right=mid-1;
}
}
return false;
}