C++STL之stack和queue以及deque详解

x33g5p2x  于2021-11-30 转载在 C/C++  
字(7.0k)|赞(0)|评价(0)|浏览(354)

stack和queue以及deque

stack文档

stack的使用

函数说明接口说明
stack()构造空的栈
empty()检测stack是否为空
size()返回stack中元素的个数
top()返回栈顶元素的引用
push()将元素val压入stack中
pop()将stack中尾部的元素弹出
  1. #include<iostream>
  2. #include<stack>
  3. #include<queue>
  4. using namespace std;
  5. void test_stack()
  6. {
  7. stack<int> st;
  8. st.push(1);
  9. st.push(2);
  10. st.push(3);
  11. st.push(4);
  12. st.push(5);
  13. cout<<st.empty()<<endl;
  14. cout<<st.size()<<endl;
  15. while(!st.empty())
  16. {
  17. cout<<st.top()<<" ";
  18. st.pop();
  19. }
  20. cout<<endl;
  21. }
  22. int main()
  23. {
  24. test_stack1();
  25. return 0;
  26. }

queue的使用

  1. void test_queue()
  2. {
  3. queue<int> q;
  4. q.push(1);
  5. q.push(2);
  6. q.push(3);
  7. q.push(4);
  8. q.push(5);
  9. cout << q.empty() << endl;
  10. cout << q.size() << endl;
  11. while(!q.empty())
  12. {
  13. cout<<q.front()<<" ";
  14. q.pop();
  15. }
  16. cout<<endl;
  17. }
  18. int main()
  19. {
  20. test_stack1();
  21. return 0;
  22. }

栈的OJ题练习

最小栈

题目链接

最小栈

题目描述

解题思路

解题代码

  1. class MinStack
  2. {
  3. public:
  4. void push(int val)
  5. {
  6. _st.push(val);
  7. if(_minst.empty() || val<=_minst.top())
  8. {
  9. _minst.push(val);
  10. }
  11. }
  12. void pop()
  13. {
  14. if(_st.top()==_minst.top())
  15. {
  16. _minst.pop();
  17. }
  18. _st.pop();
  19. }
  20. int top()
  21. {
  22. return _st.top();
  23. }
  24. int getMin()
  25. {
  26. return _minst.top();
  27. }
  28. stack<int> _st;
  29. stack<int> _minst;
  30. };

栈的压入、弹出序列

题目链接

栈的压入、弹出序列

题目描述

解题思路:

解题代码

  1. class Solution()
  2. {
  3. public:
  4. bool IsPopOrder(vector<int> pushV,vector<int> popV)
  5. {
  6. stack<int> st;
  7. size_t pushi = 0,popi = 0;
  8. while(pushi<pushV.size())
  9. {
  10. st.push(pushV[pushi++]);
  11. //栈中出的数据和出栈序列匹配上了
  12. while(!st.empty() && st.top() == popV[popi])
  13. {
  14. ++popi;
  15. st.pop();
  16. }
  17. }
  18. return st.empty();//st为空,说明全都匹配了
  19. }
  20. };

逆波兰表达式求值

题目描述

逆波兰表示法

在计算机程序处理中缀表达式不方便进行计算,因为优先级的问题

如何将中缀表达式转后缀表达式?

中缀转后缀,这里需要借助一个栈:

后缀表达式进行运算:

  1. class Solution {
  2. public:
  3. int evalRPN(vector<string>& tokens) {
  4. stack<int> st;
  5. for(const auto& str:tokens)
  6. {
  7. int left,right;
  8. if(str == "+"||str == "-"||str == "*"||str == "/")
  9. {
  10. right = st.top();
  11. st.pop();
  12. left = st.top();
  13. st.pop();
  14. switch(str[0])
  15. {
  16. case '+':
  17. st.push(left+right);
  18. break;
  19. case '-':
  20. st.push(left-right);
  21. break;
  22. case '*':
  23. st.push(left*right);
  24. break;
  25. case '/':
  26. st.push(left/right);
  27. break;
  28. }
  29. }
  30. if(str == "+")
  31. {
  32. right = st.top();
  33. st.pop();
  34. left = st.top();
  35. st.pop();
  36. st.push(left+right);
  37. }
  38. else if(str == "-")
  39. {
  40. right = st.top();
  41. st.pop();
  42. left = st.top();
  43. st.pop();
  44. st.push(left-right);
  45. }
  46. else if(str == "*")
  47. {
  48. right = st.top();
  49. st.pop();
  50. left = st.top();
  51. st.pop();
  52. st.push(left*right);
  53. }
  54. else if(str == "/")
  55. {
  56. right = st.top();
  57. st.pop();
  58. left = st.top();
  59. st.pop();
  60. st.push(left/right);
  61. }
  62. else
  63. {
  64. //操作数
  65. st.push(stoi(str));
  66. }
  67. }
  68. return st.top();
  69. }
  70. };

