堆的C实现和优先队列的应用-返回数据流中的中位数

数据结构堆(Heap)是一种二叉树的变种,它通过元素交换上浮,保证堆的根节点永远是元素集合中的最大/最小元素。

优先队列是堆的一种应用,使用者可以不断向优先队列中加入新的元素,总是可以以O(1)的时间复杂度取出其中的最大/最小值。

利用两个优先队列可以实现O(1)时间复杂度取中位数。两个优先队列分别是最大堆和最小堆,添加的元素加入大堆或者小堆中,同时需要满足大堆元素个数等于小堆或者仅多一个。由此,从大堆和小堆的根元素可以求出中位数。

下面我们用C来一步步实现上面所述的问题。

1.定义实现堆数据结构

首先使用动态数组来管理堆的数据,定义堆的类型(大堆或者小堆)。在连续存储的数组中,堆的根节点位于arr[0],左右子节点分别存储在arr[1]和arr[2]中,由此实现一组取父节点和子节点索引的函数。

#include "vector.h"
#include <stdbool.h>

#define MIN_HEAP        0
#define MAX_HEAP        1
#define INIT_HEAP_SIZE 10

typedef struct heap_t{
    vector  v;
    int     type;//max heap or min heap
    cmp_func    cmp; //cmp func ptr
}heap_t;

static int parent(int i){
    return (i-1)/2;
}

static int left(int i){
    return 2*i + 1;
}

static int right(int i){
    return 2*i + 2;
}

void init_heap(heap_t *h, int type){
    h->type = type;
    init_vec(&h->v, INIT_HEAP_SIZE);
    if(type == MIN_HEAP)
        h->cmp = less_val_cmp;
    else
        h->cmp = more_val_cmp;
}

接着实现函数维持堆的特性,将新加的元素添加在数组末尾,检查它与它父节点的顺序,若顺序不对,交换两个节点的值,由此直至检查了所有新建节点的父系节点。

void insert_heap(heap_t *h, void *item){
    push_back_vec(&h->v, item); 
    int i = h->v.len - 1; //append new element in back

    //swap element with its parent if they are not in order
    while(i > 0 && !h->cmp(h->v.items[parent(i)], h->v.items[i])){
        swap(&h->v.items[parent(i)], &h->v.items[i]);
        i = parent(i);
    }
}

从堆中移除元素时也需要保存堆的特性,其思路为,若元素的左右节点中存在顺序不对的元素,交换节点元素。

static void heapify(heap_t *h, int i){
    int l = left(i),
        r = right(i);

    int smallest = i;
    //check if child element is not in order with i
    if(l < h->v.len && h->cmp(h->v.items[l], h->v.items[i]))
        smallest = l;
    else if(r < h->v.len && h->cmp(h->v.items[r], h->v.items[i]))
        smallest = r;

    //recursively heapify until in order
    if(smallest != i){
        swap(&h->v.items[i], &h->v.items[smallest]);
        heapify(h, smallest);
    }
}

void *extract_end(heap_t *h){
    void *ret = get_end(h);//get root element
    if(!ret){
        return NULL;
    }

    pop_front_vec(&h->v);//remove root
    heapify(h, 0);//re-heapify
    return ret;
}

若只是查看堆根节点

void *extract_end(heap_t *h){
    void *ret = get_end(h);//get root element
    if(!ret){
        return NULL;
    }

    pop_front_vec(&h->v);//remove root
    heapify(h, 0);//re-heapify
    return ret;
}

最后添加测试用例来验证代码

void heap_test(){
    INIT_HEAP(h);
    insert_heap(&h, (void *)3);
    insert_heap(&h, (void *)2);
    insert_heap(&h, (void *)1);
    printf("min %d\n", (int)extract_end(&h));
    printf("min %d\n", (int)extract_end(&h));
    insert_heap(&h, (void *)1);
    printf("min %d\n", (int)extract_end(&h));
    printf("min %d\n", (int)extract_end(&h));
    FREE_HEAP(h);
}

2. 优先队列实现O(1)时间查找中位数

定义中位数查找器数据结构,查找器中包含了两个优先队列,分别包含数据元素的左半部分和右半部分,左队列的根为最大根,右队列的根为最小根,因此利用两个队列的根节点信息足以计算出中位数(若数据个数为奇数,中位数为左根,否则为左右根的平均数)。

typedef struct median_finder_t{
    heap_t  left, right;
}median_finder_t;

void init_median_finder(median_finder_t *mf);
void add_num(median_finder_t *mf, int num);
float find_median(median_finder_t *mf);
void reset_median_finder();

void init_median_finder(median_finder_t *mf){
    init_heap(&mf->left, MAX_HEAP);
    init_heap(&mf->right, MIN_HEAP);
}

定义函数用于加入新元素,新加元素的过程需要保证左队列长于右队列,但长度差距不超过2。

void add_num(median_finder_t *mf, int num){
    heap_t *left = &mf->left,
           *right = &mf->right;
    //add num and make sure left heap is longer
    if(total_heap(left) == 0 || num < (int)get_end(left)){
        insert_heap(left, (void *)(long)num);
    }else{
        insert_heap(right, (void *)(long)num);
    }

    //balance left and right heap, is left heap is shorter
    // move element from right to left
    //if left heap has 2 more elements than right
    // move from left to right
    if(total_heap(left) < total_heap(right)){
        insert_heap(left, get_end(right));
        extract_end(right);
    }else if (total_heap(left) - total_heap(right) >= 2){
        insert_heap(right, get_end(left));
        extract_end(left);
    }
}

从左右队列中计算中位数的方法是显而易见的,

float find_median(median_finder_t *mf){
    heap_t *left = &mf->left,
           *right = &mf->right;
    if(get_end(left) == NULL)
        return 0.0;

    //odd
    if(total_heap(left) > total_heap(right)){
        return (float)(long)get_end(left);
    }else{ //even
        return ((float)(long)get_end(left) + 
            (float)(long)get_end(right))/2;
    }
}

添加一个简单的测试用例来验证程序的有效

void median_finder_test(){
    median_finder_t mf;
    init_median_finder(&mf);
    add_num(&mf, 1);
    add_num(&mf, 2);
    printf("median %f\n", find_median(&mf));
    add_num(&mf, 3);
    printf("median %f\n", find_median(&mf));
}```

median 1.500000
median 2.000000


代码清单:[https://github.com/KevinACoder/Learning/blob/master/ds/heap.c](https://github.com/KevinACoder/Learning/blob/master/ds/heap.c)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 前言:题图无关,接下来开始简单学习学习优先队列和堆的相关数据结构的知识; 前序文章: 数据结构与算法(1)——数组...
    我没有三颗心脏阅读 2,135评论 1 15
  • 之前的文章中,我们有介绍过动态数组ArrayList,双向队列LinkedList,键值对集合HashMap,树集...
    Single_YAM阅读 4,004评论 1 14
  • 0.目录 1.优先队列ADT 2.几种实现 3.二叉堆 4.d-堆 5.左式堆 6.斜堆 7.二项队列 8.斐波那...
    王侦阅读 3,083评论 1 2
  • (东北行) 文/菊 清皇太祖卧东陵, 壁垒森严月旺城; 驼马石尊狮虎踞, 天罡地煞步云登。 【新韵】十一庚.平首韵...
    斌之志阅读 353评论 12 17
  • 去色(黑白):颜色变黑白(ctry+shift+U) 添加蒙版:黑透白不透,与橡皮擦的差别,它可以保留原图,而橡皮...
    叶静生阅读 205评论 0 0