二分搜索--寻找两个有序数组的中位数

一、二分搜索

顾名思义,二分搜索就是将1个区间分割成2个区间,再对这2个区间进行计算。常见的二分搜索区间是左闭右开的形式,nums[0:length)

1.1、算法框架

int binarySearch(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 if (nums[mid] > target) {
            right = mid;
        }
    }
    return -1;
}

1.2、细节:

1.2.1、终止条件:while(left < right)

while(left < right)循环终止时,left==right
因为是左闭右开的区间:
int left = 0, right = nums.size();
nums[ 0:nums.size() )

如果,采用左闭右闭的区间,那么就要修改成
int left = 0, right = nums.size() - 1;
nums[0:nums.size() - 1]

1.2.2、mid的求取:mid = left + (right - left) / 2

计算 mid 时需要防止溢出,代码中left + (right - left) / 2就和(left + right) / 2的结果相同,但是前者有效防止了left和right太大直接相加导致溢出。

1.2.3、区间的分割:left = mid + 1;right = mid;

因为算法中使用左闭右开的区间。

如果,采用左闭右闭的区间,那么就要修改成
left = mid + 1;right = mid - 1;

1.3、上述二分查找的局限

比如说给你有序数组nums = [1,2,2,2,3],target为 2,此算法返回的索引是 2,没错。但是如果我想得到target的左侧边界,即索引 1,或者我想得到target的右侧边界,即索引 3,这样的话此算法是无法处理的。

这样的需求很常见,你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。

1.4、寻找左侧边界的二分搜索

