每日两道力扣,day4
每日两道力扣,day4

每日两道力扣,今日是:
4. 寻找两个正序数组的中位数 - 力扣(LeetCode)
第一题:搜索二维矩阵


1.思路:
基于咱们前三天的努力,想必各位大佬也大致摸清了二分查找的应用场景。这个题依旧是搜素 + 有序数组,所以咱们直接掏一手二分查找拿下他。可是这个题有点硬,咱们得细细分析,才能做到万无一失。
(1)方法一:既然咱们已经知道需要运用二分查找,面对这个二维数组,而且数据要求并不是那么的苛刻,咱们其实可以考虑运用for循环,一行一行进行二分查找(或一列一列,因为小编功力有限,咱们这里采用一行一行进行二分查找)。因为这个二维数组的高是m,宽是n。所以按照这个思路进行二分查找的时间复杂度是O(mlogn)。依旧超过了100.00%的人。
(2)方法二:但你要是问我有没有更快的方法。我的回答是:"有的有的,兄弟",在方法一中咱们执行了m次二分查找。可是咱们大可以用一次二分查找就秒掉这个题。降维打击 ------ 当成一个一维数组来二分,仔细看题目的两个条件:
- 每行从左到右递增。
- 每行的第一个整数大于前一行的最后一个整数。
这两个条件连在一起说明什么?说明如果你把这个矩阵像贪吃蛇一样拉直 ,它完完全全就是一个完美的升序一维数组!既然它本质是个一维有序数组,我们为什么不直接对"整个矩阵"做一次二分查找呢?
核心魔法:一维下标转二维坐标
假设矩阵是 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)什么样的切割线是"完美的"?
切割线左边的最大值,必须小于等于右边的最小值。也就是产生交叉对比:
nums1切割线左边元素 ≤ \le ≤nums2切割线右边元素nums2切割线左边元素 ≤ \le ≤nums1切割线右边元素
即切完之后,切口附近有 4 个关键数字:
nums1切口左边的最大值:nums1LeftMaxnums1切口右边的最小值:nums1RightMinnums2切口左边的最大值:nums2LeftMaxnums2切口右边的最小值: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。
试了一刀后,看看交叉对比的结果:
-
完美命中:交叉条件都满足。直接算中位数!(奇数就取左边两个的最大值,偶数就取左边最大和右边最小的平均值)。
-
nums1LeftMax > nums2RightMin:这说明
nums1左边的数字太大了 。既然大了,说明我们在nums1里切得太靠右了,划进来了太多大数字。动作 :刀往左挪,
right = i - 1;。 -
否则(
nums2LeftMax > nums1RightMin):这说明我们在
nums2左边的数字太大了。因为 j j j 和 i i i 是此消彼长的,nums2左边太大,说明 j j j 太大,也就说明我们在nums1里切得太靠左了 , i i i 给得太少了。动作 :刀往右挪,
left = i + 1;。
(最后,代码里那几个 INT_MIN 和 INT_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;作为占位返回,来哄骗编译器,不然编译器这个认死理的家伙,跑到一半就给咱们这个完美的程序停了。

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