代码改变世界

转:海量数据找中位数

2013-08-26 01:25  youxin  阅读(2579)  评论(0编辑  收藏  举报
题目如下:
只有2G内存的pc机,在一个存有10G个整数的文件,从中找到中位数,写一个算法。
 
算法:
1.利用外排序的方法,进行排序 ,然后再去找中位数
 
2.另外还有个思路利用堆
先求第1G大,然后利用该元素求第2G大,然后利用第2G大,求第3G大...当然这样的话虽不需排序,但是磁盘操作会比较多,具体还需要分析下与外排序的效率哪个的磁盘IO会比较多
建立一个1g个整数的最大值堆,如果元素小于最大值则入堆,这样可以得到第1g大的那个元素然后利用这个元素,重新建一次堆,这次入堆的条件还要加上大于这个第1g大的元素,这样建完堆可以得到第2g大的那个 ...
 
3.借鉴基数排序思想
偶认为可以用位来判断计数,从最高位到最低位,为了方便表述我们假设为无符号整数,即0x00000000~0xFFFFFFFF依次递增,那么可以遍历所有数据,并记录最高位为0和1的个数(最高位为0的肯定是小于最高位为1的)记为N0、N1
那么根据N0和N1的大小就可以知道中位数的最高位是0还是1
假设N0>N1,那么再计算N00和N01,
如果N00>(N01+N1),则说明中位数的最高两位是00
再计算N000和N001.。。。依次计算就能找到中位数
 
如果改进一下,设定多个计数器
好像一次磁盘io也可以统计出N0,N00,....的数值
 
4.借鉴桶排序思想
一个整数假设是32位无符号数
第一次扫描把0~2^32-1分成2^16个区间,记录每个区间的整数数目
找出中位数具体所在区间65536*i~65536*(i+1)-1
第二次扫描则可找出具体中位数数值

第一次扫描已经找出中位数具体所在区间65536*i~65536*(i+1)-1
然后第二次扫描再统计在该区间内每个数出现的次数,就可以了.
 
题目:在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。
分析:既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法 :
思想:将整形的每1byte作为一个关键字,也就是说一个整形可以拆成4个keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。
第一步:把10G整数每2G读入一次内存,然后一次遍历这536,870,912个数据。每个数据用位运算">>"取出最高8位(31-24)。这8bits(0-255)最多表示255个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量,自然这个数量只需要255个整形空间即可。
代价:(1) 10G数据依次读入内存的IO代价(这个是无法避免的,CPU不能直接在磁盘上运算)。(2)在内存中遍历536,870,912个数据,这是一个O(n)的线性时间复杂度。(3)把255个桶写会到255个磁盘文件空间中,这个代价是额外的,也就是多付出一倍的10G数据转移的时间。
第二步:根据内存中255个桶内的数量,计算中位数在第几个桶中。很显然,2,684,354,560个数中位数是第1,342,177,280个。假设前127个桶的数据量相加,发现少于1,342,177,280,把第128个桶数据量加上,大于1,342,177,280。说明,中位数必在磁盘的第128个桶中。而且在这个桶的第1,342,177,280-N(0-127)个数位上。N(0-127)表示前127个桶的数据量之和。然后把第128个文件中的整数读入内存。(平均而言,每个文件的大小估计在10G/128=80M左右,当然也不一定,但是超过2G的可能性很小)。
代价:(1)循环计算255个桶中的数据量累加,需要O(M)的代价,其中m<255。(2)读入一个大概80M左右文件大小的IO代价。
注意,变态的情况下,这个需要读入的第128号文件仍然大于2G,那么整个读入仍然可以按照第一步分批来进行读取。
第三步:继续以内存中的整数的次高8bit进行桶排序(23-16)。过程和第一步相同,也是255个桶。
第四步:一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。
整个过程的时间复杂度在O(n)的线性级别上(没有任何循环嵌套)。但主要时间消耗在第一步的第二次内存-磁盘数据交换上,即10G数据分255个文件写回磁盘上。一般而言,如果第二步过后,内存可以容纳下存在中位数的某一个文件的话,直接快排就可以了。
 
 
更多:
http://www.cppblog.com/richbirdandy/archive/2008/09/09/61426.html
http://blog.csdn.net/randyjiawenjie/article/details/6968591
http://kenby.iteye.com/blog/1030055
 

题目介绍:

输入为不断地数字流,实时显示出当前已经输入的数字序列的中位数

解答:

求中位数的方法很多,对于大数据量最经典是桶的计数方法,但是对于这个问题不适用,因为数据是不断变化的

可以用最大堆和最小堆来解答这个问题:

1.假设当前的中位数为m,其中最大堆维护的是<=m的数字序列,最小堆维护的是>=m的数字序列,但是两个堆都不包含m

2.当新的数字到达时,比如为a,将a与m进行比较,若a<=m 则将其加入到最大堆中,否则将其加入到最小堆中

3.如果此时最小堆和最大堆的元素个数的差值>=2 ,则将m加入到元素个数少的堆中,然后从元素个数多的堆将根节点赋值到m,最后重建两个最大堆和最小堆,返回到2。

 

变形:

题目:5亿个int,从中找出第k大的数

 

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>

typedef struct bucket_t {
    int *buf;        /* 输出缓冲区 */
    int count;        /* 当前有多少个数 */
    int idx;        /* 缓冲区的指针 */
} bucket_t;

static unsigned int BUF_PAGES;        /* 缓冲区有多少个page */
static unsigned int PAGE_SIZE;        /* page的大小 */
static unsigned int BUF_SIZE;        /* 缓冲区的大小, BUF_SIZE = BUF_PAGES*PAGE_SIZE */
static unsigned int nbuckets;        /* 分成多少个桶 */
static unsigned int BUCKET_BUF_SIZE;

static int *buffer;                    /* 输入缓冲区 */

long get_time_usecs();
void write_to_file(bucket_t *bucket, int pos);
int partition(int *a, int s, int t);
int quick_select(int *a, int s, int t, int i);
void swap(int *p, int *q);

int main(int argc, char **argv)
{
    char                 filename[20];
    unsigned int        bp, length, bucket_size, k;
    int                    fd, i, bytes;
    bucket_t            *bucket;

    long start_usecs = get_time_usecs();

    strcpy(filename, argv[1]);
    fd = open(filename, O_RDONLY);
    if (fd < 0) {
        printf("can't open file %s\n", filename);
        exit(0);
    }
    nbuckets = 1024;
    k = atoi(argv[2]);
    PAGE_SIZE = 4096;                            /* page = 4KB */
    BUF_PAGES = 1024;
    BUF_SIZE = PAGE_SIZE*BUF_PAGES;                /* 4KB * 1024 = 4M */
    BUCKET_BUF_SIZE = PAGE_SIZE*128;            /* 4KB * 128 = 512KB */
    buffer = (int *)malloc(BUF_SIZE);

    //把1-2^32个数分成nbucket个组, nbuckets必须等于2的n次幂
    bucket = malloc(sizeof(bucket_t)*nbuckets);    
    if (bucket == NULL) exit(0);
    for (i = 0; i < nbuckets; i++) {
        bucket[i].buf = malloc(BUCKET_BUF_SIZE);
        if (bucket[i].buf == NULL) {
            exit(0);
        }
        bucket[i].idx = 0;
        bucket[i].count = 0;
    }
    bucket_size = (1<<22);        /* 分成1024个桶,每个桶容纳2^22个数 */

    // 读入第一批数据到输入缓冲区 
    bytes = read(fd, buffer, BUF_SIZE);
    length = bytes/4;
    bp = 0;

    int             element, pos;
    unsigned int    base;
    bucket_t        *p;
    
    base = 2147483648;
    while (1) {
        //从输入缓冲区取出一个数,加到对应的桶
        element = buffer[bp++];
        pos = (((long)element)+base)>>22;
        p = &bucket[pos];
        p->buf[p->idx++] = element;
        p->count++;
        //桶内的缓冲区已满,写入文件
        if (p->idx*4 == BUCKET_BUF_SIZE) {
            write_to_file(p, pos);
            p->idx = 0;
        }
        //输入缓冲区的数已用完
        if (bp == length) {
            bytes = read(fd, buffer, BUF_SIZE);
            if (bytes == 0) { 
                break;
            }
            length = bytes/4;
            bp = 0;
        }
    }

    //把每个桶剩下的数写入文件
    for (i = 0; i < nbuckets; i++) {
        write_to_file(bucket+i, i);
    }

    free(buffer);
    close(fd);

    buffer = malloc(bucket_size*4);
    if (buffer == NULL)  exit(0); 

    //找出第k大的数位于哪个文件
    unsigned sum = 0;
    for (i = 0; i < nbuckets && sum < k; i++) {
        sum += bucket[i].count;
    }
    i--;

    //把该文件读入内存
    sprintf(filename, "foo_%d.dat", i);
    printf("第%d大的数位于文件%s的第%d大的数\n", k, filename, k+bucket[i].count-sum);
    fd = open(filename, O_RDONLY);
    if (fd < 0) {
        printf("can't open file %s\n", filename);
        free(buffer);
        exit(0);
    }
    bytes = read(fd, buffer, bucket_size*4);
    length = bytes/4;

    //选择文件内第(k+bucket[i].count-sum)大的数
    int answer;
    answer = quick_select(buffer, 1, length-1, k+bucket[i].count-sum);
    printf("第%d大的数 = %d\n", k, answer);

    close(fd);
    free(buffer);

    //free buckets
    for (i = 0; i < nbuckets; i++) {
        free(bucket[i].buf);
    }
    free(bucket);

    long end_usecs = get_time_usecs();
    double secs = (double)(end_usecs - start_usecs) / (double)1000000;
    printf("it took %.02f seconds.\n", secs);

    return 0;
}

void write_to_file(bucket_t *bucket, int pos)
{
    char    filename[20];
    int        fd, bytes;

    sprintf(filename, "foo_%d.dat", pos);
    fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);    
    if (fd < 0) {
        printf("can't open file %s\n", filename);
        exit(0);
    }
    bytes = write(fd, bucket->buf, bucket->idx*4);
    if (bucket->idx*4 != bytes) {
        printf("idx = %d, bytes = %d, write error\n", bucket->idx, bytes);
        close(fd);
        exit(0);
    }
    close(fd);
}

