• 【C++】stack && queue && priority_queue的模拟实现


    目录

    一、容器适配器

    二、deque的基本介绍

    1、简介

    2、deque的底层

    3、对比

    三、stack的模拟实现

    四、queue的模拟实现

    五、priority_queue的模拟实现

    总结


    前言

    以上三个STL中的组件,并不是像vector,list,string一样,它们不是容器,而是容器适配器


    一、容器适配器

    容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。

    其实,容器适配器中的“适配器”,和生活中常见的电源适配器中“适配器”的含义非常接近。我们知道,无论是电脑、手机还是其它电器,充电时都无法直接使用 220V 的交流电,为了方便用户使用,各个电器厂商都会提供一个适用于自己产品的电源线,它可以将 220V 的交流电转换成适合电器使用的低压直流电。从用户的角度看,电源线扮演的角色就是将原本不适用的交流电变得适用,因此其又被称为电源适配器。
     

    容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其它基础容器模板类中已经写好的成员函数。当然,如果必要的话,容器适配器中也可以自创新的成员函数。

    C++中一共有三个容器适配器,分别是stack,queue和priority_queue

    stack和queue默认使用的容器是deque,priority_queue默认使用的是vector

    二、deque的基本介绍

    1、简介

    deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和 删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

    deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的。

    2、deque的底层

    deque也有节点,但是它的节点与list的不同,deque的一个节点具有很多个value,并不像list一样一个节点具有一个value。

    图片里面的map是一个二级指针它指向中控数组,该数组是一个指针数组,里面存放的是每个节点的地址。换句话说deque是由一个一个小buffer数组组成的,例如它现在有三个小数组

    其中一个数组存放的是头插的数据,另一个数组存放的是尾插的数据,另外存放的是中间的数据,如果deque满了,它不会扩容,然后挪动数据,而是会再次开辟一个新的buffer数组。

    它的方括号的实现思路是,假如访问的是下标为i的元素

    首先:i - 第一个buffer的元素个数 / 每个buffer数组能够存放元素的个数,确定出该元素存储在第几个buffer。

    然后:i- 第一个buffer的元素个数 % 每个buffer数组能够存放的元素个数,确定是那个buffer的第几个。 

    3、对比

    它就好像是vector和list的结合版,它既有vector的所有接口,同时还拥有list的全部接口,看样子他十分的NB,但实际上却不然,因为如果他这么NB,我们直接学习它不就行吗,为什么还要学习这个?

    与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不 需要搬移大量的元素,因此其效率比vector高。

    与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段,并且支持随机访问,具有[ ]

    但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构 时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作 为stack和queue的底层数据结构。

     

    由此可得:deque非常适合头尾的插入和删除,这与栈和队列的特性一致,所以采用deque来作为栈和队列的默认容器

    三、stack的模拟实现

    stack的C语言版本的模拟实现,我们在前面的博客中已经完成了,所以在这里的实现我们只说明C++不同的地方

    1. #include
    2. #include
    3. using namespace std;
    4. namespace ww
    5. {
    6. template<class T, class Container = deque>
    7. class stack
    8. {
    9. public:
    10. void push(const T& x)
    11. {
    12. _con.push_back(x);
    13. }
    14. void pop()
    15. {
    16. _con.pop_back();
    17. }
    18. T& top()
    19. {
    20. return _con.back();
    21. }
    22. const T& top()const
    23. {
    24. return _con.back();
    25. }
    26. bool empty()const
    27. {
    28. return _con.empty();
    29. }
    30. size_t size()const
    31. {
    32. return _con.size();
    33. }
    34. private:
    35. Container _con;
    36. };

    四、queue的模拟实现

    1. #include
    2. #include
    3. using namespace std;
    4. namespace ww
    5. {
    6. template<class T, class Container = deque>
    7. class queue
    8. {
    9. public:
    10. void push(const T& x)
    11. {
    12. _con.push_back(x);
    13. }
    14. void pop()
    15. {
    16. _con.pop_front();
    17. }
    18. T& back()
    19. {
    20. return _con.back();
    21. }
    22. const T& back()const
    23. {
    24. return _con.back();
    25. }
    26. T& front()
    27. {
    28. return _con.front();
    29. }
    30. const T& front()const
    31. {
    32. return _con.front();
    33. }
    34. bool empty()const
    35. {
    36. return _con.empty();
    37. }
    38. size_t size()const
    39. {
    40. return _con.size();
    41. }
    42. private:
    43. Container _con;
    44. };

    五、priority_queue的模拟实现

    我们重点说明priority_queue,它的本质就是堆,而堆我再以前的博客中也实现了,感兴趣的同学可以翻看我以前的博客,来查看C语言版本的堆。

    这里与C语言不同的地方在于,它多了一个仿函数的概念

    而什么是仿函数呢?

    仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。

    仿函数又叫做函数对象,使类对象可以像函数一样去使用

    我们手动实现一个简单的仿函数

    1. template<class T>
    2. struct less
    3. {
    4. bool operator()(const T& x, const T& y)const
    5. {
    6. return x < y;
    7. }
    8. };
    9. template<class T>
    10. struct biger
    11. {
    12. bool operator()(const T& x, const T& y)const
    13. {
    14. return x > y;
    15. }
    16. };

    这就是两个简单的仿函数

    1. #include
    2. using namespace std;
    3. namespace ww
    4. {
    5. template<class T>
    6. struct less
    7. {
    8. bool operator()(const T& x, const T& y)const
    9. {
    10. return x < y;
    11. }
    12. };
    13. template<class T>
    14. struct biger
    15. {
    16. bool operator()(const T& x, const T& y)const
    17. {
    18. return x > y;
    19. }
    20. };
    21. }
    22. int main()
    23. {
    24. ww::less<int> ls;
    25. ww::biger<int> bg;
    26. cout << ls(2, 3) << endl;
    27. cout << bg(5, 7) << endl;
    28. return 0;
    29. }

    结果正确,与我们的设想的一致,并且我们使用这个类就如同函数调用一样。

    有了以上的铺垫,我们也就明白了仿函数

    priority_queue就是利用仿函数来控制,实例化的对象是大根堆还是小根堆

    除此之外,就与普通的堆没有什么差别了,堆的基本实现方法也已在前面的博客中涉及,不在此赘述

    1. #include
    2. #include
    3. using namespace std;
    4. namespace ww
    5. {
    6. //默认大堆
    7. template<class T, class Container = vector, class Compare = std::less>
    8. class priority_queue
    9. {
    10. public:
    11. priority_queue()
    12. {
    13. }
    14. template<class InputIterator>
    15. priority_queue(InputIterator first, InputIterator last)
    16. {
    17. while(first != last)
    18. {
    19. _con.push_back(*first);
    20. first++;
    21. }
    22. for(int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
    23. {
    24. adjust_down(i);
    25. }
    26. }
    27. void adjust_down(size_t parent)
    28. {
    29. Compare com;
    30. size_t child = parent * 2 + 1;
    31. while(child < _con.size())
    32. {
    33. //if(child + 1 < _con.size() && _con[child+1] > _con[child])
    34. //if(child + 1 < _con.size() && _con[child] < _con[child+1])
    35. if(child + 1 < _con.size() && com(_con[child], _con[child+1]))
    36. {
    37. child++;
    38. }
    39. //if(_con[child] > _con[parent])
    40. //if(_con[parent] < _con[child])
    41. if(com(_con[parent], _con[child]))
    42. {
    43. std::swap(_con[child], _con[parent]);
    44. parent = child;
    45. child = parent * 2 + 1;
    46. }
    47. else
    48. {
    49. break;
    50. }
    51. }
    52. }
    53. void adjust_up(size_t child)
    54. {
    55. Compare com;
    56. size_t parent = (child - 1) / 2;
    57. while(child > 0)
    58. {
    59. //if(_con[child] > _con[parent])
    60. //if(_con[parent] < _con[child])
    61. if(com(_con[parent], _con[child]))
    62. {
    63. std::swap(_con[child], _con[parent]);
    64. child = parent;
    65. parent = (child - 1) / 2;
    66. }
    67. else
    68. {
    69. break;
    70. }
    71. }
    72. }
    73. void push(const T& x)
    74. {
    75. _con.push_back(x);
    76. adjust_up(_con.size() - 1);
    77. }
    78. void pop()
    79. {
    80. std::swap(_con[0], _con[_con.size() - 1]);
    81. _con.pop_back();
    82. adjust_down(0);
    83. }
    84. const T& top()
    85. {
    86. return _con[0];
    87. }
    88. bool empty()const
    89. {
    90. return _con.empty();
    91. }
    92. size_t size()const
    93. {
    94. return _con.size();
    95. }
    96. private:
    97. Container _con;
    98. };
    99. }

     

    在这里说明一下为什么要显式的写构造函数,因为我们已经实现了用迭代器区间的构造函数,而只要显式的写了构造函数编译器就不会自动生成,默认构造函数了,这会在某些的场景出错。


    总结


    以上就是今天要讲的内容,我们会发现STL已经过半了,并且他们的实现接口类似,实现的手段也类似。

  • 相关阅读:
    flutter android studio升级java java17
    0X02
    Codeforces Round #826 (Div. 3) D E F
    【各exporter、prometheus、grafana、alertManager】架构与部署
    fastadmin 一个表中两个字段,关联另一个表同一个字段
    Linux系统的FTP服务
    Redis主从复制的核心原理
    element ui富文本编辑器的使用(quill-editor)
    C# 实例解释面向对象编程中的开闭原则
    Innodb是如何运转的
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/127043143