【数据结构】 二叉堆

【数据结构】 二叉堆

什么是数据结构?

结构定义+结构操作

结构定义是定义了这种数据结构具有的性质,结构操作的目的是为了维护这种性质。

那么二叉堆或者说优先队列有什么性质呢?

首先二叉堆是一颗完全二叉树,其次对于大顶堆来说每个根节点的值都大于孩子节点的值,这意味着,你可以用0(1)的时间获得二叉堆中所有数的最大值。

为了维护二叉堆的性质,我们需要定义插入和删除两种o(log n)的操作。

在二叉堆数据的存储上,采用数组来存储,

因为完全二叉树是可以按层次顺序存入到一个数组中去的。且具有 i * 2 为i节点的左子树,i*2 +1 为i节点的右子树这个性质。那么数组就不是一个数组了而是一颗树了

注意,思维上的转变。

二叉堆的具体实现

二叉堆结构定义

typedef struct Heap{
    int *data;
    int n;// 指向堆的最后一个元素
    int size;// 堆的大小
}Heap;

// 显示堆的大小的
#define size(h) h->size

// 初始化一个堆
Heap* init(int n){
    Heap *h = (Heap *)malloc(sizeof(Heap));
    h->n = 0;
    size(h) = n+1;
    h->data = (int*)malloc(sizeof(int)*size(h));
    return h;
}

// 释放一个堆
void clear(Heap *h){
    if(h == NULL)return;
    free(h->data);
    free(h);
    return;
}

二叉堆的结构操作

插入一个元素

  1. 插入到堆的末尾
  2. 向上调整,维护堆的性质
    1. 比较当前节点和父节点的值
    2. 如果比父节点值大就交换父节点,更新当前节点为父节点,回到1
    3. 结束调整
#define V(h,ind) h->data[ind]
// 向上调整
void up(Heap*h){
    int ind = h->n;
    while(ind >= 1 && V(h,ind) > V(h,ind >> 1)){
        swap(V(h,ind),V(h,ind<<1));
        ind >>= 1;
    }
}

void push(Heap *h,int val){
    h->data[++h->n] = val;
    up(h);
}

删除堆顶元素

  1. 堆顶元素赋值为堆尾元素,完成删除
  2. 调整堆,维护性质
    1. 选取当前节点左右节点中较大的与当前节点比较
    2. 当前节点小就交换,更新当前节点
    3. 结束循环
// 向下调整
void down(Heap*h,int ind,int end){
    int temp;
    while(ind << 1 <= end){
        temp = ind <<1;
        if(temp < end && V(h,temp) < V(h,temp+1))++temp;
        if(V(h,temp) > V(h,ind)){
            swap(V(h,temp),V(h,ind));
            ind = temp;
        }else break;
    }
}

void pop(Heap *h){
    h->data[1] = h->data[h->n];
    down(h,1,--h->n);
}

获得堆顶元素

#define top(h) h->data[1]

c++ 优先队列使用(二叉堆)

template< class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>

>class priority_queue ;

A priority queue is a container adaptor that provides constant time lookup of the largest (by default) element, at the expense of logarithmic insertion and extraction.(以对数插入和提取为代价。)

A user-provided Compare can be supplied to change the ordering, e.g. using std::greater<T> would cause the smallest element to appear as the top().

Working with a priority_queue is similar to managing a heap in some random access container, with the benefit of not being able to accidentally invalidate the heap.

Member functions

(constructor) constructs the priority_queue (public member function)
(destructor) destructs the priority_queue (public member function)
operator= assigns values to the container adaptor (public member function)
Element access
top accesses the top element (public member function)
Capacity
empty checks whether the underlying container is empty (public member function)
size returns the number of elements (public member function)
Modifiers
push inserts element and sorts the underlying container (public member function)
emplace(C++11) constructs element in-place and sorts the underlying container (public member function)
pop removes the top element (public member function)
swap swaps the contents (public member function)
#include <functional>
#include <queue>
#include <vector>
#include <iostream>
 
template<typename T> void print_queue(T& q) {
    while(!q.empty()) {
        std::cout << q.top() << " ";
        q.pop();
    }
    std::cout << '\n';
}
 
int main() {
    // 默认大顶堆
    std::priority_queue<int> q;
 
    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q.push(n);
 
    print_queue(q);
 
    std::priority_queue<int, std::vector<int>, std::greater<int> > q2;
    
    // 小顶堆
    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q2.push(n);
 
    print_queue(q2);
}

https://en.cppreference.com/w/cpp/container/priority_queue

java 二叉堆的使用

<https://docs.oracle.com/javase/10/docs/api/java/util/PriorityQueue.html>

二叉堆的应用

703. 数据流中的第K大元素

这个体现了二叉堆的一个性质,

对于容纳了k个元素的小顶堆来说:

  1. 堆顶的最小的元素
  2. 堆内总共有k个元素,且都大于堆顶

那么在一个新的元素出现时,如果堆内元素小于k就直接将新元素压入堆,否则比较该元素是否是大于堆顶的元素,是就入堆,否则就不管了。

