堆是一种完全二叉树。
- 最大堆:对树中的每个结点来说,其结点的值都大于等于其孩子结点的值。
- 最小堆:对树中的每个结点来说,其结点的值都小于等于其孩子结点的值。
- 特性:最大堆中的最大值在堆顶,最小堆中的最小值在堆顶。
堆中操作:
- 搜索堆顶元素。O(1)
- 添加新结点。先将新结点添加到最后的位置,然后将该新结点的值与其父节点的值相比较,在最大堆中如果新结点值比父结点值大(也即父节点值比新结点值小,不符合最大堆的定义了,需要调整),则交换父子结点的数据;按照此方式逐层向上调整,直到到达堆顶或者父节点值大于等于新节点值。O(log(n))
- 删除堆顶元素。在以vector为底层容器实现的最大堆中,堆顶元素并不直接删除,而是先将其(索引为0)与最后一个元素(索引为n-1)交换,然后堆的大小减一,vector中有效元素少一个,相当于删除了堆顶元素;然后将堆顶元素的值与其左右子结点元素的值相比,如果其左(右)孩子结点值在三者中最大,则交换根和左(右)孩子结点,然后递归调整左(右)孩子结点,直到到达叶子结点或者根节点值比左右孩子结点值都大。O(log(n))
最大堆(最小堆)常用优先队列(默认底层容器类是vector)实现。
最小堆实现
#include <iostream>
#include <vector>
using namespace std;
/**
* 以vector为底层容器的最小堆(完全二叉树)
* 有效元素从索引为0的位置开始存储,
* 索引为i的元素的左、右结点索引分别是2*i+1, 2*i+2,
* 索引为i的元素的父节点的索引是(i-1)/2,
* 最后一个非叶子节点索引为n/2-1。
*/
template <class DT>
class MinHeap{
private:
vector<DT> data; // 存储元素使用的线性表
int n; // data中有效元素个数
void heapAdjust(int k){
// 将data中索引位置在[0,k]的元素调整为最小堆
// 有效元素k+1个,第一个非叶子节点的位置是(k+1)/2-1
int node_idx = (k+1)/2-1;
while(node_idx >= 0){
adjustNode(node_idx);
//this->print();
node_idx --;
}
}
void adjustNode(int i){
// 调整索引i位置的结点,它一定有左孩子,不一定有右孩子
int lc_idx = 2 * i + 1; // 索引i的左子结点索引
int rc_idx = lc_idx + 1; // 索引i的右子结点索引
if(lc_idx >= n){
// 左孩子不存在,那右孩子一定不存在,无需调整了。
return;
}
else if(rc_idx >= n){
// 左孩子存在,右孩子不存在
if(data[i] < data[lc_idx]){
// 根节点值小于左子结点,无需调整
return;
}
else{
// 根节点值大于左子结点,交换根和左子结点
this->swap(i, lc_idx);
// 由于右孩子结点不存在,因此左孩子结点一定是叶子结点,无需递归调整左子节点
}
}
else{
// 左、右孩子都存在
if(data[i] < data[lc_idx] && data[i] < data[rc_idx]){
// 根结点是最小的,无需调整
return;
}
else if(data[lc_idx] < data[i] && data[lc_idx] < data[rc_idx]){
// 左子结点最小的,根与左子结点交换,然后递归调整左子结点
this->swap(i, lc_idx);
adjustNode(lc_idx);
}
else{
// 右子结点是最小的,根与右子结点交换,然后递归调整右子结点
this->swap(i, rc_idx);
adjustNode(rc_idx);
}
}
}
void swap(int i, int j){
// 交换索引位置在i,j处的元素
DT tmp = this->data[i];
this->data[i] = this->data[j];
this->data[j] = tmp;
}
void heapUp(int i){
// 新插入时调用,将索引为i的元素的值调整到正确的位置上
// 要求原来数据已经是最小堆
int root_idx = (i-1)/2;
if(root_idx < 0 || data[root_idx] < data[i]){
return;
}
else{
// 新结点值比根结点小
this->swap(root_idx, i);
this->heapUp(root_idx);
}
}
void heapDown(int i){
// 删除结点后调用
int lc_idx = 2 * i + 1; // i的左孩子索引
int rc_idx = lc_idx + 1; // i的右孩子索引
if(lc_idx >= this->n){
// 左孩子不存在,则右孩子肯定也不存在,无需调整
return;
}
else if(rc_idx >= this->n){
// 左孩子存在,右孩子不存在
if(data[i] < data[lc_idx]){
// 根节点值小于左孩子结点值,已经是最小堆,无需调整
return;
}
else{
// 根节点值大于左孩子结点值,需要交换根和左孩子
this->swap(i, lc_idx);
// 因为左孩子已经是叶子结点,没有子树了,不用递归调整
}
}
else{
// 左右子结点都存在
if(data[i] < data[lc_idx] && data[i] < data[rc_idx]){
// 根结点是三个中最小的,符合最小堆,无需调整了
return;
}
else if(data[lc_idx] < data[i] && data[lc_idx] < data[rc_idx]){
// 左孩子是三者中最小的,交换根和左孩子结点
this->swap(i, lc_idx);
// 递归向下调整左孩子结点
this->heapDown(lc_idx);
}
else{
// 右孩子是三者中最小的,交换根和右孩子结点
this->swap(i, rc_idx);
// 递归向下调整右孩子结点
this->heapDown(rc_idx);
}
}
}
public:
MinHeap():n(0){}
MinHeap(vector<DT> _data): data(_data), n(data.size()){
this->heapAdjust(n-1);
}
void push(DT x){
if(this->data.size() < n){
// data中有空余位置
this->data[n] = x;
}
else{
// data中每个位置都有元素,也即所有元素都是有效元素
this->data.push_back(x);
}
n += 1;
this->heapUp(n-1);
this->print();
}
void pop(){
if(this->empty()){
return;
}
this->swap(0, n-1);
n -= 1;
this->heapDown(0);
this->print();
}
DT& top(){
return data[0];
}
bool empty(){
return this->n <= 0;
}
void print() {
cout << "[";
if (n > 0) {
cout << this->data[0];
}
for (int i = 1; i < n; i++) {
cout << "," << this->data[i];
}
cout << "]" << endl;
}
};
int main() {
MinHeap<int> mh(vector<int>({7, 4, 3, 5, 9, 8, 1, 2}));
vector<int> nv({91, 14, 23, 41, 22, 11, 87, 90});
for(int v : nv){
mh.push(v);
}
while (!mh.empty()){
cout << mh.top() << " , ";
mh.pop();
}
cout << endl;
return 0;
}