堆的实现及应用(优先级队列,堆排,TopK问题)

堆数据结构是一种数组对象,它可以被看做是一棵完全二叉树。
堆的实现及应用(优先级队列,堆排,TopK问题)_第1张图片
堆的二叉树存储有两种方式:
1.最大堆:每个父节点的值都大于孩子节点
2.最小堆:每个父节点的值都小于小子节点
如上图所示就是一个最小堆。

关于堆,其实说到底就是两种算法,一种是向下调整算法,一种是向上调整算法。我们先结合图来分析一下这两种算法。
就拿上图来说,上图是一个小堆,接下来比如说我们要往里插入一个9,那么因为堆的底层实际上是一个数组对象,实际上就是一个vector,那么插入的9,就是在如图的位置
堆的实现及应用(优先级队列,堆排,TopK问题)_第2张图片
但因为这是个最小堆,所以要对9进行向上调整,把9和它的父亲的值16比,因为9比16小,那么就交换9和16的位置;再拿9和11比,9比11小,交换9和11的位置;9和10比,9比10小,交换9和10的位置。直到交换到9比它的父节点大了或是9已经交换到根节点了就停止,上图在进行向上调整后的结果如下图。
堆的实现及应用(优先级队列,堆排,TopK问题)_第3张图片
在进行向上调整的过程中,只会对插入位置的祖先这一条线产生影响,对堆的其他线并不会产生影响。向下调整也是如此,比如我们在进行删除操作的时候,Pop掉的都是堆顶位置Top的值,也就是root位置的值(如向上调整好的图就是Pop 9),我们一般采用的方法就是,先交换9和16的位置,再对该堆进行一个Pop操作(因为底层是一个vector,Pop操作直接删除尾部元素),这样一来9就删除了
堆的实现及应用(优先级队列,堆排,TopK问题)_第4张图片
然而Top位置的值此时是16,所以就要对16进行向下调整,将16的两个孩子的值进行比较,这里因为是小堆,所以就选小的那个(大堆就选大的那个),所以就将16和10 的位置进行交换,以此类推,接着就是16和11交换,直到孩子都比16的值大,或是16已经是叶子节点了就停止。调整好的就是一开始的那个小堆
堆的实现及应用(优先级队列,堆排,TopK问题)_第5张图片

核心的算法我们已经掌握了,接下来就是代码的实现了。因为应用的过程中,可能既会用到大堆也会用到小堆,同时写一个大堆和一个小堆会产生代码重复,所以我们采用以下的方法来构建堆对象,这里用到仿函数。

#pragma once

#include 
#include 
using namespace std;
#include 

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

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

template <class T,class Compare=Greater>//默认构建大堆
class Heap
{
public:
    Heap()
    {}

    Heap(const T* a, size_t size)
    {
        assert(a);
        _a.reserve(size);
        for (size_t i = 0; i < size; ++i)
        {
            _a.push_back(a[i]);
        }
        for (int i = (size - 2) / 2; i >= 0; i--)
        //从第一个非叶子节点开始调整,此处i必须给int,给size_t会死循环
        {
            _Adjustdown(i);
        }
    }

    void Pushback(const T& n)//
    {
        _a.push_back(n);
        size_t child = _a.size() - 1;
        _Adjustup(child);
    }

    void Pop()
    {
        assert(!_a.empty());
        swap(_a[0], _a[_a.size() - 1]);
        _a.pop_back();
        _Adjustdown(0);
    }

    size_t Size()
    {
        return _a.size();
    }

    bool Empty()
    {
        return _a.empty();
    }

    const T& Top()
    {
        return _a[0];
    }
protected:
    void _Adjustdown(size_t root)//向下调整
    {
        size_t child = root * 2 + 1;
        while (child<_a.size())
        {
            Compare com;
            if ((child + 1)<_a.size() && com(_a[child+1],_a[child]))
            /*对括号内左右对象进行比较,若是Less,则当左值小于右值时为真
                                        若是Greater,则当左值大于右值时为真*/
            {
                ++child;
            }
            if (com(_a[child] , _a[root]))
            {
                swap(_a[child], _a[root]);
                root = child;
                child = root * 2 + 1;
            }
            else
            {
                break;
            }
        }
    }