这时,堆顶元素就是一个动态的分界线,始终动态的是k个元素中最小的元素,注意这个性质。

那么这样,我们就维护了一个结构可以求数据流中第k个大的元素了。

然后之所以用c写是为了练习实现堆的操作。

typedef struct Heap{
    int *data;
    int n,size;
}Heap;
#define swap(a,b){\
    __typeof(a) __temp = a;\
    a = b; b = __temp;\
}
Heap* init(int n){
    Heap *h = (Heap *)malloc(sizeof(Heap));
    h->n = 0;
    h->size = n+1;
    h->data = (int*)malloc(sizeof(int)*h->size);
    return h;
}
#define V(h,ind) h->data[ind]
// 向上调整
void up(Heap*h){
    int ind = h->n;
    while(ind >> 1 >= 1 && V(h,ind) < V(h,ind >> 1)){
        swap(V(h,ind),V(h,ind >> 1));
        ind >>= 1;
    }
}

void push(Heap *h,int val){
    h->data[++h->n] = val;
    up(h);
}

// 向下调整
void down(Heap*h,int ind,int end){
    int temp = ind << 1;
    while(temp <= end){
        if(temp < end && V(h,temp) > V(h,temp+1))++temp;
        if(V(h,temp) < V(h,ind)){
            swap(V(h,temp),V(h,ind));
            ind = temp;
            temp = ind <<1;
        }else break;
    }
}

void pop(Heap *h){
    h->data[1] = h->data[h->n];
    down(h,1,--h->n);
}
#define top(h) h->data[1]
void clear(Heap *h){
    if(h == NULL)return;
    free(h->data);
    free(h);
    return;
}

typedef struct {
    Heap * heap;
} KthLargest;

int kthLargestAdd(KthLargest* obj, int val);

KthLargest* kthLargestCreate(int k, int* nums, int numsSize) {
    KthLargest * kl = (KthLargest*) malloc(sizeof(KthLargest));
    kl -> heap = init(k);
    for(int i = 0;i <numsSize;++ i){
        kthLargestAdd(kl,nums[i]);
    }
    return kl;
}

int kthLargestAdd(KthLargest* obj, int val) {
    if(obj->heap->n+1< obj->heap->size){
        push(obj->heap,val);
    }else if(val > top(obj->heap)){
        pop(obj->heap);
        push(obj->heap,val);
    }
    return top(obj->heap);
}

void kthLargestFree(KthLargest* obj) {
    clear(obj->heap);
    free(obj);
}

295. 数据流的中位数

这个就是维护两个堆,形成对顶堆,从而动态维护了 第 n/2 大的元素,和 第 n/2 小的元素

class MedianFinder {
private:
    priority_queue<int,vector<int>,greater<int>> minHeap;
    priority_queue<int,vector<int>,less<int>> maxHeap;
public:
    /** initialize your data structure here. */
    MedianFinder() {
    }
    
    void addNum(int num) {
        if(maxHeap.size() == 0 || num < maxHeap.top())maxHeap.push(num);
        else minHeap.push(num);
        if(maxHeap.size() - minHeap.size() == 2){
            minHeap.push(maxHeap.top());
            maxHeap.pop();
        }else if(minHeap.size() - maxHeap.size() == 2){
            maxHeap.push(minHeap.top());
            minHeap.pop();
        }
    }
    
    double findMedian() {
        switch(minHeap.size() - maxHeap.size()){
            case 0:return (minHeap.top() + maxHeap.top())/2.0;
            case 1:return minHeap.top();
            case -1:return maxHeap.top();
            default:return -222;
        }
    }
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

23. 合并K个排序链表

264. 丑数 II

这两个题目有点类似的感觉,都是由堆顶元素扩展出新的元素入堆,然后判断条件是否满足,

然后第k大的,第k小的,和第n个元素没啥区别,第k大的就是一个递增序列中的第k个而已,

然后是取堆顶元素扩展所有可能的元素这个思路。

或者说丑数序列就可以看出一个三路的链表,当然了说是一棵树更合适。

2 2*2 2 *3 2 * 5 ...

3 3*3 3 * 5 3 * 3 * 3 ...

5 5 * 5 5 * 5 * 5 ...

class Solution {
public:
    struct cmp{
        bool operator()(ListNode*a,ListNode*b){
            return a->val > b->val;
        }
    };
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<ListNode*,vector<ListNode*>,cmp> heap;
        ListNode Vhead(0),*p = &Vhead; 
        for(ListNode *node :lists){
            if(node!=NULL)heap.push(node);
        }
        while(!heap.empty()){
            ListNode * tmp = heap.top();
            heap.pop();
            if(tmp->next!=NULL)heap.push(tmp->next);
            p->next = tmp;
            p = tmp;
        }
        return Vhead.next;
    }
};

ps: 堆维护的是一个不变的一个队列,如果是求随着时间变化的一个集合中的最小值的话,可以寻找相对不变的放到一个堆里,然后在这些堆里寻找最值。

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

推荐阅读更多精彩内容