【C++】优先级队列(容器适配器)
chian-ocean 2024-08-11 12:35:01 阅读 65
欢迎来到我的Blog,点击关注哦💕
前言
<code>string
vector
list
这种线性结构是最基础的存储结构,C++(STL
)container很好的帮助我们数据存储的问题。
容器适配器
介绍
容器适配器是C++标准模板库(STL
)中的一种设计模式,它允许将一个容器的接口转换为另一个接口,从而提供不同的操作和行为。容器适配器通常用于封装现有容器,以实现特定的数据结构特性,如栈(后进先出)、队列(先进先出)和优先队列(根据优先级排序)。
应用
栈(stack):栈是一种后进先出的数据结构,其操作包括入栈(push)、出栈(pop)、查看栈顶元素(top)等。栈适配器可以基于多种底层容器实现,如vector
、deque
或list
.
队列(queue):队列是一种先进先出的数据结构,其操作包括入队(push)、出队(pop)、查看队首元素(front)和查看队尾元素(back)。队列适配器同样可以基于deque
或list
实现,以适应不同的性能需求.
优先队列(priority_queue):优先队列是一种特殊的队列,它根据元素的优先级进行排序。其底层容器通常是vector
或deque
,并通过堆算法维护元素的优先级顺序。优先队列适配器提供了插入和删除具有最高优先级元素的操作.
双重结束队列(双端队列(deque
))
特点
双端操作效率:支持在两端进行快速的插入和删除操作。随机访问:可以通过索引直接访问容器中的元素。无需预先分配固定大小:与vector
不同,deque
不需要在创建时指定大小,它可以根据需要动态增长。内存分配策略:deque
不需要像vector
那样一次性分配大量内存,而是分散在内存中,这有助于减少内存碎片。
存储结构
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落 在了deque
的迭代器身上,因此deque
的迭代器设计就比较复杂,如下图所示:
<code>List 、vector
deque
对比
对比维度 | Vector | Deque | List |
---|---|---|---|
内存连续性 | 是 | 否 | 否 |
随机访问性能 | O(1) | O(1) 但可能不如Vector | O(n) |
插入/删除性能 | 非末尾O(n) | 两端O(1), 中间O(n) | 两端及中间O(1) |
内存重用效率 | 扩容时需移动元素 | 两端添加删除不需移动 | 不适用 |
内存分配模式 | 动态数组,连续内存 | 分段连续内存 | 非连续内存 |
迭代器失效 | 可能 | 不会 | 不会 |
支持的操作 | [] 访问、.at() 等 | [] 访问、.at() 等 | [] 访问、.at() 等 |
内存管理开销 | 高(扩容时) | 中等(两端操作) | 低 |
适用场景 | 需要快速随机访问且元素数量稳定 | 需要两端快速插入删除,随机访问需求适中 | 频繁插入删除,不关心随机访问 |
栈(stack)
栈的介绍
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
栈的模拟实现
利用容器适配器的设计原理,很容易实现栈
将栈放mystack
的命名空间,以防止和库中冲突类模板设计container
可以给缺省参数,默认deque
(容器适配器)在里面利用deque
的接口实现
namespace mystack
{ -- -->
template<class T, class Container = std::deque<T>>
class stack
{
public:
void push_back(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
size_t size()
{
return _con.size();
}
T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
队列
队列介绍
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
队列模拟实现
将栈放myqueue
的命名空间,以防止和库中冲突类模板设计container
可以给缺省参数,默认deque
(容器适配器)在里面利用deque
的接口实现
namespace myqueue
{
template<class T, class Container = std::deque<T >>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
size_t size()
{
return _con.size();
}
T& front()
{
return _con.front();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
优先级队列(priority_queue)
基本原理
优先级队列通常在内部使用堆数据结构来维护元素的优先级。堆是一种完全二叉树,可以是最大堆或最小堆。在最大堆中,父节点的值总是大于或等于其子节点的值,而在最小堆中,父节点的值总是小于或等于其子节点的值。插入操作通过在堆的适当位置插入新元素并进行上调整(heapify-up
)来维持堆的性质。删除操作则涉及到移除堆顶元素(优先级最高的元素)并进行下调整(heapify-down
),以恢复堆的结构。
priority_queue
介绍
函数声明 | 接口说明 |
---|---|
priority_queue()/priority_queue(first, last) | 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回 false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
优先级模拟实现 (可以参考)
仿函数
仿函数(Functor)是C++中的一个编程概念,它指的是一个类或结构体,通过重载函数调用运算符operator()
,使得这个类或结构体的对象可以像函数一样被调用。仿函数可以包含状态,因为它们是对象,可以在构造函数中初始化状态,并在operator()
中使用该状态。仿函数可以作为参数传递给其他函数,包括STL
算法中的函数,从而提供灵活的编程模型.
这个就是一个仿函数
//小于
template<class T>
class Less
{
public:
bool operator()(const T& a,const T& b)
{
return a < b;
}
};
//大于
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
priority_queue
两个关键
向下建堆
确定起始点:从最后一个非叶子节点开始向下建堆,这个节点也被称为堆的最后一个非叶子节点。在完全二叉树中,最后一个非叶子节点的索引可以通过 (n - 1 - 1) / 2
计算得到,其中 n
是数组的长度。执行向下调整:对每个非叶子节点执行向下调整操作,确保该节点与其子节点组成的子树满足堆的性质。向下调整的过程涉及到与子节点的比较和必要时的交换,直至到达堆的顶部或直到父节点不再违反堆的性质。迭代过程:从最后一个非叶子节点开始,逐步向上调整,直到根节点。每次调整后,更新当前节点的索引,以便进行下一次调整。完成建堆:重复步骤2和步骤3,直到根节点也满足堆的性质,此时整个数组就构建成了一个堆。
void AdjustDown(size_t parent)
{
compare com;//仿函数
size_t child = parent * 2 + 1;
//if (child+1< _con.size() && _con[child] < _con[1+child])
if (child + 1 < _con.size() && com(_con[child] ,_con[1 + child]))//和上面等价
{
++child;
}
while (child <_con.size())
{
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向上建堆
初始化堆大小**:设置堆的大小为数组的大小,即 n
。从最后一个非叶子节点开始向上调整:在完全二叉树中,最后一个非叶子节点的索引为 floor((n - 1) / 2)
。从这个节点开始向上调整,确保每个节点都满足大根堆的性质。执行向上调整操作:对于每个非叶子节点,检查其与子节点的关系,并进行必要的交换,以确保父节点的值大于或等于其子节点的值。如果子节点中有一个或两个,选择较大的子节点与父节点进行比较。如果父节点的值小于子节点的值,交换它们的位置,并重新设置父节点为当前子节点,继续向上调整。重复步骤2和3:直到达到根节点,即堆的第一个元素。
void AdjustUp(int child)
{
compare com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent] , _con[child]))
{
std::swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
初始化数据
迭代器初始化
模板嵌套给,迭代器初始化
依次push
数据,在进行堆的建立
template<class InputIterator>
void push_back(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
//向下建堆
for (int i = (_con.size() - 2) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
pop数据
将一个个数据和最后一个数据进行交换(目的:保持当前堆的结构)pop
出数据,将第一个数据进行向下调整
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
push数据
将数据进行尾插入,进行向上调整
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
priority_queue operators
top数据,返回首个数据;其他常见操作,采取容器适配器设计模式的操作
namespace mypriority_queue
{
template<class T, class container = std::vector<T>,class compare = Less<T>>
class priority_queue
{
public:
const T& top()
{
return _con[0];
}
size_t szie()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}
源码(优先级队列)
namespace mypriority_queue
{
template<class T>
class Less
{
public:
bool operator()(const T& a,const T& b)
{
return a < b;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T, class container = std::vector<T>,class compare = Less<T>>
class priority_queue
{
void AdjustDown(size_t parent)
{
compare com;
size_t child = parent * 2 + 1;
//if (child+1< _con.size() && _con[child] < _con[1+child])
if (child + 1 < _con.size() && com(_con[child] ,_con[1 + child]))
{
++child;
}
while (child <_con.size())
{
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
compare com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent] , _con[child]))
{
std::swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
public:
template<class InputIterator>
void push_back(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
//向下建堆
for (int i = (_con.size() - 2) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
size_t szie()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}
向下建堆
for (int i = (_con.size() - 2) / 2; i >= 0; i–)
{
AdjustDown(i);
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top()
{
return _con[0];
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
size_t szie()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
container _con;
};
}
上一篇: FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
下一篇: yolov8n 瑞芯微RKNN、地平线Horizon芯片部署、TensorRT部署,部署工程难度小、模型推理速度快
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。