【C++语法】手写堆与有关堆的容器/函数
文章目录
- 【C++语法】手写堆与有关堆的容器/函数
- 1. 手写堆
- 1.1 构建操作
- 1.2 插入操作
- 1.3 删除堆顶操作
- 1.4 获取堆顶
- 2. 优先队列
std::priority_queue
- 2.1 容器的定义
- 2.2 容器中的函数
- 3. 与堆有关的函数
- 3.1
std::make_heap(Literator, Riterator, Cmp)
- 3.2
std::is_heap(Literator, Riterator, Cmp)
- 3.3
std::is_heap_until(Literator, Riterator, Cmp)
- 3.4
std::push_heap(Literator, Riterator, Cmp)
- 3.5
std::pop_heap(Literator, Riterator, Cmp)
- 3.6
std::sort_heap(Literator, Riterator, Cmp)
- 3.1
【C++语法】手写堆与有关堆的容器/函数
在 C++ 中有一些底层使用堆来实现的容器,这篇文章将会做详细的介绍
前置知识点:【算法】堆(Heap)的概念与堆排序(Heap Sort)
1. 手写堆
堆应该如何手写呢?我们会发现一个特点,那就是堆的插入和删除都会想方设法地在末尾进行执行,而用顺序表手写的栈也有这一个特点,所以我们可以仿照手写栈的方式来手写堆
这里面会用到一个附加函数:swap(L, R)
,用于交换 L
与 R
的值
// 交换两个数字用的函数
void swap (int& L, int& R) { L = L ^ R;R = L ^ R;L = L ^ R;
}
1.1 构建操作
堆的构建方法很简单,那就是从后往前地修复堆的性质的方法进行下沉操作(与自己和左右孩子比较,可以进行与孩子交换的选择)
此外,在 C/C++ 中,我们可以使用位运算来提高程序运行效率
例如当前节点为 x
那么父节点为 x >> 1
左孩子为 x << 1
右孩子为 x << 1 | 1
我们可以手写一个函数:MakeHeap(Heap, Size)
用于构建大顶堆(小顶堆只需要插入它的相反数就可以,或者仿照这个函数重新手写一个)
// Heap 是堆数组,size 是大小,下标从 1 开始
void MakeHeap (int* Heap, int size) {int maxn, cur; // 用于计算最大值与调整当前节点Heap[size + 1] = -2e9; // 放一个最小值在这里防止调整最后一个父节点时出现问题for (int i = size >> 1; i; i--) {cur = i; // 不断调整while (cur <= (size >> 1)) {// 比较自己、左孩子和右孩子哪一个更大maxn = Heap[cur];if (maxn < Heap[cur << 1]) maxn = Heap[cur << 1];if (maxn < Heap[cur << 1 | 1]) maxn = Heap[cur << 1 | 1];if (maxn == Heap[cur]) {// 当前节点最大,说明调整结束break; } else if (maxn == Heap[cur << 1]) {// 左孩子更大,与左孩子交换并继续调整swap(Heap[cur], Heap[cur << 1]);cur = cur << 1;} else {// 右孩子更大,与右孩子交换并继续调整swap(Heap[cur], Heap[cur << 1 | 1]);cur = cur << 1 | 1;}}}
}
另外,提供一个省时间复杂度的方法:如果你要插入多个元素再进行操作,你可以先把这些元素给插入进去后执行构建操作,这样就能省略一些时间复杂度,也就是拿到元素就插入时间复杂度 Θ(nlogn)\Theta(n\log n)Θ(nlogn),但是插入后再构建堆时间复杂度只有 Θ(n)\Theta(n)Θ(n)
1.2 插入操作
这个操作可以先将元素放在堆最后再进行调整,时间复杂度最差也是 Θ(logn)\Theta(\log n)Θ(logn),但最优是 Θ(1)\Theta(1)Θ(1)
// Heap 是堆数组,size 数组原来的大小,x 是要插入的元素,下标从 1 开始
void Insert (int* Heap, int& size, const int x) {// 插入数字Heap[++size] = x; // size 是引用参数,原数会跟着增加Heap[size + 1] = -2e9; // 也做一个保险// 调整数字位置。遍历到 1 时就代表遍历到根节点了,根节点没有父节点for (int i = size; i != 1; i >>= 1) { if (Heap[i] > Heap[i >> 1]) swap(Heap[i], Heap[i >> 1]); // 比父节点大,与父节点交换 else break; // 比父节点小,调整结束}
}
1.3 删除堆顶操作
这个操作将最后一个元素放在第一个元素上再删除,时间复杂度不管咋样都是 Θ(logn)\Theta(\log n)Θ(logn)
注意细节:没有元素的时候删除会 RE,但是函数不管空不空
// Heap 是堆数组,size 是数组原来的大小,下标从 1 开始
void Erase (int* Heap, int& size) {// 删除元素Heap[1] = Heap[size]; // 替换数字Heap[size--] = -2e9; // 还是防访问过界的保险// 调整int maxn, cur = 1;while (cur <= (size >> 1)) {// 比较自己、左孩子和右孩子哪一个更大maxn = Heap[cur];if (maxn < Heap[cur << 1]) maxn = Heap[cur << 1];if (maxn < Heap[cur << 1 | 1]) maxn = Heap[cur << 1 | 1];if (maxn == Heap[cur]) {// 当前节点最大,说明调整结束break; } else if (maxn == Heap[cur << 1]) {// 左孩子更大,与左孩子交换并继续调整swap(Heap[cur], Heap[cur << 1]);cur = cur << 1;} else {// 右孩子更大,与右孩子交换并继续调整swap(Heap[cur], Heap[cur << 1 | 1]);cur = cur << 1 | 1;}}
}
1.4 获取堆顶
这个操作应该是最简单的
这个也一样,没有元素的时候操作会 RE
// Heap 是堆(这里根本不需要 size)
int HeapTop (int* Heap) {return Heap[1];
}
2. 优先队列 std::priority_queue
std::priority_queue
是一个 STL 容器,位于 C++ 的 <queue>
库中,是一个专门用于插入、删除最大值、查找最大值的容器,容器一般配合 std::vector
容器使用,且可以加比较器,使代码更加的直观
2.1 容器的定义
template<typename _Tp, typename _Sequence = vector<_Tp>,typename _Compare = less<typename _Sequence::value_type> >
class priority_queue
{...
}
我们看他的底层我们会发现,第一项填一个类型,第二项填用于访问的容器(默认 std::vector
),第三项是比较器(默认 std::less
比较器,大顶堆)
因此,使用 std::priority_queue
库时,应该一同写入三个头文件:<queue>
,<vector>
和 <functional>
当然万能头 <bits/stdc++.h>
最好
那么,我们应该如何定义呢?
下面是三种最基本的定义方法
std::priority_queue<int> PQ1; // 定义一个大顶堆 PQ1
std::priority_queue<int, std::vector<int>, std::less<int> > PQ2; // 还是定义一个大顶堆 PQ2
std::priority_queue<int, std::vector<int>, std::greater<int> > PQ3; // 定义一个大顶堆 PQ3
*注意比较器反着写,你可以理解为将一堆数按照比较器排序后的最后一个数字就是堆顶
然后,我们也可以自定义比较器
第一种方法: 定义一个结构体并重载 ()
为比较器
struct Cmp {bool operator() (类型 _Left, 类型 _Right) {return 比较器; // 和排序时的自定义比较器一样}
};std::priority_queue<类型, std::vector<类型>, Cmp> PQ4;
第二种方法: 定义一个新的元素类型并重载其 <
(std::less
会根据元素比较器来正着判断,而 std::greater
会反着)
struct Node {// 元素列表int x; // 比较器bool operator< (const Node& Rt) const { // 这是一个关键,啥都不能省return x % 10 > Rt.x % 10; }
};std::priority_queue<Node> PQ5; // 定义一个优先队列 PQ5,按照个位升序排序
2.2 容器中的函数
函数 | 作用 | 时间复杂度 |
---|---|---|
push(x) | 插入元素 xxx | Θ(logn)\Theta(\log n)Θ(logn) |
pop() | 删除最大的元素 | Θ(logn)\Theta(\log n)Θ(logn) |
top() | 返回最大的元素 | Θ(1)\Theta(1)Θ(1) |
size() | 返回元素大小 | Θ(1)\Theta(1)Θ(1) |
empty() | 返回元素是否为空 | Θ(1)\Theta(1)Θ(1) |
*函数中未提供 clear() 函数,但是可以使用重构的方法
如何重构?
我们可以将其赋值为自己定义时的类型后面加上个()
,例如PQ
在定义时类型为std::priority_queue<int>
,那么重构就可以PQ = std::priority_queue<int>();
这种方式适用于所有的变量定义
3. 与堆有关的函数
<algorithm>
中有一批与堆有关的函数,效率不仅比一般的优先队列高,有的时候还能超越手写堆,非常的厉害
它们可以在一个支持随机访问的数组里面进行,例如一维静态数组、一维动态数组
3.1 std::make_heap(Literator, Riterator, Cmp)
std::make_heap(Literator, Riterator, Cmp)
用于将一个顺序表做成一个二叉堆数组,以方便进行插入、删除、查询等操作。它的第一项是数组首地址,第二项是数组尾地址(最后一个数字的下一项所指向的地址),第三项是比较器,填法和 std::sort(Literator, Riterator, Cmp)
与 std::stable_sort(Literator, Riterator, Cmp)
的填法一样,放一个布尔类型的比较器函数进去
3.2 std::is_heap(Literator, Riterator, Cmp)
std::is_heap(Literator, Riterator, Cmp)
的参数填法相同,函数用于检测一个顺序表的某个部分是否是一个二叉堆,如果是返回 111,否则返回 000
3.3 std::is_heap_until(Literator, Riterator, Cmp)
std::is_heap_until(Literator, Riterator, Cmp)
的参数填法相同,函数返回顺序表的某个部分第一处不遵守二叉堆性质的地方,如果都遵守返回 Riterator
3.4 std::push_heap(Literator, Riterator, Cmp)
std::push_heap(Literator, Riterator, Cmp)
的参数填法相同,用于将一个二叉堆数组的最后一项进行维护,也就是插入元素到末尾后继续维护元素的过程。插入元素需要你自己来实现,不归它管
3.5 std::pop_heap(Literator, Riterator, Cmp)
std::pop_heap(Literator, Riterator, Cmp)
的参数填法相同,用于将一个二叉堆数组的最后一项替换到第一项,然后对堆进行维护,也就是对二叉堆删除的一个维护,但是是否真正删除元素还是伪删除它不管,由你自己来定
3.6 std::sort_heap(Literator, Riterator, Cmp)
这个函数用于将一个二叉堆数组进行堆排序从而使二叉堆数组变成一个单调数组(最终排出来的比较器就是 Cmp
)