LeetCode刷题——4.寻找两个正序数组的中位数(困难)——二分查找

我的解法:二分查找

class Solution {

public:

    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {

        

        if(nums1.size()>nums2.size()){

            auto temp=nums1;

            nums1=nums2;

            nums2=temp;

        }



        int m=nums1.size();

        int n=nums2.size();


        //处理其中一个有序数组为空的情况
        if(n==0){

            if(m%2){

                return nums1[m/2];

            }else{

                return double(nums1[m/2-1]+nums1[m/2]);

            }



        }        

        int middle_of_totalnum=(m+n+1)/2;//向上取整



        int left=0;

        int right=m;


        //二分查找主体
        //查找范围[0,m]

        while(left<right){

            //依然中位数向上取整 使最终left==right,避免陷入死循环
            int i=left+(right-left+1)/2;

            int j=middle_of_totalnum-i;

            if(nums1[i-1]>nums2[j]){

                //查找范围[left,i-1]
                right=i-1;

            }else{

                //查找范围[i,right]
                left=i;

            }

        }

        int i=left;

        int j=middle_of_totalnum-i;



        int nums1left=i==0?INT_MIN:nums1[i-1];//设置最小/最大值,避免后面比较时产生干扰

        int nums1right=i==m?INT_MAX:nums1[i];

        int nums2left=j==0?INT_MIN:nums2[j-1];

        int nums2right=j==n?INT_MAX:nums2[j];



        if((m+n)%2){

            return max(nums1left,nums2left);

        }else{
            
            //不可截断,进行类型转换
            return double(max(nums1left,nums2left)+min(nums1right,nums2right))/2;
        }

    }



};

官方解法及解析:

缺点:忽略咯其中一个数组为空的情况,一些函数不可在调试中直接调用。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int leftLength = nums1.length;
        int rightLength = nums2.length;
        // 为了保证第一个数组比第二个数组小(或者相等)
        if (leftLength > rightLength) {
            return findMedianSortedArrays(nums2, nums1);
        }
        // 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
        // 两个数组长度之和为偶数时,当在长度之和上+1时,由于整除是向下取整,所以不会改变结果
        // 两个数组长度之和为奇数时,按照分割线的左边比右边多一个元素的要求,此时在长度之和上+1,就会被2整除,会在原来的数
        //的基础上+1,于是多出来的那个1就是左边比右边多出来的一个元素
        int totalLeft = (leftLength + rightLength + 1) / 2;
        // 在 nums1 的区间 [0, leftLength] 里查找恰当的分割线,
        // 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
        int left = 0;
        int right = leftLength;
        // nums1[i - 1] <= nums2[j]
        //  此处要求第一个数组中分割线的左边的值 不大于(小于等于) 第二个数组中分割线的右边的值
        // nums2[j - 1] <= nums1[i]
        //  此处要求第二个数组中分割线的左边的值 不大于(小于等于) 第一个数组中分割线的右边的值
        // 循环条件结束的条件为指针重合,即分割线已找到
        while (left < right) {
            // 二分查找,此处为取第一个数组中左右指针下标的中位数,决定起始位置
            // 此处+1首先是为了不出现死循环,即left永远小于right的情况
            // left和right最小差距是1,此时下面的计算结果如果不加1会出现i一直=left的情况,而+1之后i才会=right
            // 于是在left=i的时候可以破坏循环条件,其次下标+1还会保证下标不会越界,因为+1之后向上取整,保证了
            // i不会取到0值,即i-1不会小于0
            // 此时i也代表着在一个数组中左边的元素的个数
            int i = left + (right - left + 1) / 2;
            // 第一个数组中左边的元素个数确定后,用左边元素的总和-第一个数组中元素的总和=第二个元素中左边的元素的总和
            // 此时j就是第二个元素中左边的元素的个数
            int j = totalLeft - i;
            // 此处用了nums1[i - 1] <= nums2[j]的取反,当第一个数组中分割线的左边的值大于第二个数组中分割线的右边的值
            // 说明又指针应该左移,即-1
            if (nums1[i - 1] > nums2[j]) {
                // 下一轮搜索的区间 [left, i - 1]
                right = i - 1;
                // 此时说明条件满足,应当将左指针右移到i的位置,至于为什么是右移,请看i的定义
            } else {
                // 下一轮搜索的区间 [i, right]
                left = i;
            }
        }
        // 退出循环时left一定等于right,所以此时等于left和right都可以
        // 为什么left一定不会大于right?因为left=i。
        // 此时i代表分割线在第一个数组中所在的位置
        // nums1[i]为第一个数组中分割线右边的第一个值
        // nums[i-1]即第一个数组中分割线左边的第一个值
        int i = left;
        // 此时j代表分割线在第二个数组中的位置
        // nums2[j]为第一个数组中分割线右边的第一个值
        // nums2[j-1]即第一个数组中分割线左边的第一个值
        int j = totalLeft - i;
        // 当i=0时,说明第一个数组分割线左边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        // 等i=第一个数组的长度时,说明第一个数组分割线右边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums1RightMin = i == leftLength ? Integer.MAX_VALUE : nums1[i];
        // 当j=0时,说明第二个数组分割线左边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        // 等j=第二个数组的长度时,说明第二个数组分割线右边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums2RightMin = j == rightLength ? Integer.MAX_VALUE : nums2[j];
        // 如果两个数组的长度之和为奇数,直接返回两个数组在分割线左边的最大值即可
        if (((leftLength + rightLength) % 2) == 1) {
            return Math.max(nums1LeftMax, nums2LeftMax);
        } else {
            // 如果两个数组的长度之和为偶数,返回的是两个数组在左边的最大值和两个数组在右边的最小值的和的二分之一
            // 此处不能被向下取整,所以要强制转换为double类型
            return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
        }
    }
}

