堆
堆是结点间具有层序关系的完全二叉树。
最大堆(maxheap):每个父节点的都大于孩子节点。在最大堆中,根的元素最大。
最小堆(minheap):每个父节点的都小于孩子节点。在最小堆中,根的元素最小。
堆实现了插入、删除的操作,但在插入删除之后必须对树进行更新,维护堆排序。我主要讲插入和删除操作。
首先我们要建堆,现在有一组数:10, 11, 13, 12, 16, 18, 15, 17, 14, 19
我们把这组数构成如下图所示的树状结构。现在我们要用向下调整算法使它成为一个小堆(或大堆)。这里我们以大堆为例。
向下调整算法的思想是:从第一个非叶子节点开始,判断它的左右孩子是否比自己大,若比自己大,就交换。再依次向下调整,最终我们可以得到大堆。
Push:在一个大堆里插入一个数据的时候,我们利用向上调整算法(思路与向下调整算法相似),不同的是向上调整算法是根据孩子的下标求出父亲的下标,不断调整,使它最终还是一个大堆。例如,在上图的大堆中插入20。
Pop:这里是先把第一个数据与最后一个交换,再把最后一个数据删除。然后向下调整,使它依旧还是大堆。例如:我们删除19。
下面是实现的代码:
为了让代码复用,这里我用到了仿函数,在建堆的时候,根据你传的参数来建大堆还是小堆。
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<class T>
struct Less
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template<class T>
struct Greater
{
bool operator()(const T& l, const T& r)
{
return l > r;
}
};
template<class T, class Compare>//编写与类型无关的代码
class Heap
{
public:
Heap()
{}
Heap(T* a, size_t n)
{
_a.reserve(n); //reserve()只开空间 resize()开空间并初始化
for (size_t i = 0; i < n; i++)
{
_a.push_back(a[i]);
}
//建堆
for (int i = (_a.size() - 2) / 2; i >= 0; i--)
{
AdjustDown(i);//从第一个非叶子节点开始
}
}
void Push(const T& x)
{
_a.push_back(x);
AdjustUp(_a.size() - 1);
}
void Pop()
{
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
AdjustDown(0);
}
bool Empty()
{
return _a.empty();
}
size_t Size()
{
return _a.size();
}
const T& Top()
{
return _a[0];
}
void Print()
{
for (size_t i = 0; i < _a.size(); i++)
{
cout << _a[i] << " ";
}
}
protected:
void AdjustDown(size_t root)//向下调整算法
{
Compare com;
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < _a.size())
{
if (child + 1 < _a.size() && com(_a[child + 1], _a[child]))
if ((child + 1 < _a.size()) && (_a[child + 1] > _a[child]))
{
child++; //只需得到大的(小的)孩子的下标即可
}
if (com(_a[child], _a[parent]))
if (_a[child] > _a[parent])
{
swap(_a[child], _a[parent]);
parent = child; //子问题
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(size_t child)//向上调整算法
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (com(_a[child], _a[parent]))
if (_a[child] > _a[parent])
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
private:
//T* _a;
//size_t _size;
//size_t _capacity;
vector<T> _a;
};
//测试
//void TestHeap()
//{
// int a[] = { 10, 11, 13, 12, 16, 18, 15, 17, 14, 19 };
// //Heap<int> hp();
// //Heap<int> hp1(a, sizeof(a) / sizeof(int));
// Heap<int,Less<int>> hp1(a, sizeof(a) / sizeof(int));
// Heap<int, Greater<int>> hp1(a, sizeof(a) / sizeof(int));
//
// hp1.Push(20);
// hp1.Push(14);
// hp1.Pop();
// hp1.Print();
//}
我们再讲一下堆的应用TopK问题以及堆排序。
堆在实现优先级队列以及对一维数组元素排序方面发挥着重要作用。
TopK问题:从100w个数中找出最大的前K个数。
求最大的前K个数,我们要建小堆,让最大的前K个数进堆。这样就可以找出最大的前K个数。
这里我用的是面向过程的方法写的
//TopK问题
//找最大的前K个,建小堆
void GetTopK(int* a, size_t n, size_t k)
{
assert(a);
//建小堆
int* heap = new int[k];
for (size_t i = 0; i < k; i++)//把前K个数入堆
{
heap[i] = a[i];
}
for (int i = (k - 2) / 2; i >= 0; i--)//利用前K个数建小堆
{
AdjustDown(heap, k, i);
}
for (size_t i = 0; i < n; i++)//再把剩下的数与堆顶的比较,要比它大就再向下调整
{
if (a[i] > heap[0])
{
heap[0] = a[i];
AdjustDown(heap, k, 0);
}
}
for (size_t i = 0; i < k; i++)
{
cout << heap[i] << " ";
}
}
void AdjustDown(int* heap, int k, int root)//向下调整算法
{
assert(heap);
int parent = root;
int child = parent * 2 + 1;
while (child < k)
{
if (child + 1 < k && heap[child + 1] < heap[child])
{
++child;
}
if (heap[child] < heap[parent])
{
swap(heap[child], heap[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
堆排序(用到上面TopK问题的向下调整算法,所以我们排降序(建小堆))
思路:把堆里的第一个元素与最后一个元素交换,把最后一个不看做堆里的,然后再把剩下的元素进行向下调整,以此类推......
//排降序 (建小堆)
//把第一个数放在最后一个数的位置
void HeapSort(int* a, size_t n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end)
{
swap(a[0], a[end]);
--end;
AdjustDown(a, end, 0);
}
}