每日两道力扣,day4

每日两道力扣,day4

每日两道力扣,day4

每日两道力扣,今日是:

74. 搜索二维矩阵 - 力扣(LeetCode)

4. 寻找两个正序数组的中位数 - 力扣(LeetCode)

第一题:搜索二维矩阵

74. 搜索二维矩阵 - 力扣(LeetCode)

1.思路:

基于咱们前三天的努力,想必各位大佬也大致摸清了二分查找的应用场景。这个题依旧是搜素 + 有序数组,所以咱们直接掏一手二分查找拿下他。可是这个题有点硬,咱们得细细分析,才能做到万无一失。

(1)方法一:既然咱们已经知道需要运用二分查找,面对这个二维数组,而且数据要求并不是那么的苛刻,咱们其实可以考虑运用for循环,一行一行进行二分查找(或一列一列,因为小编功力有限,咱们这里采用一行一行进行二分查找)。因为这个二维数组的高是m,宽是n。所以按照这个思路进行二分查找的时间复杂度是O(mlogn)。依旧超过了100.00%的人。

(2)方法二:但你要是问我有没有更快的方法。我的回答是:"有的有的,兄弟",在方法一中咱们执行了m次二分查找。可是咱们大可以用一次二分查找就秒掉这个题。降维打击 ------ 当成一个一维数组来二分,仔细看题目的两个条件:

  1. 每行从左到右递增。
  2. 每行的第一个整数大于前一行的最后一个整数。

这两个条件连在一起说明什么?说明如果你把这个矩阵像贪吃蛇一样拉直 ,它完完全全就是一个完美的升序一维数组!既然它本质是个一维有序数组,我们为什么不直接对"整个矩阵"做一次二分查找呢?

核心魔法:一维下标转二维坐标

假设矩阵是 M M M 行 N N N 列。一维数组的下标 idx,怎么还原成二维矩阵里的行 row 和列 col 呢?

  • 行号 row = idx / n (除了列数,就知道在第几行)
  • 列号 col = idx % n (对列数取余,就知道在该行的第几列)

注:row是高,column是宽。

2.代码实现:

(1)方法一:

复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(),n = matrix[0].size();
        bool res = false;
        for(int i = 0; i < m;i++)
        {
            
                int left = 0, right = n-1;
                

                while(left <= right)
                {
                    int mid = left + (right - left)/2;

                    if(matrix[i][mid] > target)
                    {
                        right = mid - 1;
                    }
                    else if(matrix[i][mid] < target)
                    {
                        left = mid + 1;
                    }
                    else 
                    {
                        res = true;
                        break;
                    }
                }
                if(res == true)
                break;            
        }
        return res;
    }
};

(2)方法一:(优化版)

复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        
        for(int i = 0; i < m; i++)
        {
            int left = 0, right = n - 1;

            while(left <= right)
            {
                int mid = left + (right - left) / 2;

                if(matrix[i][mid] > target)
                {
                    right = mid - 1;
                }
                else if(matrix[i][mid] < target)
                {
                    left = mid + 1;
                }
                else 
                {
                    // 修复:找到了直接返回 true,结束战斗!
                    return true; 
                }
            }
        }
        
        // 如果所有的行都找完了还没 return true,说明真的没有
        return false;
    }
};

(3)方法二:

复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size();
        int n = matrix[0].size();
        
        // 虚拟的一维数组的首尾指针
        int left = 0;
        int right = m * n - 1; 
        
        while(left <= right) {
            int mid = left + (right - left) / 2;
            
            // 魔法:把一维的 mid 还原成二维的行列坐标
            int row = mid / n;
            int col = mid % n;
            int mid_val = matrix[row][col];
            
            // 下面就是最最普通的标准二分查找了!
            if(mid_val == target) {
                return true;
            }
            else if(mid_val < target) {
                left = mid + 1;
            }
            else {
                right = mid - 1;
            }
        }
        
        return false;
    }
};
3.细节

