为了更好的理解优先级队列
priority_queue
,这里会同时进行栈和队列的提及
文章目录
简要概念(栈和队列)
- 栈:后进先出的结构,通常使用数组栈的形式
- 队列:先进先出的结构,通常使用链表式的队列
- 优先级队列:优先级队列是基于堆实现的一种数据结构。在 C++ STL 中,我们将实现的priority_queue 类就是通过堆来实现的。
- 这里不再对堆进行详细介绍,具体在这:堆详解
栈和队列的模拟实现与使用
stack(栈)
模拟实现
进行stack的实现前先介绍一下deque<T>
:
deque的理解和操作
概念(简单看看)
概念简单看看就行
deque
是 C++ STL 中的一种双端队列(double-ended queue),它允许在队列两端高效地添加、删除元素,同时支持随机访问。- 与vector的不同:deque 具有良好的内存分配策略,可以避免 vector 扩容时带来的大量元素复制操作。此外,deque 也具有更好的迭代器安全性,因为它不能像 vector 那样通过引起扩容而使得旧迭代器失效。
- deque 内部使用了一个中央控制器,管理了一系列固定大小的连续空间块,每个块内部存储多个元素。当 deque 需要增加或删除元素时,中央控制器会根据需要进行块的扩容或收缩。
操作(重要)
这里介绍deque
与stack && queue
的不同处,介绍 为什么要用deque实现栈和队列
- 插入和删除方式不同:栈只能在栈顶进行插入和删除元素,而队列只能在队尾插入,在队头删除;而 deque 可以在队列的两端都插入和删除元素。
- 访问方式不同:栈只能访问栈顶元素,队列只能访问队头元素;而 deque 可以随机访问任意位置的元素。
- 功能上不同:栈的主要功能是实现“后进先出”(LIFO),队列的主要功能是实现“先进先出”(FIFO),而 deque 不仅可以实现这两种功能,还能够满足双向遍历、支持随机插入和删除等操作。
继续对 stack 进行模拟实现:
这里需要注意的:
- 对于模板template: T 表示栈中元素的类型,Container 表示底层容器的类型,默认为
deque<T>
- 成员变量: _con 为Container类型,如果调用者不给Container类型,_con默认为deque,剩下的增删查改调用_con的操作函数即可
- 通过
push()
将元素依次尾插到 底层容器con 中; - 当
pop()
时,容器进行尾删;top()
则返回容器尾部元素,即此时的栈顶元素; - 相当于实现了栈的 后进先出
(LIFO)
的特性。
namespace aiyimu
{
//Container 表示用于存储队列元素的底层容器类型,默认值为 deque<T>
template<class T, class Container = deque<T>>
class stack
{
public:
void push(const T& x)
{
// 将元素x插入栈顶: 在底层容器尾部插入元素
_con.push_back(x);
}
void pop()
{
// 从栈顶弹出元素: 删除容器的最后元素
_con.pop_back();
}
T& top() // 非常量引用: 可修改返回值
{
return _con.back();
}
const T& top() const // 常量引用: 不可修改返回值
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con; // 底层容器类
};
}
queue
queue
的实现在接口类型上与stack
相同,具体在于相关实现的细节上:
push()
将元素尾插到容器中pop()
时,容器进行头删- 先插入的元素通过尾插到容器内部,pop头删将先插入的元素(在容器底部)删除掉。
back()
与front()
返回容器的尾部与头部元素即可
namespace aiyimu
{
template<class T, class Container = deque<T>>
class queue
{
public:
void push(const T& x)
{
// 将元素x插入队顶: 容器 尾插
_con.push_back(x);
}
void pop()
{
// 从队顶弹出元素: 容器 头删
_con.pop_front();
}
T& back()
{
// 返回队尾元素: 即容器尾部元素
return _con.back();
}
T& front()
{
// 返回队顶元素: 容器头部元素
return _con.front();
}
const T& back() const // 返回队列中最后一个元素的只读引用
{
return _con.back();
}
const T& front() const // 返回队列中第一个元素的只读引用
{
return _con.front();
}
bool empty() const // 检查队列是否为空
{
return _con.empty();
}
size_t size() const // 返回队列中元素个数
{
return _con.size();
}
private:
Container _con;
};
}
priority_queue(优先级队列)
概念
什么是优先级队列?
优先级队列(Priority Queue)
是一种特殊的队列,其中的元素具有优先级。在优先级队列中,元素按照优先级的顺序进行插入和删除操作,优先级最高的元素最先被删除(类似于堆的性质)。
优先级队列的常见实现有下面两种:
-
基于堆(Heap)的优先级队列:堆是一种特殊的二叉树结构,满足 堆属性,即父节点的优先级高于或等于其子节点。通过维护堆的性质,可以实现高效的插入和删除操作。插入与删除的时间复杂度均为
O(log n)
。(n为元素个数) -
基于有序数组或链表的优先级队列:使用有序数组或链表来存储元素,并保持元素按照优先级有序。插入操作时,根据优先级找到合适的位置插入元素。删除操作时,直接删除优先级最高的元素。插入操作的时间复杂度为
O(n)
,删除时间复杂度为O(1)
。
下面将实现的为 基于堆的优先级队列。
框架
Container
:用于存储元素的容器类型,默认为vector<T>
:可以通过指定不同的容器类型来实现不同的功能。Compare
:用于选择该堆为大堆还是小堆,默认为std::less
则为小堆std::less
: 是一个函数对象,用于比较两个参数的大小关系。重载了小于运算符,用于比较两个类型为 T 的对象的大小。当 a < b 时,std::less<T>()(a, b)
返回 true,否则返回 false。
template<class T, class Container = vector<T>, class Compare = std::less<T>>
class priority_queue
{
public:
// 默认构造
priority_queue()
{}
// 利用迭代器构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{}
// O(logN)
// 向上调整
void adjust_up(size_t child)
{}
//向下调整
void adjust_down(size_t parent)
{}
// 其余操作函数
void push(const T& x)
{}
void pop()
{}
const T& top()
{}
bool empty() const
{}
size_t size() const
{}
private:
Container _con;
};
具体实现
push()
- 插入操作即为堆的插入操作,所以执行_con.push_back()后进行向上调整(从尾部
_con.size()-1
进行调整)
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
adjust_up()(向上调整)
- 向上调整即堆heap的操作,具体思路在上文给到的堆详解中
- 主要对if语句中的内容进行解释:
(小堆)向上调整过程中,当父节点的值小于子节点时,交换父子节点
- 原始方法直接进行
<
判断:if (_con[parent] < _con[child])
- 但这种写法此时就锁定小堆了, 为了使调用者可以根据需要进行大堆小堆的更改
- 所以这里创建一个Compare对象com,将比较改写为:
if(com(_con[parent], _con[child]))
- 该写法当调用者不做更改时默认为小堆(std::less),当调用者给出std::greater(比较大于),此时的
priority_queue
就变为大堆
void adjust_up(size_t child)
{
Compare com; //
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if(com(_con[parent], _con[child]))// 默认为less,调用者可以自行指定
{
std::swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
pop()
- 交换堆顶和堆底的元素,删除堆顶元素,向下调整
void pop()
{
// 交换堆顶和堆底的元素,删除堆顶元素,向下调整
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
adjust_down()(向下调整)
- 向下调整需要注意的仍为
com(_con[child], _con[child + 1])
写法的意义 - 具体内容在adjust_up中已经提到
void adjust_down(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1; //先假设右孩子大
while (child < _con.size())
{
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if(com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
top()
- 返回堆顶元素,即_con[0]
const T& top()
{
return _con[0];
}
empty()
- bool类型:判断堆是否为空
bool empty() const
{
return _con.empty();
}
size()
- 返回堆大小(堆元素个数)
size_t size() const
{
return _con.size();
}
priority_queue 的 验证 / 使用
插入元素 && 打印priority_queue
// 头文件
void test_priority_queue1()
{
aiyimu::priority_queue<int> pq;
//priority_queue<int> pq;
// 插入元素
pq.push(3);
pq.push(2);
pq.push(5);
pq.push(9);
pq.push(4);
pq.push(6);
pq.push(1);
// 打印pq的所有元素
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
输出结果:
降序 / 升序
int arr[] = { 3,5,6,8,19,7,1,0,9,1,20,4,12 };
// 不传入Compare类型,默认less类型:小于,即降序
aiyimu::priority_queue<int> heap_less(arr, arr + sizeof(arr) / sizeof(arr[0]));
// 传入Compare类型,为greater类型:大于,即升序
aiyimu::priority_queue<int, vector<int>, greater<int>> heap_greater(arr, arr + sizeof(arr) / sizeof(arr[0]));
while (!heap_less.empty())
{
cout << heap_less.top() << " ";
heap_less.pop();
}
cout << endl;
while (!heap_greater.empty())
{
cout << heap_greater.top() << " ";
heap_greater.pop();
}
cout << endl;
输出结果:
由此可以看出,当使用
std::less
时堆为降序,std::greater
时堆为升序