什么是适配器?

假设一个代码模块 A,它的构成如下所示:

  1. class A{
  2. public:
  3. void f1(){}
  4. void f2(){}
  5. void f3(){}
  6. void f4(){}
  7. };

现在我们需要设计一个模板 B,但发现,其实只需要组合一下模块 A 中的 f1()、f2()、f3(),就可以实现模板 B 需要的功能。其中 f1() 单独使用即可,而 f2() 和 f3() 需要组合起来使用,如下所示:

  1. class B{
  2. private:
  3. A _a;//封装A
  4. public:
  5. void g1(){
  6. _a->f1();
  7. }
  8. void g2(){
  9. _a->f2();
  10. _a->f3();
  11. }
  12. };

模板 B 将不适合直接拿来用的模板 A 变得适用了,因此我们可以将模板 B 称为 B 适配器。

容器适配器也是同样的道理,简单的理解容器适配器,其就是将不适用的序列式容器(包括 vector、deque 和 list)变得适用。容器适配器的底层实现和模板 A、B 的关系是完全相同的,即通过封装某个序列式容器,并重新组合该容器中包含的成员函数,使其满足某些特定场景的需要。

栈和队列都是容器适配器

栈和队列的模拟实现

栈的模拟实现

栈满足后进先出的特性,在数据结构当中,我们可以使用顺序表和链表实现它,显然顺序表实现更优一些,因为顺序表进行尾插尾插的效率很高,而栈就是在一个方向上进行插入和删除。而无论是顺序表还是链表,都是可以实现栈这个数据结构的,所以我们可以封装容器,组合该容器中包含的成员函数。

  1. #include<iostream>
  2. #include<list>
  3. #include<deque>
  4. using namespace std;
  5. //Stack
  6. namespace Z
  7. {
  8. template<class T,class Container>
  9. class stack
  10. {
  11. private:
  12. Container _con;
  13. };
  14. }
  1. #include<iostream>
  2. #include<list>
  3. #include<deque>
  4. using namespace std;
  5. //Stack
  6. namespace Z
  7. {
  8. class stack
  9. {
  10. //stack是一个Container适配(封装转换)出来的
  11. //template<class T,class Container = std::vector<T>>//可以给缺省类型
  12. template<class T,class Container = std::deque<T>>//可以给缺省类型
  13. //Container 尾认为是栈顶
  14. public:
  15. void push(const T& x)
  16. {
  17. _con.push_back(x);
  18. }
  19. void pop()
  20. {
  21. _con.pop_back();
  22. }
  23. const T& top()
  24. {
  25. return _con.back();
  26. }
  27. size_t size()
  28. {
  29. return _con.size();
  30. }
  31. bool empty()
  32. {
  33. return _con.empty();
  34. }
  35. private:
  36. Container _con;
  37. };
  38. }

我们就可以这样显式实例化创建栈对象:

  1. stack<int,std::vector<int>> st;

或者使用链表来构造栈数据结构:

  1. stack<int,std::list<int>> st;

我们为了还可以这样创建栈对象,不显式实例化不传第二个参数:

  1. stack<int> st;

为了可以这样我们在定义模板参数那里,给第二个参数缺省值:

  1. template<class T,class Container = std::deque<T>>//可以给缺省类型
  2. //template<class T,class Container = std::vector<T>>//可以给缺省类型
  3. //template<class T,class Container = std::list<T>>//可以给缺省类型

我们来测试一下:

  1. void test_stack1()
  2. {
  3. stack<int, std::vector<int>> st;
  4. st.push(1);
  5. st.push(2);
  6. st.push(3);
  7. st.push(4);
  8. stack<int> st1;
  9. st1.push(10);
  10. st1.push(20);
  11. st1.push(30);
  12. st1.push(40);
  13. cout << "st:";
  14. while (!st.empty())
  15. {
  16. cout << st.top() << " ";
  17. st.pop();
  18. }
  19. cout << endl;
  20. cout << "st1:";
  21. while (!st1.empty())
  22. {
  23. cout << st1.top() << " ";
  24. st1.pop();
  25. }
  26. cout << endl;
  27. }

队列的模拟实现

