数据结构-堆

堆的概念

堆是一个完全二叉树,堆中的每一个节点的值都必须大于等于(或者小于等于)其子树中每个节点的值。

大顶堆:每个节点的值都大于等于子树中每个节点值的堆
小顶堆:每个节点的值都小于等于子树中每个节点值的堆

堆的存储:使用数组存储比较节省空间

堆的操作

堆化

1、插入数据
自下而上,将数据插入到末尾,然后与父节点比较并进行调整,直到合适或者达到顶点
2、删除堆顶元素
自上而下,将末尾的数据移动到堆顶,然后与左右子节点进行比较并调整,直到合适或者到达叶子节点

class Heap {
private:
    int * a;  //数组,从下标1开始存储数据
    int size;    //堆可以存储的最大数据个数
    int count; //堆中已经存储的数据个数
public:
    Heap(int n) {
        a =  new int[n + 1];
        size = n;
        count = 0;
    }
    ~Heap() {
        delete[] a;
    }
    void Insert(int data); //往堆中插入一个元素
    void RemoveMax();      //从堆顶删除元素
}

//往堆中插入一个元素
void Heap::Insert(int data) {
    if (count >= size) {
        cout << "heap is full, insert failed" << endl;
        return;
    }
    ++count;
    a[count] = data;
    int i = count;
    //自下往上进行堆化
    while (i / 2 > 0 && a[i] > a[i / 2]) {
        int tmp = a[i / 2];
        a[i / 2] = a[i];
        a[i] = tmp;
        i = i / 2;
    }
}

//从堆顶删除元素
void Heap::RemoveMax() {
    if (count == 0) {
        cout << "headp is empty, remove failed" << endl;
        return;
    }
    a[1] = a[count];
    count--;
    Heapify(a, count, 1);
}

//自上往下堆化
void Heap::Heapify(int* a, int count, int index) {
    while (true) {
       int maxpos = index;
       if ((2 * index + 1 <= count) && (a[2 * index + 1] > a[index])) {
           maxpos = 2 * index + 1;
       }
       if ((2 * index <= count) && (a[2 * index] > a[maxpos])) {
           maxpos = 2 * index;
       }
       // 如果maxpos没有更新则退出
       if (maxpos == index ) break;
       // swap a[index] and a[maxpos]
       int tmp = a[index];
       a[index] = a[maxpos];
       a[maxpos] = tmp;
       index = maxpos;
    }
}

基于堆进行排序

1、建堆
思路1: 初始时堆中只包含1个数据,然后将下标从2到n的数据依次插入到堆中(从下往上堆化)
思路2:从最后一个非叶子节点开始,依次从上往下进行堆化
时间复杂度o(n),每个节点堆化过程中需要比较和交换的节点个数跟这个节点的高度k成正比
2、排序
将数组第一个元素与最后一个元素交换,剩下n-1个元素重新堆化,完成后再取堆顶元素放到n-1位置,重复到堆中只剩下下标为1的一个元素
时间复杂度o(nlogn),原地排序算法,不稳定,因为存在将堆的最后一个节点跟堆顶节点互换操作,可能改变值相同数据的原始相对顺序

3、堆排序为什么没有快排好?
(1)数据访问没有快排好,快排数据是顺序访问的,堆排序是跳着访问,堆CPU缓存不友好
(2)同样的数据,堆排序算法数据交换次数要多于快排,快排交换次数不会比逆序度多,堆排序建堆打乱原顺序,有序数据建堆后变无序

//堆排序
void HeapSort(int * a, int n) {
    //建堆
    buildHeap(a, n);
    int k = n;
    while(k > 1) {
        //将堆顶元素与最后一个进行交换
        swap(a, 1, k);
        --k;
        //调整堆
        heapify(a, k, 1);
    }
    for (int i = 1; i <= n; i++) {
        cout << a[i] << " ";
    }
    cout << endl;
}

//建堆
void buildHeap(int * a, int n) {
   for (int i = n / 2; i >= 1; --i) {
       //从第一个非叶子节点到堆顶元算逐一进行自上而下的堆化调整
       heapify(a, n, i);
   }
}


void heapify(int * a, int n, int index) {
    while(true) {
        int maxpos = index;
        if ((index * 2 <= n) && (a[index * 2] > a[maxpos])) {
            maxpos = index * 2;
        }
        if ((index * 2 + 1 <= n) && (a[index * 2 + 1] > a[maxpos])) {
            maxpos = index * 2 + 1;
        }
        if (maxpos == index) {
            break;
        }
        swap(a, index, maxpos);
        index = maxpos;
    }
}

堆的应用

优先级队列

不同语言中应用

java的PriorityQueue,C++的priority_queue

合并有序小文件

