• 【C++】stack | queue | priority_queue | deque


    目录

    一、stack栈

    介绍

    使用

    二、queue队列

    介绍

    使用

    三、priority_queue优先级队列

    介绍

    使用

    模拟实现

    ➡️复用代码

    ➡️push

    ➡️pop

    ➡️如何排降序

    仿函数

    用仿函数实现升降序

    模拟实现的总代码

    四、deque双端队列

    介绍

    底层实现

    关于deque的小结


    一、stack

    介绍

    1.栈是一种特殊的线性表,其元素遵循“后进先出”的原则,即仅允许在在表的一端进行插入、删除操作,这一模式被称为“后进先出”或LIFO(last in fisrt out)。

    2.从底层实现来看,stack是作为容器适配器被实现的,什么是容器适配器?我们来解释一下先。

    先来看看我们身边的适配器。比方说,你有注意到笔记本电脑的充电器吗?

    其实,笔记本的充电器就是一个适配器。适配器要做的就是电压的转换。一般来说,电脑的电池电压为14V左右,而标准电压为220V,要想给电脑充电,这就需要对标准电压进行转换,这就是适配器发挥的作用,适配就是转换的意思。

    适配器是一种设计模式,该模式将一个类的接口转换为用户希望的另一个类的接口,你可以认为适配器是一种转换器。

    容器适配器,能让程序员选择一种合适的底层数据结构。如stack,之前我们写c语言时,要用到栈的数据结构时,是不是得还先模拟一个栈出来?

    那现在就不用这么麻烦了,要用到栈和队列的地方,直接用就行

    3.stack的底层容器可以是 任何容器类模板 or 其他特定的容器类。

    这些容器类应该支持以下操作:

    empty:判空操作

    back:获取尾部元素操作

    push_back:尾部插入元素操作

    pop_back:尾部删除元素操作

    栈的底层实现(简易版):

    可以看到,栈的这些方法都是直接复用容器的方法,体现了代码复用的思想。

    这里的Container就是容器类,它可以是vector、deque、list,也可以省略不传。若省略,那默认情况下是deque类。

    使用

    使用时要包头文件。

    栈提供的方法有:判空、取大小、取栈顶数据、压栈(插入)、出栈(删除)、交换 还有emplace安放。

    这里对其中几点方法做个说明:

    1.构造函数:

    stack (<数据类型,容器类型>) stackName;

    数据类型一定要有,容器类型可以是vector、deque、list。(容器类型可省略,默认是deque)

    2.在用top()前,得判断下栈是否为空。只有不为空的时候,才能调用top(),不然会发生段错误。

    这是因为,在栈为空时,stack 的top函数返回的是超尾-1,而不是NULL。

    3.和上面的top一样,在调用pop函数前,也要确保栈中至少有一个元素。

    如果栈为空,调用pop会抛出EmptyStackException异常。

    4.关于emplace,意为“安放”,就是说,构造一个新的元素放入栈顶。emplace和push很像,这里做一下区分。

    push的话,得是你先构造好对象,然后push插进栈里;emplace则是主动帮你调用构造函数,构造出对象,然后插进栈里。

    示例:

    1. #include
    2. #include
    3. using namespace std;
    4. int main() {
    5. stack<int> st;
    6. st.push(1);
    7. st.push(2);
    8. st.push(3);
    9. while (!st.empty()) {
    10. cout << st.top() << " ";
    11. st.pop();
    12. }
    13. cout << endl;
    14. return 0;
    15. }

    从底层来看,Container是一个模板,你传数组or链表都可以。

    来看看它是怎么适配的:

    二、queue队列

    介绍

    1.队列是一种特殊的线性表,它只能在队尾插入数据,队头出数据,这一模式被称为“先进先出”或FIFO(first in first out)。

    2.从底层实现来看,queue也是作为容器适配器被实现的。

    展示下queue简易版的底层实现:

    底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。

    该底层容器应至少支持以下操作:

    empty:检测队列是否为空

    size:返回队列中有效元素的个数

    front:返回队头元素的引用

    back:返回队尾元素的引用

    push_back:在队列尾部入队列

    pop_front:在队列头部出队列

    标准容器类deque和list满足了这些要求。也就是说,Container的位置可以传deque/ list过去,也可以省略不传。当省略不传时,那默认Container是deque

    (为什么vector不满足呢?因为vector没有头删。之前我们说过,vector嫌头删效率低就没实现)

    使用

    使用时要包头文件

    根据队列的特性,实现的方法有:判空、取大小、取队头、取队尾、(队尾)插入、(队头)删除、交换 和emplace安放。

    几点说明:

    1.构造函数:

    queue (<数据类型,容器类型>) queueName;

    在初始化时必须要有数据类型,容器可省略,省略时默认为deque 类型。

    所以构造一个queue可以是这样两种情况:

    1. queue<int> q;
    2. queue<int,list<int>> q;

    示例:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main() {
    6. queue<int> q;
    7. q.push(1);
    8. q.push(2);
    9. q.push(3);
    10. while (!q.empty()) {
    11. cout << q.front()<<" ";
    12. q.pop();
    13. }
    14. cout << endl;
    15. return 0;
    16. }

    这张图说明了,为什么deque是容器适配器:

    三、priority_queue优先级队列

    介绍

    1.优先级队列是一种特殊的队列结构,它队列中的顺序并非插入的顺序,而是按权重来排序。

    它给每个元素加上了优先级,每次出队的元素是队列中优先级最高的那个元素。

    注:不是越大的元素,优先级就越高。

    那优先级是如何评定的呢?其实,优先级队列在创建伊始,它的优先级就已经定好了,默认为降序。

    那要如何排升序呢?一会会在仿函数那说到。

    2.优先级队列 底层被实现为容器适配器。

    底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。

    容器应该可以通过随机访问迭代器访问,并支持以下操作:

    empty():检测容器是否为空

    size():返回容器中有效元素个数

    front():返回容器中第一个元素的引用

    push_back():在容器尾部插入元素

    pop_back():删除容器尾部元素

    容器类vector和deque满足这些需求。如果容器类被省略不写,那默认使用vector。

    3.需要支持随机访问迭代器,以便始终在内部保持堆结构。

    priority_queue是用堆实现的,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

    注:默认情况下priority_queue是大堆。

    使用

    使用时要包头文件

    注:取顶端数据不是用front了!是用top。

    示例:

    1. #include
    2. #include
    3. using namespace std;
    4. int main() {
    5. priority_queue<int> q;
    6. q.push(4);
    7. q.push(12);
    8. q.push(3);
    9. while (!q.empty()) {
    10. cout << q.top()<<" ";
    11. q.pop();
    12. }
    13. cout << endl;
    14. return 0;
    15. }

    模拟实现

    priority_queue具有队列的所有特性,只是在此基础上又在内部加入了排序。它的底层原理是用堆实现的。

    现在我们来模拟实现一下它的简易版,这样能更好地理解它的底层原理。

    ➡️复用代码

    先把top、empty这种,通过复用代码而实现的 函数给写了:

    1. #pragma once
    2. #include
    3. #include
    4. using namespace std;
    5. namespace jzy
    6. {
    7. template<class T,class Container = vector> //T是优先级队列中的 数据存储类型
    8. class priority_queue   //Container是优先级队列中的数据结构
    9. {
    10. public:
    11. void push(const T& val) {}
    12. void pop() {}
    13. T top() {
    14. return _con.front();   //直接复用容器的方法
    15. }
    16. bool empty() {
    17. return _con.empty();
    18. }
    19. size_t size() {
    20. return _con.size();
    21. }
    22. private:
    23. T _val;
    24. Container _con;
    25. };
    26. }

    ➡️push

    push的实现:(大根堆)先尾插再向上调整。

    1. void AdjustUp(Container& _con) {
    2.   int child = _con.size()-1;
    3.   int parent = (child - 1) / 2;
    4.   while (child > 0){
    5.       if (_con[child] > _con[parent]) {
    6.           swap(_con[child], _con[parent]);
    7.           child = parent;
    8.           parent = (child - 1) / 2;
    9.       }
    10.       else {
    11.           break;
    12.       }
    13.   }
    14. }
    15. void push(const T& val) {
    16.   _con.push_back(val);
    17.   AdjustUp(_con);
    18. }

    ➡️pop

    pop的实现:先把首尾交换,让优先级最高的元素来到队尾,以便尾删。再向下调整。

    1. void AdjustDown(Container& _con) {
    2.   int parent = 0;
    3.   int child = 2 * parent + 1;
    4.   while (child < _con.size()) {
    5.       if (child + 1 < _con.size() && _con[child + 1] > _con[child]) {
    6.           child++;
    7.       }
    8.       if(_con[parent] <_con[child]) {
    9.           swap(_con[parent], _con[child]);
    10.           parent = child;
    11.           child = 2 * parent + 1;
    12.       }
    13.       else {
    14.           break;
    15.       }
    16.   }
    17. }
    18. void pop() {
    19.   swap(_con.front(), _con[_con.size()-1]);
    20.   _con.pop_back();
    21.   AdjustDown(_con);
    22. }

    ➡️如何排降序

    目前排的是升序,那要想排降序,要怎么办呢?

    我倒是有个很朴素的办法:每次插入元素,向上调整时,我把小的往上调,不就行了吗?

    其实就只要改个符号:

    1. void AdjustUp(Container& _con) {
    2.   int child = _con.size()-1;
    3.   int parent = (child - 1) / 2;
    4.   while (child > 0){
    5.       if (_con[child] < _con[parent]) {   //这里原本是>,现在给改成<。越小越往上调
    6.           swap(_con[child], _con[parent]);
    7.           child = parent;
    8.           parent = (child - 1) / 2;
    9.       }
    10.       else {
    11.           break;
    12.       }
    13.   }
    14. }

    但是,这种方法并不好。我想排升序,要把符号改成>;想排降序,又要改回<,麻烦。

    能不能用泛型解决呢?

    其实在STL库里,就是用泛型解决的,我们来学习一下。

    STL里,priority_queue有3个模板参数:

    template <class T, class Container = vector, class Compare = less<typename Container::value_type>>

    第三个模板参数Compare,定义了比较的方式。就是说,我们通过定义Compare类,来制定比较的规则。

    不过,要想理解参数Compare,我们得先学习一个知识点:仿函数。

    仿函数

    仿函数(functor)不是函数,它描述的是对象,只不过这个对象重载了operator(),所以它使用起来像函数一样。

    比方说,你定义了一个A类,用A实例化出对象a。此时a是一个普通的对象。

    但当你在A里面重载了operator()(即函数调用运算符),那a就是仿函数,就可以这样用:

    1. class A
    2. {
    3. ……
    4. int operator() (int x,int y){
    5. return x+y;
    6. }
    7. };
    8. int mian(){
    9. A a;
    10. cout<<a(1,2);   //A实例化出的对象a,就可以像函数一样使用,a就叫仿函数
    11. return 0;
    12. }

    关于():

    我一开始大为不解,怎么圆括号()也能重载?

    没错!()叫做”函数调用运算符“,我们在调用函数时,往往要传参,此时就用到这个运算符。

    所以说,要想让对象成为仿函数,只需要在它的类里面重载operator()。

    用仿函数实现升降序

    先实现两个类:Greater、Less,分别用于定义升序、降序(这俩类实现为模板)。

    然后在priority_queue类中增加一个模板参数compare,此参数用于接收 比较方式 的类型(是升序,还是降序)。

    compare默认为降序。当我们想要升序,那就在传参时,实例化出Greator类型的对象,传过去即可。

    1. //先实现Less和Greater两个类,来定义比大小的规则
    2. template<class T>
    3. struct Less   //less意为越来越小,即降序
    4. {
    5.   bool operator() (const T& x, const T& y) {
    6.       return x > y;
    7.   }
    8. };
    9. template<class T>
    10. struct Greater     //升序
    11. {
    12.   bool operator() (const T& x, const T& y) {
    13.       return x < y;
    14.   }
    15. };
    16. template<class T,class Container = vector,class Compare=Less> //增加模板参数,默认为降序
    17. class priority_queue
    18. {
    19.   Compare com;
    20. public:
    21.   void AdjustUp(Container& _con) {
    22.       int child = _con.size()-1;
    23.       int parent = (child - 1) / 2;
    24.       while (child > 0){
    25.           if (com(_con[child],_con[parent])) {
    26.               swap(_con[child], _con[parent]);
    27.               child = parent;
    28.               parent = (child - 1) / 2;
    29.           }
    30.           else {
    31.               break;
    32.           }
    33.       }
    34.   }
    35.   void AdjustDown(Container& _con) {
    36.       Compare com;
    37.       int parent = 0;
    38.       int child = 2 * parent + 1;
    39.       while (child < _con.size()) {
    40.           if (child + 1 < _con.size() && com(_con[child+1],_con[child])) {  
    41.               child++;
    42.           }
    43.           if(com(_con[child], _con[parent])) {
    44.               swap(_con[parent], _con[child]);
    45.               parent = child;
    46.               child = 2 * parent + 1;
    47.           }
    48.           else {
    49.               break;
    50.           }
    51.         }
    52.   }
    53.   void push(const T& val) {
    54.       _con.push_back(val);
    55.       AdjustUp(_con);
    56.   }
    57.   void pop() {
    58.       swap(_con.front(), _con[_con.size()-1]);
    59.       _con.pop_back();
    60.       AdjustDown(_con);
    61.   }
    62. ……
    63. };

    注:因为我们展开了STL库,所以在命名Less/Greater时,注意不要写成less/greater,不然就和库里的重名了,编译器就就晕了 不知道用哪个。

    测试:排升序

    1. #include"priority_queue.h"
    2. using namespace jzy;
    3. int main() {
    4. priority_queue<int,vector<int>,Greater<int>> pq;
    5. pq.push(7);
    6. pq.push(3);
    7. pq.push(8);
    8. pq.push(2);
    9. pq.push(12);
    10. pq.push(6);
    11. pq.push(3);
    12. while (!pq.empty()) {
    13. cout << pq.top() << " ";
    14. pq.pop();
    15. }
    16. return 0;
    17. }

    模拟实现的总代码

    priority_queue.h:

    1. #pragma once
    2. #include
    3. #include
    4. using namespace std;
    5. namespace jzy
    6. {
    7. template<class T>
    8. struct Less   //less意为越来越小,即降序
    9. {
    10. bool operator() (const T& x, const T& y) {
    11. return x > y;
    12. }
    13. };
    14. template<class T>
    15. struct Greater     //升序
    16. {
    17. bool operator() (const T& x, const T& y) {
    18. return x < y;
    19. }
    20. };
    21. template<class T,class Container = vector,class Compare=Less>
    22. class priority_queue
    23. {
    24. Compare com;
    25. public:
    26. void AdjustUp(Container& _con) {
    27. int child = _con.size()-1;
    28. int parent = (child - 1) / 2;
    29. while (child > 0){
    30. if (com(_con[child],_con[parent])) {
    31. swap(_con[child], _con[parent]);
    32. child = parent;
    33. parent = (child - 1) / 2;
    34. }
    35. else {
    36. break;
    37. }
    38. }
    39. }
    40. void AdjustDown(Container& _con) {
    41. Compare com;
    42. int parent = 0;
    43. int child = 2 * parent + 1;
    44. while (child < _con.size()) {
    45. if (child + 1 < _con.size() && com(_con[child+1],_con[child])) {  
    46. child++;
    47. }
    48. if(com(_con[child], _con[parent])) {
    49. swap(_con[parent], _con[child]);
    50. parent = child;
    51. child = 2 * parent + 1;
    52. }
    53. else {
    54. break;
    55. }
    56. }
    57. }
    58. void push(const T& val) {
    59. _con.push_back(val);
    60. AdjustUp(_con);
    61. }
    62. void pop() {
    63. swap(_con.front(), _con[_con.size()-1]);
    64. _con.pop_back();
    65. AdjustDown(_con);
    66. }
    67. T top() {
    68. return _con.front();
    69. }
    70. bool empty() {
    71. return _con.empty();
    72. }
    73. size_t size() {
    74. return _con.size();
    75. }
    76. private:
    77. T _val;
    78. Container _con;
    79. };
    80. }

    四、deque双端队列

    (这个容器了解即可,这个容器很少用)

    介绍

    1.deque是“double-ended queue”的缩写。

    虽然叫双端队列,但它和队列没啥关系。deque是一种双开口的"连续"空间的数据结构。双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。

    deque支持随机访问,也支持任意位置的插入、删除。似乎,deque结合了vector和list的优点。

    但,deque真的是完美的吗?

    并不是!deque随机访问的效率比较低。至于为什么,那先让我们了解一下它的底层实现。

    底层实现

    和vector不同,deque的底层并不是 真正意义上的连续的空间,而是由一段段连续的小空间拼接而成的,这些小空间不一定是连续的,可能是位于内存的不同区域。如图:

    这里的map,并不是STL里的map容器。而是数组,数组的每个元素都是 指向另一块连续空间(缓冲区buffer)的 指针。我们插入的元素,实际上是存进buffer里的。

    可见,deque对空间利用率很高,几乎没什么空间的浪费。

    ➡️如何进行扩容呢?

    当一个buffer存满了,要尾插,此时不会给当前buffer扩容(因为它是大小固定的),而是再开一个buffer空间,尾插的内容存进新的buffer里。指针数组的下一个元素指向新的buffer。头插同理。下图可以帮助我们理解:

    可见,头插、尾插不需要挪动数据,效率自然高。

    如果map满了,那就再开一个更大的map,如何把数据拷到新的map里。

    ➡️关于deque的迭代器,它的原理很复杂,由四个指针组成。node指向中控区的指针数组,first和node分别指向一段空间的开始和结束,cur指向访问到的位置。如果访问的元素不在当前空间,cur就等于下一个空间的first,再继续访问。

    ➡️当需要随机访问时,deque得先完整复制到一个vector上,如何vector排序后,再复制deque。这就导致了deque的随机访问效率较vector更低。

    关于deque的小结

    优势:1.相比vector,deque的头插、头删不需要挪动数据,而是直接开新buffer插入,效率很高;

    在扩容时,也无需开新空间、拷数据,而是直接开个新buffer,效率高。

    2.相比list,deque的底层是连续的存指针的map数组,空间利用率比较高,不需要存储额外字段。况且,deque还支持随机访问,虽然效率不高。

    劣势:1.不适合遍历。

    因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。

    而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

    2.中间插入、删除的效率并不高。(因为下标需要经过计算,并且要挪动元素)

  • 相关阅读:
    或许我们并不需要 default exports
    【vue-router】Vue路由从创建到使用
    无声的世界,精神科用药并结合临床的一些分析及笔记(八)
    LibreCAD windows 编译
    python将word文件转换成pdf文件
    按键点亮led灯
    疫情使我被迫在家朋友选择躺平,我选择了学习软件测试
    账号安全及应用
    传统车企数字化转型如何打通最后一公里?
    【计算机网络】初识网络
  • 原文地址:https://blog.csdn.net/2301_76540463/article/details/134301158