堆数据结构是一种数组对象,它可以被看做是一棵完全二叉树。
堆的二叉树存储有两种方式:
1.最大堆:每个父节点的值都大于孩子节点
2.最小堆:每个父节点的值都小于小子节点
如上图所示就是一个最小堆。
关于堆,其实说到底就是两种算法,一种是向下调整算法,一种是向上调整算法。我们先结合图来分析一下这两种算法。
就拿上图来说,上图是一个小堆,接下来比如说我们要往里插入一个9,那么因为堆的底层实际上是一个数组对象,实际上就是一个vector,那么插入的9,就是在如图的位置
但因为这是个最小堆,所以要对9进行向上调整,把9和它的父亲的值16比,因为9比16小,那么就交换9和16的位置;再拿9和11比,9比11小,交换9和11的位置;9和10比,9比10小,交换9和10的位置。直到交换到9比它的父节点大了或是9已经交换到根节点了就停止,上图在进行向上调整后的结果如下图。
在进行向上调整的过程中,只会对插入位置的祖先这一条线产生影响,对堆的其他线并不会产生影响。向下调整也是如此,比如我们在进行删除操作的时候,Pop掉的都是堆顶位置Top的值,也就是root位置的值(如向上调整好的图就是Pop 9),我们一般采用的方法就是,先交换9和16的位置,再对该堆进行一个Pop操作(因为底层是一个vector,Pop操作直接删除尾部元素),这样一来9就删除了
然而Top位置的值此时是16,所以就要对16进行向下调整,将16的两个孩子的值进行比较,这里因为是小堆,所以就选小的那个(大堆就选大的那个),所以就将16和10 的位置进行交换,以此类推,接着就是16和11交换,直到孩子都比16的值大,或是16已经是叶子节点了就停止。调整好的就是一开始的那个小堆
核心的算法我们已经掌握了,接下来就是代码的实现了。因为应用的过程中,可能既会用到大堆也会用到小堆,同时写一个大堆和一个小堆会产生代码重复,所以我们采用以下的方法来构建堆对象,这里用到仿函数。
#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);
}