【数据结构】再谈堆与优先级队列
堆是继
栈、队列
以及二叉树之后又一个重要的数据结构,堆在底层也是没有自己的容器的,而是通过binary heap
这种complete binary tree
(完全二叉树)来实现的。下面就堆的实现原理及底层容器作以阐述和实现。
1. 完全二叉堆
complete binary tree(完全二叉树),整棵树除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右不得有空隙,这样带来的好处是:我们可以利用array来储存所有的结点。
如上图所示,我们可以将完全二叉树
的结点按照层序遍历的顺序
储存在一个数组中,那么当完全二叉树中的某个结点位于array的i处时,其左子节点必位于2i+1处(i>=0),其右结点必位于array的2i+2处。这样我们就可以轻易的实现完全二叉树的存储。
若将结点v的编号(秩)记作i(v),则满足以下关系:
i(root) = 0;
i(leftChild(root)) = 1;
i(rightChild(root)) = 2;
i(leftChild(leftChild(root))) = 3;
....
对于任意结点,则有:
1)若v有左孩子,则 i(leftChild(v)) = 2 * i(v) + 1;
2)若v有右孩子,则 i(rightChild(v))= 2 * i(v) + 2;
3)若v有父节点,则 i(parent(v)) = i(v)/2 - 1;
一个array和一组heap算法(用于插入元素、删除元素、取极值,将某一个数组数据排列成一个heap),array的缺点是无法动态改变大小,而heap却需要这样的功能,因此,以vector代替array是再好不过了。
堆存储在下标为0开始计数的数组中,因此在堆中给定小标为i的结点时:
①如果i=0: 结点i是根节点,没有双亲节点;否则结点i的双亲结点为结点(i-1)/2。
②如果2*i+1>n-1: 则结点i无左孩子,否则结点i的左孩子为结点2*i+1。
③如果2*i+2>n-1: 则结点i无右孩子,否则结点i的右孩子为结点2*i+2。
2. 最大堆和最小堆
根据元素排列方式,heap可分为max-heap
和min-heap
两种,前者每个节点的键值(key)都大于或等于其子节点键值,后者的每个节点键值(key)都小于或等于其子节点键值。
因此,max-heap的最大值在根节点,并总是处于底层array或vector的头部。min-heap的最小值在根节点,总是位于底层array或vector的起头处。
//max-heap and min-heap
max-heap(最大堆):每个父节点的值都大于孩子节点。
min-heap(最小堆):每个父节点的值都小于孩子节点。
以测试用例数组的{3,4,6,5,7,2}为例,其大小堆结构分别为:
3. 源代码及注释
#pragma once
#include <iostream>
#include <vector>
#include <assert.h>
#define DataType int
using namespace std;
template<class T>
class Greate//大堆
{
public:
bool operator()(const T& left, const T& right)//仿函数,重载()大于
{
return left > right;
}
};
template<class T>
class Less//小堆
{
public:
bool operator()(const T& left, const T& right)//仿函数,重载()小于
{
return left < right;
}
};
template<class T,class Compare = Greater<T>>//第二个模板参数默认为大堆
class Myheap
{
public:
//空堆
Myheap()
{}
Myheap(const T* arr, DataType size)
{
assert(arr);
_heap.resize(size);//开辟空间(比vector的pushback好)
for (size_t idx = 0; idx < size; ++idx)
{
_heap[idx] = arr[idx];
}
//size-1为最后一个节点的下标,(size-1-1)/2 = 父节点
size_t Root = (size - 2) >> 1;
for (DataType idx = Root; idx >= 0; idx--)
{
AdjustDown(idx);
}
}
//判空
bool Empty()const
{
return _heap.empty();
}
//堆的大小
size_t Size()const
{
return _heap.size();
}
//堆顶元素
T& Top()
{
assert(Size() > 0);
return _heap[0];
}
const T& Top()const
{
assert(Size() > 0);
return _heap[0];
}
void PushHeap(const T& elem)//向堆中插入元素
{
//先将新节点插入末尾
_heap.push_back(elem);
//在将最后一个元素向上调整
AdjustUp(_heap.size() - 1);
}
void PopHeap()//从堆中取出堆顶的元素
{
assert(!Empty());//堆不为空
//先交换堆顶节点与堆尾节点
std::swap(_heap[0], _heap[_heap.size() - 1]);
//删除最后一个节点
_heap.pop_back();
//从堆顶向下调整,存在多个节点时才调整
if (_heap.size() > 1)
{
AdjustDown(0);
}
}
//①先找最后一个非叶子结点
//②如果当前结点的孩子结点左孩子大于右孩子,就让child指向最大孩子结点(右孩子存在)
//③如果当前父结点小于孩子结点,交换,父节点指向孩子,孩子指向孙子
void AdjustDown(DataType _root)//下滤
{
DataType Parent = _root;//此时传入的_root为倒数第一个非叶子结点
DataType Child = Parent * 2 + 1;//左孩子的下标等于父节点*2+1
Compare _Compare;//仿函数用于比较优先级
//左孩子存在
while (Child < _heap.size())
{
//右孩子存在且右孩子的值大于左孩子
if (Child + 1 < _heap.size() && _Compare(_heap[Child+1],_heap[Child]))
{
Child = Child + 1;//让Child结点保存大值
}
//孩子结点的优先级大于父节点(Child的值大于Parent的值)
if (_Compare(_heap[Child], _heap[Parent]))
{
std::swap(_heap[Child], _heap[Parent]);//交换两节点的值
//下滤
Parent = Child;//让父节点指向孩子节点
Child = Parent * 2 + 1;//孩子节点指向孙子节点
}
//孩子结点的优先级小于父节点(Child的值小于Parent的值),则不需要调整
else
break;
}
}
//①传入尾节点
//②只要当前节点未到根节点,就一直上滤
//③孩子节点的优先级大于父节点的优先级,交换,孩子指向父亲,父亲指向爷爷
//注意:在此不用考虑左右子数谁大谁小,上调是如果孩子结点比父结点大,那它肯定比兄弟结点大。
void AdjustUp(DataType Child)//上滤
{
//此时传入的Child为最后一个节点的坐标
DataType Parent = (Child - 1) >> 1;
Compare _Compare;//仿函数用于比较优先级
//上滤的过程,只要没到根节点就一直上移
while (Child > 0 )
{
//孩子节点的优先级大于父节点的优先级
if (_Compare(_heap[Child], _heap[Parent]))
{
std::swap(_heap[Child], _heap[Parent]);//交换节点的值
//上滤
Child = Parent;//孩子节点指向父节点
Parent = (Child - 1) >> 1;//父节点指向爷爷节点
}
//孩子节点的优先级小于父节点的优先级,不需要调整
else
break;
}
}
private:
std::vector<T> _heap;
};
//test.cpp
#include "heap.h"
void Test()
{
int Array[] = { 3 ,4 ,6 ,5 ,7 ,2 };
Myheap<int,Greate<int>> _h(Array,sizeof(Array)/sizeof(Array[0]));
cout << _h.Size() << endl;
cout << _h.Top() << endl;
_h.PushHeap(2);
cout << _h.Size() << endl;
cout << _h.Top() << endl;
Myheap<int, Less<int>> _h1(Array, sizeof(Array) / sizeof(Array[0]));
cout << _h1.Size() << endl;
cout << _h1.Top() << endl;
_h1.PushHeap(2);
cout << _h1.Size() << endl;
cout << _h1.Top() << endl;
_h1.PopHeap();
cout << _h1.Size() << endl;
cout << _h1.Top() << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
4. 优先级队列
优先级队列
的实现调用的是堆的方法
。priority-queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值进行排列。通过complete binary tree以vector为底层容器就可以实现底端加入元素,顶端取出元素的操作。
//优先级队列(调用堆的方法即可)
template<class T,class Compare = Greate<T>>
class PriorityQueue
{
public:
PriorityQueue()
{}
void Push(const T& data)
{
_hp.PushHeap(data);
}
void Pop()
{
_hp.PopHeap();
}
bool Empty()const
{
return _hp.Empty();
}
const T& Top()const
{
return _hp.Top();
}
size_t Size()
{
return _hp.Size();
}
protected:
Myheap<T, Greate<T>> _hp;
};