如果我是力扣出题人,我为了恶心人。我会毫不犹豫得扩大 m ,n。使得方法一运行超时。由此这个题就变成了hard题。不会方法二的人,看到了这个题就只能干瞪眼,在心里咒骂出题的臭老头(小编今年刚好18,正值青春大好年华,可不是臭老头,顶多是个死宅男)

可惜了,可惜我不是。

第二题:寻找两个正序数组的中位数

4. 寻找两个正序数组的中位数 - 力扣(LeetCode)

1.思路

hard题...作为一个力扣才写了80多道的初学者,小编也只能尝试一下。讲一下小编遇见这个题的第一思路:我想创建一个大小为(m+n)的数组nums3用来合并nums1,nums2,然后直接在nums3内部直接二分查找就ok了,可事实真的如此嘛?我这种方法的时间复杂度是O(m+n),不符合题目要求.

难道普通人真的就写不出hard题了吗?我不信,然后我尝试了半小时。没有任何思绪,还是功夫不够深。我暂时认了。

于是,我求助了gemini,它提出了一种我从所未见的方法。

寻找"完美切割线"。

(1)我们不需要真的合并数组。中位数的本质是什么?中位数就是一条"切割线",它把所有数字分成了长度相等的左右两半,并且左半边的所有数字都小于右半边的所有数字。

假设我们在 nums1 的第 i i i 个位置切一刀,在 nums2 的第 j j j 个位置切一刀。

切割线左边共有 i + j i + j i+j 个元素,右边也有这么多元素。

为了让左右两半元素个数相等(或左边多 1 个),必须满足:

i + j = m + n + 1 2 i + j = \frac{m + n + 1}{2} i+j=2m+n+1

既然 j j j 可以通过 i i i 算出来,我们只需要在较短的那个数组(假设是 nums1)中,对切割线的位置 i i i 进行二分查找就行了!

(2)什么样的切割线是"完美的"?

切割线左边的最大值,必须小于等于右边的最小值。也就是产生交叉对比:

  1. nums1 切割线左边元素 ≤ \le ≤ nums2 切割线右边元素
  2. nums2 切割线左边元素 ≤ \le ≤ nums1 切割线右边元素

即切完之后,切口附近有 4 个关键数字:

  • nums1 切口左边的最大值:nums1LeftMax
  • nums1 切口右边的最小值:nums1RightMin
  • nums2 切口左边的最大值:nums2LeftMax
  • nums2 切口右边的最小值:nums2RightMin

因为数组本身就是有序的,所以 nums1LeftMax 肯定 ≤ \le ≤ nums1RightMin

要想满足"总左半边 ≤ \le ≤ 总右半边",我们只需要交叉对比

  • 必须满足:nums1LeftMax <= nums2RightMin
  • 必须满足:nums2LeftMax <= nums1RightMin

只要这两个交叉条件同时成立,恭喜你,这"一刀切"就是完美的! (对应代码里的 if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin))

(3)用二分查找来"挪动"这把刀

既然 j j j 是由 i i i 决定的,那问题就变得极其简单了:我们只需要在 nums1 这个数组里,寻找正确的切割点 i i i。

怎么找 i i i 最快?用二分查找!

初始状态:left = 0, right = m。每次试一个中点 i = left + (right - left) / 2

