一、堆的性质
堆的本质是用完全二叉树来进行存储,而完全二叉树是可以用数组实现。
所以,堆的存储用数组来实现,而其左右子树以及父亲节点的位置关系依靠数学计算来推导
在数学推导过程中,有一点必须要牢记:数组的索引是从0开始的
数学关系如下
假设一个节点在数组当中的索引为x
x为叶子节点:n/2 <= x < n
(对于完全二叉树而言,n/2及以后的节点全部为叶子节点)
为什么这里n取不到呢?因为数组的索引是从0开始
x的左子节点:2*x + 1 (这里+1是因为数组的索引是从0开始的)
x的右子节点:2*x + 2
x的父亲节点:(x-1) / 2
知道了这些以后,下面就是堆的原理了:
在计算机应用中,我们往往需要在许多任务中挑选出最优先的那一个,依照优先度依次执行
可以考虑用BST来完成这个任务,但是BST可能会变得不平衡导致效率变差,因此,我们发明了堆。
最大堆:任意一个节点存储的值都大于等于其任意一个子节点的值
最小堆:任意一个节点存储的值都小于等于其任意一个子节点的值
因此,最大堆的根节点是整个堆中最大的,最小堆的根节点是整个堆中最小的
堆的原理已经清楚了,那要怎样维持一个堆呢?
下面是堆对于元素的基本操作
上推:将此元素与父节点进行比较,如果该元素比父节点优先级更大,则与父节点交换
下推:将此元素与左右子节点中优先级较大的进行比较,如果该节点优先级更小,则交换
下推操作的要求:节点的左右子树都为堆
下推操作的代码:
while (!isLeaf(pos))//当pos节点是叶子节点时停止 { int j = leftchild(pos); int rc = rightchild(pos); if ((rc < n) && comp(heap[rc], heap[j]))//如果rc在堆中并且 rc比j大 或rc比j小 { j = rc; //那么就将索引j移动到rc(即左右子树中较大或较小的位置) //如果不符合要求,这步不执行,那么索引j的元素依然是左右子树较大或较小的位置 //这个if的妙处就在于:会保持j始终为左右子树较突出的位置,下面只需要将pos位置和j位置比较即可 } if (comp(heap[pos], heap[j]))//这里j已经是左右子树中较为突出的位置 //如果pos还是排在j的前面,那么不需要进行下推了 { return; } swap(heap, pos, j);//将pos节点的元素值与j的元素值进行交换,完成一次下推操作 pos = j;//将完成下推的元素的索引更新,以便进行下一次的下推操作 }
上推操作的代码:
while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) { //上推操作 //将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环 swap(heap, curr, parent(curr));//将curr与其父节点进行交换 curr = parent(curr); //更新上推后的节点值,以便后面继续进行上推 }
在堆中,元素的移动无非两个方向:向上(上推)或者向下(下推)。
二、接下来介绍堆作为优先队列的基本操作:
1.建堆
建堆的基本原理就是从低到高全部下推,就像从地板上一点一点往上垒砖块
因为叶子节点无法下推、只能交换,所以我们只需将所有的非叶子节点下推完毕,即完成建堆操作
代码:
template<typename E> inline void Heap<E>::buildHeap() { for (int i = n / 2 - 1; i >= 0; i--)//由于完全二叉树的前n/2个节点全部不是叶子节点,那么,最后一个不是叶子节点的节点 { //的序号为n/2-1,这个减一是因为数组是从零开始数的。 //如此以来,我们只要将所有不是叶子节点的节点全部下推完毕,即完成了一个堆的构建 //for循环中的--操作 实现了操作节点向前的移动 siftdown(i); } }
注意:由于完全二叉树的特性,前 n / 2 个节点全部为非叶子节点,所以我们只需对n/2 个节点全部进行下推操作完毕即可。
n / 2 - 1 减一是因为数组的索引是从0开始的,而倒着向前进行是因为:
对堆的元素进行下推操作的要求是:左右子树都为标准的堆
2.插入元素
由于堆是由数组实现的完全二叉树,因此我们每次插入新的元素只能将其按顺序放在数组的末尾,然后再进行上推操作将其放到合适的位置(因为此时新元素处于叶子节点,只能上推)
代码:
template<typename E> inline void Heap<E>::insert(const E& it) { Assert(n < maxsize, "Heap is full");//判断堆是否为满 int curr = n++; //该语句的执行顺序是:先赋值,再自增,取数组最后的位置给curr赋值 heap[curr] = it;//将该元素放到堆的最后一个位置 while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) { //上推操作 //将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环 swap(heap, curr, parent(curr));//将curr与其父节点进行交换 curr = parent(curr); //更新上推后的节点值,以便后面继续进行上推 } }
注意这里 int curr = n++; 语句的顺序是:先赋值,再自增。
3.取根节点并移除(取队首元素)
由于堆是起到优先队列的作用,因此,取根节点并移除的操作等同于从队列中取队首元素
我们需要将根节点与数组的最后一个元素位置交换,交换后将数组的n--,再将交换到根节点的元素进行下推操作:
代码:
template<typename E> inline E Heap<E>::removefirst() { Assert(n > 0, "Heap is empty");//检查堆是否为空堆 swap(heap, 0, --n);//该语句的执行顺序为:n先--,再执行swap中的内容,因为n表示元素个数,而数组从0开始 //那么n-1刚好为堆中的最后一个元素的位置,此时的n的值已经-1 if (n != 0) { siftdown(0);//如果删除后数组元素仍不为空,对刚换到堆顶的元素进行下推操作 } return heap[n]; //将换下来的第n个位置的元素返回,注意此时n已经完成自减 }
注意:这里 swap(heap, 0, --n); 语句执行顺序是:n先自减,再交换。自减后的n
刚好是堆中最后一个元素的位置(因为数组的索引是从0开始的)
一系列操作进行完后,此时数组前 n - 1 个元素构成堆,第 n 个元素为原来的根节点
4.删除特定位置的值
堆还有一个实现的功能是删除特定位置的元素值
基本原理是:将这个元素与堆中最后一个元素交换,再对这个最后换过来的新元素进行位置调整
类比移除根节点的操作,我们可以得出一个结论:
在堆中删除元素的基本原理:与最后一个节点交换后再调整位置
而此时交换后的节点在堆的中间,可上可下。
那到底是上推还是下推呢?
代码:
template<typename E> inline E Heap<E>::remove(int pos) { Assert((pos >= 0) && (pos < n), "Bad position");//判断pos位置是否在堆中 if (pos == n - 1)//删除堆中最后一个元素,不需要做任何处理 { n--; } else { swap(heap, pos, --n);//将pos元素与 n-1的位置(即堆中最后一个元素的位置)交换,并删除交换后的最后一个元素 //注意:此时n已经完成自减操作,也就是说已经完成了删除 while (pos != 0 && comp(heap[pos], heap[parent(pos)]))//上推操作 { swap(heap, pos, parent(pos));//交换pos和他的父亲节点 pos = parent(pos); //更新pos的索引,以便进行下一次上推操作 } if (n != 0) //如果堆为非空,对其进行下推操作 //如果上一步上推成立,则这一步下推不下去 //如果上一步上推不成立,则这一步可能会下推下去 //反正中间的元素无非两个方向移动,向上或者向下 { siftdown(pos); } return heap[n];//返回这个被删除的元素 } }
这段代码的基本逻辑就是:
1.交换
2.尝试上推
3.尝试下推
他没有用条件来判断到底是进行上推还是进行下推,因为上推或者下推操作自带判断。
如果上推成功,那么下推自然推不下去,如果上推不成功,也不影响后面下推的操作
三、完整代码
注意:在堆的构造函数有四个参数
分别为(建堆数组的指针,数组中存的元素个数,数组最大空间,排序函数指针)
排序函数是用户自己定义的函数,是bool类型的函数,两个参数(a,b),如果a排在b前面则返回true,否则返回false
例:
bool comp(int a,int b) { return a >= b; }
这个函数用于构建最大堆
注:在shuffer的书中,比较的函数是用一个comp类来作为模板参数传入
原型是这样 template < typename E , typename comp > class Heap
但是我在实际实验时发现,这样的方法在使用堆的过程中较为麻烦,因此,我将第二个比较类删除,改为由函数指针传入排序函数,将排序函数作为Heap类的一个成员。
完整代码:
heap.h
#pragma once /*堆的代码实现,由于堆是一个完全二叉树的结构,因此用数组来实现*/ #include<bits/stdc++.h> template<typename E> class Heap { private: void Assert(bool a, std::string b);//断言函数 typedef bool(*FUNC_POINTER)(E a,E b);//声明一个函数指针 private: E* heap;//指向堆存储数据的数组 int maxsize;//堆的最大存储空间 int n;//当前堆中存储的元素个数 FUNC_POINTER comp = NULL;//用于排序的函数 void siftdown(int pos);//进行下推的函数 void swap(E* heap,int a,int b);//交换符合要求的元素,是下推和上推所需要用到的工具函数 public: Heap(E* h, int num, int max,const FUNC_POINTER pot);//构造函数,初始化三个成员 int size() const;//读取当前堆的元素个数 bool isLeaf(int pos) const;//判断pos位置的节点是否是叶子节点 int leftchild(int pos)const;//找到pos节点的左孩子的位置 int rightchild(int pos)const;//找到pos节点右孩子的位置 int parent(int pos)const;//返回pos节点的父亲节点 void buildHeap();//建堆函数 void insert(const E& it);//将it元素插入堆中 E removefirst();//删除堆顶的元素 E remove(int pos);//删除特定位置的元素 void print();//打印堆中的元素用于测试 }; //========================================================================= template<typename E> inline void Heap<E>::Assert(bool a, std::string b) { if (!a) { std::cout << b << std::endl; } } template<typename E> inline void Heap<E>::siftdown(int pos) { while (!isLeaf(pos))//当pos节点是叶子节点时停止 { int j = leftchild(pos); int rc = rightchild(pos); if ((rc < n) && comp(heap[rc], heap[j]))//如果rc在堆中并且 rc比j大 或rc比j小 { j = rc; //那么就将索引j移动到rc(即左右子树中较大或较小的位置) //如果不符合要求,这步不执行,那么索引j的元素依然是左右子树较大或较小的位置 //这个if的妙处就在于:会保持j始终为左右子树较突出的位置,下面只需要将pos位置和j位置比较即可 } if (comp(heap[pos], heap[j]))//这里j已经是左右子树中较为突出的位置 //如果pos还是排在j的前面,那么不需要进行下推了 { return; } swap(heap, pos, j);//将pos节点的元素值与j的元素值进行交换,完成一次下推操作 pos = j;//将完成下推的元素的索引更新,以便进行下一次的下推操作 } } template<typename E> inline Heap<E>::Heap(E* h, int num, int max, const FUNC_POINTER pot)//这里留一点问题:为什么初始化要初始化n? { //初始化n,我们可以在已知一个数组元素时直接进行建堆操作,而无需一个一个插入 heap = h; n = num; maxsize = max; comp = pot; buildHeap(); } template<typename E> inline int Heap<E>::size() const { return n; } template<typename E> inline bool Heap<E>::isLeaf(int pos) const { return (pos >= n / 2) && (pos < n);//对于完全二叉树而言,n/2之后,n之前的节点都是叶子节点 //至于最后的 pos < n 为什么不是 <= n,因为数组是从0开始数的 } template<typename E> inline int Heap<E>::leftchild(int pos) const { return 2 * pos + 1; } template<typename E> inline int Heap<E>::rightchild(int pos) const { return 2 * pos + 2; } template<typename E> inline int Heap<E>::parent(int pos) const { return (pos -1)/2; } template<typename E> inline void Heap<E>::buildHeap() { for (int i = n / 2 - 1; i >= 0; i--)//由于完全二叉树的前n/2个节点全部不是叶子节点,那么,最后一个不是叶子节点的节点 { //的序号为n/2-1,这个减一是因为数组是从零开始数的。 //如此以来,我们只要将所有不是叶子节点的节点全部下推完毕,即完成了一个堆的构建 //for循环中的--操作 实现了操作节点向前的移动 siftdown(i); } } template<typename E> inline void Heap<E>::insert(const E& it) { Assert(n < maxsize, "Heap is full");//判断堆是否为满 int curr = n++; //该语句的执行顺序是:先赋值,再自增,取数组最后的位置给curr赋值 heap[curr] = it;//将该元素放到堆的最后一个位置 while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) { //上推操作 //将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环 swap(heap, curr, parent(curr));//将curr与其父节点进行交换 curr = parent(curr); //更新上推后的节点值,以便后面继续进行上推 } } template<typename E> inline E Heap<E>::removefirst() { Assert(n > 0, "Heap is empty");//检查堆是否为空堆 swap(heap, 0, --n);//该语句的执行顺序为:n先--,再执行swap中的内容,因为n表示元素个数,而数组从0开始 //那么n-1刚好为堆中的最后一个元素的位置,此时的n的值已经-1 if (n != 0) { siftdown(0);//如果删除后数组元素仍不为空,对刚换到堆顶的元素进行下推操作 } return heap[n]; //将换下来的第n个位置的元素返回,注意此时n已经完成自减 } template<typename E> inline E Heap<E>::remove(int pos) { Assert((pos >= 0) && (pos < n), "Bad position");//判断pos位置是否在堆中 if (pos == n - 1)//删除堆中最后一个元素,不需要做任何处理 { n--; } else { swap(heap, pos, --n);//将pos元素与 n-1的位置(即堆中最后一个元素的位置)交换,并删除交换后的最后一个元素 //注意:此时n已经完成自减操作,也就是说已经完成了删除 while (pos != 0 && comp(heap[pos], heap[parent(pos)]))//上推操作 { swap(heap, pos, parent(pos));//交换pos和他的父亲节点 pos = parent(pos); //更新pos的索引,以便进行下一次上推操作 } if (n != 0) //如果堆为非空,对其进行下推操作 //如果上一步上推成立,则这一步下推不下去 //如果上一步上推不成立,则这一步可能会下推下去 //反正中间的元素无非两个方向移动,向上或者向下 { siftdown(pos); } return heap[n];//返回这个被删除的元素 } } template<typename E> inline void Heap<E>::print() { for (int i = 0; i < n; i++) { std::cout << heap[i] << " "; } std::cout << std::endl; } template<typename E> inline void Heap<E>::swap(E* heap, int a, int b) { int temp = heap[b]; heap[b] = heap[a]; heap[a] = temp; }
测试代码
test.cpp
#include"Heap.h" bool comp(int a,int b) { return a >= b; } int main() { int b[10] = {1,2,3,4,5,6,7}; //测试原数组 Heap<int> c(b, 7, 10, comp); c.print(); std::cout << c.removefirst() << std::endl;//提取队首元素 c.print(); c.insert(8);//插入元素8 c.print(); c.remove(6);//这个remove函数有个缺点,就是只能删除指定位置的元素,而不能删除指定值的元素 c.print(); return 0; }
测试结果:
通过此图来看符合堆的要求