int left_bound(vector<int> nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length(); // 注意

    while (left < right) { // 注意
        int mid = left + (right - left) / 2;// 注意
        if (nums[mid] == target) {
            right = mid;// 新的细节
        } else if (nums[mid] < target) {
            left = mid + 1;// 注意
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return nums[left] == target ? left : -1;// 新的细节
}
1.4.1、如何寻找左侧边界
        if (nums[mid] == target) {
            right = mid;// 新的细节
  • 找到 target 时不要立即返回,而是缩小「搜索区间」的上界right,在区间[left, mid)中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

左侧边界的特殊含义:

比如对于有序数组nums = [2,3,5,7],target = 1,算法会得到 left = 0,含义是:nums中小于 1 的元素有 0 个。

再比如说nums = [2,3,5,7], target = 8,算法会得到 left = 4,含义是:nums中小于 8 的元素有 4 个。

再比如说nums = [2,3,3,7], target = 3,算法会得到 left = 1,含义是:nums中小于 3 的元素有 1 个。
返回值:return nums[left] == target ? left : -1;

1.5、寻找右侧边界的二分搜索

类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:

int right_bound(vector<int> nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length();

    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return nums[left-1] == target ? (left-1) : -1;//注意
}
1.5.1、如何寻找右侧边界
        if (nums[mid] == target) {
            left = mid + 1; // 注意

当nums[mid] == target时,不要立即返回,而是增大「搜索区间」的下界left,使得区间不断向右收缩,达到锁定右侧边界的目的。

为什么最后返回left-1??
return left - 1; // 注意

  • 循环终止时,left==right
  • 最后1个判断if (nums[mid] == target)为真后,left = mid + 1;,然后left==right终止循环,可见最后一个target位于mid、即mid = left - 1

二、4. 寻找两个有序数组的中位数

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。

请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

示例 1:

nums1 = [1, 3] nums2 = [2]

则中位数是 2.0

示例 2:

nums1 = [1, 2] nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

2.1、解法1:合并数组再输出中位数

将2个有序数组合并成1个,合并的同时排序,最后再输出中位数即可。
但是,时间复杂度为O(n+m)

2.2、解法2:二分法查找中位数

总体思路

对于给定的2个数组A和B:

首先,让我们在任一位置 i 将 A 划分成两个部分:

  • len(left_A)=i, len(right_A)=m−i

      left_A                |        right_A
    

    A[0], A[1], …, A[i-1] | A[i], A[i+1], …, A[m-1]

由于 A 中有 m 个元素, 所以我们有 m+1 种划分的方法(i =0∼m)。

  • 注意:当 i=0 时,left_A 为空集, 而当 i=m 时, right_A 为空集。

采用同样的方式,我们在任一位置 j 将 B 划分成两个部分:

	   left_B            |        right_B
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
  • len(left_B)=j, len(right_B)=n−j

那么,将 left_A 和 left_B 放入一个集合,并将 right_A 和 right_B 放入另一个集合。 再把这两个新的集合分别命名为 left_part 和 right_part:

      left_part          |        right_part
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

假设当前的划分位置 i 与 j 是最终解,那么必然成立:

len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
max(left_part) <= min(right_part)

以上也是循环结束的条件!

二分查找的循环步骤:
目标:寻找合适的 i 和 j ,i=0…m,j=0…n
因为条件:

len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
等价于:
i + j = m − i + n − j​(或 i + j = m - i + n - j + 1, m+n为奇数时)
等价于:
j = (m + n + 1) / 2 - i

所以,只要找 i ,就能计算得到 j

  • 1、初始条件:设 imin=0,imax=m, 然后开始在[imin,imax] 中进行搜索合适的 i

  • 2、二分区间:令 i = (imin + imax) / 2 , j = (m + n + 1) / 2 - i,由此保证 len(left_part) == len(right_part)

  • 3、每次循环我们会遇到三种情况:

    • B[j−1] <= A[i] && A[i−1] <= B[j]:标志我们找到了 i 目标,停止循环

    • B[j−1] > A[i]:这意味着 A[i] 太小,我们必须调整 i 以使 B[j−1]≤A[i]。
      我们可以增大 i 吗?
      是的,因为当 i 被增大的时候,j 就会被减小。
      因此 B[j−1] 会减小,而 A[i] 会增大,那么B[j−1]≤A[i] 就可能被满足。
      我们可以减小 i 吗?
      不行,因为当 i 被减小的时候,j 就会被增大。
      因此 B[j−1] 会增大,而 A[i] 会减小,那么 B[j−1]≤A[i] 就可能不满足。
      所以我们必须增大 i。也就是说,我们必须将搜索范围调整为 [i+1,imax]。
      因此,设 imin=i+1,并转到步骤 2。

    • A[i−1] > B[j]:这意味着 A[i−1] 太大,我们必须减小 i 以使 A[i−1]≤B[j]。
      也就是说,我们必须将搜索范围调整为[imin,i−1]。

所求的中位数在上面的集合中,可以表示成:

median = ( max(left_part)+min(right_part) ) / 2 , 当m+n为偶数时
median = max(left_part) , 当m+n为基数时

以上也是寻找终止时,return的内容!

细节剖析
循环终止的条件
len(left_part) = len(right_part) , m+n为偶数时
len(left_part + 1)=len(right_part) , m+n为奇数时
max(left_part) <= min(right_part)

循环终止条件,也可以表示成:

i + j = m − i + n − j​(或 i + j = m - i + n - j + 1, m+n为奇数时)
B[j − 1] <= A[i]  &&  A[i − 1] <= B[j]
  • i 的含义:A数组左半部分的长度,j 同理。

所以,我们需要做的就是,在区间 [0, m]中,找到 i 使得:

B[j − 1] <= A[i] && A[i − 1] <= B[j],其中 j = (m + n + 1) / 2 - i(不论m+n奇偶)

  • 为了 j = (m + n + 1) / 2 - i 不出现负数!!需要增加条件 m <= n, 即要求数组A比数组B短
边界情况

i = 0时,len(left_A)=0, len(right_A)=m
那么 A[i−1] 不存在,我们就不需要检查 A[i−1]≤B[j] 是否成立。
j = n时,同理。
if(i == 0 || j ==n || A[i - 1] <= B[j]){ }

      left_part          |        right_part
-----------------------  |  A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

i = m时,len(left_A)=m, len(right_A)=0
那么 A[i] 不存在,我们就不需要检查 B[j−1]≤A[i] 是否成立。
j = 0时,同理。

      left_part          |        right_part
A[0], A[1], ..., A[i-1]  |  -------------------------
B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

if(j == 0 || i ==m || B[j - 1] <= A[i]){ }

如何对区间进行划分

1、设 imin=0,imax=m, 然后开始在[imin,imax] 中进行搜索合适的 i

2、令 i = (imin + imax) / 2 , j = (m + n + 1) / 2 - i,由此保证 len(left_part) == len(right_part)

  • 3、每次循环我们会遇到三种情况:
    • (j == 0 || i ==m || B[j - 1] <= A[i]) && (i == 0 || j ==n || A[i - 1] <= B[j]):标志我们找到了 i 目标,停止循环

    • (j > 0) && (i < m) && (B[j−1] > A[i]):这意味着 A[i] 太小,我们必须调整 i 以使 B[j−1]≤A[i]。
      我们可以增大 i 吗?
      是的,因为当 i 被增大的时候,j 就会被减小,即(j > 0) && (i < m)的由来。
      因此 B[j−1] 会减小,而 A[i] 会增大,那么B[j−1]≤A[i] 就可能被满足。
      我们可以减小 i 吗?
      不行,因为当 i 被减小的时候,j 就会被增大。
      因此 B[j−1] 会增大,而 A[i] 会减小,那么 B[j−1]≤A[i] 就可能不满足。
      所以我们必须增大 i。也就是说,我们必须将搜索范围调整为 [i+1,imax]。
      因此,设 imin=i+1,并转到步骤 2。

    • (i > 0) && (j < n) && (A[i−1] > B[j]):这意味着 A[i−1] 太大,我们必须减小 i 以使 A[i−1]≤B[j]。
      也就是说,我们必须将搜索范围调整为[imin,i−1]。

C++程序
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    	// 为确保j不会出现负数,要让A.size()<B.size()
        if (nums1.size() > nums2.size()) return binarySearch(nums2, nums1);
        return binarySearch(nums1, nums2);
    }
private:
    double binarySearch(vector<int>& A, vector<int>& B){
        int m = A.size(), n = B.size(), i, j;

        // 为什么循环终止条件是<=而不是<,因为[imin, imax]是左闭右闭区间 
        int imin = 0, imax = m;
       // while循环中,寻找的是i的值,明确这一点,才能明白判断条件的含义
        while (imin <= imax){
            // 求取mid二分线
            i =imin + (imax - imin) / 2 ;
            // i与j存在关系事如下,保证 len(left_part) == len(right_part) 
            j = (m + n + 1) / 2 - i;
            // 情况1:找到目标i 
            if((j == 0 || i == m || B[j - 1] <= A[i]) && (i == 0 || j == n || A[i - 1] <= B[j])){
                int leftmax, rightmin;
                // 处理左半部分的数据,求取leftmax
                if (i == 0) {
                    leftmax = B[j - 1];
                } else if (j == 0) {
                    leftmax = A[i - 1]; 
                } else {
                    leftmax = max(A[i - 1], B[j - 1]);
                }
                // m + n是奇数 
                if((m + n) & 1){
                    cout << i << j << endl;
                    return leftmax;
                // m + n是偶数 
                } else {
                    // 处理右半部分的数据,求取rightmin
                    if (i == m) rightmin = B[j];
                    else if (j == n) rightmin = A[i];
                    else rightmin = min(B[j], A[i]); 
                    return 0.5 * (leftmax + rightmin);
                }
            } else if (i < imax && B[j-1] > A[i]){
                // 情况2:A[i] 太小,我们必须增大 i 以使 B[j−1] <= A[i]
                imin = i + 1;
            } else if (i > imin && A[i-1] > B[j]){
                // 情况3:A[i−1] 太大,我们必须减小 i 以使 A[i−1] <= B[j] 
                imax = i - 1;
            }
        }
        cout << "while end!" << endl;

        return 0.0;
        
    }
};
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述: 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。 请出这两个有序数组中位数,并且要求算法的时间复杂度为 O(log(m+n))。 你可以假设 nums1 和 nums2 不会同时为空。 示例 1: nums1 = [1, 3] nums2 = [2] 则中位数是 2.0 示例 2: nums1 = [1, 2] nums2 = [3, 4] 则中位数是 (2 + 3)/2 = 2.5 解题思路: 题目要求时间复杂度为 O(log(m+n)),很明显是要用到二分查找的思想。 首先,我们需要确定中位数的位置。对于两个长度分别为 m 和 n 的有序数组,它们的中位数位置为 (m+n+1)/2 和 (m+n+2)/2,因为当 m+n 为奇数时,这两个位置的值是相同的;当 m+n 为偶数时,这两个位置的值分别为中间两个数。 然后,我们需要在两个数组中分别到第 k/2 个数(k 为中位数位置),比较它们的大小,如果 nums1[k/2-1] < nums2[k/2-1],则说明中位数位于 nums1 的右半部分和 nums2 的左半部分之间,此时可以舍弃 nums1 的左半部分,将 k 减去 nums1 的左半部分的长度,继续在 nums1 的右半部分和 nums2 的左半部分中寻找第 k/2 个数;反之,如果 nums1[k/2-1] >= nums2[k/2-1],则说明中位数位于 nums1 的左半部分和 nums2 的右半部分之间,此时可以舍弃 nums2 的左半部分,将 k 减去 nums2 的左半部分的长度,继续在 nums1 的左半部分和 nums2 的右半部分中寻找第 k/2 个数。 当 k=1 时,中位数两个数组中的最小值。 Java代码实现: class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int k = (m + n + 1) / 2; double median = findKth(nums1, 0, m - 1, nums2, 0, n - 1, k); if ((m + n) % 2 == 0) { int k2 = k + 1; double median2 = findKth(nums1, 0, m - 1, nums2, 0, n - 1, k2); median = (median + median2) / 2; } return median; } private double findKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) { int len1 = end1 - start1 + 1; int len2 = end2 - start2 + 1; if (len1 > len2) { return findKth(nums2, start2, end2, nums1, start1, end1, k); } if (len1 == 0) { return nums2[start2 + k - 1]; } if (k == 1) { return Math.min(nums1[start1], nums2[start2]); } int i = start1 + Math.min(len1, k / 2) - 1; int j = start2 + Math.min(len2, k / 2) - 1; if (nums1[i] > nums2[j]) { return findKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); } else { return findKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值