4-三种方法彻底解决中位数问题

4. 寻找两个正序数组的中位数

两种思想

  • 真合并:使用归并的方式,合并两个有序数组,得到一个大的有序数组,大的有序数组中的中间位置的元素即为中位数。$O(n+m),O(n+m)$
  • 假合并:不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标为0的位置,每次将指向较小的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到达到中位数的位置。$O(n+m),O(1)$

常见的思想改进:假合并、奇偶合并

通过假合并的思想可以将空间复杂度优化到$O(1)$但对于时间复杂度并没有什么优化,此方法代码复杂,不仅要考虑奇偶问题,更需要高了一个数组遍历后的各种边界问题。

假合并的一个优化点是 将奇偶两种情况合并到了一起:

  • 如果是奇数,我们需要知道第(len+1)/2 个数就可以了,如果遍历的话需要遍历int(len/2)+1次
  • 如果是偶数,需要知道第(len/2) 和 len/2 + 1个数,也是需要遍历len/2 + 1次
  • 返回中位数,奇数需要最后一次遍历结果就可以,偶数需要最后一次和上一次的结果。所以用两个变量left和right。right保存当前循环的结果,在每次循环前将right赋值给left。这样在最后一次循环的时候,left将得到right的值,也就是上一次的循环结果,加下来right更新为最后一次的结果len/2+1次

另一种合并的思想是: 我们可以在奇数的时候, 在末尾等处添加一个占位符#等, 这样也是可以将奇数合并成偶数的情况的.此方法的另一个优化点就是 通过在if条件中加入大量的限制条件, 从而实现了对于各种边界问题的处理, 这也是一种很重要的思想.

此方法的时间复杂度相对于下面两种思想还是太高了, 大家不用特意掌握此方法, 但是这两个优化的思想还是很重要的, 要好好的理解一下.

接下来我们就来详细讲解两个时间复杂度超低的算法代码思想.

寻找第k小数(记住这个)

主要就是根据两个数的三种比较结果, 不断地去除不满足的元素的过程.

这个思想最难的点在于 三种特殊情况的处理, 我们能否想到这三种情况, 并将他们完美的融入到代码之中, 我感觉这才是真正的难点所在.

最开始对于奇数和偶数的两种情况进行了判断, 其实是可以将两种情况合并的, 只需要在奇数时求两次同样的k就可以了.

接下来处理了三种特殊情况中的两种特殊情况: 一个数组为空 和 k=1.

下面的几个定义就非常重要了, 一定要弄清这些定义的含义, 才能更轻松的理解代码.

index1, index2作为数组的起始点的下标, 初值都是0, 但是随着两个数组不断被删除元素, 这两个起始点也是在不断的进行变化, 具体变化方式就是 index1 = newIndex1 + 1, 因为在删除元素的时候 连同比较位置也一同删去了, 所以新的开始是 比较位置 的后一位.

newindex1, newindex2作为比较点就是图中被框中的两个数的下标, 它的赋值过程就涉及到了 最后一个边界情况. 因为当一个数组较短时, 其中一个比较点可能已经到达了数组的最后, 所以它的值是 两种情况下较小的那个数.

接下来就是根据两个比较点的大小来进行不同的操作过程了, 这里最难理解的点就是 k -= (newIndex1 - index1 + 1), 也就是减去元素的个数问题了. 我们根据上面的图来举例, 图中index1的值为0, newindex1的值经过计算为1, 通过比较后, 可以看到 红色的数 就是被删除的数, 也就是两个, 所以我们需要在最后+1才是真实被删去的个数. 对于此类问题在确定最终个数的时候, 我们都可以通过这样的特例来决定代码的书写, 至此代码就全部讲解完成了.

理解中位数作用进行划分数组

最后这种思想时间复杂度比上面的还低,上面的思想每一轮循环可以将查找范围减少一半,因此时间复杂度是$O(log(m+n))$ ,但这种思想可以对确定的较短的数组进行二分查找,所以它的时间复杂度是$O(logmin(m,n))$

划分数组正好和上面算法完全相反,它的思想特别复杂,但思想理解了,代码写起来倒是没太大难度。

首先要明白中位数的作用:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。这种思想无论是在机构数组中都是适用的,这就衍生出了下面的思想。

首先讨论奇偶两种不同情况的不同划分方式,

然后在写代码时,由于计算机的取整操作,我们是可以将这两种情况合并成一种代码书写方式,其中的$i$和$j$分别是两个数组的划分位置。

同样我们也会遇到复杂的边界问题, 但下面这种处理方式是真的非常优秀.

上面问题都考虑完了, 其实就可以写代码了, 但是我们需要进行两个条件的判断: B[j−1]≤A[i] 以及A[i−1]≤B[j], 为了优化代码, 经过分析后, 我们发现这两种情况是可以等价转换的. 也就是只需要进行一个条件的判断即可.

代码中有个注意点就是java中的三目运算符? : 在Python中是没有引入这个符号的, 但是Python利用了已有的关键字if…else实现了这个功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class Solution {

//常规思想 假合并
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int len = m+n;
int left = -1, right= -1; // 记录前后两个数
int aStart=0, bStart=0; // 记录两个数组的移动
for(int i=0;i<=len/2;i++){
left = right; // 每次循环前将right的值赋给left
// A移动的条件:B遍历到最后 或当前 A<B 满足一个即可
if(aStart < m && (bStart>=n || nums1[aStart] < nums2[bStart])){
right = nums1[aStart++];
}else{
right= nums2[bStart++];
}
}
if((len & 1) == 0) // 与1交,判断奇偶数,更快速
return (left+right)/2.0;
else
return right;
}


