前言
之前在面试的时候,面试官非常喜欢问:你好,请问在一个很大的数组中怎样快速地找出它的中位数?
当时很迷惑,为什么面试官总喜欢找中位数?后来了解到快速排序算法的思想后,发现如果大概知道待排序数组中位数的大小(或者提前找出中位数),将在数量级上提高快速排序算法的效率,这个后面有空再讲。
如果你想先把数组排序,在找出中间那一个,那就。。。。
大家看到算法一定立刻想到时间复杂度和空间复杂度,这是基本的思维方式。我这里提供两个方法,
- 方法一提供代码,可以现场给面试官手撕代码(这就直接发offer了);
- 方法二提供思路,只要你说出思路来,已经可以征服面试官了。
1.方法一
方法API:
//这里k任意取,如果k=strlen(s)/2,那么就是寻找中位数
char select(char *s, int k);
上面返回的是char
,当然也可以为int
.
利用快速排序算法的思想,使用切分法来缩小数组的范围。
这个方法能够在线性时间内解决寻找中位数的问题,神不神奇?
1.1第一步:切分
//将数组切分成s[lo .. i-1], s[i], s[i+1 .. hi];
int partition(char *s, int lo, int hi);
结果:它会将数组 s[lo]
到 s[hi]
重新排列,并返回一个整数j
,使得:
s[lo ... j - 1]
小于等于s[j]
,但是s[lo ... j - 1]
内部并不有序;s[j + 1 ... hi]
大于等于s[j]
,但是s[j + 1 ... hi]
内部并不有序;- 返回整数
j
,下一次使用。
int partition(char *s, int lo, int hi) {
//将数组切分成s[lo .. i-1], s[i], s[i+1 .. hi];
int i = lo, j = hi + 1;
char v = s[lo];//数组第一个元素,作为基准元素
while (true) {
//扫描左右元素,检查是否需要交换(大于基准元素 和 小于基准元素)
while (s[++i] < v) { if (i == hi) { break; } }
while (s[--j] > v) { if (j == lo) { break; } }
if (i >= j) { break; }
char temp = s[i];
s[i] = s[j];
s[j] = temp;
}
//将基准元素放回数组中正确的位置
char temp = s[lo];
s[lo] = s[j];
s[j] = temp;
//s[lo ..j - 1] <= s[j] <= s[j + 1 ..hi]
return j;
}
1.2 第二步:寻找
//这里k任意取,如果k=strlen(s)/2,那么就是寻找中位数
char select(char *s, int k);
- 如果
k = j
,问题就解决了; - 如果
k < j
,继续切分左子数组(令hi = j - 1
); - 如果
k > j
,继续切分右子数组(令lo = j + 1
);
char select(char *s, int k) {
int lo = 0, hi = strlen(s) - 1;
while (hi > lo) {
int j = partition(s, lo, hi);
if (j == k) { return s[k]; }
else if (j > k) { hi = j - 1; }
else if (j < k) { lo = j + 1; }
}
return s[k];
}
1.3 第三步:完整代码,举例分析
#include<string.h>
#include <stdio.h>
#include <time.h>
#define _CRT_SECURE_NO_DEPRECATE;
#define _CRT_SECURE_NO_WARNINGS;
int partition(char *s, int lo, int hi);
char select(char *s, int k);
int main()
{
int start;
start = clock();
char s1[] = "abcfed";
char s2[] = "bcadfe";
char s3[] = "fshskbbsadasdaafsdgfgntyasfafasfahsfkasfapqipowejq1231nkdsk,1213";
int k = 5;//这里k任意取,如果k=strlen(s)/2,那么就是寻找中位数
char res = select(s1, k);
printf("\n");
printf(" ");
printf("%c",res);
printf("\n");
getchar();
//return 0;
}
char select(char *s, int k) {
int lo = 0, hi = strlen(s) - 1;
while (hi > lo) {
int j = partition(s, lo, hi);
if (j == k) { return s[k]; }
else if (j > k) { hi = j - 1; }
else if (j < k) { lo = j + 1; }
}
return s[k];
}
int partition(char *s, int lo, int hi) {
//将数组切分成s[lo .. i-1], s[i], s[i+1 .. hi];
int i = lo, j = hi + 1;
char v = s[lo];//数组第一个元素,作为基准元素
while (true) {
//扫描左右元素,检查是否需要交换(大于基准元素 和 小于基准元素)
while (s[++i] < v) { if (i == hi) { break; } }
while (s[--j] > v) { if (j == lo) { break; } }
if (i >= j) { break; }
char temp = s[i];
s[i] = s[j];
s[j] = temp;
}
//将基准元素放回数组中正确的位置
char temp = s[lo];
s[lo] = s[j];
s[j] = temp;
//s[lo ..j - 1] <= s[j] <= s[j + 1 ..hi]
return j;
}
1.4分析
1.假设每次都正好将数组二分,那么总比较次数为:
N
+
N
2
+
N
4
+
N
8
+
.
.
.
≈
2
N
N+\frac{N}{2}+\frac{N}{4}+\frac{N}{8}+... \approx2N
N+2N+4N+8N+...≈2N 式中:N
为数组大小。
2.如果
k
=
N
2
k=\frac{N}{2}
k=2N ,那么这个代码就是找中位数,当然也可以找任意第几大(小)的数。
2.方法二
方法二也是基于切分的思想,我们想想,加入现在有100G的数据需要你找出中位数。不能依次放到内存中,你怎么办?
基本知识
- 我们都知道,数据在硬盘中都是以二进制储存的,也就是11001011这样。
- 从左到右依次是最高位到最低位(不考虑符号位),最左边位为
1
的肯定比最左边为0
的大,(100000000000 >
011111111111111)。
操作步骤
那么我们建立这样的桶,每进来一个数据,我们就放到相应的桶。
如下是取最高的两位,也就是4个桶:四个桶可以代表4个硬盘,内存里面的代码每次将进来的数分到4个桶里。计算每个桶里面数据的个数。初步确定中位数在哪一个桶里,再对那个桶进行一样的分法,是不是很简单。
思维拓展
当然,你也可以建立8个桶,这样步骤更少,也更快: