一、引言
首先堆是一种特殊的树结构。你就当让他是树得了就行。
堆就是父节点与子节点存在特定的关系。
比如大顶堆小顶堆,你这听名字就知道了。
二、二叉堆
可以把堆类比成类,而二叉堆就是其的一个派生类(还有很多什么斐波那契堆等等……)
很常用。
这里讲讲二叉堆的性质:
1、首先他是个完全二叉树
下面简单思考一下实现
首先得有个底层的数据结构用来存储每个节点的数据
要么用链式结构 要么数组
为了方便我们这里用数组会比较方便。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template<typename T>
//小顶堆
class Myheap
{
public:
Myheap(int capacity) : size(0)
{
heap.resize(capacity);
} //构造函数,初始化堆的大小为0
int getsize() const { return size; } //获取堆的大小
int peak()
{
return heap[0]; // 返回堆顶元素
}
void push(T value) //插入元素,时间复杂度O(logn),因为是跟层数相关
{
heap[size] = value; //将元素插入到堆的末尾
swim(size); //向上调整堆
size++; //堆的大小加1
}
T pop() //删除堆顶元素,时间复杂度O(logn),因为是跟层数相关t
{
T value = heap[0]; //保存堆顶元素
heap[0] = heap[size - 1]; //将堆的末尾元素放到堆顶
size--; //堆的大小减1
sink(0); //向下调整堆
return value; //返回堆顶元素
}
private://私有成员
vector<T> heap; // 存储堆的数组
int size; // 堆的大小
static int parent(int i) { return (i - 1) / 2; } // 获取父节点的索引
static int left(int i) { return 2 * i + 1; } // 获取左子节点的索引
static int right(int i) { return 2 * i + 2; } // 获取右子节点的索引
// 向上调整堆
void swim(int i) //时间复杂度O(logn),因为是跟层数相关
{
while(i > 0 && heap[parent(i)] > heap[i]) // 如果当前节点比父节点小
{
swap(heap[i], heap[parent(i)]); // 交换当前节点和父节点
i = parent(i); // 更新当前节点的索引为父节点的索引
}
}
void sink(int i) //时间复杂度O(logn),因为是跟层数相关
{
// 这里要看一下,我往下走,我要跟左右子节点比较,哪个小,我就跟哪个交换,直到我比左右子节点都小,或者我到了叶子节点
while (condition)
{
/* code */
temp = i;
if(left(i) < size&& heap[left(temp)]<heap[temp])
{
temp = left(temp);
} // 如果有左子节点
if(right(i) < size&& heap[right(temp)]<heap[temp])
{
temp = right(temp);
} // 如果有右子节点
if(temp == i) break; // 如果没有子节点
swap(heap[i], heap[temp]); // 交换当前节点和子节点
i = temp; // 更新当前节点的索引为子节点的索引
}
}
}
这里以小顶堆为例啊,大顶堆改一下大于号就行
私有成员:我内部自己维护所需要的东西
一个vector保存数据
三个静态成员函数 计算父节点,左右子节点的索引
然后就是最关键的上浮和下沉了
上浮就是我当前节点和父节点比啊,他要是比我大我就网上走啊。
所以具体实现就是
计算父节点索引,根据索引获得父节点的值
比较我的值和父节点的值,判断他大于我
那么交换两索引对应的值,然后我的索引变成父节点的索引。
void swim(int i) //时间复杂度O(logn),因为是跟层数相关
{
while(i > 0 && heap[parent(i)] > heap[i]) // 如果当前节点比父节点小
{
swap(heap[i], heap[parent(i)]); // 交换当前节点和父节点
i = parent(i); // 更新当前节点的索引为父节点的索引
}
}
上浮的循环终止条件就是 要么我到顶了,要么我的父节点值小于我的值
下沉也是
还是得有个循环因为得一直下沉
那么循环条件就很简单,要么是我到底了,要么是我已经是我和我的子节点比最小的值了。
那么下沉就要比对我和左右子节点的值。
所以还是找谁最小
先和左边比,更新最小的(带上左节点存在)
在和右边比(带上有节点存在)
如果发现上面两个更新完还是我最小,或者我根本就没有子节点 那就结束啦,退出循环
暴露给外部的接口:
插入元素:很简单,查到最后,自己往上排吧。
弹出第一个元素:这个就很有巧思了
第一个元素出去后,按理来说,应该让整体都往前移一个
这样复杂度就是0(N);很麻烦
那么存不存在更合适的呢?
我们已知的是,只有顶元素为空,其他都是复合堆结构的啊,感觉其实不需要动啊!
那要不随便找一个元素替代一下这原本的顶元素然后让他自己下沉好了。
诶这就把最后一个元素拉过来,移到头,然后让他下沉。
三、优先级队列
上面我们看完了二叉堆的性质,你会发现我创建一个二叉堆后它自然会让整体保持二叉堆的性质,不管你有没有增加元素或者删减元素他都可以维持二叉堆的性质。
那么利用二叉堆的性质——就可以实现优先级队列。优先级队列是二叉堆的一个应用。
代码其实和上面差不多。这里就不写了。我们直接来看STL封装好的优先级队列给我们提供的接口
1.头文件与定义
#include <queue>
priority_queue<typename, container, functional>
typename是数据的类型;
container是容器类型,可以是vector,queue等用数组实现的容器,不能是list,默认可以用vector;
functional是比较的方式,默认是大顶堆(就是元素值越大,优先级越高);如果使用C++基本数据类型,可以直接使用自带的less和greater这两个仿函数(默认使用的是less,就是构造大顶堆,元素小于当前节点时下沉)。使用自定义的数据类型的时候,可以重写比较函数,也可以进行运算符重载(less重载小于“<”运算符,构造大顶堆;greater重载大于“>”运算符,构造小顶堆)。
//构造一个大顶堆,堆中小于当前节点的元素需要下沉,因此使用less
priority_queue<int, vector<int>, less<int>> priQueMaxFirst;
//构造一个小顶堆,堆中大于当前节点的元素需要下沉,因此使用greater
priority_queue<string, vector<string>, greater<string>> priQueMinFirst;
还可以通过仿函数来支持自定义数据类型
通过运算符重载支持自定义比较函数
2.基本操作
push():入队,无返回值
pop():将优先级最高的弹出来,无返回值
top():获取优先级最高的
size():队列大小,size_t
enpty():bool
基本用法和队列都差不多,没啥区别