有100个文件,每个文件大小100MB文件内有序
从这100个里面取出字符放到小顶堆中,堆顶元素是最小的,删除,然后再从小文件总取下一个字符,循环。

高性能定时器

按照任务设定的执行时间,存储在优先级队列中。
队首存储的是最先执行的任务。
这样定时器不用每隔1秒扫描一遍,直接拿到队首任务计算间隔时间T,T秒之后再执行,然后调整堆,再计算新队首任务时间,不用轮询,不用遍历整个任务表,性能提高。

求TopK系列

求TOP-K

可以使用堆来实现:维护一个大小为k的小顶堆,顺序遍历数组数据,与堆顶元素比较,如果比堆顶大就删除堆顶,插入该数组数据;如果比堆顶小,不处理。遍历完后就是前K大数据。

#include<iostream>
using namespace std;

class MinHeap {
private:
    int * a;  //数组,从下标1开始存储数据
    int size;    //堆可以存储的最大数据个数
    int count; //堆中已经存储的数据个数
public:
    MinHeap(int n) {
        a =  new int[n + 1];
        size = n;
        count = 0;
    }
    ~MinHeap() {
        delete[] a;
    }
    void Insert(int data); //往堆中插入一个元素
    void Print();
    void Swap(int * a, int index1, int index2);
};

//往堆中插入一个元素
void MinHeap::Insert(int data) {
    //如果堆还没有满,则插入
    if (count < size) {
        ++count;
        a[count] = data;
        int i = count;
        //自下往上进行堆化
        while (i / 2 > 0 && a[i] < a[i / 2]) {
            cout << a[i] << " " << a[i/2] << endl;
            int tmp = a[i / 2];
            a[i / 2] = a[i];
            a[i] = tmp;
            i = i / 2;
        }
    } else {
        // 如果堆满,判断跟堆顶元素的大小关系,比堆顶大则插入,比其小则忽略
        if (data < a[1])  return;
        a[1] = data;
        //自上而下进行堆化
        int index = 1;
        int maxpos = 1;
        while(true) {
            maxpos = index;
            if ((2 * index) <= count && (a[2 * index] < a[maxpos])) {
                maxpos = 2 * index;
            }
            if ((2 * index + 1 <= count) && (a[2 * index + 1] < a[maxpos])) {
                maxpos = 2 * index + 1;
            }
            if (maxpos == index){
                break;
            }
            Swap(a, maxpos, index);
            index = maxpos;
        }

    }
}

int main() {
    int a[10] = {0, 4, 9, 1, 2, 7, 6, 8, 3, 5};
    MinHeap * heap = new MinHeap(5);
    for (int i = 0; i < 10; i++) {
        heap->Insert(a[i]);
    }
    heap->Print();
}

求中位数

方法:
1、维护两个堆,一个大顶堆,一个小顶堆。大顶堆存储前半部分数据,小顶堆存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。
2、如果新加入的数据小于等于大顶堆堆顶元素,就将数据插入到大顶堆,否则就插入到小顶堆
3、如果不满足大顶堆n/2+1 or n/2、小顶堆n/2的个数要求,则从一个堆的堆顶元素移动到另一个堆中

说明:插入时间复杂度o(logn),求中位数时间复杂度o(1)
求其他百分位数据,原理与求中位数类似,比如求99%的数据,大顶堆保存99%数据,小顶堆保存1%数据,大顶堆堆顶数据是所求。

代码实现:
方法一:使用上述求TOPK的代码思路,生成一个大顶堆,一个小顶堆。
方法二:使用c++中的priority_queue优先级队列
优先队列是c++中的一种重要数据结构,它由二项队列编写而成的,可以以O(log n) 的效率查找一个队列中的最大值或者最小值,其中是最大值还是最小值是根据创建的优先队列的性质来决定的。
声明形式如下:
priority_queue< type, container, function >
其中:
type:数据类型;
container:实现优先队列的底层容器;
function:元素之间的比较方式;
对于container,要求必须是数组形式实现的容器,例如vector、deque,而不能使list。在STL中,默认情况下(不加后面两个参数)是以vector为容器,以 operator< 为比较方式,所以在只使用第一个参数时,优先队列默认是一个最大堆,每次输出的堆顶元素是此时堆中的最大元素。

//构造一个空的优先队列(此优先队列默认为大顶堆)
priority_queue<int> big_heap;   

//另一种构建大顶堆的方法
priority_queue<int,vector<int>,less<int> > big_heap2;  

//构造一个空的优先队列,此优先队列是一个小顶堆
priority_queue<int,vector<int>,greater<int> > small_heap;

本次使用方法二来实现代码:

#include <iostream>
#include <queue>
using namespace std;

