欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

堆及topk问题

程序员文章站 2024-02-14 18:36:40
...

堆的本质

vector+向下调整算法/向上调整算法。
注意:这个二叉树为完全二叉树

向下调整算法:

已知条件:从一个节点开始向下调整,已知这个节点的左右子树已经是大堆或小堆。
所以需要从第一个不是叶子节点的节点开始调整,而这个节点正好是最后一个节点的父节点。
i = (_v.size() - 2)>>1; i即是最后一个不是叶子节点的节点。

以小堆为例:

1.以删除堆顶元素为例来说明向下调整算法:

堆及topk问题

2.以向堆中插入节点来说明堆的向上调整算法:
堆及topk问题

代码如下:

Heap.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include <assert.h>
using namespace std;
#include<vector>

template<class T>
struct Less
{
    bool operator()(T& left,T& right)
    {
       return left < right;
    }
};

template<class T>
struct Greater
{
    bool operator()(T& left, T& right)
    {
       return left > right;
    }
};

template<class T, class Compare = Greater<T> >
class Heap
{
public:
    Heap()
    {}

    Heap(const T* a, size_t n)
    {
       _v.reserve(n);//可以提高效率
       for (size_t i = 0; i < n; ++i)
       {
           _v.push_back(a[i]);
       }

       //建堆
       for (int i = ((int)_v.size() - 2) >> 1; i >= 0; --i)
       {
           AjustDown(i);
       }
    }

    void Push(const T& data)
    {
       _v.push_back(data);
       AdjustUp(_v.size() - 1);
    }

    void Pop()
    {
       //1。先将堆顶元素与堆的最后一个元素交换
       swap(_v[0], _v[_v.size() - 1]);
       //2.删除掉最后一个节点
       _v.pop_back();
       //3.向下调整
       AjustDown(0);
    }

protected:
    //向上调整算法
    void AdjustUp(int index)
    {
       int child = index;
       int parent = (child - 1) >> 1;
       while (child > 0)
       {
           if (Compare()(_v[parent],_v[child]))
           {
               break;
           }
           else
           {
               swap(_v[child], _v[parent]);
               child = parent;
               parent = (child - 1) >> 1;
           }
       }

    }

    //向下调整
    void AjustDown(int root)
    {
       int parent = root;
       int child = parent * 2 + 1;//默认是左孩子
       while (child < _v.size())
       {
           if (child + 1 < _v.size() && Compare()(_v[child + 1], _v[child]))
           {
               child++;//保证child是最大的孩子节点
           }
           if (Compare()(_v[child],_v[parent]))
           {
               swap(_v[child], _v[parent]);//交换父节点和孩子节点
               parent = child;
               child = parent * 2 + 1;
           }
           else//不用交换:说明已经是最大的堆了
           {
               break;
           }
       }
    }

private:
    vector<T> _v;
};

测试用例:

#include"Heap1.h"

void Test()
{
    int array[] = {53,17,78,9,45,65,87,23};
    Heap<int,Less<int> > h(array,sizeof(array)/sizeof(int));
    //h.Push(80);
    h.Pop();
}

int main()
{
    Test();
    return 0;
}

堆的应用:

1.实现优先级队列:

#pragma once
#include"Heap1.h"

template<class T>
class PriorityQueue
{
public:
    PriorityQueue()
    {}

    void Push(const T& data)
    {
       hp.Push(data);
    }

    void Pop()
    {
       hp.Pop();
    }

    bool Empty()
    {
       return hp.Empty();
    }

    T& Top()
    {
       return hp.Top();
    }

    size_t Size()
    {
       return hp.Size();
    }

private:
    Heap<T> hp;
};

测试代码:

#include"Heap1.h"
#include"PriorityQueue1.h"

void TestHeap()
{
    int array[] = {53,17,78,9,45,65,87,23};
    Heap<int,Greater<int> > h(array,sizeof(array)/sizeof(int));
    cout << h.IsMaxHeap() << endl;
    //h.Push(80);
    h.Pop();
    cout << h.IsMaxHeap() << endl;
}

