前置知识
-
堆是一个完全二叉树(最后一层可以不满,上面的每一层都是满的。一个结点若只有一个孩子结点,那一定是它的左孩子。如下图)这是一个逻辑上基于完全二叉树、物理上一般基于线性数据结构(如数组、向量、链表等)的一种数据结构。
-
完全二叉树最重要的性质:如果n个节点的完全二叉树的节点按照层次并按从左到右的顺序从0开始编号,对于人一个绩点都有:
- 序号为0的节点是根
- 对于 i > 0 i>0 i>0,其父节点的编号为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2。
- 若 2 ⋅ i + 1 < n 2·i+1<n 2⋅i+1<n ,其左子节点的序号为 2 ⋅ i + 1 2·i+1 2⋅i+1 ,否则没有左子节点。
- 若 2 ⋅ i + 2 < n 2·i+2<n 2⋅i+2<n ,其右子节点的序号为 2 ⋅ i + 2 2·i+2 2⋅i+2 ,否则没有右子节点。
另外我们下边举例子根节点为数组下标为1的位子,相应的公式也会有点变化
-
堆的有序性
-
堆可分为两种:大根堆(最大堆)、小根堆(最小堆)。
-
大根堆
何为大根堆?顾名思义,大根堆即指在逻辑上的二叉树结构中,根结点>子结点,总是最大的,并且在堆的每一个局部都是如此。例如 3 , 1 , 2 {3,1,2} 3,1,2 可以看作为大根堆,而 3 , 2 , 1 {3,2,1} 3,2,1 亦可以看作为大根堆。大根堆的根结点在整个堆中是最大的元素。 -
小根堆
小根堆的性质与大根堆类似,只不过在二叉树的结构中,根结点<子结点。例如 1 , 2 , 3 {1,2,3} 1,2,3 为小根堆, 1 , 3 , 2 {1,3,2} 1,3,2 同样也是小根堆。小根堆的根结点在整个堆中是最小的元素。
-
堆操作
这边主要以小根堆实现讲解
小根堆插入
-
往堆中加入一个元素的算法(put)如下:
(1)在堆尾加入一个元素,并把这个结点置为当前结点。
(2)比较当前结点和它父结点的大小
如果当前结点小于父结点,则交换它们的值,并把父结点置为当前结点。转(2)
如果当前结点大于等于父结点,则转(3)(3)结束。
类似于这样,这是一个循环操作,直到所有父节点小于其子节点,且注意,在插入之前堆内就是符合性质的,只需要将新插入的节点放好。
代码实现
void put(int d) { int now, next; heap[++heap_size] = d; now = heap_size; //新节点 while (now > 1) { //1为根节点位置 next = now >> 1; //父节点 if (heap[now] >= heap[next])//若插入的节点值小于父节点,则不用调整 break; swap(heap[now], heap[next]);//交换父节点和新节点的值进行调整 now = next; //追踪新节点的位置,继续进行调整 } }
小根堆删除
这种大根堆小根堆的操作中的删除一般都是删除根节点,因为根节点具有最值性
从堆中取出并删除一个元素的算法(get)如下
(1)取出堆的根结点的值。(2)把堆的最后一个结点放到根的位置上,把根覆盖掉。把堆的长度减一。
(3)用p指向当前被覆盖后的根节点
(4)如果p无儿子(2*p>len,完全二叉树性质,左子树节点的位置,前置知识),则转(6);否则,选出 p 的两(或一)个儿子中值最小的那个称为son。
(5)比较p与son的值,如果p的值小于或等于son,则转(6);否则,交换这两个结点的值,接着用p追踪被交换过去的新值,转(4)
(6)结束。
代码实现
int get() //取出堆顶元素,heap[1]为堆顶
{
int now = 1, next, res = heap[1];
heap[1] = heap[heap_size]; //取出堆顶元素,将最后一个元素放于堆顶
heap_size--;
//重新调整为小根堆
while (now * 2 <= heap_size) { //存在左孩子next = now*2;
//找出左右孩子最小的那个
if (next < heap_size && heap[next + 1] < heap[next])
next++;
if (heap[now] <= heap[next])
break;
swap(heap[now], heap[next]); //交换
now = next; //追踪过去
}
return res;
}
测试代码
#include <iostream>
#include <vector>
using namespace std;
int heap[1000];
int heap_size;
void put(int d)
{
int now, next;
heap[++heap_size] = d;
now = heap_size;
while (now > 1) {
next = now >> 1;
if (heap[now] >= heap[next])
break;
swap(heap[now], heap[next]);
now = next;
}
}
int get() //取出堆顶元素,heap[1]为堆顶
{
int now = 1, next, res = heap[1];
heap[1] = heap[heap_size]; //取出堆顶元素,将最后一个元素放于堆顶
heap_size--;
//重新调整为小根堆
while (now * 2 <= heap_size) { //存在左孩子
next = now*2; //找出左右孩子最小的那个
if (next < heap_size && heap[next + 1] < heap[next])
next++;
if (heap[now] <= heap[next])
break;
swap(heap[now], heap[next]); //交换
now = next;
}
return res;
}
int main(int argc, char)
{
int n;
cin >> n;
int temp;
for (int i = 1; i <= n; i++){
cin>>temp;
put(temp);
}
for(int i=1;i<=n;i++){
cout<<heap[i]<<" ";
}
}
STL实现——priority_queue
priority_queue 容器适配器定义了一个元素有序排列的队列。默认队列头部的元素优先级最高。因为它是一个队列,所以只能访问第一个元素,这也意味着优先级最高的元素总是第一个被处理。但是如何定义“优先级”完全取决于我们自己。如果一个优先级队列记录的是医院里等待接受急救的病人,那么病人病情的严重性就是优先级。如果队列元素是银行的借贷业务,那么借记可能会优先于信贷。
1.使用
由于 priority_queue 容器适配器模板位于<queue>
头文件中,并定义在 std 命名空间里,因此在试图创建该类型容器之前,程序中需包含以下 2 行代码:
#include <queue>
using namespace std;
priority_queue 容器适配器的定义如下:
template <typename T,
typename Container=std::vector<T>,
typename Compare=std::less<T> >
class priority_queue{
//......
}
可以看到,priority_queue 容器适配器模板类最多可以传入 3 个参数,它们各自的含义如下:
-
typename T:指定存储元素的具体类型;
-
typename Container:指定 priority_queue 底层使用的基础容器,默认使用 vector 容器。
作为 priority_queue 容器适配器的底层容器,其必须包含 empty()、size()、front()、push_back()、pop_back() 这几个成员函数,STL 序列式容器中只有 vector 和 deque 容器符合条件。
-
typename Compare:指定容器中评定元素优先级所遵循的排序规则,默认使用
std::less<T>
按照元素值从大到小进行排序,还可以使用std::greater<T>
按照元素值从小到大排序,但更多情况下是使用自定义的排序规则。
2.创建
- 创建一个空的 priority_queue 容器适配器,第底层采用默认的 vector 容器,排序方式也采用默认的 std::less 方法:
std::priority_queue<int> values;
- 可以使用普通数组或其它容器中指定范围内的数据,对 priority_queue 容器适配器进行初始化:
//使用普通数组
int values[]{4,1,3,2};
std::priority_queue<int>copy_values(values,values+4);//{4,2,3,1}
//使用序列式容器
std::array<int,4>values{ 4,1,3,2 };
std::priority_queue<int>copy_values(values.begin(),values.end());//{4,2,3,1}
注意,以上 2 种方式必须保证数组或容器中存储的元素类型和 priority_queue 指定的存储类型相同。另外,用来初始化的数组或容器中的数据不需要有序,priority_queue 会自动对它们进行排序。
- 还可以手动指定 priority_queue 使用的底层容器以及排序规则,比如:
int values[]{ 4,1,2,3 };
std::priority_queue<int, std::deque<int>, std::greater<int>> copy_values(values, values+4);//{1,3,2,4}
事实上,std::less 和 std::greater 适用的场景是有限的,更多场景中我们会使用自定义的排序规则。
3.成员函数
成员函数 | 功能 |
---|---|
empty() | 如果 priority_queue 为空的话,返回 true;反之,返回 false。 |
size() | 返回 priority_queue 中存储元素的个数。 |
top() | 返回 priority_queue 中第一个元素的引用形式。 |
push(const T& obj) | 根据既定的排序规则,将元素 obj 的副本存储到 priority_queue 中适当的位置。 |
push(T&& obj) | 根据既定的排序规则,将元素 obj 移动存储到 priority_queue 中适当的位置。 |
emplace(Args&&… args) | Args&&… args 表示构造一个存储类型的元素所需要的数据(对于类对象来说,可能需要多个数据构造出一个对象)。此函数的功能是根据既定的排序规则,在容器适配器适当的位置直接生成该新元素。 |
pop() | 移除 priority_queue 容器适配器中第一个元素。 |
swap(priority_queue& other) | 将两个 priority_queue 容器适配器中的元素进行互换,需要注意的是,进行互换的 2 个 priority_queue 容器适配器中存储的元素类型以及底层采用的基础容器类型,都必须相同。 |
和 queue 一样,priority_queue 也没有迭代器,因此访问元素的唯一方式是遍历容器,通过不断移除访问过的元素,去访问下一个元素。