经过了栈适配器的模拟实现,实现队列适配器就是举手之劳了,只需要注意Queue是队头出数据,队尾入数据,相当于是头删和尾插,因为vector容器没有实现头删,所以Queue不能封装vector:

  1. //Queue
  2. #include<iostream>
  3. #include<list>
  4. #include<vector>
  5. #include<deque>
  6. using namespace std;
  7. namespace Z
  8. {
  9. //Queue是一个Container适配(封装转换)出来的
  10. //template<class T,class Container = std::list<T>>//可以给缺省类型
  11. template<class T, class Container = std::deque<T>>//可以给缺省类型
  12. class queue
  13. {
  14. //Container 尾认为是队尾,头认为是队头,队头出数据,队尾入数据
  15. public:
  16. void push(const T& x)
  17. {
  18. _con.push_back(x);
  19. }
  20. void pop()
  21. {
  22. _con.pop_front();
  23. }
  24. const T& front()
  25. {
  26. return _con.front();
  27. }
  28. const T& back()
  29. {
  30. return _con.back();
  31. }
  32. size_t size()
  33. {
  34. return _con.size();
  35. }
  36. bool empty()
  37. {
  38. return _con.empty();
  39. }
  40. private:
  41. Container _con;
  42. };
  43. }

我们对队列进行测试:

  1. void test_queue()
  2. {
  3. //queue<int, std::vector<int>> q;//error,vector不支持头删头插,所以vector不能
  4. queue<int, std::list<int>> q;
  5. q.push(1);
  6. q.push(2);
  7. q.push(3);
  8. q.push(4);
  9. while (!q.empty())
  10. {
  11. cout << q.front() << " ";
  12. q.pop();
  13. }
  14. cout << endl;
  15. }
  16. int main()
  17. {
  18. Z::test_queue();
  19. return 0;
  20. }

上面接触到了deque,接下来我们了解一下deque:

deque

deque的使用

  1. #include<deque>
  2. #include<iostream>
  3. using namespace std;
  4. void test_deque()
  5. {
  6. deque<int> dp;
  7. //尾插
  8. dp.push_back(1);
  9. dp.push_back(2);
  10. dp.push_back(3);
  11. dp.push_back(4);
  12. //头插
  13. dp.push_front(10);
  14. dp.push_front(20);
  15. dp.push_front(30);
  16. dp.push_front(40);
  17. //尾删
  18. dp.pop_back();
  19. //头删
  20. dp.pop_front();
  21. //随机访问
  22. for (size_t i = 0; i < dp.size(); i++)
  23. {
  24. cout << dp[i] <<" ";
  25. }
  26. cout << endl;
  27. }
  28. int main()
  29. {
  30. test_deque();
  31. return 0;
  32. }

可以看到它既支持头插尾插,又支持头删尾删,还支持随机访问,它融合了vector和list的优点,从使用的角度,避开了它们各自的缺点:

list的缺点:不支持随机访问

vector的缺点:头部和中间插入删除效率低

看起来deque是一个非常完美的,那么真的有那么完美吗?我们来看deque的底层实现

deque的底层实现

分析:如果deque真的像上面说的那么优秀,那么vector和list可能就被淘汰了,他还是有缺陷的,它的底层怎么实现的呢?

首先我们来看一下vector的优缺点:

vector

优点:

1、支持随机访问

2、CPU高速缓存命中率很高

缺点:

1、空间不够就需要增容,增容代价大,还存在一定空间浪费

2、头部和中间插入删除,效率低。O(N)

list

优点:

1、按需申请释放空间

2、任意位置插入删除数据都是O(1),效率高

缺点:

1、不支持随机访问

2、cpu高速缓存命中率低

结合vector和list优缺点,进行设计:

我们创建一个能够存储10个元素的数组,头插或者尾插时,当10个元素满了时,再开辟一个数组来存储,我们还需要一个中控数组存放来数组指针指向这一个个的数组,当是头插时,元素满了时,指向新开辟的数组的指针需要存储在中控数组中的那个指向满了的数组的前面,当是尾插时,元素满了时,指向新开辟的数组的指针需要存储在中控数组中的那个指向满了的数组的后面,当中控数组满了的时候就需要增容,但是代价较小

它的迭代器很复杂,迭代器封装了四个指针来维护结构,first是指向buff数组的起始位置,last是指向buff数组的结束位置,cur指向当前迭代器指向数组中的值的位置,node指向存储buff的指针数组的对应中控位置的指针:

  1. iterator begin()
  2. {
  3. return start;
  4. }
  5. iterator end()
  6. {
  7. return finish;
  8. }
  9. deque<int> dp;
  10. iterator it = dp.begin();
  11. while(it!=dp.end())
  12. {
  13. *it;
  14. ++it;
  15. }

尝试分析遍历deque时的迭代器遍历的操作:

deque的优缺点

1、双端队列,他很适合头插头删,尾插尾删,他去做stack和queue的默认适配容器很适合

2、双端队列中间插入删除数据,非常麻烦。效率不高

实现方案1:挪动整体数据

实现方案2:插入或者删除数据时,只挪动当前buff数据,搞一个变量记录每个buff数组的大小,但是这样会导致每个buff数组大小不一致,此时随机访问就会麻烦:

3、deque是一种折中(妥协)方案设计,不够极致,随机访问效率不及vector,任意位置插入删除不及list

相关文章