void TestQueue()
{
    int array[] = { 53, 17, 78, 9, 45, 65, 87, 23 };
    size_t len = sizeof(array) / sizeof(int);
    PriorityQueue<int> p;
    for (size_t index = 0; index < len; ++index)
    {
       p.Push(array[index]);
    }

    cout << p.Top() << endl;
    p.Pop();
    cout << p.Top() << endl;
}

int main()
{
    //TestHeap();
    TestQueue();
    return 0;
}

2.topk问题(也是海量数据处理问题):

问题:需要从十亿个数据中找出最大的前k个数。

分析思路:

思路:不能使用排序,因为使用排序的话,内存就必须可以容纳十亿个数据,但是这很明显不可能,所以不能使用排序。

我们可以先从十亿个数据中取出前k(假如为100)个数据,使用这k个数据来建一个小堆,那么堆顶就是这个堆中最小的数据,
然后我们就从十亿减k个数据中取出一个数据赖和堆顶数据来进行比较,如果比堆大,那么就拿这个数据来替换堆顶数据。
然后采用向下调整算法,使之堆顶又是这个堆中最小的数据。依次比较。。替换。。。调整。。。
最终,堆中的前k个数据就是十亿个数据中最大的k个数据。

这里应该可以想到:最大的前k个数据一定会进堆。–>因为每次都是比较堆顶的数据和从剩余的数据进行比较,
而这个堆顶数据又是这个堆中最小的数据。

注意:不能建大堆,如果建大堆,然后从剩余的十亿数据中取数据,拿取到的数据和堆顶数据进行比较,
如果这个数据大于堆顶的数据,那么就交换两者的数据。最终只能找出十亿数据中最大的一个数据。—->不符合。

代码如下:

void Topk()
{
    int arr[N];
    int res[k];
    for (size_t i = 0; i < N; ++i)
    {
       arr[i] = rand() % N;
    }

    //验证topk问题
    /*arr[999] = N + 10;
    arr[898] = N + 23;
    arr[766] = N + 46;
    arr[3] = N + 59;
    arr[310] = N + 29;
    arr[19] = N + 58;
    arr[20] = N + 40;
    arr[209] = N + 12;
    arr[58] = N + 60;
    arr[334] = N + 14;*/

    //1.建一个小堆
    Heap<int, Less<int> > hp;
    for (size_t index = 0; index < k; ++index)
    {
       res[index] = arr[index];
    }

    //向下调整
    for (int i = (k - 2) / 2; i >= 0; --i)
    {
       AdjustDown(res, k, i);
    }

    //找出k个最大的数据
    for (size_t j = k; j < N; ++j)
    {
       if (res[0] < arr[j])
       {
           res[0] = arr[j];
           AdjustDown(res, k, 0);
       }
    }

    for (int j = 0; j < k; ++j)
    {
       cout << res[j] << " ";
    }
    cout << endl;
}
3.堆排序问题

升序(需要建一个大堆)

将0号下标的数据和最后一个数据交换,堆中数据个数减1,然后再向下调整,使得0号下标的数据是最大值,然后继续和堆中最后一个数据交换,继续将堆中元素个数减一。

降序(需要建一个小堆)

同上,将0号下标的数据和堆中最后一个数据进行交换,然后将堆的元素个数减一,再向下调整堆中数据,使得堆中的0号下标的数据称为现在堆中最小的数据。

代码如下:

//向下调整算法
void AdjustDown(int* heap, int n, int root)
{
    assert(heap);

    int parent = root;
    int child = parent*2+1;

    while (child < n)
    {
        if (child+1<n && heap[child+1] > heap[child])
        {
            ++child;
        }

        if (heap[child] > heap[parent])
        {
            swap(heap[child], heap[parent]);
            parent = child;
            child = parent*2+1;
        }
        else
        {
            break;
        }
    }
}

//堆排序

void HeapSort(int* a, size_t n)
{
    assert(a);

    // 建堆:时间复杂度 O(N*lgN)
    //从最后一个非叶子节点处开始向下调整
    for(int i = (n-2)/2; i >=0; --i)
    {
        AdjustDown(a, n, i);
    }

    // 选择排序
    int end = n-1;
    while (end > 0)
    {
        swap(a[0], a[end]);
        AdjustDown(a, end, 0);
        --end;
    }
}