priority_queue 的底层结构
我们已经学习过栈和队列了,他们都是用一种容器适配出来的。今天我们要学习的 prority_queue
也是一个容器适配器。在 priority_queue
的使用部分我们已经知道想要适配出 priority_queue
,这个底层的容器必须有以下接口:
- empty():检测容器是否为空。
- size():返回容器中有效元素个数。
- front():返回容器中第一个元素的引用。
- push_back():在容器尾部插入元素。
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
在学习 C 语言的时候,我们已经学习过堆这种数据结构了,当时我们使用的是用数组作为堆的底层数据结构,因此,我们模拟实现 priority_queue
会使用 vector
作为 priority_queue
的底层数据结构。当然 deque
也是没有问题的,但是不及 vector
高效。因为我们会大量使用 operator[]
,对比下来我们会倾向于选择 vector
来模拟实现 priority_queue
。
priority_queue 的基本结构实现
实现 priority_queue
的大体思路和模拟实现 stack
和 queue
差不多。priority_queue
涉及更多的算法和细节处理。
#pragma once
namespace Tchey
{
template<class T, class Container = vector<T>>
class priority_queue
{
public:
private:
Container _con;
};
}
但是我们这样定义出来的 priority_queue
的模板类好像不对啊!因为库里面的 priority_queue
想要构建小堆的时候,需要传递三个模板参数哒:存储 int 类型的小堆:priority_queue<int, vector<int>, greater<int>> heap
。这个greater<int>
是个什么东西呢?
这个 greater<int>
就是一个模板类 priority_queue
能够实例化出来大堆和小堆的关键所在!
仿函数
看他的结构,greater<int>
应该也是一个模板类。在这个类的里面,重载了圆括号运算符,即:operator()
,使得一个类的对象,能够像函数那样调用。这个东东就叫做仿函数。我们来看一个简单的例子吧:
struct GetLessNum
{
int operator()(const int& a, const int& b)
{
return a < b ? a : b;
}
};
int main()
{
GetLessNum getLessNum;
int a = 10, b = 5;
cout << getLessNum(a, b) << endl; //输出:5
return 0;
}
在上面的例子中,我们定义了一个 GetLessNum
的类,类中重载了圆括号运算符。在 main
函数中,实例化出了一个对象,通过对象调用 operator()
从而通过类对象实现了函数调用的效果,这个就是仿函数啦!
揭秘 greater<int>
同样地,在 greater
这个模板类中,也重载了圆括号运算符。当我们构建大堆的时候,没有传入后两个模板参数,可见是给了缺省值。在 C 语言阶段,我们已经学习了堆,知道了构建大堆与小堆的区别:仅仅是在向上调整算法,和向下调整算法中的比较逻辑上不同,其余均是相同的。
C语言数据结构初阶(11)----堆_姬如祎的博客-CSDN博客
于是我们就可用通过模板参数来控制 priority_queue
中两个算法实现的比较逻辑!这样就能够实现根据传入模板参数的不同,构建出来不同的堆了!
namespace Tchey
{
template<class T>
struct less
{
bool operator()(const T& e1, const T& e2)
{
return e1 < e2;
}
};
template<class T>
struct greater
{
bool operator()(const T& e1, const T& e2)
{
return e1 > e2;
}
};
}
在使用 priority_queue
的 push
函数的时候,就会使用向上调整算法:对于大堆,如果子节点的值大于父节点的值,那么就需要交换两个节点的值,对于小堆;如果子节点的值小于父节点的值,那么就需要交换两个节点的值。
于是我们就可以通过传入的第三个模板参数来控制:如果传入 less<T>
,刚好 less<T>
中的 opertor()
是小于的比较逻辑,就是小堆的向上调整算法;如果传入 greater<T>
,刚好 greater<T>
中的 opertor()
是大于的比较逻辑,就是大堆的向上调整算法。
通过模板参数的控制,priority_queue
同时能够实现大堆和小堆,而不用单独写大堆和小堆。
priority_queue
的基本结构就可以这么写:
namespace Tchey
{
template<class T>
struct less
{
bool operator()(const T& e1, const T& e2)
{
return e1 < e2;
}
};
template<class T>
struct greater
{
bool operator()(const T& e1, const T& e2)
{
return e1 > e2;
}
};
template<class T, class Container = vector<T>, class Cmp = Less<T>>
class priority_queue
{
public:
private:
Container _con;
};
}
priority_queue
的函数实现
向上调整算法
我们在向一个堆插入数据的时候就会用到向上调整算法:就拿大堆来说,你向大堆里面插入了一个数据,自然是要通过调整,使得插入的数据和原来的大堆重新形成一个新的大堆!
在这个插入的例子中,插入节点 9,不妨命名为 child。他的父节点 6,不妨命名为 parent。child 大于parent,交换两个节点的值。然后更新 child 和 parent,此时 child 依然大于 parent 交换两个节点的值。再次更新 child 和 parent。发现 parent < 0,结束循环。或者在比较的过程中 child < parent 也要结束循环。
这就是大堆的向上调整算法,至于小堆,比较逻辑反过来就行。
想要 priority_queue
根据传入的参数来决定是大堆的比较逻辑还是小堆的比较逻辑,这里就不能将比较逻辑写死,而是根据仿函数来比较!
void AdjustUp(int child)
{
Cmp cmp; //根据第三个模板参数的类,实例化出来一个对象
int parent = (child - 1) / 2; //根据child 找到 parent
while(child)
{
if(cmp(_con[parent], _con[child])) //调用 operator() 来比较
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else //不满足条件直接退出循环
break;
}
}
向下调整算法
向下调整算法在 pop
函数中使用哈!pop
的逻辑就是将堆顶的数据与堆中最后一个数据交换,然后对下标为 0 的元素来一次向下调整算法,就满足堆的要求啦!
我们来看上面的例子:这是一个大堆,删除堆顶的元素:我们先将堆顶的 7 和堆的最后一个数据 0 交换。然后对下标为 0 的元素,不妨命名为 parent。向下调整的逻辑是:选择 parent
的左右孩子中较大的那个孩子,不妨命名为 child,然后与 parent 进行比较,如果 child 大于 parent,那么交换 parent 和 child 两个节点。反之结束向下调整算法的逻辑。如果当 child 大于等于 vector
的 size 也要结束循环。
同向上调整算法,如果是小堆的话,只是比较逻辑不相同,其他的步骤均是一样的!因此向下调整算法 child 与 parent 的比较不能使用大于小于符号将比较逻辑写死。而是要使用仿函数来实现比较逻辑。
void AdjustDown(int parent)
{
Cmp cmp; //实例化仿函数的对象
int n = _con.size(); //vector 的大小
int child = parent * 2; //通过 parent 找到 child
while(child < n)
{
//选择左右孩子中较大的那个或者较小的那个,取决于第三个模板参数的传值
if(child + 1 < n && cmp(_con[child], _con[child + 1]))
child++;
//如果满足条件,交换
if(cmp(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2;
}
else //不满足直接退出
break;
}
}
void push(const T& val)
当你完成了向上调整算法与向下调整算法的书写,堆的接口实现就是信手拈来哈!push
函数是向堆中插入如一个元素,我们只需要在 vector
中 push_back
一个元素,然后使用向上调整算法就可以啦!
void push(const T& val)
{
_con.push_back(val);
AdjustUp(_con.size() - 1);
}
void pop()
删除堆顶的元素只需要我们将 vector
中下标为 0 的元素与 vector
中的最后一个元素交换位置,然后调用 pop_back()
接口,最后对下标为 0 的元素使用一次向下调整算法就可以啦!
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
bool empty()
我们只需要返回 vector
是否为空就行啦!
void empty()
{
return _con.empty();
}
size_t size()
同样地,我们只需要返回 vector 的 size 就可以啦!
size_t size()
{
return _con.size();
}
T& top()
返回堆顶的元素就是返回 vector
中下标为 0 的元素。
void top()
{
assert(_con.size());
return _con[0];
}