class MiddleFind {
private:
    priority_queue<int> big_queue;
    priority_queue<int, vector<int>, greater<int>> small_queue;
public:
    MiddleFind() {
    }

    void AddItem(int val);
    double GetMiddle();
};
void MiddleFind::AddItem(int val) {
    //如果大顶堆为空则在大顶堆中插入元素
    if (big_queue.size() == 0) {
        big_queue.push(val);
        return;
    }
    //如果大顶堆与小顶堆元素个数相同
    if (big_queue.size() == small_queue.size()) {
        if (val < big_queue.top()) {
            //如果待插入值比大顶堆堆顶元素小则插入大顶堆并调整
            big_queue.push(val);
        } else {
            //如果待插入值比大顶堆堆顶元素大则插入小顶堆并调整
            small_queue.push(val);
        }
    }
    //如果大顶堆元素个数多
    else if (big_queue.size() > small_queue.size()) {
        if (val > big_queue.top()) {
            //如果待插入值比大顶堆堆顶元素大则插入小顶堆并调整
            small_queue.push(val);
        } else {
            //如果待插入值比大顶堆堆顶元素小则将大顶堆顶元素插入小顶堆,将元素插入大顶堆
            small_queue.push(big_queue.top());
            big_queue.pop();
            big_queue.push(val);
        }
    }
    //如果小顶堆元素个数多
    else if (big_queue.size() < small_queue.size()) {
        if (val < small_queue.top()) {
            //如果待插入值比小顶堆堆顶元素小,则插入大顶堆
            big_queue.push(val);
        } else {
            //如果待插入值比小顶堆元素大,则把小顶堆堆顶元素移入大顶堆,将待插入元素插入小顶堆
            big_queue.push(small_queue.top());
            small_queue.pop();
            small_queue.push(val);
        }
    }
}

double MiddleFind::GetMiddle() {
    if (big_queue.size() ==  small_queue.size()){
        return (big_queue.top() + small_queue.top()) / 2.0;
    }
    else if  (big_queue.size() >  small_queue.size()){
        return big_queue.top();
    }
    else {
        return small_queue.top();
    }
}

int main(){
    MiddleFind mf;
    int a[10] = {1, 3, 5, 7, 9, 19, 17, 11, 15, 13};
    for (int i = 0; i < 10; ++i) {
        mf.AddItem(a[i]);
    }
    cout << "GetMiddle:" << mf.GetMiddle() << endl;
}            

海量数据求TOP数

问题:10亿个搜索日志文件,如何快速获得TOP10热门关键词?场景限定单机,内存1GB。

方法:
1、将10亿条搜索关键词通过哈希算法分片到10个文件中
2、针对每个文件,利用散列表和堆,分别求出TOP10
3、把10个TOP10放一起,取TOP10

问题:求点击量排名TOP10,一个访问量非常大的新闻网站,希望将点击量排名 Top 10 的新闻摘要滚动显示在网站首页 banner 上,每隔 1 小时更新一次。

方法:
1、对每篇新闻摘要计算一个hashcode,使用map存储
2、每小时一个文件方式记录被点击的摘要
3、1小时结束后,计算最新一小时的点击TOP10,将摘要的hashcode分片到多个文件中,针对每个文件使用小顶堆统计TOP10
4、合并所有分片的TOP10,小顶堆,计算TOP10

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
十大经典排序算法是指在计算机科学中常用的排序算法,它们分别是: 1. 冒泡排序(Bubble Sort):重复地比较相邻的两个元素,将较大的元素逐渐向右移动。 2. 选择排序(Selection Sort):每次从未排序的部分选择最小(或最大)的元素,并放在已排序的部分的末尾。 3. 插入排序(Insertion Sort):将未排序的元素逐个插入到已排序的部分中的正确位置。 4. 希尔排序(Shell Sort):将待排序的数组按照一定步长进行分组,对每组进行插入排序,逐渐减小步长。 5. 归并排序(Merge Sort):将待排序的数组递归地分成两半,对每一半进行排序,然后合并两个有序数组。 6. 快速排序(Quick Sort):选择一个基准元素,将数组划分为两部分,左边部分都小于基准,右边部分都大于基准,递归地对两部分进行排序。 7. 堆排序(Heap Sort):将待排序的数组构建成一个最大(或最小),然后依次取出顶元素并调整结构。 8. 计数排序(Counting Sort):统计数组中每个元素出现的次数,然后根据统计结果对元素进行排序。 9. 桶排序(Bucket Sort):将待排序的数组划分为多个桶,对每个桶中的元素进行排序,最后将桶中的元素按顺序合并。 10. 基数排序(Radix Sort):按照元素的位数,将待排序的数组从低位到高位进行排序。 以上是十大经典排序算法,每种算法都有其适用的场景和性能特点,选择合适的排序算法可以提高程序的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值