long get_time_usecs()
{
    struct timeval time;
    struct timezone tz;
    memset(&tz, '\0', sizeof(struct timezone));
    gettimeofday(&time, &tz);
    long usecs = time.tv_sec*1000000 + time.tv_usec;

    return usecs;
}

void swap(int *p, int *q)
{
    int        tmp;

    tmp = *p;
    *p = *q;
    *q = tmp;
}

/* 把a[t]作为参考,将数组分成三部分: 小于等于a[t],
 * a[t]以及大于a[t],分割完毕后,a[t]所在的下标即是a[t]的顺序
 */
int partition(int *a, int s, int t)
{
    int        i, j;    /* i用来遍历a[s]...a[t-1], j指向大于x部分的第一个元素 */

    for (i = j = s; i < t; i++) {
        if (a[i] < a[t]) {
            swap(a+i, a+j);
            j++;
        }
    }
    swap(a+j, a+t);

    return j;
}

/* 选择数组中第i大的元素并返回 */
int quick_select(int *a, int s, int t, int i)
{
    int        p, m;

    if (s == t) return a[t];
    p = partition(a, s, t);
    m = p - s + 1;
    if (m == i) return a[p];
    if (m > i) {
        return quick_select(a, s, p-1, i);
    }
    return quick_select(a, p+1, t, i-m);
}