堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构。
堆结构的二叉存储分为2种形式,分别是最大堆和最小堆,最大堆就是每个父节点的值都大于或等于孩子节点的值,最小堆刚好与之相反,每个父节点的值都小于或等于孩子节点的值。
用以下代码实现一个堆,默认情况下是大堆的形式:
#include <iostream>
#include <assert.h>
#include <vector>
using namespace std;
//仿函数
template <typename T>
struct GREATER
{
bool operator()(const T& x1, const T& x2)
{
return x1 > x2;
}
};
template <typename T>
struct LESS
{
bool operator()(const T& x1, const T& x2)
{
return x1 < x2;
}
};
template<typename T,typename Compare=GREATER<T>>
class HEAP
{
public:
HEAP(T* a, size_t sz)
{
for (size_t i = 0; i < sz; i++)
{
_a.push_back(a[i]);
}
//建堆,找到最后一个叶子节点的父节点
for (int i = (sz - 2) / 2; i >= 0; --i)
_AdjustDown(i);
}
HEAP()
{}
void Push(const T& x)
{
_a.push_back(x);
_AdjustUp(_a.size()-1);
}
void Pop()
{
assert(!_a.empty());
swap(_a[0],_a[_a.size()-1]);
_a.pop_back();
_AdjustDown(0);
}
T Top()
{
return _a[0];
}
bool Empty()
{
return _a.empty();
}
size_t Size()
{
return _a.size();
}
private:
//向下调整
//考虑兄弟之间的关系
void _AdjustDown(int parent)
{
int child = parent * 2 + 1;
int size = _a.size();
Compare com;
while (child < size)
{
if (child + 1 < size&&com(_a[child + 1], _a[child]))
{
++child;
}
if (com(_a[child], _a[parent]))
{
swap(_a[child], _a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//向上调整
void _AdjustUp(int child)
{
int parent = (child-1) / 2;
Compare com;
while (child>=0)
{
if (com(_a[child], _a[parent]))
{
swap(_a[child],_a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
private:
vector<T> _a;
};
void Test()
{
int a[] = { 2, 1, 3, 4, 7 };
size_t size = sizeof(a) / sizeof(a[0]);
HEAP<int,GREATER<int>>heap((int*)a,size);
cout << heap.Top() << endl;
}
其中都是以最后一个叶子节点的父节点开始建堆,push的时候,是采用向上调整的方式,一直调整到根节点为止,来保持大堆或者小堆,pop的时候只能是pop掉最后一个节点,但我们想删除根节点。所以我们可以先将根节点与最后一个叶子节点交换,然后再进行向下调整,直到变为最大堆或者最小堆。
堆的应用:
1.TopK的问题,当要求寻找一堆数据中最大的前K个值时,我们往往会想到排序,在找出最大的前K个数据,这在数据N比较小的时候还行的通,但是数据N特别大,不能加载到内存中,我们就用堆来解决这个问题。
首先我们先取出前K个数据保存在数组a中,然后用这些数据建立小堆,a[0]就是K个数据中最小的,从第K+1个数据开始和a[0]进行比较,若其大于a[0],就替换a[0],从根结点开始进行向下调整,调整为新的最小堆,第K+2个元素在与此时的a[0]进行比较,如此反复,直到比较到最后一个数据为止。
代码如下:
//小堆
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child<n)
{
if (child + 1 < n&&a[child+1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//找最大的前K个数
void TopK(int* a, const int n, int k)
{
assert(n > k && a);
int* heap = new int[k];
for (int i = 0; i < k; i++)
{
heap[i] = a[i];
}
//建堆
for (int i = (k - 2) / 2; i >= 0; --i)
{
AdjustDown(heap,i,k);
}
for (int i = k; i < n; i++)
{
if (a[i]>heap[0])
heap[0] = a[i];
AdjustDown(heap,k,0);
}
cout << "最大的前K个数据:" << endl;
for (int i = 0; i < k; i++)
{
cout << heap[i] << " ";
}
delete[] heap;
}
2.堆排序问题
降序建小堆,升序建大堆
以降序为例进行解析:
首先建立小堆,根节点为最小的元素,将根节点与最后一个叶子节点进行交换,这样就把最小的元素放到了最后的位置,进行向下调整,除去最后一个结点,变成子问题。如图
代码如下:
//降序
void HeapSort(int* a, int n)
{
assert(a);
//建小堆
for (int i = (n - 2) / 2; i < n; i++)
{
AdjustDown(a,n,i);
}
int end = n - 1;
while (end>0)
{
swap(a[0], a[end]);
AdjustDown(a,end,0);
end--;
}
}
在这里说一下,缺省的优先级队列是利用一个大堆来实现的。