试了一刀后,看看交叉对比的结果:

  1. 完美命中:交叉条件都满足。直接算中位数!(奇数就取左边两个的最大值,偶数就取左边最大和右边最小的平均值)。

  2. nums1LeftMax > nums2RightMin

    这说明 nums1 左边的数字太大了 。既然大了,说明我们在 nums1 里切得太靠右了,划进来了太多大数字。

    动作 :刀往左挪,right = i - 1;

  3. 否则(nums2LeftMax > nums1RightMin

    这说明我们在 nums2 左边的数字太大了。因为 j j j 和 i i i 是此消彼长的,nums2 左边太大,说明 j j j 太大,也就说明我们在 nums1 里切得太靠左了 , i i i 给得太少了。

    动作 :刀往右挪,left = i + 1;

(最后,代码里那几个 INT_MININT_MAX 是干嘛的?如果一刀切在了数组最边缘,左边没数字了,就当它是极小值 − ∞ -\infty −∞;右边没数字了,就当它是极大值 + ∞ +\infty +∞​。这样就不妨碍我们进行交叉对比,也不会发生越界报错。)

2.代码实现:
复制代码
#include <vector>
#include <algorithm>
#include <climits>

using namespace std;

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        // 强制保证 nums1 是较短的数组,这样我们只在短数组上做二分,速度最快,也最安全
        if(nums1.size() > nums2.size())
        {
            return findMedianSortedArrays(nums2,nums1);
        }
        
        int m = nums1.size();
        int n = nums2.size();

        // 分割线左边应该有的总元素个数
        int totalLeft = (m + n + 1)/2;

        // 在 nums1 的区间 [0, m] 里二分查找切割线位置 i
        int left = 0, right = m;

        while(left <= right)
        {
            // nums1 的切割点
            int i = left + (right - left)/2;

            // nums2 的切割点(由 i 推导出来)
            int j = totalLeft - i;

            
            // 获取切割线左右两侧的 4 个元素
        // 如果切割线在最边缘,为了不越界,赋予极值(INT_MIN 或 INT_MAX)
            int nums1LeftMax = (i==0) ? INT_MIN : nums1[i-1];
            int nums1RightMin = (i==m) ? INT_MAX : nums1[i];

            int nums2LeftMax = (j==0) ? INT_MIN : nums2[j-1];
            int nums2RightMin = (j==n) ? INT_MAX : nums2[j];

            // 灵魂拷问:这条切割线完美吗?
            if(nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin)
            {
            // 完美!已经找到了正确的切割线

            // 如果总长度是奇数,中位数就是左半边最大的那个元素
            if((m + n) % 2 == 1)
            {
                //这里编译器帮我做了隐式类型转换,将int优化为double
                return max(nums1LeftMax,nums2LeftMax);
            }
            // 如果总长度是偶数,中位数是 (左半边最大 + 右半边最小) / 2.0
            
            else 
            {
                return (max(nums1LeftMax,nums2LeftMax) + min(nums1RightMin,nums2RightMin)) / 2.0;
            }

            }

            // 如果 nums1 左边的元素太大了,说明 nums1 的切割线划得太靠右了,往左逼近
            else if(nums1LeftMax > nums2RightMin)
            {
                right = i - 1;
            }

            // 否则说明 nums1 的切割线划得太靠左了,往右逼近
            else 
            {
                left = i + 1;
            }
        }

        // 语法需要,正常情况一定会在 while 里 return
        return 0.0;
    }
};
3.细节

(1)我们必须保证nums1是一个短数组,不然数组会越界。

(2)关于 切割点i可能的疑惑

(3)最后面我们一定要return 0.0;作为占位返回,来哄骗编译器,不然编译器这个认死理的家伙,跑到一半就给咱们这个完美的程序停了。

好了,今天的每日两道力扣到这里就算是结束了,看完是不是感觉有所收获呢?如果学有所获的话,麻烦给个三连支持一下呗。感谢观看,您的支持,将是我前进路上的重要动力。

相关推荐
菜鸟‍2 小时前
【论文学习】Disco:基于邻接感知协同着色的密集重叠细胞实例分割方法
人工智能·学习·算法
牧天白衣.2 小时前
力扣215.数组中的第K个最大元素
算法·leetcode
cxr8282 小时前
控制理论基础
人工智能·算法
明灯伴古佛2 小时前
面试:synchronized用过吗,其原理是什么
面试·职场和发展
平平淡淡才是true2 小时前
偏序关系、哈斯图、最长链长度、最长链条数
算法
小钊(求职中)3 小时前
算法知识、常用方法总结
java·算法·排序算法·力扣
paeamecium3 小时前
【PAT甲级真题】- Talent and Virtue (25)
数据结构·c++·算法·pat
Mr_Xuhhh3 小时前
蓝桥杯复习清单真题(C++版本)
c++·算法·蓝桥杯
tankeven3 小时前
HJ163 时津风的资源收集
c++·算法