这篇笔记,将给大家介绍二分查找的各种常见形式,并且附有leetcode题解,一起来学习二分查找把!
文章目录
- 二分查找的原理
- 704.二分查找
- [374. 猜数字大小](https://leetcode.cn/problems/guess-number-higher-or-lower/)
- [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/)
- [278. 第一个错误的版本](https://leetcode.cn/problems/first-bad-version/)
- [69. x 的平方根 ](https://leetcode.cn/problems/sqrtx/)
- [367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/)
- [441. 排列硬币](https://leetcode.cn/problems/arranging-coins/)
- [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/)
- [349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/)
- [744. 寻找比目标字母大的最小字母](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/)
- [852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/)
- 总结
二分查找的原理
二分查找就是在一个给定的有序区间内找一个值,每次找中间值,然后舍弃一半的区间,这样每次查找就可以减少一半的数据,时间复杂度是O(logN).
二分查找可以应用于数组,是因为数组具有有随机访问的特点,并且数组是有序的。二分查找体现的数学思想是「减而治之」,可以通过当前看到的中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果
了解完原理来看一下第一题。
704.二分查找
这是最简单的二分查找,思路就是上面所说的。
int search(int* nums, int numsSize, int target){
int left = 0;
int right = numsSize-1;
while(left<=right)
{
int mid = left+(right-left)/2;
if(nums[mid]==target)
{
return mid;
}
if(nums[mid]<target)
{
left = mid+1;
}
else
{
right = mid-1;
}
}
return -1;
}
374. 猜数字大小
思路很简单就是通过二分查找找一个1—n,之间的数字,要注意mid如果直接用(left+right)/2会溢出,所以我们可以巧用减法。left+(right-left)/2。就好了。
int guessNumber(int n){
int left = 1;
int right = n;
while(left<=right)
{
int mid = left+(right-left)/2;//防止溢出
if(guess(mid)==0)//猜对了
{
return mid;
}
else if(guess(mid)==1)//猜小了
{
left = mid+1;
}
else
{
right = mid-1;
}
}
return -1;
}
35. 搜索插入位置
该题的思路就是,在这个数组里面找target,找到了还是返回下标,找不到就返回插入位置的下标,也就是我们要找的这个下标对应的值一定是>=target的,并且还要接近target,所以也就是寻找大于等于target的最小下标,所以一开始定义答案下标为numsize,也就是数组所有元素都小于target的时候,元素就该在数组尾部插入。
int searchInsert(int* nums, int numsSize, int target){
int left = 0;
int right = numsSize-1;
int ans = numsSize;
while(left<=right)
{
int mid = (left+right)>>1;
if(nums[mid]>=target)
{
ans = mid;
right = mid-1;
}
else
{
left = mid+1;
}
}
return ans;
}
278. 第一个错误的版本
思路:就是一组数字中,后半部分都是错的(或者说都是一样的数字)找到这个错误出现的第一个位置,可以转换成,求>=target的最小下标。先二分如果找到了error则向左缩小区间,直到找到那个最小的target的下标值。
int firstBadVersion(int n) {
int left = 1;
int right = n;
int ans = 0;
while(left<=right)
{
int mid = left+(right-left)/2;
if(isBadVersion(mid))
{
ans = mid;
right = mid-1;
}
else
{
left = mid+1;
}
}
return ans;
}
69. x 的平方根
思路1:暴力循环
思路2:二分思想,就是我们要找y使得y * y <=x,因为返回的是整数舍弃了小数,所以我们就要找的这个y就是<=x的最大值。先定义一个较大的区间,然后找到<=x的值保存,不断逼近x即可。
int mySqrt(int x){
int left = 0;
int right = 100000;
int y = 0;
while(left<=right)
{
long long mid = (left+right)>>1;
if((mid*mid)<=x)
{
y = mid;
left = mid+1;
}
else
{
right = mid-1;
}
}
return y;
}
367. 有效的完全平方数
方法1:暴力遍历,从1开始遍历,直到sqr大于num则返回false,若是其中有sqr==num则返回true
bool isPerfectSquare(int num){
long long int i = 1;
while(1)
{
long long int sqr = i*i;
i++;
if(sqr==num)
{
return true;
}
if(sqr>num)
{
return false;
}
}
}
方法2:使用二分查找,因为满足条件的值,假设为mid(与代码中保持一致),mid * mid一定是这个范围 1<= mid * mid <=num,所以查找的边界也就固定了,二分查找直接梭哈就可以了。如果能找到就返回true,找不到也就是循环结束,就返回false。
bool isPerfectSquare(int num){
long long left = 1;
long long right = num;
while(left<=right)
{
long long mid = (left+right)>>1;
if(mid*mid == num)
return true;
if(mid*mid<num)
{
left = mid+1;
}
else
{
right = mid-1;
}
}
return false;
}
441. 排列硬币
思路:假设有x行完整,那么1+2+3+…+x<=n,用等差数列求和即可得到x*(1+x)/2<=n。用二分查找寻找这个x即可,也就是使得该表达式值小于等于n的最大值。
注意防止溢出,要使用long long类型,最后返回的时候在强转回int
int arrangeCoins(int n){
long long left = 0;
long long right = n;
long long ans = 0;
while(left<=right)
{
long long x = (left+right)>>1;
if(x*(x+1)/2<=n)
{
ans = x;
left = x+1;
}
else
{
right = x-1;
}
}
return (int)ans;
}
34. 在排序数组中查找元素的第一个和最后一个位置
思路:首先有序数组中有一组target区间,或者没有,假设有,找到>=target的最小下标,即为开始位置,找到>=target+1的下标然后再-1,就是结束位置。然后运用前面的知识,写一个求>=target的最小下标的函数,即可,对返回值进行判断,如果不合理,就是数组内没有target,直接两个数都是-1.如果符合,就是代表,数组中target存在,那么再求出>=target+1的最小下标再-1,就是结束位置。
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int Binary_find(int* nums,int numsSize,int target)
{
int left = 0;
int right = numsSize-1;
int ans = numsSize;
while(left<=right)
{
int mid = (left+right)>>1;
if(nums[mid]>=target)
{
ans = mid;
right = mid-1;
}
else
{
left = mid+1;
}
}
return ans;
}
int* searchRange(int* nums, int numsSize, int target, int* returnSize){
int* ans = (int*)malloc(sizeof(int)*2);
*returnSize = 2;
int left = 0 ;
int right = numsSize-1;
//求>=target的最小下标
int ret = Binary_find(nums,numsSize,target);
if(ret!numsSize && nums[ret]==target)//通过返回值判断数组中有没有target
//ret!numsSize(这一句判断如果数组为空数组,那么二分的循环就进不去就会返回numsize给ret,防止num[ret]越界)
{
ans[0] = ret;
ans[1] = Binary_find(nums,numsSize,target+1)-1;//找到了>=target+1的最小下标,
//那么下标-1,就是target的最大下标。
}
else
{
ans[1] = ans[0] = -1;
}
return ans;
}
349. 两个数组的交集
思路:先将数组排序,然后遍历第一个数组,将每个元素到nums2中进行二分查找,如果找到了就放入新数组,因为交集不能有重复的,所以定义一个pre = -1,pre用来存储当前交集元素的前一个元素,如果后一个交集元素与pre相同,直接跳出二分进行下一次二分,如果后一个交集元素与pre不相同,则放入数组并更新pre。
int cmp(const void* str1,const void* str2)
{
return *(int*)str1 - *(int*)str2;
}
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
qsort(nums1,nums1Size,sizeof(int),cmp);
qsort(nums2,nums2Size,sizeof(int),cmp);
*returnSize = 0;
int* ans = (int*)malloc(sizeof(int)*nums1Size);
int pre = -1;
for(int i =0;i<nums1Size;i++)
{
int target = nums1[i];
int left = 0;
int right = nums2Size-1;
while(left<=right)
{
int mid = left+(right-left)/2;
if(nums2[mid]==target)
{
if(pre != target)
{
ans[*returnSize] = target;
(*returnSize)++;
pre = target;
break;
}
}
if(nums2[mid]<target)
{
left = mid+1;
}
else
{
right = mid-1;
}
}
}
return ans;
}
744. 寻找比目标字母大的最小字母
思路还是二分思想,目标字母是大于等于target的那个最小的字母,也就是再数组中找到一个大于等于target的最小的元素
特殊情况:题中规定,如果数组最大的元素都比target小,那就返回数组第一个元素。
char nextGreatestLetter(char* letters, int lettersSize, char target){
int left = 0;
int right = lettersSize-1;
char ans = letters[lettersSize-1];
while(left<=right)
{
int mid = left+(right-left)/2;
if(letters[mid] > target)
{
ans = letters[mid];
right = mid-1;
}
else
{
left = mid+1;
}
}
return ans>target?ans:letters[0];//注意字母是循环出现的,如果最大的字符都没有比target大,那就返回第一个字母
}
852. 山脉数组的峰顶索引
思路:为了找最大值,山峰的左边是递增的,山峰的右边是递减的,找到山峰则直接返回,如果mid+1大于了mid则说明此时mid位于左山坡,所以left = mid+1;反之则在右山坡,right = mid-1;
int peakIndexInMountainArray(int* arr, int arrSize){
int left = 0;
int right = arrSize-1;
int mid = 0;
while(left<=right)
{
mid = (left+right)>>1;
if(arr[mid]>arr[mid+1] && arr[mid]>arr[mid-1])
{
return mid;
}
if(arr[mid]<arr[mid+1])
{
left = mid+1;
}
else
{
right = mid-1;
}
}
return mid;
}
总结
相信做完这些练习,你一定对二分有了更深的理解,其实二分不一定要在有序数组内,比如最后的山峰问题,但是一定是区间有序的,根据中间值你可以判断舍弃掉那一部分区间,这才是二分的思想。