目录
总结:
(1):最基本的二分查找算法
因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回
(2):寻找左侧边界的二分查找
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界
(3):寻找右侧边界的二分查找
因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一
————————————————
版权声明:本文为CSDN博主「肥叔菌」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:blog.csdn.net/asmartkille…
一些小问题我在(上已经详细说过了)对于一些小地方存在疑惑的同学可以点点下方链接
注:这是我自己总结的模板,比较简单,应用场景也比较多(已打了很多注释)
//这是我自己最习惯用的模板
//注意场景应用于常见的有序数组情况:对于非有序需要进一步约束条件
#include<iostream>
#include<vector>
using namespace std;
int binarySearch(vector<int>& nums, int target)
{
if (nums.size() == 0)//如果数组为空
{
return -1;
}
//常规初始化
int left = 0;
int right = nums.size() - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == target)
{
right = mid;//找左侧区间
left = mid + 1;//找右侧区间
//return mid;//属于找到了目标值就直接返回的简单二分查找
}
else if (nums[mid] > target)//如果目标值在nums[mid]的左边
{
right = mid;//则将right移到mid位置来压缩区间
}
else if (nums[mid] < target)//如果目标值在nums[mid]的有百年
{
left = mid + 1;//则将left移动到mid位置来压缩区间
}
}
//因为循环退出的条件是:left=right就退出了
//所以下面写成left或是right都是可以的
if (nums[left] == target)//进一步判断
{
return left;//返回索引
}
return -1;//如果找不到返回-1
}
1:在排序数组中查找元素的第一个和最后一个位置
解析:看到这个时间复杂度和有序数组不难想到二分查找,再联系上文的总结:寻找左侧右侧区间即是元素的第一个和最后一个位置,注释已经写得很清楚了^ ^
代码实现:
#include<stdio.h>
#include<malloc.h>
int* searchRange(int* nums, int numsSize, int target, int* returnSize)
{
int* ans = (int*)malloc(sizeof(int) * 2);//开一个存两个int类型的数组
ans[0] = ans[1] = -1;//将这两个元素初始化为-1
*returnSize = 2;//告诉这个函数我们要返回的是2个元素:没什么用
if (numsSize == 0)//如果数组长度是0:意味着数组为空
return ans;//直接返回{-1,-1}
int left = 0, right = numsSize - 1;//初始化
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] >= target)//寻找左侧边界,也就是元素一开始出现的位置
{
right = mid;//将right指针移动到mid位置,压缩区间
}
else
left = mid + 1;
}
if (nums[right] == target)//如果找到了第一个目标值
ans[0] = right;//将下标赋值给ans[0]
left = 0; right = numsSize - 1;
while (left < right)
{
int mid = (left + right + 1) / 2;
if (nums[mid] <= target)//寻找右侧边界,也就是元素结束的位置
{
left = mid;//将left指针移动到mid位置,压缩区间
}
else
right = mid - 1;
}
if (nums[right] == target) {
ans[1] = right;
}
return ans;
}
int main()
{
int nums[] = { 5,7,7,8,8,10 };
int n = sizeof(nums) / sizeof(nums[0]);
int target = 8;
int returnSize = 2;
for (int i = 0; i < 2; i++)
{
printf("%d", *(searchRange(nums, n, target, &returnSize) + i));
}
return 0;
}
2:找K个最接近的数
解析:已排序的好的数组,不难想到就是二分算法,这里参考了力扣大佬的解答(思路写得非常好)
我把Java换成了C ^ ^(缝合怪)
#include<stdio.h>
#include<malloc.h>
int* findClossElements(int* arr, int arrSize, int k, int x, int* returnSize)
{
*returnSize = k;
int* ans = (int*)malloc(sizeof(int) * k);
int left = 0;
int right = arrSize - k;
//(1)当x在mid的左边时:x离nums[mid]更近
//(2)当x在mid[mid+k]的右边时:x离nums[mid]更近
//(3)当x在[mid,mid+k]区间时,再将它的情况分为两种,即离num[mid]更近和离nums[mid+k]更近
//但是以上的4种情况都可以根据x于nums[x],和nums[x+mid]的位置分为两种关系,即下面的if 和 else if
while (left < right)
{
int mid = left + (right - left) / 2;
if (x - arr[mid] > arr[mid + k] - x)//x离nums[mid+k]更近
{
left = mid + 1;//将左指针移动到mid+1位置来压缩区间
}
//注:因为题目说了,正数优先,所以是相当于取目标值的右侧区间
else if (x - arr[mid] <= arr[mid + k] - x)//x离nums[mid]更近
{
right = mid;//将右指针移到mid位置来压缩区间
}
}
for (int i = 0; i < k; i++)
{
ans[i] = arr[left];
left++;
}
return ans;
}
int main()
{
int arr[] = { 1,2,3,4,5 };
int n = sizeof(arr) / sizeof(arr[0]);
int k = 4;
int x = 3;
int returnSize = k;
for (int i = 0; i < k; i++)
{
printf("%d ",*(findClossElements(arr, n, k, 2, &returnSize)+i));
}
return 0;
}
下面这种的写法与上面差不多,但是返回类型和代码都很简洁
int* findClosestElements(int* arr, int arrSize, int k, int x, int* returnSize)
{
assert(arr);
int left = 0, right = arrSize - 1;
while (right - left >= k)
{
if (abs(arr[left] - x) <= abs(arr[right] - x))
right--;
else
left++;
}
*returnSize = k;
return &arr[left];
}
3:寻找峰值:
解析:logn->二分,套用之前的模板+峰值肯定是在>号的那边,这道题就非常简单了
int findPeakElement(int* nums, int numsSize)
{
if (nums == NULL || numsSize == 0)
{
return -1;
}
int left = 0;
int right = numsSize - 1;
int mid = 0;
while (left < right)
{
if (left == right)
{
return left;
}
if (nums[mid] > nums[mid + 1])
{
right = mid;
}
else
{
left = mid + 1;
}
}
return left;
}
4:有效的完全平方数
解析:这道题的难点就在于不能使用sqrt内置库函数,这道题和之前的求平方根向下取整的题很相似,还是用二分,将left=1,将right=num,这样就确定了两个端点,然后常规写法求出mid
接下来判断的时候有两个选择:
(1):将 mid*mid 的值与num目标值进行大小比较
(2):将mid 与num/mid的的值进行大小比较
你会选择哪种?相信看过我上一篇的同学都会选择第二种,因为第一种会有溢出问题
好了,第二个难点我们已经突破了
本题还有一个让人意向不到的漏洞:就是由于你选择方法二导致的
因为是整除,所以就会导致有偏差,比如 5
大伙都知道5是没有整数的平方数的,但是当你代入我们这个流程 就会有 2=5/2; return true;
既然我既然能平方后是你,那你是不是可以整除我,也就是说还得加个条件&& num%mid=0
再套用我给出的模板就很简单了:
bool isPerfectSquare(int num){
if (num == 0 || num == 1 )
{
return true;
}
if (num == 5)
{
return false;
}
int left = 1;
int right = num;
while (left < right)
{
int mid = left + (right - left) / 2;
if (mid == num/mid && num%mid==0)
{
return true;
}
else if (mid > num/mid)
{
right = mid;
}
else
{
left = mid + 1;
}
}
return false;
}
注:在递归时,增加一些明显的可以直接return的元素,可以增加运行效率
5:寻找比目标字母大的最小字母
解析:有序的字母表,字母比较的时是对应ascll码的比较,不用想了,直接二分+一个特殊情况即可,特殊情况是:如果字母表中最后一个字母都比目标字母小,则返回数组的第一个元素
char nextGreatestLetter(char* letters, int lettersSize, char target){
int left = 0;
int right = lettersSize - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
if (letters[mid] > target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
if (target >= letters[lettersSize - 1])
{
return letters[0];
}
return letters[left];
}
6:寻找旋转排序数组中的最小值 II
解析:这道题和我之前写的(上)不同的地方在于,这个数组有重复值了,之前我们是将nums[mid]与nums[right]进行比较,从而压缩区间,现在多了一个特殊情况,当nums[mid]与nums[right]相等时呢?没关系,我们让right--逐一缩小区间即可
这里有个问题?(我突然想到的)
为什么不直接将right=mid?
这样不是更高效吗,反正它是递增的,mid和right之间的数肯定都是一样的
比如:0,1,2,4,4,4,4,随后我就发现我错了= =
举个例子:4,4,4,4,0,1,4 ,如果像我将right=mid,就会跳过最小值0
代码实现:我的模板+特殊情况具体分析
int findMin(int* nums, int numsSize){
int left = 0;
int right = numsSize - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] == nums[right])
{
right--;
}
else
{
if (nums[mid] > nums[right])
{
left = mid + 1;
}
else
{
right = mid;
}
}
}
return nums[left];
}
7:两个数组的交集
解析:malloc一个新空间ans,将nums1,nums2进行排序,遍历nums1,在nums2中用二分法寻找nums1中相同的元素,如果找到了相同的元素,就将其储存如ans空间,最后return ans即可
注:一些小的东西可以提高效率,我会进行标出
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
int cmp(const void* a, const void* b)
{ //按升序排序
return (*(int*)a - *(int*)b);
}
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
int size = nums1Size > nums2Size ? nums2Size : nums1Size;//输出两者中短的字符串
int* ans = (int*)malloc(sizeof(int) * size); //结果数组
*returnSize = 0;
qsort(nums1, nums1Size, sizeof(nums1[0]), cmp); //num1排序
qsort(nums2, nums2Size, sizeof(nums2[0]), cmp); //num2排序
for (int i = 0, j = 0; i < nums1Size; ++i)
{ //遍历num1
if (i != 0 && nums1[i] == nums1[i - 1]) //跳过重复值提高效率
continue;
int left = 0, right = nums2Size - 1, mid;
while (left <= right)
{ //二分法查找num2中是否有同样的值
mid = (left + right) / 2;
if (nums2[mid] == nums1[i])
{ //有同样的值则存入结果数组
ans[j++] = nums1[i];
(*returnSize)++;
break;
}
else if (nums2[mid] < nums1[i])
left = mid + 1;
else
right = mid - 1;
}
}
return res;
}
问:为什么这里的while写的=号?
答:因为当数组中只有一个元素时,nums2进不了循环,导致ans数组为空
问:为什么这里的right=mid-1?
答:问就是背的模板.....其实时因为while取了=号,如果还让mid=right会进入死循环
注:如果想让代码变得更简洁,C++完全可以做到,以下C++的vecor容器排序,本题还可用set去重
#include<iostream>
#include<vector>
#include<algorithm>
#include<cmath>
using namespace std;
int* intersection(vector<int>& nums1, vector<int>& nums2, int* returnSize)
{
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
}
8:两个数组的交集 II
解析:上一题要求的只是交集,这道题是不仅是交集,还要要求返回2个数组中出现该重复元素的最少次数比如nums1=[2,2,2,2] nums[2]=[2,2,2] 返回ans=[2,2,2],这下你应该懂题目意思了吧
与上相同,先对两个数组进行排序,获取两者中的最短长度,去malloc一个ans数组,接下来将i指针指向nums1,将j指针指向nums2,如果它俩相同即nums1[i]==nums2[j],就将这个元素储存入ans数组,其实相当于上面少了那个判是否重复的,只不过这里换了个思路采用了双指针
int cmp(const void* a, const void* b)
{ //按升序排序
return (*(int*)a - *(int*)b);
}
int* intersect(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize)
{
qsort(nums1, nums1Size, sizeof(nums1[0]), cmp);//排序
qsort(nums2, nums2Size, sizeof(nums2[0]), cmp);
int i = 0;
int j = 0;
int k = 0;
int len = nums1Size < nums2Size ? nums2Size : nums1Size;//找最短的长度去开空间
int* ans = (int*)malloc(sizeof(int) * len);//开ans数组来储存
while (i < nums1Size && j < nums2Size)//范围
{
if (nums1[i] == nums2[j])//若相等
{
ans[k++] = nums1[i];//ans数组依次存储
i++;
j++;
}
else if (nums1[i] > nums2[j])
{
j++;
}
else
{
i++;
}
}
*returnSize = k;
return ans;
}
9:两数之和 II - 输入有序数组
解析:注意这里相当于进阶版了,不能使用重复的元素,相当于两次数组都遍历这个暴力方法是不行的了
这个代码采用的是一个数组遍历,另外一个数组二分的方法
#include<stdio.h>
#include<malloc.h>
int* twoSum(int* numbers, int numbersSize, int target, int* returnSize)
{
int* ret = (int*)malloc(sizeof(int) * 2);
*returnSize = 2;
for (int i = 0; i < numbersSize; ++i)
{
int left = i + 1, right = numbersSize - 1;
while (left <= right)
{
int mid = (right - left) / 2 + left;
if (numbers[mid] + numbers[i] == target)
{
ret[0] = i + 1, ret[1] = mid + 1;
return ret;
}
else if (numbers[mid] + numbers[i] > target)
{
right = mid + 1;
}
else {
left = mid + 1;
}
}
}
ret[0] = -1, ret[1] = -1;
return ret;
}
问:为什么我这里的while加了=号?
答:因为我提交的不加=号,和right=mid的结果是错的,改成这样就对了
如果不加=号,它就无法进入while循环,就会导致新开的ret数组没办法储存答案,导致错误
10:寻找重复数
解析:最简单的办法:排序+遍历,破解中等题的可耻办法,人称不讲码德,时间复杂度为O(N+N),牺牲时间换空间,至少比O(N^2)要好
int findDuplicate(vector<int>& nums) {
sort(nums.begin(), nums.end());
for (int i = 1; i < nums.size(); i++)
{
if (nums[i] == nums[i - 1])
{
return nums[i];
}
}
return NULL;
}
好了,不闹了好好运用二分吧!
思路:
方法:二分查找
二分查找的思路是先猜一个数(有效范围 [left..right] 里位于中间的数 mid),然后统计原始数组中 小于等于 mid 的元素的个数 cnt:如果 cnt 严格大于 mid。根据抽屉原理,重复元素就在区间 [left..mid] 里;
否则,重复元素就在区间 [mid + 1..right] 里。
与绝大多数使用二分查找问题不同的是,这道题正着思考是容易的,即:思考哪边区间存在重复数是容易的,因为有抽屉原理做保证
转自
作者:liweiwei1419
链接:leetcode-cn.com/problems/fi…
来源:力扣(LeetCode)
接下来就是缝合怪将Java转为C了^ ^
int findDuplicate(int* nums, int numsSize)
{
int left = 0;
int right = numsSize - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
int cnt = 0;
for (int i = 0; i < numsSize; i++)
{
if (nums[i] <= mid)
{
cnt++;//小于mid的元素有cnt个
}
}
if (cnt > mid )//根据抽屉原理,小于等于 4 的个数如果严格大于 4 个,此时重复元素一定出现在 [1..4] 区间里
{
right = mid;
}
else//如果小于等于mid个,就说明至少有2个被存储在mid的右边
{
left = mid + 1;
}
}
return left;
}
11:寻找两个正序数组中的中位数(困难)
解析:我只会这种暴力方法,以后有时间再学到更深的时候再研究研究= =
将nums2尾插入nums1,然后排序,如果是奇数的话输出中间的元素,偶数的话就看下面吧= =
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
nums1.insert(nums1.end(),nums2.begin(),nums2.end());
int n = nums1.size();
sort(nums1.begin(),nums1.end());
if(n % 2==1)//奇数情况
{
return nums1[n / 2];
}
else//偶数情况
{
return 1.0 * (nums1[n / 2 - 1] +nums1[n / 2]) / 2;
}
究极总结:
做题顺序:先套我这个模板,懂?= =
再结合你自己的分析,和题目的特殊性
如果提交错误,把while加上个=号,把right变为right=mid+1
还不行的话,这道题就已经很难了,不能单纯套模板了,得靠你自己了