• 二分搜索法整理


    参考:

    https://www.cnblogs.com/grandyang/p/6854825.html  (二分搜索小结)

    二分查找法作为一种常见的查找方法,将原本是线性时间提升到了对数时间范围,大大缩短了搜索时间,具有很大的应用场景,而在LeetCode中,要运用二分搜索法来解的题目也有很多,但是实际上二分查找法的查找目标有很多种,而且在细节写法也有一些变化。

    第一类: 需查找和目标值完全相等的数

    这是最简单的一类,也是我们最开始学二分查找法需要解决的问题,比如我们有数组[2, 4, 5, 6, 9],target = 6,那么我们可以写出二分查找法的代码如下:

    int find(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) return mid;
            else if (nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return -1;
    }

    像博主的那种写法,若right初始化为了nums.size(),那么就必须用left < right,而最后的right的赋值,用哪个都可以,博主偷懒就不写-1了。但是如果我们right初始化为 nums.size() - 1,那么就必须用 left <= right,并且right的赋值要写成 right = mid - 1,不然就会出错。所以博主的建议是选择一套自己喜欢的写法,并且记住,实在不行就带简单的例子来一步一步执行,确定正确的写法也行。

    第一类应用实例:

    Intersection of Two Arrays

    这里关于二分搜索的使用习惯有多种。为了不影响文章思路,我把另外两种的测试加上来:

    #include <iostream>
    #include <cmath>
    #include <cstring>
    using namespace std;
    
    //copyright@2011 July
    //随时欢迎读者找bug,email:zhoulei0907@yahoo.cn。
    
    //首先要把握下面几个要点:
    //right=n-1 => while(left <= right) => right=middle-1;
    //right=n   => while(left <  right) => right=middle;
    //middle的计算不能写在while循环外,否则无法得到更新。
    
    int binary_searchIteration(int array[], int n, int value)
    {
    	int left = 0;
    	int right = n - 1;
    	
    	while (left <= right)          //循环条件,适时而变
    	{
    		int middle = left + ((right - left) >> 1);  //防止溢出,移位也更高效。同时,每次循环都需要更新。
    
    		if (array[middle]>value)
    		{
    			right = middle - 1;   //right赋值,适时而变
    		}
    		else if (array[middle]<value)
    		{
    			left = middle + 1;
    		}
    		else
    			return middle;
    	}
    	return -1;
    }
    
    
    int binary_searchRecursion(int arr[], int low, int high, int key) {
    	if (low > high) return -1;
    
    	int mid = (high + low) / 2;   //中间元素,防止溢出
    	if (key == arr[mid])return mid; //找到时返回
    	else if (key > arr[mid]) {
    		return binary_searchRecursion(arr, mid + 1, high, key); // 在更高的区间搜索
    	}
    	else {
    		return binary_searchRecursion(arr, low, mid - 1, key); // 在更低的区间搜索
    	}
    }
    
    
    int main()
    {
    	int arr[] = { 2, 4, 5, 6, 9 };
    	cout << binary_searchIteration(arr,5, 5) << endl;
    	cout << binary_searchRecursion(arr, 0,5, 2) << endl;
    	getchar();
    	return 0;
    }
    

      

    第二类: 查找第一个不小于目标值的数,可变形为查找最后一个小于目标值的数

    这是比较常见的一类,因为我们要查找的目标值不一定会在数组中出现,也有可能是跟目标值相等的数在数组中并不唯一,而是有多个,那么这种情况下nums[mid] == target这条判断语句就没有必要存在。比如在数组[2, 4, 5, 6, 9]中查找数字3,就会返回数字4的位置;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回第一个数字1的位置。我们可以使用如下代码:

    复制代码
    int find(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return right;
    }
    复制代码

    最后我们需要返回的位置就是right指针指向的地方。在C++的STL中有专门的查找第一个不小于目标值的数的函数lower_bound,在博主的解法中也会时不时的用到这个函数。但是如果面试的时候人家不让使用内置函数,那么我们只能老老实实写上面这段二分查找的函数。

    这一类可以轻松的变形为查找最后一个小于目标值的数,怎么变呢。我们已经找到了第一个不小于目标值的数,那么再往前退一位,返回right - 1,就是最后一个小于目标值的数。

    第二类应用实例:

     
    第二类变形应用:Valid Triangle Number
     

    第三类: 查找第一个大于目标值的数,可变形为查找最后一个不大于目标值的数

    这一类也比较常见,尤其是查找第一个大于目标值的数,在C++的STL也有专门的函数upper_bound,这里跟上面的那种情况的写法上很相似,只需要添加一个等号,将之前的 nums[mid] < target 变成 nums[mid] <= target,就这一个小小的变化,其实直接就改变了搜索的方向,使得在数组中有很多跟目标值相同的数字存在的情况下,返回最后一个相同的数字的下一个位置。比如在数组[2, 4, 5, 6, 9]中查找数字3,还是返回数字4的位置,这跟上面那查找方式返回的结果相同,因为数字4在此数组中既是第一个不小于目标值3的数,也是第一个大于目标值3的数,所以make sense;在数组[0, 1, 1, 1, 1]中查找数字1,就会返回坐标5,通过对比返回的坐标和数组的长度,我们就知道是否存在这样一个大于目标值的数。参见下面的代码:

    复制代码
    int find(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) left = mid + 1;
            else right = mid;
        }
        return right;
    }
    复制代码

    这一类可以轻松的变形为查找最后一个不大于目标值的数,怎么变呢。我们已经找到了第一个大于目标值的数,那么再往前退一位,返回right - 1,就是最后一个不大于目标值的数。比如在数组[0, 1, 1, 1, 1]中查找数字1,就会返回最后一个数字1的位置4,这在有些情况下是需要这么做的。

    第三类应用实例:

    Kth Smallest Element in a Sorted Matrix

    第三类变形应用示例:

    Sqrt(x)

    第四类: 用子函数当作判断关系

    这是最令博主头疼的一类,而且通常情况下都很难。因为这里在二分查找法重要的比较大小的地方使用到了子函数,并不是之前三类中简单的数字大小的比较,比如Split Array Largest Sum那道题中的解法一,就是根据是否能分割数组来确定下一步搜索的范围。类似的还有Guess Number Higher or Lower这道题,是根据给定函数guess的返回值情况来确定搜索的范围。对于这类题目,博主也很无奈,遇到了只能自求多福了。

    第四类应用实例:

    Split Array Largest Sum, Guess Number Higher or LowerFind K Closest ElementsFind K-th Smallest Pair DistanceKth Smallest Number in Multiplication TableMaximum Average Subarray IIMinimize Max Distance to Gas StationSwim in Rising Water

    综上所述,博主大致将二分搜索法的应用场景分成了主要这四类,其中第二类和第三类还有各自的扩展。根据目前博主的经验来看,第二类和第三类的应用场景最多,也是最重要的两类。第一类和第四类较少,其中第一类最简单,第四类最难,遇到这类,博主也没啥好建议,多多练习吧~

  • 相关阅读:
    Leetcode 2047. 句子中的有效单词数(可以,已解决)
    Leetcode 1979. 找出数组的最大公约数
    Leetcode 1967. 作为子字符串出现在单词中的字符串数目
    Leetcode 2048. 下一个更大的数值平衡数(有点意思,已解决)
    tensorflow 自动求导
    WeisfeilerLehman(WL) 算法和WL Test
    sklearn的train_test_split() 各函数参数含义解释(非常全)
    Lesson1——Tensor
    numpy 中 * 和 np.dot() 的区别
    tf.convert_to_tensor()函数的使用 | 数据类型转换
  • 原文地址:https://www.cnblogs.com/shuqingstudy/p/9575007.html
Copyright © 2020-2023  润新知