1.介绍
堆的逻辑结构是完美二叉树,其物理结构实际是一维数组。
在完全二叉树的存储学习中,我们已经知道可以用一维数组来表示。
一维数组以位序0开始,假设有一个结点i,则其双亲结点为(i-1)/2。
如果有左孩子,则左孩子结点为2i+1。如果有右孩子,则右孩子结点为2i+2。
堆分为大根堆和小根堆。
大根堆,即其每个结点都比其子结点(假如存在的话)要大,则根节点最大,故称为大根堆。
小根堆则相反。
2.优先级队列
堆逻辑上相当于一个优先级队列,出队优先级高的先出队。小根堆就是最小的元素也就是树的根节点先出队,这里根据元素的大小来区分优先级大小。
优先级队列有三种实现方式。
方法1:入队时,在合适的地方插入,需要保持队列元素之间的优先级顺序。出队,就只需要直接推出队首即可。
时间复杂度分别为O(n)和O(1)。
方法2:入队直接插入队尾。出队选择优先级最高的出队。时间复杂度分别为O(1)和O(n)。
方法3:利用堆实现优先队列。
以小根堆为例:
入队
假设已有size个元素,我们将结点先插在队尾,在树中是层序遍历的最后一个结点,其位序为size(一维数组以0开始)。
然后向上调整。假如插入结点小于其双亲结点,双亲结点位序为(size-1)/2(不明白怎么回事的看一开始的地方),则将其与双亲结点互换。再比较现在的双亲结点,直至不能互换。
这里不需要考虑兄弟结点和插入结点的大小关系,因为原来的堆结构就说明了兄弟结点一定大于双亲结点,如果插入的结点小于父结点,那么它也一定小于兄弟结点。
因此,时间复杂度为O(logn)。
出队
先将根节点出队,然后我们将队尾元素插到出队元素的位置(就是根节点位置)上,假如队尾元素大于其较小的孩子结点,则两者互换,否则停止向下比较,停下的位置就是队尾结点应在的位置。
在互换的过程中,我们保持了堆的结构。
建堆
有了出队入队,堆建立也就非常的简单。只需要多次入队即可。时间复杂度为O(nlogn)。
上面是一种自上向下的建堆方法,还有一种自下而上的建堆方法。
就是将一堆节点一股脑插入树中,先不管其父结点和子结点的大小关系。
等插入完后,再从位序为n/2-1的结点调整下面的结点(至于为什么是这个位序,等下解释),这个结点是最后一个有子结点的结点。调整的操作和入队类似,就把n/2-1位序的结点及其以下的结点当成一颗单独的树,将n/2-1的结点当成刚插入这棵树的结点,和入队作相似的调整。
这样层层向上依次调整每颗子树,最后成为合格的堆。
至于为什么从n/2-1开始调整呢:
从另一个角度解释或许更容易想到答案:叶子节点数量n0和树结点数量n的关系是什么?
我们注意到完美二叉树的叶子结点总是排在最后面,知道了叶子节点数量和树结点数量的关系,我们就可以知道最后一个有子结点的结点的位序。
假设二叉树有n0的结点其没有子结点,n1个结点其只有一个子结点,n2个结点其有两个子结点。
这n0个结点就是叶子结点。
现不考虑n=0或者1的情况,没有意义,因为没有调整的必要。
则有两种情况:
情况一:树有奇数个结点,则这n个结点中要么是有0个子结点,要么是有2个子结点。则n1=0, 即n=n0+n2。
由二叉树知识n0+n1+n2=2*n2+1*n1+0*n0+1(即顶点数=边数+1,这是因为二叉树除了根节 点,其余每个结点都有一个父结点到它的边。而二叉树中n2个结点有两个边到它的子结点, n1个结点有一个边到它的子结点,n0个结点有零个边到它的子结点,则式子右边就是二叉树 的边数)
因此n0+n2=2*n2+1,n0=n2+1,而顶点数n=n0+n2,则n=2*no-1,则树中有(n+1)/2个叶 子结点。则有(n-1)/2个不是叶子结点。
设n=2k+1(k>=1),则有k个不是叶子结点,则最后一个有子结点的位序为k-1。
情况二:树有偶数个结点,在完美二叉树中有偶数个结点对应的情况是最后一个结点它没有兄弟 结点,而其父结点仅有唯一的子结点(即该结点),即n1=1。则由二叉树的知识得出式子: n0+n1+n2=2*n2+1*n1+0*n0+1,即n0=n2+1,而n=n0+n1+n2=n0+1+n2,则n=2n0,则有 一半不是叶子节点。
设n=2k(k>=1),则最后一个有子结点的位序为k-1。
而上面的k-1都可以用n/2-1得到,注意这里的除法是整数除法,5/2得2,而不得3。这也就是为什么从n/2-1结点开始调整。
从下到上的建堆方法如图:
先一股脑复制进队列,叶节点只有一个,其子树认为有序,然后对每个双亲结点进行向下调整。这样,我们保证在调整双亲结点时,它的左右子树先分别有序,从而完成建堆。
#pragma once
#include<iostream>
template<class T>
class priorityQueue
{
T* data;
int cur_size;
int max_size;
void resize();
void revise_down(int parent);//从双亲结点向下调整
void revise_up(int son);
public:
priorityQueue(T*d,int init_size = 20);
~priorityQueue() { delete[]data; }
void push(const T& obj);
void pop();
T front()const;
void print()const;
};
template<class T>
void priorityQueue<T>::resize()
{
T* temp = data;
data = new T[max_size * 2];
max_size *= 2;
for (int i = 0; i < max_size; i++)
data[i] = temp[i];
delete[]temp;
}
template<class T>
void priorityQueue<T>::revise_down(int parent)
{
if (parent < 0 || parent >= cur_size)
throw("out if range");
while (true)
{
int small_son = 2 * parent + 1;
if (small_son >= cur_size)
break;
//比较孩子大小
if (small_son + 1 < cur_size && data[small_son] > data[small_son + 1])
small_son++;
//交换
if (data[parent] > data[small_son])
swap(data[parent] , data[small_son]);
else break;
parent = small_son;
}
}
template<class T>
void priorityQueue<T>::revise_up(int son)
{
if (son < 0 || son >= cur_size)
throw("out if range");
if (son == 0)
return;
while (true)
{
int parent = (son - 1) / 2;
if (data[parent] > data[son])
swap(data[parent],data[son]);
else break;
son = parent;
}
}
template<class T>
priorityQueue<T>::priorityQueue(T* d, int init_size)
{
//我们用自下而上的方法
//首先初始化
cur_size = max_size = init_size;
data = new T[max_size];
for (int i = 0; i < cur_size; i++)
data[i] = d[i];
//开始自下而上的调整
//至于为什么从cur_size/2开始,因为一些叶子结点不需要调整,当然调整也是可以的
for (int i = cur_size / 2; i >= 0; i--)
revise_down(i);
}
template<class T>
T priorityQueue<T>::front()const
{
if (cur_size != 0)
return data[0];
else
throw("error");
}
template<class T>
void priorityQueue<T>::push(const T& obj)
{
if (cur_size == max_size)
resize();
data[cur_size] = obj;
cur_size++;
revise_up(cur_size - 1);
}
template<class T>
void priorityQueue<T>::pop()
{
if (cur_size == 0)
throw("no data");
data[0] = data[cur_size - 1];
cur_size--;
revise_down(0);
}
template<class T>
void priorityQueue<T>::print()const
{
for (int i = 0; i < cur_size; i++)
std::cout << data[i] << " ";
std::cout<<std::endl;
}