O(n)的时间复杂度求中位数
O(n)
中位数问题是指:在O(n)
的时间复杂度内找到一个无序序列的中位数。
在开始O(n)
时间复杂度求中位数之前,先手写一下快速排序。
快速排序的实现
Reference:
快速排序|菜鸟教程
白话经典算法系列之六 快速排序 快速搞定
快速排序的原理
如果想正序排序一个序列:
- 从序列中找到一个 基准数 。
- 分区:将序列中比基准数大的数字都放到基准数的右边,将序列中比基准数小的数字都放在基准数的左边。
- 此时,序列被分成了三部分:
左序列
+基准数
+右序列
。 - 实现
2
的方法可以是 挖坑法 。
- 此时,序列被分成了三部分:
- 对左序列和有序列进行排序即可(递归),直到左/右序列长度为
1
。
快速排序的复杂度
- 时间复杂度:
- 最好:
O(nlogn)
- 最坏:
O(n^2)
- 平均:
O(nlogn)
- 最好:
- 空间复杂度:
O(nlogn)
- 稳定性:不稳定
快速排序的实现1
void quick_sort(int a[], int left, int right){
if (left >= right){
return;
}
int l = left, r = right;
int x = a[left]; // 选取当前序列最左边的数字为基准数
while (l < r){
while (l < r && a[r] >= x){
r --;
}
if (l < r){
a[l] = a[r];
l ++;
}
while (l < r && a[l] < x){
l ++;
}
if (l < r){
a[r] = a[l];
r ++;
}
}
a[l] = x;
quick_sort(a, left, l - 1);
quick_sort(a, l + 1, right);
}
- 每一次快排都会确定一个位置,这个位置是
l
,大小是基准数
。 - 如果我们想每一次都知道 快速排序确定的位置 ,那么可以写一个
partition函数
。(事实上,这是在为O(n)解决中位数问题做铺垫。)
快速排序的实现2——partition函数
int partition(int a[], int l, int r){
int x = a[l];
while (l < r){
while (l < r && a[r] >= x){
r --;
}
if (l < r){
a[l] = a[r];
l ++;
}
while (l < r && a[l] < x){
l ++;
}
if (l < r){
a[r] = a[l];
r --;
}
}
a[l] = x;
return l;
}
void Q_sort(int a[], int l, int r){
if (l < r){
int index = partition(a, l, r);
Q_sort(a, l, index-1);
Q_sort(a, index+1, r);
}
}
-
partiton()
负责将获得每一次进行步骤2:分区
得到的基准数
在最终递增序列中的 位置 。 -
Q_sort()
中的index
就是该位置。根据该位置将序列分为左右序列。
有了partition
函数,就可以实现O(n)
时间复杂度找中位数的工作了。
基于partition函数的O(n)中位数
显然,如果没有O(n)
的限制,那么一个直白的想法是:将无序序列排序,然后输出序列的 第n/2
个位置 的元素。
n
是序列的长度。- 其实,对于中位数应该分情况讨论:
- 当
n
是奇数时,中位数是a[n/2]
; - 当
n
是偶数时,中位数是(a[n/2] + a[n/2 - 1]) / 2.0
。
- 当
上述算法的时间复杂度是O(nlogn)
。
考虑到,对于partition函数,每一个可以确定一个位置! 那么,假设这个位置是index
,那么对于中位数的位置pos
:
- 如果
index = pos
,显然,找到了中位数a[index]
。 - 如果
index > pos
,显然,中位数位于区间[l, index-1]
。- 此时,只需对区间
[l, index-1]
再次进行partition
操作即可。
- 此时,只需对区间
- 如果
index < pos
,显然,中位数位于区间[index+1, r]
。- 同上。
根据上述思想,编写函数getMidNumber()
:
int getMidNumber(int a[], int l, int r, int pos){
while (true){
int index = partition(a, l, r); // 获得基准数的位置
if (index == pos){
return a[index];
}else if (index > pos){ // 只需要在[l, index-1]区间内找pos位置即可
r = index - 1;
}else { // 只需要在[index, r]区间内找pos位置即可
l = index + 1;
}
}
return -1; // 一般程序不会到这里
}
- 其中,
pos
的含义是中位数的位置:- 当序列长度为奇数时,pos = n/2
- 当序列长度为偶数时,pos = n/2 或 n/2 - 1
比如,可以编写如下代码测试:
int main(){
int a[] = {10, 8, 3, 5, 6, 2, 1, 7, 9, 4};
int aLen = sizeof(a) / sizeof(int);
if (aLen&1){
cout << "Mid= " << getMidNumber(a, 0, aLen-1, aLen/2) << endl;
}else{
cout << "Mid= " << (getMidNumber(a, 0, aLen-1, aLen/2) + getMidNumber(a, 0, aLen-1, aLen/2-1)) / 2.0 << endl;
}
return 0;
}
aLen
是序列a
的长度。
时间复杂度分析
最坏:O(n^2)
- 假设一种极端情况,每一次选取的基准数都是序列中最小的那个数字,因此partition函数会依次返回
0,1,2...n/2
,每一次partition函数都需要O(n)
的时间复杂度。因此,最坏的时间复杂度为O(n^2)
。
最好: O(n)
- 假设一种完美情况,第一次得到的基准数就是中位数,那么只需要执行一次partition函数,因此时间复杂度是
O(n)
。
平均: O(n)
数学不好,证明不会。据 他们 说该算法的 期望 时间复杂度是O(n)
。
这好像是涉及 主定理MasterTheorem。搜了好多网页博客也没看懂。
Reference:
主定理 Master Theorem
拓展:找序列中第K
大的数字
其实找中位数就是找序列中第n/2
大的数字。
因此找需要调用getMidNumber(a, 0, aLen-1, k)
即可找到序列中第k
大的数字了。
大佬的一种更简洁的写法
2020.9.26 14:41 周六