    void _Adjustup(int child)//向上调整
    {
        int parent = (child - 1) / 2;
        while (parent >= 0)
        {
            Compare com;
            if (com(_a[child] , _a[parent]))
            {
                swap(_a[parent], _a[child]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else
            {
                break;
            }
        }
    }
protected:
    vector _a;
};

void TestHeap()
{
    int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
    Heap<int> hp(a, sizeof(a) / sizeof(a[0]));
    hp.Pushback(20);
    hp.Pop();
}

堆的应用有很多,下面我们来看一下
一.优先级队列
优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素,这里的最高优先权就可以理解为最大堆和最小堆的Top元素,通过上面实现的堆来实现优先级队列

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

    PriorityQueue(T* a, size_t n)
    {
        for (size_t i = 0; i < n; ++i)
        {
            _h.Pushback(a[i]);
        }
    }

    void Push(const T& x)
    {
        _h.Pushback(x);
    }

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

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

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

    const T& Top()
    {
        return _h.Top();
    }
protected:
    Heap<T, Compare> _h;
};

void TestPriorityQueue()
{
    int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
    PriorityQueue q(a, 10);
    //q.Push(20);
    //q.Pop();
    size_t b = q.Top();
    cout << b << endl ;
}

这里测试的时候我们默认实现大堆,测试结果如下
这里写图片描述

二.堆排序
我们使用堆排序肯定是因为它的时间复杂度O(N*lgN)相对于其他排序可能更好,
就升序而言,它的基本思想就是建好大堆后,将Top元素和最后一个数据元素交换,此时最后一个元素就是最大的数,再对剩下的数进行向下调整(不包括从Top位置交换下来的元素),把Top元素和倒数第二个元素进行交换,以此类推。降序同样,建立小堆,思想类似。
升序的代码如下

void AdJustDown(int a[], size_t n, int root)
{
    if (a == NULL)
    {
        return;
    }

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

    while (child < n)
    {
        if (child + 1 < n&&a[child] < a[child + 1])
        {
            child = child + 1;
        }
        if (a[parent] < a[child])
        {
            swap(a[parent], a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

void HeapSort(int a[], size_t n)
{
    if (a == NULL)
    {
        return;
    }

    for (int i = (n - 2) / 2; i >= 0; --i)
    {
        AdJustDown(a, n, i);
    }

    int end = n - 1;
    for (size_t i = 0; i < n; ++i)
    {
        swap(a[0], a[end]);
        AdJustDown(a, end, 0);
        --end;
    }
}

void TestHeapSort()
{
    int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
    HeapSort(a, sizeof(a) / sizeof(a[0]));
    for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
    {
        cout << a[i] << " ";
    }
}

测试结果如下
这里写图片描述

三.TopK问题
给一串数组,要求找出这串数组中最大的前K个数。这里可以使用小堆来实现

template <class T,class Compare>
class TopK
{
public:
    TopK()
    {}

    TopK(int* a,int n,int k)
    //n表示数组大小,k表示前k个
    {
        assert(k <= n);
        size_t i = 0;
        while (i < n)
        {
            //先将数组的前K个数放入堆中
            if (i < k)
            {
                _minhp.Pushback(a[i]);
            }
            else
            {
                if (a[i]>_minhp.Top())
                //如果接下去的数比Top元素大,删除Top元素,插入该元素
                {
                    _minhp.Pop();
                    _minhp.Pushback(a[i]);
                }
            }
            ++i;
        }

    }

protected:
    Heap> _minhp;
};

void TestTopK()
{
    int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
    TopK<int,Less<int>> top(a, sizeof(a) / sizeof(a[0]), 5);
}

测试的堆中结果如下
堆的实现及应用(优先级队列,堆排,TopK问题)_第6张图片
以上堆中的5个元素即是最大的前5个数。

你可能感兴趣的:(数据结构,数据结构,堆)