【LeetCode】《LeetCode 101》第四章:二分查找
创始人
2025-05-30 20:45:21

文章目录

  • 4.1 算法解释
  • 4.2 求开方
    • 69. 求开方(简单)
  • 4.3 查找区间
    • 34. 在排序数组中查找元素的第一个和最后一个位置(中等)
  • 4.4 旋转数组查找数字
    • 81. 搜索旋转排序数组 II(中等)
  • 4.5 练习
    • 154. 寻找旋转排序数组中的最小值 II(困难)
    • 540. 有序数组中的单一元素(中等)
    • 4. 寻找两个正序数组的中位数
  • 4.6 总结

4.1 算法解释

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为O(n)的数组,二分查找的时间复杂度为O(log n)

举例来说,给定一个排好序的数组{3,4,5,6,7},我们希望查找4在不在这个数组内。第一次折半时考虑中位数5,因为5大于4,所以如果4存在于这个数组,那么其必定存在于5左边这一半。于是我们的查找区间变成了{3,4,5}。(注意,根据具体情况和您的刷题习惯,这里的5可以保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数4,正好是我们需要查找的数字。于是我们发现,对于一个长度为5的数组,我们只进行了2次查找。如果是遍历数组,最坏的情况则需要查找5次。

二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足C++、Python等语言的习惯)或左闭右闭((便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

4.2 求开方

69. 求开方(简单)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    • 我们可以把这道题想象成,给定一个非负整数 a ,求 f(x) = x^2 - a = 0 的解。因为我们只考虑 x > 0,所以 f(x) 在定义域上是单调递增的。考虑到 f(0) = -a < 0, f(a) = a2 - a > 0,我们可以对 [0,a] 区间使用二分法找到 f(x) = 0 的解。
    • 为了防止除以 0,我们把 a = 0的情况单独考虑,然后对区间 [1,a] 进行二分查找。使用了 左闭右闭 的写法。
    • 最后返回 right 的原因:退出循环的条件是 left > right ,此时 left 到了 right 的右边,题目要求向下取整,则 right 才符合这个条件。
  2. 代码

    class Solution {
    public:int mySqrt(int a) {if (a == 0) return 0;int left = 1, right = a, mid, sqrt;while(left <= right){mid = left + (right - left) / 2;sqrt = a / mid;if(sqrt == mid) return mid;else if(sqrt > mid){// mid比较小,增大left = mid + 1;}else{// mid比较大,减小right = mid - 1;}}return right;}
    };
    
  3. 收获

    • 看到简单题就想着自己试着做,但是没做出来,二分查找完全忘记了,核心就在于 mid / left / right 。

    • 这题作者还给了第二种解法 —— 牛顿迭代法 , 其公式为 : xn+1 = xn - f(xn) / f’(xn) 。给定 f(x) = x2 - a = 0,所以迭代公式为: xn+1 = (x + a/xn) / 2。

      // 牛顿迭代法
      class Solution {
      public:int mySqrt(int a) {long x = a;while(x * x > a){x = (x + a/x) / 2;}return x;}
      };
      

4.3 查找区间

34. 在排序数组中查找元素的第一个和最后一个位置(中等)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    • 这道题我用了 左闭右闭 的写法,将左指针 left 设置为 0, 右指针 right 设置为 nums.size() - 1;
    • nums[mid] == target 时,说明找到了给定值,此时再设置两个指针 start 和 end,寻找该元素第一次出现和最后一次出现的位置。
    • nums[mid] < target 时,说明给定值在右边,改变 left 指针;
    • nums[mid] > target 时,说明给定值在左边,改变 right 指针;
    • 当左右指针交换位置时,遍历结束。
  2. 代码

    class Solution {
    public:vector searchRange(vector& nums, int target) {int left = 0, right = nums.size() - 1;int mid;while(left <= right){mid = left + (right - left) / 2;if(nums[mid] == target){int begin = mid, end = mid;while(begin > 0){if(nums[--begin] == target);else{begin ++;break;}}while(end < nums.size()-1){if(nums[++end] == target);else{end --;break; }}return vector{begin, end};}else if(nums[mid] > target){// 往左边一半找right = mid - 1;}else{// 往右边一半找left = mid + 1;}}return vector{-1,-1};}
    };
    
  3. 收获

    • 在寻找始末位置的时候,废了挺大功夫。一开始没考虑到边界情况,即 start >= 0 && end <= nums.size() - 1
    • 题解是使用了两个函数,分别寻找给定值的始位和末位,两者区别在于:
      • 第一个函数: if(nums[mid] >= target) r = mid; ,如果找到给定值,那么将 右指针指向它的左侧,寻找是否存在比当前序列更小的指定元素;
      • 第二个函数,if(nums[mid] <= target) l = mid + 1; ,如果找到给定值,那么左指针移动到它的右侧,寻找是否存在比当前序列更大的指定元素。

4.4 旋转数组查找数字

81. 搜索旋转排序数组 II(中等)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    本题是需要使用二分查找,怎么分是关键,举个例子:

    • 第一类:1 0 1 1 1 和 1 1 1 0 1 ,此种情况下nums[start] == nums[mid],分不清到底是前面有序还是后面有序,此时 start++ 即可,相当于去掉一个重复的干扰项
    • 第二类:2 3 4 5 6 7 1,也就是 nums[start] < nums[mid]。此例子中就是2<5;这种情况下,前半部分有序。因此如果nums[start] <= target < nums[mid],则在前半部分找,否则去后半部分找。
    • 第三类: 6 7 1 2 3 4 5 ,也就是 nums[start] > nums[mid]。此例子中就是6 >2 ;这种情况下,后半部分有序。因此如果nums[mid] < target <= nums[end]。则在后半部分找,否则去前半部分找。
  2. 代码

    class Solution {
    public:bool search(vector& nums, int target) {int left = 0, right = nums.size() - 1;while(left <= right){int mid = left + (right - left) / 2;if(nums[mid] == target) return true;if(nums[mid] == nums[left]) left++;else if(nums[mid] == nums[right])   right--;else if(nums[mid] < nums[right]){// 右区间增序 在右区间查找if(target > nums[mid] && target <= nums[right]){left = mid + 1;}else{// 在左区间right = mid - 1;}}else{// 左区间増序 在左区间查找if(target < nums[mid] && target >= nums[left]){right = mid - 1;}else{left = mid + 1;}}}return false;}
    };
    
  3. 收获

    • 我自己是先将旋转数组恢复成原来的数组,再进行二分查找,虽然有些麻烦,但是思路比较简单。

      class Solution {
      public:bool search(vector& nums, int target) {int k=0;int n = nums.size();// 查找旋转下标while(k < n - 1 && nums[k] <= nums[k + 1])    ++k;// 恢复原始非降序排列数组vector origin(n);for(int i = 0; iorigin[i] = nums[k+i+1];}for(int i = n - k - 1, j = 0; i < n; ++i){origin[i] = nums[j++];}// 二分查找int left = 0, right = n - 1;int mid;while(left <= right){mid = left + (right - left) / 2;if(origin[mid] == target)   return true;else if(origin[mid] < target) left = mid + 1;else right = mid - 1;}return false;}
      };
      

4.5 练习

154. 寻找旋转排序数组中的最小值 II(困难)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    • 因为题目给出了 nums[i] 的范围,最大不超过 5000, 因此首先设置 ans = 5001,用于保存最小值;
    • 这道题和 81 很像,一样分成三类讨论
      • 如果 nums[mid] 和左边界或右边界相等,分不清哪部分是有序的,因此此时需要缩减范围,即 left ++right --,去除干扰的重复项;
      • 如果 nums[mid] > nums[left] ,说明左半部分有序,那么左半部分的最小值一定是 nums[left] ,将其与 ans 比较后得到目前的最小值,然后将 left 指针指向 mid+1 ,寻找右半部分的最小值;
      • 如果 nums[mid] < nums[right] ,说明右半部分有序,那么右半部分的最小值一定是 nums[mid] ,将其与 ans 比较后得到目前的最小值,然后将 right 指针指向 mid - 1 ,寻找左半部分的最小值;
      • 特别地,当 左指针和 mid 指针重合的时候,需要将其指向的值与 ans 比较,得到局部最小值;
    • 当遍历结束后,得到的一定是全局最小值。
  2. 代码

    class Solution {
    public:int findMin(vector& nums) {int ans = 5001;int left = 0, right = nums.size() - 1;while(left <= right){int mid = left + (right - left) / 2;if(left == mid)   ans = min(ans, nums[left]);if(nums[left] == nums[mid]) ++left;else if(nums[right] == nums[mid])    --right;else if(nums[mid] > nums[left]){// 左边升序ans = min(ans, nums[left]);left = mid + 1;}else if(nums[mid] < nums[right]){// 右边升序ans = min(ans, nums[mid]);right = mid - 1;}}return ans;}
    };
    
  3. 收获

    • 这道题和昨天的题很像,是自己写出来的,有进步!

540. 有序数组中的单一元素(中等)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    • 首先,加入 nums[mid] 不是单一元素,那么随着数组长度的不同,另一个元素可能在它的前面,也可能在它的后面,要分开考虑;
      • 1 2 2 3 3 为例,此时 nums[mid] = 2 ,它的另一元素在它的前面,即 nums[mid] == nums[mid - 1] 。由于数组的长度一定是奇数,所以 mid 的下标一定是奇数,当 mid % 2 == 0 时,说明 mid 之前有偶数个数字,那么一定包括 单一元素 ,所以移动 right 指针到 mid - 2(扣除 nums[mid] 及 nums[mid + 1]);反之,如果 mid % 2 == 1 ,说明 mid 之前有奇数个数字,所以单一元素在右半边,移动 left 指针 到 mid + 1 ;
      • 另外一种情况如 1 2 2 3 3 4 4 ,此时 nums[mid] = 3 ,它的另一元素在它的后面,分析过程和上面一致。
      • 当然,如果 nums[mid] 既不和前一元素相等,也不和后一元素相等,说明它就是单一元素,直接返回;
    • 最后,当 left == right 的时候,循环结束,此时两个指针指向的就是单一元素。
  2. 代码

    class Solution {
    public:int singleNonDuplicate(vector& nums) {int left = 0, right = nums.size() - 1;while(left < right){int mid = left + (right - left) / 2;if(nums[mid] == nums[mid - 1]){if(mid % 2 == 0){// 前面有偶数个数,说明这个数在左半边right = mid - 2;}else{// 前面有奇数个数,说明这个数在右半边left = mid + 1;}}else if(nums[mid] == nums[mid + 1]){if(mid % 2 == 1){// 单一元素在左半边right = mid - 1;}else left = mid + 2;}else return nums[mid];}return nums[left];}
    };
    
  3. 收获

    • 这道题想了比较久,不过还是自己做出来了,以后如果遇到题目要求为时间复杂度为 O(log n)的,可以直接考虑二分,做多了还是比较容易上手的。

4. 寻找两个正序数组的中位数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 题解

    • 由于题目限制了时间复杂度为 O(log(m+n)),所以很容易就想到要使用二分查找。

    • 回顾中位数的定义,如果数组的个数为奇数,那么中位数就是中间的值;如果数组的个数为偶数,那么中位数的值就是中间两个数的平均值。

      这一题两个数组长度之和不确定,所以需要分成奇偶两种情况讨论。

      因此,为了简便运算,我们这里使用了一个小窍门:分别寻找有序数组的第 (m + n + 1) / 2 个 和第 (m + n + 2) / 2 个。这样的话,对于偶数个数的数组而言,恰好对应了中间的两个数;对于奇数个数的数组而言,两个数都是最中间的值。此时只要取它们的平均值即可。

    • 接着我们定义一个能在两个有序数组中找到第 k 个元素的函数,我们设置了 start1 和 start2 分别标记两个数组的起始位置。

    • 首先进行边界判断

      • 如果 len1 == 0,说明第一个数组中已经不存在元素,那么第 k 个元素一定在第二个数组中,返回 nums2[start 2 + k - 1];
      • 同理,如果 len2 == 0,返回 nums1[start 1 + k - 1];
      • 当 k == 1时,此时需要返回两个数组中较小的那个元素。
    • 一般情况的处理:

      • 因为我们需要在两个有序数组中找到第 K 个元素,为了加快搜索的速度,我们要使用二分法对 K 二分,我们需要分别在 nums1 和 nums2 中查找第 K/2 个元素

      • 由于两个数组的长度不定,所以有可能某个数组没有第 K/2 个数字,因此我们需要将 K/2 和 长度 len 比较,选择较小的值,比如: midVal1 = nums1[start1 + min(len1, k / 2) - 1];

      • 最后,比较这两个数组的第 K/2 小的数字 midVal1midVal2 的大小。

        如果第一个数组的第 K/2 个数字小的话,那么说明我们要找的数字肯定不在 nums1 中的前 K/2 个数字,可以将其淘汰。将 nums1 的起始位置向后移动 diff 个位置,并且此时的K也自减去 diff,调用递归,这里 diff = min(len2, k / 2); ,表示实际移动的值,因为上面提过,数组中不一定还有 k/2 个元素。

        反之,我们淘汰nums2中的前K/2个数字,并将nums2的起始位置向后移动 diff 个,并且此时的K也自减去 diff ,调用递归即可。

  2. 代码

    class Solution {
    public:double findMedianSortedArrays(vector& nums1, vector& nums2) {int m = nums1.size(), n = nums2.size(); // 中位数是第 k1 个 和第 k2 个元素的平均值// 巧妙处理后,奇偶通用// 奇数会指向两个中位数// 偶数会指向同一个中位数int k1 = (m + n + 1) / 2, k2 = (m + n + 2) / 2;return ((findKthNumber(nums1, 0, nums2, 0, k1) + findKthNumber(nums1, 0, nums2, 0, k2)) * 0.5);}int findKthNumber(vector& nums1, int start1, vector& nums2, int start2, int k){int len1 = nums1.size() - start1;int len2 = nums2.size() - start2;// 边界判断if(len1 == 0){// nums1 为空数组, 返回 nums2 的第 k 个元素return nums2[start2 + k - 1]; }   if(len2 == 0){// nums2 为空数组, 返回 nums1 的第 k 个元素return nums1[start1 + k - 1];}if(k == 1){// 当 k == 1 时, 返回 nums1 和 nums2当前的最小元素return min(nums1[start1], nums2[start2]);}// 正常情况int midVal1 = nums1[start1 + min(len1, k / 2) - 1];int midVal2 = nums2[start2 + min(len2, k / 2) - 1];if(midVal1 >= midVal2){int diff = min(len2, k / 2); // 实际移动的值return findKthNumber(nums1, start1, nums2, start2 + diff, k - diff);}else{int diff = min(len1, k / 2);return findKthNumber(nums1, start1 + diff, nums2, start2, k - diff);}}
    };
    
  3. 收获

    • 这道题不是传统意义上的二分查找,之前的套路都是定义左右指针,然后计算出中间指针,将它们相互比较;
    • 这道题没想出来,是看了题解才明白的,值得回顾。

4.6 总结

  • 其实二分查找的问题都蛮类似的,找到 mid 后,判断移动左指针还是右指针,然后根据具体问题处理;
  • 我比较习惯使用 左闭右闭 ,思考不容易出错;
  • 练习题的 4,比较难,它是对 k 二分,而不是对数组二分。

相关内容

热门资讯

深南电A涨1.55%,成交额4... 12月19日,深南电A涨1.55%,成交额4206.78万元,换手率1.47%,总市值51.36亿元...
中集集团涨5.61%,成交额6... 12月19日,中集集团涨5.61%,成交额6.87亿元,换手率3.20%,总市值507.98亿元。异...
特发信息涨1.12%,成交额8... 12月19日,特发信息(维权)涨1.12%,成交额8.09亿元,换手率7.74%,总市值105.88...
白云机场涨0.62%,成交额1... 12月19日,白云机场涨0.62%,成交额1.65亿元,换手率0.72%,总市值231.47亿元。异...
黄河旋风涨1.58%,成交额2... 12月19日,黄河旋风涨1.58%,成交额2.77亿元,换手率3.77%,总市值83.65亿元。异动...