@BlackLii 感谢整理思路和细节,我认真看了一下,注释这里有一点点小补充:int i = left + (right - left + 1) / 2; 这里是先看到了 left = i ,然后把中位数调整成为上取整,这一点是提示我们改成上取整的主导因素。

接着从上取整的角度,发现在区间 [0, m] 在循环体内部正好不会取到 0,因此验证了 nums1[i - 1] 这种写法是「安全」的。

所以,这里的提法不准确:「此处+1是为了不会出现i为0,于是i-1不会发生下标越界的情况」。

我个人的意思是:此处 +1 是首先为了避免出现死循环,同时 +1 以后,还不会出现下标越界的情况,两个方面都保证了我们设计的算法是正确有效的。


解答if条件只有其中一部分

(nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]) -> (nums1[i-1]>nums2[j])


问:请问下里面交叉小于等于是两个条件,但是while 取反时只有一个小于条件,是怎么满足第二个小于条件的?


liweiwei1419:
@DongNie 条件1并且条件2,这种形式取反,只需要否定其中一个条件就可以了。这个是高中数学真值表里的内容,不知道我说清楚没有,欢迎讨论。


风云:
@liweiwei1419 否定一个条件从效果上看,确实可以达到缩小查找区间的目的。我想 @DongNie的疑惑在于,(1) 为什么通过否定一个条件能够持续缩小查找区间,直至变空。另外,区间变空后退出循环得出的分割线位置,(2) 为什么能够保证另外一个条件也满足

liweiwei1419:
@风云 我简单回答一下吧。

问题(1),否定一个条件能够持续缩小查找区间,这是和代码的语义相关的,不是左移,就是右移,我在文字题解和视频里都有很详细的分析;

问题(2):if 里面继续搜索的区间分析清楚了,else 就是 if 的反面区间,到底 else 的具体逻辑是什么,可以不用管。因为要保证 if 和 else 两个区间和起来是原来的区间(这一点很重要)。要搞懂 else 的逻辑是什么也可以,稍微认真一点都可以分析得出来,只是没有必要了。因为二分查找是非黑即白的过程,if 里面向左走,else 里面就得向右走。

二分查找就是不断的缩小搜索区间,直到找到我们要找的元素。


风云:
@liweiwei1419 谢谢答复。通过否定其中一个条件,能够缩小查找区间至空这一点,从代码中还算容易推理出来。针对问题(2) ,A&&B的否定等价于!A || !B, 即 !(A && B) <=> !A || !B. 而 if 分支中选择了!A, 则else 分支中对应A。查找空间变空时,分割线满足条件A是可以理解的,但说最终分割线满足A&&B, 缺乏理论证明吧。

下面根据个人浅显的理解,给出一个推导,做个简单证明吧。:


 分割线位置应满足nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
 这里仅对第一个条件取反,也可以持续缩小查找区间。并且确立的分割线,nums2也满足第二个条件,即nums2[j-1]<=nums1[i]


 原因在于: nums1确立的极致分割线(区间为空时分割线的状态)位置分2种情况,


 (1) 不在边界处; 假设已经极致满足了第一个条件,此时若继续右移(else分支)时,nums1[i]必定<=nums2[j-1]. 否则的话
 不满足极致边界. 由此可以判定第二个条件也是满足的,即nums2[j-1]<=nums1[i]。 
  举个例子。
  极致条件下(区间已为空)时, 根据否定条件1,算法得出来的最终分割线中nums1和nums2分割线的状态如下。Ap<=Bq+1 
  nums1:   A1 A2 ... Ap | Ap+1 .. Ai , 分割线在Ap和Ap+1之间
  nums2:   B1 B2... Bq | Bq+1 ...Bj,分割线在Bq和Bq+1之间
当前要证明的是,极致条件下,Ap<=Bq+1时,Bq<=Ap+1成立。


反证法,假设Bq>Ap+1, 则算法进入else分支,分割线可继续右移。
使得 
nums1:  A1 A2 ... Ap Ap+1| Ap+2 .. Ai;   
nums2:  B1 B2... Bq-1 |  Bq Bq+1 ...Bj。
与已经达到极致条件冲突。得证。


(2) 在边界处;假设已经极致满足了第一个条件,nums1整个数组已经被包含掉,分割线右侧无元素,
这种情况在算法尾部边界判断时认定了右侧无元素表示最大值,因此nums2[j-1]<=nums1[i]


**总结:把 (A&&B  取反)作为条件时可只用!A or !B**

 官方详解:

1.二分查找

2.划分数组

力扣https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值