3种方法解决中位数问题:
1.常规思想的改进: 假合并/奇偶合并
本题的常规思想还是挺简单的: 使用归并的方式, 合并两个有序数组, 得到一个大的有序数组. 大的有序数组的中间位置的元素, 即为中位数. 但是这种思路的时间复杂度是 O(m+n), 空间复杂度是 O(m+n),不符理想化!
①因此我们必须想办法将算法进行优化, 这里先介绍一种简单的优化方式, 就是 **假合并**, 即我们并不需要真的合并两个有序数组, 只要找到中位数的位置即可.
它的思想并不复杂, 由于两个数组的长度已知, 因此**中位数对应的两个数组的下标之和也是已知的**。维护两个指针, 初始时分别指向两个数组的下标0的位置, 每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针), 直到到达中位数的位置.
通过这种 假合并 的方式, 我们可以成功的将空间复杂度优化到了O(1), 但是对于时间复杂度并没有什么优化. 此方法理解是比较容易的, 但是真正写代码时候还是很有挑战的, 你不仅要考虑**奇偶的问题**, 更要考虑 一个数组遍历结束后 的各种边界问题, 其实很多困难题就是难在了对于边界的处理上面了.
这种思想是很有必要的, 对于数组来说, 我们经常会遇到奇偶的两种情况处理, 如果想办法将他们合并在一起, 那代码写起来就是非常顺畅和整洁.
②另一种合并的思想是: 我们可以在奇数的时候, 在**末尾等处添加一个占位符**等, 这样也是可以将奇数合并成偶数的情况的.
此方法的另一个优化点就是 通过在if条件中**加入大量的限制条件**, 从而实现了对于各种边界问题的处理, 这也是一种很重要的思想.
2.寻找第k小数 代码详解
关于本题转换为 **第k小数** 的思想, 就不用纠结怎么想到的了, 大家就安心的理解思想和代码并将它记在脑中就可以了.
其实关于这个算法的思想并不是太难理解, 主要就是根据两个数的三种比较结果, **不断地去除不满足的元素**的过程。
我认为这个思想最难的点在于 **三种特殊情况的处理**, 我们能否想到这三种情况, 并将他们**完美的融入到代码之中**, 我感觉这才是真正的难点所在.
接下来我们来详细解读此思想的代码实现.
最开始对于奇数和偶数的两种情况进行了判断, 其实是可以将两种情况合并的, 只需要在奇数时求两次同样的k就可以了.
接下来处理了三种特殊情况中的两种特殊情况: 一个数组为空 和 k=1.
下面的**几个定义**就非常重要了, 一定要弄清这些定义的含义, 才能更轻松的理解代码.
index1, index2作为数组的**起始点的下标**, 初值都是0, 但是随着两个数组不断被删除元素, 这两个起始点也是在不断的进行变化, 具体变化方式就是 **index1 = newIndex1 + 1**, 因为在删除元素的时候 **连同比较位置也一同删去了**, 所以新的开始是 比较位置 的后一位.
newindex1, newindex2作为比较点就是图中**被框中的两个数的下标**, 它的赋值过程就涉及到了 **最后一个边界情况**. 因为当一个数组较短时, 其中一个比较点可能已经到达了数组的最后, 所以它的值是 **两种情况下较小的那个数**.
接下来就是根据两个比较点的大小来进行不同的操作过程了, 这里最难理解的点就是 **k -= (newIndex1 - index1 + 1)**, 也就是**减去元素的个数问题**了. 我们根据上面的图来举例, 图中index1的值为0, newindex1的值经过计算为1, 通过比较后, 可以看到 红色的数 就是被删除的数, 也就是两个, 所以我们需要在最后+1才是真实被删去的个数. 对于此类问题在确定最终个数的时候, 我们都可以通过这样的**特例来决定代码的书写**, 至此代码就全部讲解完成了.
## 3.理解中位数作用进行 划分数组
最后这种思想的时间复杂度甚至比上面的还低, 上面的思想每一轮循环可以将查找范围减少一半,因此时间复杂度是O(log(m+n)), 但这种思想可以对确定的较短的数组进行二分查找, 所以它的时间复杂度是 O(log min(m,n)).
划分数组 正好和上面算法完全相反, 它的**思想特别复杂**, 但思想理解了, 代码写起来倒是没太大的难度, 所以我们重点说说它的思想.
首先我们要明白**中位数的作用**: 将一个集合划分为两个长度相等的子集, 其中一个子集中的元素总是大于另一个子集中的元素, 这种思想无论是在几个数组中都是适用的, 这就衍生出了下面的算法思想.
首先来讨论奇偶的两种不同情况下的不同划分方式
然后在编写代码的时候, 由于计算机的**取整操作**, 我们是可以将这两种情况合并成一种代码书写方式的. 其中的i和j分别是两个数组的划分位置.
同样我们也会遇到复杂的边界问题, 但下面这种处理方式是真的非常优秀.
上面问题都考虑完了, 其实就可以写代码了, 但是我们需要进行两个条件的判断: B[j−1]≤A[i] 以及A[i−1]≤B[j], 为了优化代码, 经过分析后, 我们发现这两种情况是可以**等价转换**的. 也就是只需要进行一个条件的判断即可.
相关运行代码,部分讲述在代码中已经讲清。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len = nums1.length + nums2.length;
if(len % 2 == 0){
return (getKNums(nums1, nums2, len / 2) + getKNums(nums1, nums2, len / 2 + 1)) / 2.0;
}else{
return getKNums(nums1, nums2, len / 2 + 1);
}
}
private int getKNums(int[] nums1, int[] nums2, int k) {
// 1 2 4 6 8
// 2 3 4 9 10
// 求第k大,主要思想就是通过二分削减不可能参与第k大筛选的序列。
// 比如上面的序列求第8大,则8/2=4,6<9,那么6以及之前的序列1 2 4 6不可能有第8大。
// 通过这种比较确定不了到底哪个是第8大,因为8后面是6后面还可能有别的数(比如8),这也可能是第八大
// 因此只能排除,不能确定。如果要确定的话,得憋到k=1,这就是返回条件. 当然,另一组已经溢出也可以直接获得结果。
if(nums1.length == 0){
return nums2[k - 1];
}
if(nums2.length == 0)
return nums1[k - 1];
int i = -1, j = -1;
while(k > 0) {
if(i >= nums1.length - 1){
return nums2[j + k];
}
if(j >= nums2.length - 1)
return nums1[i + k];
if(k == 1) {
if( i + 1 >= nums1.length)
return nums2[j + 1];
else if (j + 1 >= nums2.length){
return nums1[i + 1];
}else {
return Math.min(nums2[j + 1], nums1[i + 1]);
}
}
int inc1 = Math.min(nums1.length - i - 1, k/2);
int inc2 = Math.min(nums2.length - j - 1, k/2);
int cmp = nums1[i + inc1] - nums2[j + inc2];
if(cmp < 0) {
i += inc1;
k -= inc1;
}else {
j += inc2;
k -= inc2;
}
}
return 0;
}
}