LeetCode 第 04 题:给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m+n))。你可以假设 nums1 和 nums2 不会同时为空。
示例1
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0
示例2
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
因为两个数组都是排好序的,可以利用归并排序将它们合并成一个长度为 m+n 的有序数组,合并的时间复杂度是 m+n,然后从中选取中位数,整体的时间复杂度就是 O(m+n)。
假设 m+n = L,若 L 为奇数,即两个数组的元素总个数为奇数,那么它们的中位数就是第 int(L / 2) + 1 小的数。例如,数组 { 1, 2, 3 } 的中位数是 2,2 就是第二小的数 2 = int(3/2) + 1。如果 L 是偶数,那么中位数就是第 int(L/2) 小与第 int(L/2)+1 小的数的和的平均值。例如,数组 {1, 2, 3, 4} 的中位数是 (2 + 3) / 2 = 2.5。因此这个问题就转变为在两个有序数组中寻找第 k 小的数 f(k),当 L 是奇数的时候,令 k = L/2,结果为 f(k + 1);而当 L 是偶数的时候,结果为 f(k) + f(k + 1) / 2。如何从两个排好序的数组里找出第 k 小的数?我们从第一个数组里取出前面 k1 个数,从第二个数组里取出前面 k2 个数。
double findMedianSortedArrays(int nums1[], int nums2[]) {
int m = nums1.length;
int n = nums2.length;
int k = (m + n) / 2;
if ((m + n) % 2 == 1) {
// 奇数
return findKth(nums1, 0, m - 1, nums2, 0, n - 1, k + 1);
} else {
// 偶数
return (
findKth(nums1, 0, m - 1, nums2, 0, n - 1, k) +
findKth(nums1, 0, m - 1, nums2, 0, n - 1, k + 1)
) / 2.0;
}
}
// 从两个有序数组中寻找第k小的元素
double findKth(int[] nums1, int l1, int h1, int[] nums2, int l2, int h2, int k) {
int m = h1 - l1 + 1;
int n = h2 - l2 + 1;
// 如果 nums1 数组的长度大于 nums2 数组的长度,将它们互换一下,这样可以让程序结束得快一些
if (m > n) {
return findKth(nums2, l2, h2, nums1, l1, h1, k);
}
// 当 nums1 的长度为 0 时,直接返回 nums2 数组里第 k 小的数
if (m == 0) {
return nums2[l2 + k - 1];
}
// 当 k 等于 1 的时候,返回两个数组中的最小值
if (k == 1) {
return Math.min(nums1[l1], nums2[l2]);
}
// 求解k1
int na = Math.min(k/2, m);
// 求解k2
int nb = k - na;
// nums1中第k1小的元素
int va = nums1[l1 + na - 1];
// nums2中第k2小的元素
int vb = nums2[l2 + nb - 1];
if (va == vb) {
// 可以肯定 va 和 vb 就是第 k 小的数
return va;
} else if (va < vb) {
// 比如 nums1[] = {1, 2, 3, 5, 6}, nums2[] = {4, 7, 8, 9}, va = 3, vb = 7
// 把搜索的范围缩小,从 nums1 的后半段以及 nums2 的前半段中继续寻找
return findKth(nums1, l1 + na, h1, nums2, l2, l2 + nb - 1, k - na);
} else {
// 比如 nums1[] = {5, 6, 7, 8, 9}, nums2[] = {1, 2, 3, 4}, va = 7,vb = 2
// 把搜索的范围缩小,从 nums2 的后半段以及 nums1 中继续寻找
return findKth(nums1, l1, l1 + na - 1, nums2, l2 + nb, h2, k - nb);
}
}
如果给定的两个数组是没有经过排序处理的,应该怎么找出中位数呢?
快速选择算法,可以在 O(n) 的时间内从长度为 n 的没有排序的数组中取出第 k 小的数,运用了快速排序的思想。假如将 nums1[] 与 nums2[] 数组组合成一个数组变成 nums[]:{2, 5, 3, 1, 6, 8, 9, 7, 4},那么如何在这个没有排好序的数组中找到第 k 小的数呢?
随机地从数组中选择一个数作为基准值,比如 7。一般而言,随机地选择基准值可以避免最坏的情况出现。
将数组排列成两个部分,以基准值作为分界点,左边的数都小于基准值,右边的都大于基准值。
判断一下基准值所在位置 p:a. 如果 p 刚好等于 k,那么基准值就是所求数,直接返回。b. 如果 k < p,即基准值太大,搜索的范围应该缩小到基准值的左边。c. 如果 k > p,即基准值太小,搜索的范围应该缩小到基准值的右边。此时需要找的应该是第 k - p 小的数,因为前 p 个数被淘汰。
重复第一步,直到基准值的位置 p 刚好就是要找的 k。
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, k);
}
int quickSelect(int[] nums, int low, int high, int k) {
int pivot = randRange(low, high), i = low;
// 将随机找的基准和最后一个元素互换
swap(nums, pivot, high);
// 比基准值小的数放左边,把比基准值大的数放右边
for (int j = low; j < high; j++) {
if (nums[j] <= nums[high]) {
swap(nums, i++, j);
}
}
// i的位置即为基准的位置
swap(nums, i, high);
// 判断基准值的位置是不是第 k 小的元素
int count = i + 1;
// 如果是,就返回结果。
if (count == k) return nums[i];
// 如果发现基准值小了,继续往右边搜索
if (count < k) return quickSelect(nums, i + 1, high, k - count);
// 如果发现基准值大了,就往左边搜索
return quickSelect(nums, low, i - 1, k);
}
从 5 亿个数中找出中位数,假设内存只有1G。
分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。
对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num,如果它对应的二进制中最高位为 1,则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数。
划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中。假设 f1 中有 1 亿个数,那么中位数一定在 f0 中,且是在 f0 中,从小到大排列的第 1.5 亿个数与它后面的一个数的平均值。
对于 f0 可以用次高位的二进制继续将文件一分为二,如此划分下去,直到划分后的文件可以被加载到内存中,把数据加载到内存中以后直接排序或使用快排找出第K大的数,从而找出中位数。
有一万个服务器,每个服务器上存储了十亿个没有排好序的数,现在要找所有数当中的中位数,怎么找?
对于分布式地大数据处理,应当考虑两个方面的限制:
每台服务器进行算法计算的复杂度限制,包括时间和空间复杂度;
服务器与服务器之间进行通信时的网络带宽限制;
限制一:空间复杂度假设存储的数都是 32 位整型,即 4 个字节,那么 10 亿个数需占用 40 亿字节,大约 4GB,若单服务内存有限制,使用分治法 + 快排对10亿数据进行排序或直接找出比基准大的个数。
限制二:网络带宽如何有效地限制流量,避免过多的服务器之间的通信,就是要考量的重点。
借助扩展一的思路。
从 1万 个服务器中选择一个作为主机(master server)。这台主机将扮演主导快速选择算法的角色。
在主机上随机选择一个基准值,然后广播到其他各个服务器上。
每台服务器都必须记录下最后小于、等于或大于基准值数字的数量:less count,equal count,greater count。
每台服务器将 less count,equal count 以及 greater count 发送回主机。
主机统计所有的 less count,equal count 以及 greater count,得出所有比基准值小的数的总和 total less count,等于基准值的总和 total equal count,以及大于基准值的总和 total greater count。进行如下判断。a. 如果 total less count >= total count / 2,表明基准值太大。b. 如果 total less count + total equal count >= total count / 2,表明基准值即为所求结果。c. 否则 total less count + total equal count < total count / 2,表明基准值太小。
a、c 两种情况,主机会把新的基准值广播给各个服务器,服务器根据新的基准值的大小判断往左半边或者右半边继续进行快速选择。直到最后找到中位数。