// 第k小数
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length1 = nums1.length;
int length2 = nums2.length;
int totallength = length1+ length2;
if(totallength % 2 == 1){ // 可以将两种情况合并,奇数会求两次同样的k
int midIndex = totallength/2;
double median = getKthElement(nums1, nums2, midIndex+1);
return median;
} else{
int midIndex1 = totallength/2 -1 , midIndex2 = totallength/2;
double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
return median;
}
}

public int getKthElement(int[] nums1, int[] nums2, int k){
/**
主要思路:要找到第k(k>1)小的元素,那么就取pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
这里的“/” 表示整除
nums1 中小于等于pivot1的元素有 nums1[0..k/2-2] 共计k/2-1个
nums2 中小于等于pivot2的元素有 nums2[0..k/2-2] 共计k/2-1个
取 pivot = min(pivot1 , pivot2) 两个数组中小于等于 pivot的元素共计不会超过(k/2-1)+(k/2-1) <=k-2个
这样pivot本身最大也只能是第k-1小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/
int length1 = nums1.length, length2=nums2.length;
int index1 = 0, index2=0;
int kthElemnt=0;
while(true){
//特殊情况
if(index1==length1){//第二种特殊情况,一个数组为空
return nums2[index2+k-1];
}
if(index2==length2){//一个数组为空
return nums1[index1+k-1];
}
if(k==1){ // 第三种情况,k=1
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情况, index1,index2作为起始点,newindex1,newindex2作为比较点 在不停的更新
int half = k/2;
int newIndex1 = Math.min(index1 + half, length1) -1;// 第一种情况,发生越界,需要记录比较的位置
int newIndex2 = Math.min(index2 + half, length2) - 1;
int pivot1 = nums1[newIndex1], pivot2=nums2[newIndex2]; //获取两个需要比较的数
if(pivot1<=pivot2){
// 将两种情况合并
k -= (newIndex1 -index1 +1); //两者相减后+1,这才是真正减去的长度
index1 = newIndex1+1; //连同比较位置也一同删去了,所以新的开始是 比较位置的后一位
} else{
k -= (newIndex2-index2+1);
index2 = newIndex2+1;
}
}
}

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}

int m = nums1.length;
int n = nums2.length;
int left = 0, right = m;
// median1:前一部分的最大值
// median2:后一部分的最小值
int median1 = 0, median2 = 0;

while (left <= right) { // 一直循环找到一个最大的i满足A[i-1]≤B[j]
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i = (left + right) / 2; //二分法,i从区间中间开始
int j = (m + n + 1) / 2 - i;//+1的操作将总数为奇数和偶数合并为一种情况

//nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
//当一个数组不出现在前一部分时,对应的值为负无穷,就不会对前一部分的最大值产生影响
int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
//当一个数组不出现在后一部分时,对应的值为正无穷,就不会对后一部分的最小值产生影响
int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

if (nums_im1 <= nums_j) {
median1 = Math.max(nums_im1, nums_jm1);
median2 = Math.min(nums_i, nums_j);
left = i + 1;
}
else {
right = i - 1;
}
}
return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class Solution(object):
# 常规思想
def findMedianSortedArrays1(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: float
"""
m = len(nums1)
n = len(nums2)
lens = m+n
left, right = -1, -1
aStart,bStart = 0,0
for i in range(lens//2+1):
left = right # 每次循环将right的值赋值给left
# A移动的条件,B遍历到最后 或A<B满足一个
if aStart<m and (bStart >= n or nums1[aStart] < nums2[bStart]):
right = nums1[aStart]
aStart+=1
else:
right = nums2[bStart]
bStart+=1
if (lens & 1) == 0:
return (left+right)/2.0
else:
return right


# 第k小数
def findMedianSortedArrays(self, nums1, nums2):
def getKthElemnt(k):
index1, index2 = 0, 0
while True:
# 特殊情况
if index1 == m:
return nums2[index2+k-1]
if index2 == n:
return nums1[index1+k-1]
if k==1:
return min(nums1[index1], nums2[index2])

newIndex1 = min(index1 + k//2 -1, m-1)
newIndex2 = min(index2 + k//2 -1, n-1)
privot1,privot2 =nums1[newIndex1], nums2[newIndex2]
if privot1<=privot2:
k-=newIndex2-index1 +1
index1 = newIndex2 + 1
else:
k -= newIndex2 - index2 +1
index2 = newIndex2 +1

m,n = len(nums1), len(nums2)
totalLength = m+n
if totalLength % 2==1:
return getKthElemnt((totalLength+1)//2)
else:
return (getKthElemnt(totalLength//2)+ getKthElemnt(totalLength//2+1)) /2

# 划分数组
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums1) > len(nums2):
return self.findMedianSortedArrays(nums2, nums1)


infinty = 2**40 # 代表正无穷
m, n = len(nums1), len(nums2)
left, right = 0, m
# median1:前一部分的最大值
# median2:后一部分的最小值
median1, median2 = 0, 0


while left <= right: # 一直循环找到一个最大的i满足A[i−1]≤B[j]
# 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
# // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
i = (left + right) // 2
j = (m + n + 1) // 2 - i


# nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
# 当一个数组不出现在前一部分时,对应的值为负无穷,就不会对前一部分的最大值产生影响
nums_im1 = (-infinty if i == 0 else nums1[i - 1]) # 注意写法与java不同
# 当一个数组不出现在后一部分时,对应的值为正无穷,就不会对后一部分的最小值产生影响
nums_i = (infinty if i == m else nums1[i])
nums_jm1 = (-infinty if j == 0 else nums2[j - 1])
nums_j = (infinty if j == n else nums2[j])


if nums_im1 <= nums_j:
median1, median2 = max(nums_im1, nums_jm1), min(nums_i, nums_j)
left = i + 1
else:
right = i - 1


return (median1 + median2) / 2 if (m + n) % 2 == 0 else median1