最简单算法?二分算法的万字详解(中)+图解

95 阅读13分钟

目录

总结:

(1):最基本的二分查找算法

(2):寻找左侧边界的二分查找

(3):寻找右侧边界的二分查找

1:在排序数组中查找元素的第一个和最后一个位置

2:找K个最接近的数

3:寻找峰值:

4:有效的完全平方数

5:寻找比目标字母大的最小字母

6:寻找旋转排序数组中的最小值 II

7:两个数组的交集

8:两个数组的交集 II

9:两数之和 II - 输入有序数组

10:寻找重复数

思路:

11:寻找两个正序数组中的中位数(困难)

总结:


总结:

(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…


一些小问题我在(上已经详细说过了)对于一些小地方存在疑惑的同学可以点点下方链接

(45条消息) 最简单算法?二分算法的万字详解(上)+图解_lihua777的博客-CSDN博客https://blog.csdn.net/lihua777/article/details/122527636?spm=1001.2014.3001.5502

注:这是我自己总结的模板,比较简单,应用场景也比较多(已打了很多注释)

//这是我自己最习惯用的模板
//注意场景应用于常见的有序数组情况:对于非有序需要进一步约束条件
#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个最接近的数

 解析:已排序的好的数组,不难想到就是二分算法,这里参考了力扣大佬的解答(思路写得非常好)

力扣https://leetcode-cn.com/problems/find-k-closest-elements/solution/pai-chu-fa-shuang-zhi-zhen-er-fen-fa-python-dai-ma/

 我把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

还不行的话,这道题就已经很难了,不能单纯套模板了,得靠你自己了