二叉堆

在介绍堆之前, 先看一下一些概念
完全二叉树(Complete Binary Tree)
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
维基百科的定义
树的深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
树的高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
二叉堆
二叉堆是一个数组, 可以看成近似的完全二叉树,树上的每一节点对应数组中的一个数,除最低层外,该树是完全充满的,且从左往右填充
下标计算
由于数组下标从 0 开始,故处理方式:将把索引从 0 开始转化成从 1 开始,然后算出结果 x,则实际索引为 x-1
// 先加1, 再减1
left = ((i+1)x2-1
right = leftChild-1
parent = (i+1)/2-1
各个操作时间复杂度
1. keep_heap: 时间复杂度O(lgn),维护最大堆性质的关键
2. build_heap: 时间复杂度O(n), 作用是将一个无序数组建立成一个堆
3. heap_sort: 时间复杂度O(nlgn),对一个数组原址排序
4. pop_heap, push_heap: 在堆中, 插入删除元素时间复杂度都是 O(lgn)
等比数列求和公式
如果等比数列的公比为 \(q\), 等比数列的和为 \(S_{n}=a_{1}+a_{2}+a_{3}+\cdots +a_{n}\), 求和如下
\[{S_{n}=
\begin{cases}{
\frac {a_{1}-a_{n}q}{1-q}}&q\neq 1\\
na_{1}&q=1\end{cases} }
\]
深度为 h 的满二叉树的节点个数为
\[\begin{align*}
S(n) &= 2^0 + 2^1+2^2 \cdots+2^h \\
&=\frac{1-2^{h}\cdot 2}{1-2} \\
&= 2^{(h+1)}-1
\end{align*}
\]

简单代码实现二叉堆
#include<iostream>
#include<vector>
#include <map>
using namespace std;
// 这里操作的是大顶堆
// keep_heap 维持堆的性质, 时间复杂度 O(lgn)
// 1. 输入数组 nums 和下标 parent,假设左右孩子节点 left 和 right 的二叉树都是最大堆
// 2. nums[parent] 可能小于其孩子,这时违反了最大堆的性质
// 3. keep_heap 的作用就是让 A[parent] 在最大堆里逐级下降,
// 最后使得每一个子堆都满足最大堆的性质
void keep_heap(vector<int> &nums, int parent){
int left = (parent+1)*2-1;
int right = left + 1;
int largest = parent;
if (left < nums.size() && nums[left] > nums[largest]) largest = left;
if (right < nums.size() && nums[right] > nums[largest]) largest = right;
if (largest != parent) {
swap(nums[largest], nums[parent]);
keep_heap(nums, largest);
}
}
// 时间复杂度为线性时间复杂度 O(n)
// 证明: 当用数组表示存储 n 个元素的堆时, 叶节点的下标分别是 floor(n/2)+1, floor(n/2)+2, ..., n
// 我们知道,堆是一个完全二叉树,最后一个节点 n 的父节点为 parent = floor(n/2) (索引从 1 开始);
// 假设父节点的右兄弟存在, 那么父节点的右兄弟 (parent+1) 的左孩子节点为 2x(floor(n/2)+1);
// 当 n 为偶数时, floor(n/2) > (n-1)/2;
// 当 n 为奇数时, floor(n/2) = (n-1)/2,
// 所以 floor(n/2) ≥ (n-1)/2
// 故 2x(floor(n/2)+1) ≥ 2x((n-1)/2+1) = n+1, 而 n 是最大的索引, 故假设不成立
// 与题设矛盾 所以说存储 n 个元素的堆的叶节点的下标分别是 floor(n/2)+1, floor(n/2)+2, ..., n
void build_heap(vector<int> &nums) {
for (int i = nums.size()/2-1; i >= 0; i--) {
keep_heap(nums, i);
}
}
// 顶端节点出队, 然后将最后一个元素放在顶部, 然后下滤
int pop_heap(vector<int> &nums) {
int answer = -1;
if (nums.size() == 0) {
cout << "heap is already empty!";
return answer;
}
answer = nums[0];
nums[0] = nums[nums.size()-1];
nums.pop_back();
keep_heap(nums, 0);
return answer;
}
// 将插入元素放在数组尾部, 然后上滤
void push_heap(vector<int> &nums, int val) {
nums.push_back(val);
int child = nums.size() - 1;
while(child > 0) {
// child > 0, father >= 0
int father = (child+1)/2-1;
if (nums[father] < nums[child]) {
swap(nums[father], nums[child]);
child = father;
} else {
break;
}
}
}
int main() {
vector<int> v = {4, 5 ,7, 10};
build_heap(v);
for (auto e : v) {
cout << e << " ";
}
cout << endl << endl;
int size = v.size();
for (int i = 0; i < size; i++) {
cout << pop_heap(v) << " ";
}
cout << endl << endl;
push_heap(v, 1);
push_heap(v, 5);
push_heap(v, 7);
size = int(v.size());
for (int i = 0; i < size; i++) {
cout << v[i] << " ";
}
cout << endl;
}
C++ 自带的与堆相关的函数
下面介绍 STL 中与堆相关的 4 个函数
1. 建立堆 make_heap(_First, _Last, _Comp)
默认是建立最大堆的。对int类型,可以在第三个参数传入greater()得到最小堆。
2. 在堆中添加数据 push_heap (_First, _Last)
要先在容器中加入数据,再调用 push_heap()
3. 在堆中删除数据 pop_heap(_First, _Last)**
要先调用 pop_heap() 再在容器中删除数据
4. 堆排序 sort_heap(_First, _Last)**
排序之后就不再是一个合法的heap了
#include <iostream>
#include <vector>
#include <map>
using namespace std;
int main() {
vector<int> vec = {2, 3, 5, 1, 34, 5};
make_heap(vec.begin(), vec.end(), greater<int>());
for (auto e : vec) {
cout << e << " ";
}
cout << endl;
// 不管是 push 还是 pop, 都要提供 comp 函数, 不然有错
// 1. 先在容器中加入元素, 然后调用push_heap进行调整
vec.push_back(10);
push_heap(vec.begin(), vec.end(), greater<int>());
for (auto e : vec) {
cout << e << " ";
}
cout << endl;
// 2. 删除容器元素, 先调用pop_heap, 将删除元素放到容器末尾, 然后删除
pop_heap(vec.begin(), vec.end(), greater<int>());
vec.pop_back();
for (auto e : vec) {
cout << e << " ";
}
return 0;
}