O(n)的时间复杂度求中位数

O(n)的时间复杂度求中位数

O(n)中位数问题是指:在O(n)的时间复杂度内找到一个无序序列的中位数。

在开始O(n)时间复杂度求中位数之前,先手写一下快速排序。

快速排序的实现

Reference:
快速排序|菜鸟教程
白话经典算法系列之六 快速排序 快速搞定

快速排序的原理

如果想正序排序一个序列:

  1. 从序列中找到一个 基准数
  2. 分区:将序列中比基准数大的数字都放到基准数的右边,将序列中比基准数小的数字都放在基准数的左边。
    • 此时,序列被分成了三部分: 左序列 + 基准数 + 右序列
    • 实现2的方法可以是 挖坑法
  3. 对左序列和有序列进行排序即可(递归),直到左/右序列长度为1

快速排序的复杂度

  1. 时间复杂度:
    • 最好:O(nlogn)
    • 最坏:O(n^2)
    • 平均:O(nlogn)
  2. 空间复杂度:O(nlogn)
  3. 稳定性:不稳定

快速排序的实现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大的数字了。

大佬的一种更简洁的写法

O(n)求中位数和第k大数


2020.9.26 14:41 周六

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值