1 前言
本来想按算法专题进行刷题,所以今天打算先从分治算法开始。于是选择了分治标签下的题目,看到一个寻找两个正序数组中位数的题,看题目感觉和在算法课上课后题差不多,而且此题还标注困难,遂点开开刷。
2 寻找两个正序数组的中位数(LeetCode4)
2.1 题目描述
给定两个大小分别位m和n的正序数组nums1和nums2。返回两个正序数组合并后的中位数,要求算法的时间复杂度为
O
(
log
(
m
+
n
)
)
O(\log(m+n))
O(log(m+n))。
示例:

2.2 题目分析与解决
要寻找合并后数组的中位数,即找两个数组中第
(
m
+
n
)
/
2
(m+n)/2
(m+n)/2或者第
(
m
+
n
)
/
2
−
1
,
(
m
+
n
)
/
2
(m+n)/2-1,(m+n)/2
(m+n)/2−1,(m+n)/2小的数。(分别对应总数为奇偶的情况,方便起见以下统称为
k
k
k或
k
−
1
,
k
k-1,k
k−1,k)
首先我们可以定义两个指针分别指向两个数组的头,然后不断选择指向较小的数的指针并且移动该指针,直到操作
k
k
k次,这样我们便能找到第
k
k
k小的数或者第
k
−
1
,
k
k-1,k
k−1,k小的数。注意由于两个数组不一定等长,因此要处理一个数组完全遍历完的情况,且要记录前一次的数。总之有许多细节需要注意。但该算法的时间复杂度为
O
(
m
+
n
)
O(m+n)
O(m+n),显然不符合题意。
另一种思路是分治的思想,我们先考虑等长的情况,设nums1[n/2]=
m
1
m_1
m1,nums2[n/2]的=
m
2
m_2
m2。则两个数组分别别其中位数划分成两个部分:
nums1
=
p
1
∣
∣
m
1
∣
∣
p
2
nums2
=
q
1
∣
∣
m
2
∣
∣
q
2
\text{nums1}=p_1||m_1||p_2\\\text{nums2}=q_1||m_2||q_2
nums1=p1∣∣m1∣∣p2nums2=q1∣∣m2∣∣q2 考虑两个数组的中位数大小,若
m
1
=
m
2
m_1=m_2
m1=m2,则中位数就是
m
1
m_1
m1或
m
2
m_2
m2。若
m
1
<
m
2
m_1<m_2
m1<m2,则
p
1
<
m
1
<
m
2
<
q
2
p_1<m_1<m_2<q_2
p1<m1<m2<q2,所以
p
1
p_1
p1和
q
2
q_2
q2中一定不会有合并后的中位数,合并后的中位数只能在
m
1
∣
∣
p
2
m_1||p_2
m1∣∣p2和
q
1
∣
∣
m
2
q_1||m_2
q1∣∣m2中,这样问题的规模就被我们减少了一般。另一种情况也类似。注意,这里偶数个数的中位数是两个数的平均值,因此偶数个数情况还有许多细节需要考虑,这里只是介绍思路。
上述的情况是一种特殊情况,我刷的算法课后题也是这种情况,本来以为可以秒,结果想了半天。
这里我们看一般情况。考虑中位数的定义:即将数组划分为两个相等的部分,一部分比中位数小,一部分比中位数大。所以我们考虑将两个数组进行划分:
nums1=nums1
[
0
:
i
−
1
]
(
l
1
)
∣
∣
nums1
[
i
:
m
−
1
]
(
r
1
)
nums2=nums2
[
0
:
j
−
1
]
(
l
2
)
∣
∣
nums2
[
j
:
n
−
1
]
(
r
2
)
\text{nums1=nums1}[0:i-1](l_1) \ ||\ \text{nums1}[i:m-1](r_1)\\\text{nums2=nums2}[0:j-1](l_2) \ ||\ \text{nums2}[j:n-1](r_2)
nums1=nums1[0:i−1](l1) ∣∣ nums1[i:m−1](r1)nums2=nums2[0:j−1](l2) ∣∣ nums2[j:n−1](r2)其中
0
≤
i
≤
m
,
0
≤
j
≤
n
0\leq i\leq m,0\leq j\leq n
0≤i≤m,0≤j≤n,
i
=
0
i=0
i=0时
nums
[
0
:
i
−
1
]
\text{nums}[0:i-1]
nums[0:i−1]为空集,其他边界情况同理。
由上述划分可以知道, l 1 < r 1 , l 2 < r 2 l_1<r_1,l_2<r_2 l1<r1,l2<r2。我们的目标是找到这样一组划分 i , j i,j i,j:
- 若 m + n m+n m+n为偶数,则 l 1 + l 2 = r 1 + r 2 l_1+l_2=r_1+r_2 l1+l2=r1+r2,即 i + j = m − i + n − j i+j=m-i+n-j i+j=m−i+n−j;若 m + n m+n m+n为奇数,则 i + j = m − i + n − j + 1 i+j=m-i+n-j+1 i+j=m−i+n−j+1。所以 i + j = ( m + n + 1 ) / 2 i+j=(m+n+1)/2 i+j=(m+n+1)/2。(C++中是下取整,奇数和偶数情况均符合)
- max { l 1 } < min { r 2 } , max { l 2 } < min { r 1 } \text{max}\{ l_1\}<\text{min}\{r_2\},\text{max}\{l_2\}<\text{min}\{r_1\} max{l1}<min{r2},max{l2}<min{r1},即 l 1 l_1 l1和 l 2 l_2 l2所有的数都要小于 r 1 r_1 r1和 r 2 r_2 r2中的数。
综合上述两条,若
m
+
n
m+n
m+n为偶数,则中位数为
l
1
,
l
2
l_1,l_2
l1,l2中的最大值与
r
1
,
r
2
r_1,r_2
r1,r2中的最小值的平均值。若
m
+
n
m+n
m+n为奇数,则中位数为
l
1
,
l
2
l_1,l_2
l1,l2中的最大值。(因为
l
1
,
l
2
l_1,l_2
l1,l2比
r
1
,
r
2
r_1,r_2
r1,r2元素多1)
因此问题转化为如何找到这样的一组划分
i
,
j
i,j
i,j。因为
i
+
j
=
(
m
+
n
+
1
)
/
2
i+j=(m+n+1)/2
i+j=(m+n+1)/2,所以
j
=
(
m
+
n
+
1
)
/
2
−
i
j=(m+n+1)/2-i
j=(m+n+1)/2−i。这里要注意,
i
∈
[
0
,
m
]
,
j
∈
[
0
,
n
]
i\in[0,m],j\in[0,n]
i∈[0,m],j∈[0,n],因此必须令
m
<
n
m<n
m<n,这样不断选择
i
i
i时,
j
=
(
m
+
n
+
1
)
/
2
−
i
∈
[
0
,
n
]
j=(m+n+1)/2-i\in[0,n]
j=(m+n+1)/2−i∈[0,n]。那么如何选择
i
i
i呢,由于要求时间复杂度为
O
(
log
(
m
+
n
)
)
O(\log(m+n))
O(log(m+n)),因此可以借助二分查找的思路,每次选择合法区间的中间值作为
i
i
i判断是否满足条件:
- 初始 left = 0 , right=m , i = (left+right) / 2 , j = ( m + n + 1 ) / 2 − i \text{left}=0,\text{right=m},i=\text{(left+right)}/2,j=(m+n+1)/2-i left=0,right=m,i=(left+right)/2,j=(m+n+1)/2−i。
- 若此时符合上述第二点条件,则找到了这样的划分,进而可以求出中位数。
- 若 max { l 1 } > min { r 2 } \text{max}\{ l_1\}>\text{min}\{r_2\} max{l1}>min{r2},说明 nums1 [ i − 1 ] \text{nums1}[i-1] nums1[i−1]太大,我们需要移动右区间找更小的 nums1 [ i − 1 ] \text{nums1}[i-1] nums1[i−1]: right ← i − 1 \text{right}\leftarrow i-1 right←i−1。
- 若
max
{
l
2
}
>
min
{
r
1
}
\text{max}\{l_2\}>\text{min}\{r_1\}
max{l2}>min{r1},说明
nums1
[
i
]
\text{nums1}[i]
nums1[i]太小,我们需要移动左区间找更大的
nums1
[
i
]
\text{nums1}[i]
nums1[i]:
left ← i + 1 \text{left}\leftarrow i+1 left←i+1。
这样我们就能求出合并后数组的中位数,时间复杂度为 O ( log ( min ( m , n ) ) ) O(\log(\min(m,n))) O(log(min(m,n))),空间复杂度为 O ( 1 ) O(1) O(1)。具体实现代码如下:
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size(),n=nums2.size();
//使nums1是短的那一个
if(m>n){
return findMedianSortedArrays(nums2,nums1);
}
int left=0,right=m;
//每次缩小一半区间
while(left<=right){
//寻找nums1的划分位置
int nums1_partition=(left+right)/2;
//此时nums2的划分位置
int nums2_partition=(m+n+1)/2-nums1_partition;
//nums1和nums2左边的最大值
//若左部分是空集令其为无穷小,这样不影响两个左部分的最大值
int nums1_left_max=(nums1_partition==0)?INT_MIN:nums1[nums1_partition-1];
int nums2_left_max=(nums2_partition==0)?INT_MIN:nums2[nums2_partition-1];
//nums1和nums2右边的最小值
//若右部分为空集则令其为无穷大,这样不影响两个右部分的最小值
int nums1_right_min=(nums1_partition==m)?INT_MAX:nums1[nums1_partition];
int nums2_right_min=(nums2_partition==n)?INT_MAX:nums2[nums2_partition];
//符合划分条件
if(nums1_left_max<=nums2_right_min&&nums2_left_max<=nums1_right_min){
//根据奇偶情况计算中位数
return (m+n)%2==0? (max(nums1_left_max,nums2_left_max)+min(nums1_right_min,nums2_right_min))/2.0:max(nums1_left_max,nums2_left_max);
}
//nums1划分值太大,从原区间的左一半选
else if(nums1_left_max>nums2_right_min){
right=nums1_partition-1;
}
//nums1划分值太小,从原区间的右一半选
else{
left=nums1_partition+1;
}
}
return 0.0;
}
};
3 统计坏数对的数目(LeetCode2364)
3.1 题目描述
偷个懒,看图:

