【二分查找】你真的会二分查找吗?(C语言实现,附题目训练)

初学者在学完二分查找后,往往会低估二分查找的功能,他具有更广泛的使用场景不单单只是寻找其中特定的值随意一点的改动都会使二分查找的功能发生很大的变化
正如Knuth 大佬(发明 KMP 算法的那位)都说二分查找:

思路很简单,细节是魔鬼。

这篇文章主要带会从二分查找的两种形式实现题目训练加强对二分查找的理解与使用,更重要的是要学会变通

二分查找的两种实现形式

俗话说得好,万变不离其宗

这两种写法就是二分查找的基础,任何变化都离不开这两种写法
这是二分查找的基本框架:

int Search(int[] nums,int len,int target) 
{
    
    
    int left = 0, right = ...;

    while(...) 
    {
    
    
        int mid = left + (right - left) / 2;
        //不直接相加除2是为了防止溢出
        if (nums[mid] > target) 
            right = ...
        else if (nums[mid] < target) 
            left = mid + 1;
        else
            return mid;
    }
    return -1;
}

我们发现这个框架中唯一需要变化的地方就是while主循环与nums[mid]>target的情况
这也就是二分查找细节与魔鬼的地方

我们还要知道二分查找要查找的话至少能遍历一遍数组,而为了能够遍历这个数组有了常用的两种方法:左闭右开与左闭右闭

左闭右闭

那么什么是左闭右闭呢?
我们看下边这个例子:

设有一个数组{1,2,3,4}
则下标为{0,1,2,3}
则left=0;right=len-1;
那么检索范围的下标是[left,right],这也就是我们说的左闭右闭

接下来看需要变化的两个地方:

既然有了检索的下标范围
那我们就可以知道left==right时是有意义的
主循环就可以写为while(left<=right)
target < arr[mid]时,因为target!=arr[mid]
所以right=mid-1

代码实现:

int search(int arr[], int len, int target)
//target是查找的目标值
{
    
    
	int left = 0;
	int right = len - 1;
	while (left <= right)
	{
    
    
		int mid = (right - left) / 2 + left;
		if (target < arr[mid])
			right = mid - 1;
		else if (target > arr[mid])
			left = mid + 1;
		else
			return mid;
	}
	return -1;
}

左闭右开

什么是左闭右开呢?

设有一个数组{1,2,3,4}
则下标为{0,1,2,3}
则left=0;right=len;
那么检索范围的下标是[left,right),这也就是我们说的左闭右开

接下来看两个细节的地方:

既然有了检索的下标范围
那我们就可以知道left==right时是无意义的
主循环就可以写为while(left<right)
target < arr[mid]时,因为target!=arr[mid],且是左闭右开区间
所以right=mid,正好不需要重新检查arr[mid]

代码实现:

int search(int arr[], int len, int target)
{
    
    
	int left = 0;
	int right = len ;
	while (left < right)
	{
    
    
		int mid = (right - left) / 2 + left;
		if (target < arr[mid])
			right = mid;
		else if (target > arr[mid])
			left = mid + 1;
		else
			return mid;
	}
	return -1;
}

题目训练

旋转数组的最小数字

牛客链接,链接奉上。
在这里插入图片描述

相信大家学习二分查找时都听说过这样一句话,二分查找的前提是有序数组,然而真的是这样吗?
答案显然时否定的,

如果能够明确二分之后,答案存在于二分的某一侧,就可以使用二分。

思路:

我们发现这题没有给出目标值
如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
如果没有目标值,一般可以考虑 端点
这里我们考虑右端点
arr[mid]>target时,说明mid在最小值左边
arr[mid]<target时,说明mid在最小值右边,但不确定mid是不是最小值,所以right=mid
注意:这是不是对应着左闭右开区间,所以主循环中是left<rigth
因为会有重复的现象出现,所以重复时right--

代码实现:

int minNumberInRotateArray(int* nums, int numsLen ) 
{
    
    
    // write code here二分查找
    int right=numsLen-1;
    int left=0;
    int mid=0;
    while(left<right)
    {
    
    
        mid=(left+right)/2;
        if(nums[mid]>nums[right])
//利用right是因为利用右界实现比左界便捷,不需要额外的判断
//情况1 :1 2 3 4 5 , nums[mid] = 3. target = 1, 
//nums[mid] > target, 答案在mid 的左侧

//情况2 :3 4 5 1 2 , nums[mid] = 5, target = 3, 
//nums[mid] > target, 答案却在mid 的右侧
            left=mid+1;
        else if(nums[mid]<nums[right])
            right=mid;
        else 
            right--;
            //使用left++时会跳过最小值
            //例如当直接为顺序数组时,会直接跳过
    }
    return nums[left];
}

数字在升序数组中出现的次数(上下界问题)

牛客链接,链接奉上。
在这里插入图片描述
思路:

看到这题的第一眼就想到二分查找,因为这题也是寻找一个数,不过是这个数的上下界
但是我们需要做一些调整,
比如寻找左界时,相等要左移,寻找右界时,相等要右移
还要注意特殊情况的判断

代码实现:

int GetNumberOfK(int* data, int dataLen, int k) 
{
    
    
    // write code here
    if (dataLen == 0)
        //特殊情况,数组长度为0
        return 0;
    else if (data[dataLen - 1]<k || data[0]>k)
        //特殊情况,k小于或大于最小值或最大值
        return 0;
    else
    {
    
    
        int leftbound, rightbound;
        int mid;
        int left = 0, right = dataLen - 1;
        if (left == right)
            return k == data[left];
        else
        {
    
    
            //寻找左界
            while (left <= right)
            {
    
    
                //可以利用第一种情况,左闭右闭
                //第二种也可以,但是要改主循环条件为left<right
                //right要=mid
                mid = (left + right) / 2;
                if (data[mid] > k)
                    right = mid - 1;
                else if (data[mid] < k)
                    left = mid + 1;
                else
                    right--;
            }
            leftbound = left;
            //寻找右界
            left = 0, right = dataLen - 1;
            while (left <= right)
            {
    
    
                mid = (left + right) / 2;
                if (data[mid] > k)
                    right = mid - 1;
                else if (data[mid] < k)
                    left = mid + 1;
                else
                    left++;
                rightbound = right;
            }
            return rightbound - leftbound + 1;
        }
    }
}

有错的话欢迎纠错,绝对改正

猜你喜欢

转载自blog.csdn.net/2301_78636079/article/details/132641291