堆(C++实现)
二叉堆
二叉堆在结构上我们可以看成一棵“完全二叉树”,其分为:
- 大根堆(根 > 左右子树)
- 小根堆(根 < 左右子树)
其具有特性:①堆顶元素最大(或最小) ②父节点 > 子节点(或大于)
因为堆可以看作二叉树,所以在存储为容器形式时,我们有如下规律
- i节点的 左子树:2i,右子树:2i+1
- i节点的父节点:$\lfloor \frac{i}{2} \rfloor $
我们通常使用堆来排序,而这种排序是一种不稳定(即排序后,元素相对位置可能改变)的排序方式。
堆排序 空间复杂度(因为属于原地排序,不需要辅助空间):O(1)
堆排序 时间复杂度:
- 建堆(O(n) <= 4n);
- n-1次调整,每轮:(log n)
- 好&坏&平:O(nlogn)
以下以构建**小根堆**为例,大根堆的构造方式类似
BinaryHeap 基础框架
template <typename Comparable>
class BinaryHeap
{
public:
// 构造空堆
explicit BinaryHeap(int capacity = 100);
// 用vector 构造堆
explicit BinaryHeap(const vector<Comparable>& items);
// 判断heap是否为空
bool isEmpty() const;
// 访问最小元素
const Comparable& findMin() const;
// 插入新元素
void insert(const Comparable& x);
void insert(Comparable& x);
// 删除最小项
// 如果为空则抛出out_of_range异常
void deleteMin();
// 删除最小项并将其放在minItem处
// 若为空抛出异常
void deleteMin(Comparable& minItem);
// 将堆置为空
void makeEmpty();
private:
/* data */
int curSize; // 堆中元素个数
vector<Comparable> array; // 堆的数组
void bulidHeap(); // 构造堆
// 调整以hole位置为根节点的子树排序
void percolateDown(int hole);
};
注意:在array
中我们使用array[1]
位置作为堆的根节点,将array[0]
视作一个临时位,而在curSize存储堆中真正的有效元素,即array[1, curSize]
构建堆
步骤(使用vector items初始化):
1、初始设置 array.size() == items.size() + 10,cursize = items.size()
2、把items的元素从array[1]
开始全部填充到array
中
3、从cursize / 2
位置开始调整堆,不断往上,直到调整完根节点array[1]
3、堆的调整方式:
- 传入pos,将pos视作根节点(这是重点,也就是我们在遍历调整某个节点时,我们只关注往下。)
- 使用tmp缓存array[pos] 中的数据
- 比较tmp与当前pos位的较小子节点的大小【根据二叉树特性,我们可知子节点为 pos*2 or pos*2 +1 】
- 若 tmp > child ,array[pos] == array[child] ; pos位==child位; 往下循环
- 若 tmp <= child 直接跳出
- 最后将tmp 存放在pos位中。
代码如下
// 构造函数
explicit BinaryHeap(const vector<Comparable>& items) : array(items.size() + 10), curSize(static_cast<int>(items.size()))
{
for (int i = 0; i < items.size(); ++i)
array[i + 1] = items[i];
bulidHeap();
}
// 构建堆,使堆满足其特性
void bulidHeap()
{
// 从中间节点往根节点遍历
for (int i = curSize / 2; i > 0; --i)
percolateDown(i);
std::cout << "The Heap is bulid over ! " << std::endl;
}
// 调整以hole位置为根节点的子树排序
void percolateDown(int hole)
{
int child;
Comparable tmp = std::move(array[hole]);
for (; hole * 2 <= curSize; hole = child)
{
child = hole * 2;
// 要和更小那个子节点比较
if (child != curSize && array[child + 1] < array[child])
++child;
if (array[child] < tmp)
array[hole] = std::move(array[child]);
// 不会更小,不用再遍历子节点,直接跳出循环
else
break;
}
// 此处的hole记录原有元素应该存在的位置
array[hole] = std::move(tmp);
}
插入元素
1、传入一个待排序元素x,如果array
已经满了,需要扩容
2、cursize ++,然后把待排元素x 放置最后一个位置hole(这步是我们假设的,事实上现在x元素缓存在array[0]
)
3、从hole位置不断往上和父节点比较
- x > array[hole.father] ; 跳出循环
- x < array[hole.father] ; array[hole.father] 元素放到hole位置,hole = hole.father
4、最终的hole位是x实际应该插入的位置。
代码如下
void insert(const Comparable& x)
{
// 如果array的大小不够
if (curSize == array.size() - 1)
array.resize(array.size() * 2);
// 上滤,curSize + 1
int hole = ++curSize;
// 存储insert元素
Comparable copy = x;
array[0] = std::move(copy);
// 遍历其父节点,找到合适的位置
for (; x < array[hole / 2]; hole /= 2)
{
array[hole] = std::move(array[hole / 2]);
}
// 找到insert元素最合适的位置
array[hole] = std::move(array[0]);
}
访问最小(最大)元素
时间复杂度:O(1)
因为我们构建堆及插入元素时,已经让堆按照它的特性排序,所以我们只需要访问堆顶元素就可以得到最小(最大)元素
bool isEmpty() const { return curSize == 0; }
const Comparable& findMin() const
{
if (!isEmpty())
return array[1];
else
throw std::out_of_range("[Warning]the heap is empty!");
}
删除堆顶元素
1、用末尾元素覆盖堆顶元素,cursize–,以堆顶为根节点调整堆(方式与构造时相同)
代码如下:
void deleteMin()
{
if (isEmpty())
throw std::out_of_range("[Warning]the heap is empty!");
// 抛出顶端元素,把尾元素放到顶端
array[1] = std::move(array[curSize--]);
// 重新调整位置
percolateDown(1);
}
附:完整的BinaryHeap.h
#ifndef BINARYHEAP_H
#define BINARYHEAP_H
#include<vector>
#include<utility>
#include<stdexcept>
#include<iostream>
using std::vector;
template <typename Comparable>
class BinaryHeap
{
public:
explicit BinaryHeap(int capacity = 100):array(capacity + 1), curSize(0) {}
explicit BinaryHeap(const vector<Comparable>& items) : array(items.size() + 10), curSize(static_cast<int>(items.size()))
{
for (int i = 0; i < items.size(); ++i)
array[i + 1] = items[i];
bulidHeap();
}
bool isEmpty() const { return curSize == 0; }
const Comparable& findMin() const {
if (!isEmpty())
return array[1];
else
throw std::out_of_range("[Warning]the heap is empty!");
};
// 因为普通形参的const会忽略实参的顶层const,所以不用再重写非const的insert
void insert(const Comparable& x)
{
// 如果array的大小不够
if (curSize == array.size() - 1)
array.resize(array.size() * 2);
// 上滤,curSize + 1
int hole = ++curSize;
// 存储insert元素
Comparable copy = x;
array[0] = std::move(copy);
// 遍历其父节点,找到合适的位置
for (; x < array[hole / 2]; hole /= 2)
{
array[hole] = std::move(array[hole / 2]);
}
// 找到insert元素最合适的位置
array[hole] = std::move(array[0]);
}
// 删除最小项
// 如果为空则抛出out_of_range异常
void deleteMin()
{
if (isEmpty())
throw std::out_of_range("[Warning]the heap is empty!");
// 抛出顶端元素,把尾元素放到顶端
array[1] = std::move(array[curSize--]);
// 重新调整位置
percolateDown(1);
}
// 删除最小项并将其放在minItem处
// 若为空抛出异常
void deleteMin(Comparable& minItem)
{
if (isEmpty())
throw std::out_of_range("[Warning]the heap is empty!");
minItem = std::move(array[1]);
array[1] = std::move(array[curSize--]);
percolateDown(1);
}
void makeEmpty() {
curSize = 0;
}
private:
/* data */
int curSize; // 堆中元素个数
vector<Comparable> array; // 堆的数组
void bulidHeap()
{
for (int i = curSize / 2; i > 0; --i)
percolateDown(i);
std::cout << "The Heap is bulid over ! " << std::endl;
}
// 调整以hole位置为根节点的子树排序
void percolateDown(int hole)
{
int child;
Comparable tmp = std::move(array[hole]);
for (; hole * 2 <= curSize; hole = child)
{
child = hole * 2;
// 要和更小那个子节点比较
if (child != curSize && array[child + 1] < array[child])
++child;
if (array[child] < tmp)
array[hole] = std::move(array[child]);
// 不会更小,不用再遍历子节点,直接跳出循环
else
break;
}
// 此处的hole记录原有元素应该存在的位置
array[hole] = std::move(tmp);
}
};
#endif
进行测试
#include<iostream>
#include "BinaryHeap.h"
int main() {
std::vector<int> items = { 24, 13, 46, 25, 31, 19 };
BinaryHeap<int> biheap = BinaryHeap<int>(items);
const int x = 14;
biheap.insert(x);
biheap.deleteMin();
while (!biheap.isEmpty()) {
std::cout << biheap.findMin() << " ";
biheap.deleteMin();
} // 输出:14 19 24 25 31 46
std::cout << std::endl;
return 0;
}
参考:
[1] 《数据分析与算法分析——C++语言描述(第四版) by: Mark Allen Weiss