3.2 问题分析与解决
这里和之前一道好的子数组的题差不多。筛选的分治题目居然给了一道哈希表。
如果不是坏数对,则满足
i
<
j
,
j
−
i
=
n
u
m
s
[
j
]
−
n
u
m
s
[
i
]
i<j,j-i=nums[j]-nums[i]
i<j,j−i=nums[j]−nums[i],也就是
n
u
m
s
[
j
]
−
j
=
n
u
m
s
[
i
]
−
i
nums[j]-j=nums[i]-i
nums[j]−j=nums[i]−i,因此我们只需要遍历数组
n
u
m
s
[
i
]
nums[i]
nums[i],考虑
n
u
m
s
[
i
]
nums[i]
nums[i]与其前面的数组成的好数对的个数,然后用
n
u
m
s
[
i
]
nums[i]
nums[i]与其前面的数组成的对数减去好数对的个数,最后将所有的
n
u
m
s
[
i
]
nums[i]
nums[i]得到的结果累加即可。
n
u
m
s
[
i
]
nums[i]
nums[i]可与其前面的数组成
i
i
i对,
n
u
m
s
[
i
]
nums[i]
nums[i]可与其前面组成的好数对的个数为前面的
n
u
m
s
[
i
]
−
i
nums[i]-i
nums[i]−i出现的次数,想到这一点就很好解决了:
class Solution {
public:
long long countBadPairs(vector<int>& nums) {
map<int,int> hash;
long long ans=0;
long long n=nums.size();
for(int i=0;i<n;i++){
ans+=(i-hash[nums[i]-i]);
hash[nums[i]-i]++;
}
return ans;
}
};
时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)。

988

被折叠的 条评论
为什么被折叠?



