做 Heap 的忠爱粉
二进制堆
二叉堆是一种形态特征是完全二叉树,且数值特征是父节点优于/劣于其左右子节点的一种数据结构。
- 父节点优于其左右子节点的堆是大根堆
- 父节点劣于其左右子节点的堆是小根堆
由于形状近似完全二叉树,我们可以使用顺序结构(arr )来表示其。
这里我们使用顺序结构表示时需要记住两个性质:
- 完全二叉树的父节点为对应数组下标为 N,那么它的左孩子下标为 2 * N + 1,右孩子下标是 2 * N + 2
- 完全二叉树的最后一个父节点的下标是 (数组长度 / 2 - 1)
建堆
假定我们有数组:
INT ARR [N] = {7,5,5,9,4,3};
建一个大根堆:
第一步我们当然是建一个最小的堆了,从那开始呢? 当然是从最后一个可拥有孩子节点的父节点开始了,其下标为 (数组长度N / 2 -1)即对应数组的下标为 2 的元素,另外它的左孩子是下标为 5 的元素,其没有右孩子 。
这时我们利用 3 和 5 建立一个堆。
这时一个最小的堆就建立好了,但是呢我们要完成一整个数组的建堆过程还是很遥远的,我们知道一整个完全二叉树是很多子树构成的,那么我们堆也是如此,接下来我们再建立一个堆把(为了能和上面建的堆进行合并大堆,我们选择倒数第二个可拥有子节点的父节点):
其下标为 (数组长度N / 2 -2)即对应数组的下标为 1 的元素(5),另外它的左孩子是下标为 3 的元素(9),右孩子为下标 4 的元素 (4)
上面我们构成的堆非大根堆(根元素小于其左孩子),因此我们需要进行堆顶的交换过程:
- 先比较左右孩子, 用其胜者与父比较,如果孩子胜利,交换父亲与孩子, 父亲交换到原孩子之下后变成了儿子,再次检查原父亲在该位置上的稳定性(重复这一过程),直到其结构稳定即可结束。
伪代码:
int dad = N/2 - 2, son = dad * 2 + 1;
while(son < N) {
if(son + 1 < N && arr[son + 1] > arr[son])
++son;
if(arr[son] > arr[dad]) {
swap(arr[son], arr[dad]);
} else
break;
//交换后可设置其dad 和 son 检查 原 dad 的稳定性,不交换可直接退出
dad = son;
son = dad * 2 + 1;
}
最后一次建堆:
总结: 建堆的流程
- 从最后一个父亲节点开始,先比较左孩子和右孩子,选取最强者与父亲比较,若孩子胜利,并交换,父亲交换到孩子位置之后仍要检查自己的地位是否足以撼动现在的新孩子(检查其稳定性), 若不与孩子交换则直接退出
- 向前循环其余新父亲,直到第一个父亲节点(根节点)完成上述过程即可。
C++ 代码:
void AdjustMaxHeap(std::vector<int> *arr, int pos, int len) {
int dad = pos;
int son = (dad << 1) + 1;
while(son < len) {
if(son + 1 < len && arr[son] < arr[son+1])
++son;
if(arr[son] > arr[dad])
std::swap(arr[son], arr[dad]);
else
break;
dad = son;
son = (dad << 1) + 1;
}
}
void BuildHeap(std::vector<int> *arr) {
for (size_t i = arr->size() / 2 - 1; i >= 0; --i) {
AdjustMaxHeap(arr, i, arr->size());
}
}
堆排序
建树之后根元素就是最大元素,如果我们将根元素直接与最后一个节点交换,并缩小堆的规模(-1),重新建堆,模拟选择最大值放到最后一位的过程,重复 N -1 ,我们就能得到一个有序的序列。
将 9 抹去,因为其已经排到序列末尾,之后重新建树:
此时 7 最大,将 7 与 4 交换,缩小堆规模,重建堆。。。重复如此,即可实现堆排序:
代码:
void HeapSort(std::vector<int> *arr) {
BuildHeap(arr);
std::swap(arr[0], arr[arr->size() - 1]);
for (size_t i = arr->size() - 1; i > 1; ++i) {
//由于新堆的堆顶与规模中的最底元素发生了交换,需要检查新堆顶的合法与稳定性。
AdjustMaxHeap(arr, 0, i);
std::swap(arr[0], arr[i-1]);
}
}
Heap 在 STL 中的职责
Heap 并不属于STL容器组件,它是一个幕后英雄,提供了诸多 Heap 算法和为适配器提供了底层操作。
- Heap 的迭代器是 双向迭代器 RadomAccessIterator
下面呢,我向诸位朋友来介绍一下 STL 中提供的几种 Heap 算法操作:
STL Heap 算法
- make_heap : 对指定序列建堆,从末尾父节点向前循环建堆。
void BuildHeap(std::vector<int> *arr) {
for (size_t i = arr->size() / 2 - 1; i >= 0; --i) {
AdjustMaxHeap(arr, i, arr->size()); //调整堆
}
}
- push_heap: 入堆,将序列的末尾,堆规模外的第一个元素放入堆,提升堆规模,新堆元素向上循环比较找到自己合适的位置。
template <class RandomAccessIterator, class Distance, class T>
void push_heap(RandomAccessIterator first, Distance holeIndex, Distance topIndex, T value) {
Distance parent = (holeIndex - 1) / 2; //找出父节点,从 0 开始的隐述完全二叉树的父亲计算等于 当前节点-1再除2
while(holeIndex > topIndex && *(first + parent) < value) {
// 尚未到达顶端且 父节点小于新值(不符合其 max-heap 次序)
*(first + holeIndex) = *(first + parent);
holeIndex = parent;
parent = (holeIndex - 1) / 2;
}
*(first + holeIndex) = value;
}
- pop_heap : 出堆顶,会将堆顶与最后一个堆元素交换,并缩小堆的规模(出堆顶),之后新堆顶去向下循环找到自己合适的位置。
{
std::swap(arr[0], arr[arr->size() - 1]); //交换堆顶与最后一个堆元素
AdjustMaxHeap(arr, 0, arr -> size() - 1); //缩小堆规模,调整堆
}
void AdjustMaxHeap(std::vector<int> *arr, int pos, int len) {
int dad = pos;
int son = (dad << 1) + 1;
while(son < len) {
if(son + 1 < len && arr[son] < arr[son+1])
++son;
if(arr[son] > arr[dad])
std::swap(arr[son], arr[dad]);
else
break;
dad = son;
son = (dad << 1) + 1;
}
}
- sort_heap: 对堆中存放的元素序列进行排序(不停的 pop_heap)
void HeapSort(std::vector<int> *arr) {
BuildHeap(arr);
std::swap(arr[0], arr[arr->size() - 1]);
for (size_t i = arr->size() - 1; i > 1; ++i) {
AdjustMaxHeap(arr, 0, i);
std::swap(arr[0], arr[i-1]);
}
}
测试实例:
void TestSTLHeap() {
int ia[9] = { 0,1,2,3,4,8,9,3,5};
std::vector<int> ivec(ia, ia + 9);
std::cout <<"origin seq: ";
for (const auto &i : ivec)
std::cout << i << ' ';
std::cout << std::endl;
std::cout << "make_heap :" ;
std::make_heap(ivec.begin(), ivec.end());
for (const auto &i : ivec)
std::cout << i << ' ';
std::cout << std::endl;
ivec.push_back(7);
std::cout << "push_heap :";
std::push_heap(ivec.begin(), ivec.end());
for (const auto &i : ivec)
std::cout << i << ' ';
std::cout << std::endl;
std::cout << "pop_heap :";
std::pop_heap(ivec.begin(), ivec.end());
for (const auto &i : ivec)
std::cout << i << ' ';
std::cout << std::endl;
ivec.pop_back();
std::sort_heap(ivec.begin(), ivec.end());
std::cout << "sort_heap :";
for (const auto &i : ivec)
std::cout << i << ' ';
std::cout << std::endl;
}
为配接器 priority_queue 提供了底层运作机制
1. 优先队列(priority_queue)介绍
priority_queue 是一个拥有权值观念的 queue,
- 它允许加入新元素,移除旧元素,审视元素值等功能,
- 由于名字表示其是一个 queue,因此只允许从尾后加入元素,从顶端取出元素,除此之外别无其他存取元素的途径。
- 其带有权值观念,其内的元素并非按照推入的次序排列, 而是自动按照元素的权值排列,权值者高者,排在最前面。
- 默认情况下 prority_queue 利用一个 max-heap 完成, 其max-heap 是以一个 vector 表现的 complete bindary tree, max-heap 可以满足 priority_queue 所需要的 “依靠权值高低自动递减排序” 的特性。
2. priority_queue 源码分析
priority_queue 完全以底部容器为根据,加上 heap 算法处理规则,所以实现起来十分简单,同时,我们将 “修改某物接口, 形成另一种风貌 ” 这一性质的拥有者称为 “配接器 adapter”
- 初始化优先队列,提供序列及比较仿函数,序列用于初始化底层vector 容器,比较仿函数用于为 heap 算法提供(优劣)建堆方式。
- push 时,vector 调用 push_back 将元素存入尾部, 并执行 push_heap 操作调整堆
- pop时, pop_heap 出堆头并调整堆,并利用 vector 的 pop_back 将已经在 vector 尾部的旧堆头删除。
template<class T, class Sequence = std::vector<T>,
class Compare = std::less<typename Sequence::value_type>>
class my_priority_queue {
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_reference;
protected:
Sequence c; //底层容器
Compare comp; //元素大小比较标准
public:
my_priority_queue(): c() { }
explicit my_priority_queue(const Compare& x) : c(), comp(x) { }
// 以下用到的 make_heap(), push_heap(), pop_heap() 都是泛型算法
// 注意,任何一个构造函数都立刻于底层容器内产生一个 implict 的 heap
template<class InputIterator>
my_priority_queue(InputIterator first, InputIterator second) : c(first, second) {
std::make_heap(c.begin(),c.end(), comp);
}
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
const_reference top() { return c.front();}
void push(const value_type &x) {
c.push_back(x);
std::push_heap(c.begin(), c.end(), comp);
}
void pop() {
std::pop_heap(c.begin(), c.end(), comp);
c.pop_back();
}
};
3. 使用演示:
源码位置
想自己实现 STL 的各种容器组件,可以参考博主的 项目哦!
目前更新的有这些,下节我们讲 hash_table
项目链接: STLAnalyse