• 剑指offer --- 用两个栈实现队列的先进先出特性


    目录

    前言

    一、读懂题目

    二、思路分析

    三、代码呈现

    总结


    前言

            当我们需要实现队列的先进先出特性时,可以使用栈来模拟队列的行为。本文将介绍如何使用两个栈来实现队列,并给出具体的思路和代码实现。


    一、读懂题目

    题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead,分别完成在队列尾部插入结点和在队列头部删除结点的功能。

            当我们需要利用栈来实现队列先进先出的特性时,考虑到单个栈对存入的一组数据push后再逐位pop取出,对应的序列和输入的顺序相反,那我们是否可以用两个栈,抽象类似于负负得正的手法,使得top位逐个取出得到的序列和插入时是相同的?

    二、思路分析

    首先基础类定义如下,我们只需要补充里面 appendTail 和 deleteHead 两函数的定义即可:

    1. // 用两个栈实现队列
    2. template<typename T>
    3. class MyQueue
    4. {
    5. public:
    6. void appendTail(const T& n);
    7. T deleteHead();
    8. private:
    9. stack st1;
    10. stack st2;
    11. };

    为了便于表示和说明,设定一组数据 {a, b, c, d, e} 作为测试序列。

    我们看到两个栈都可存储数据,那我们不妨先将数据全部存入st1中,那么根据栈先进后出的特性,两栈中数据的存储此时应该是这样的:

    既然我们想到试试“负负得正”这种方法,那要是把st1中的元素再一 一取出放入st2中,是不是相当于把序列的顺序调转再调转,这样当我们不断取 st2.top() 时,最后按照取出的序列就是原来顺序的初始序列!

    我们模拟一下这个过程:

    1)对st1执行两次取首压入st2中:

    2)将剩余元素从st1中全部压入st2中,此时st1为空:

     3)最后每当该模拟队列执行一次 front() 操作就返回 top2 所指向的值,每次 pop() 就从 st2 取出栈顶元素(前提是st1为空),直到 st2 栈为空:

    那如果在执行 front() 过程中,夹杂着新的push()指令,应该将新元素放入st1还是st2呢?如果st1不为空,要先将st1全部按照上面的规则移入st2后再插入还是先放入st1栈顶,再统一移入st2呢?

    首先我们明确每个元素都需要经历“负负得正”的过程,所以首先新加元素肯定要先经过st1才能进入st2中。那是否需要st1及时清空呢?

    我们不妨以 {a, b, c, d}, {e} 模拟两批次对模拟队列执行push()指令:

    假定我们在每批操作结束就将该批的元素依次弹出并置入st2中,那么当需要输入上面第二组(即{e})时,st1和st2的存储情况如下:

    接下来该插入第二批数据了,但是实际功能调用过程中,不可能每次单批次插入操作之间是连续的,中间可能会有删除队列中已有元素的操作,所以我们不妨在两次 push() 指令中间加一句 pop_front() 指令,那么当接着执行一次pop_front() 指令时,对于st2而言应该将 a 元素剔除。同时我们注意到 a 元素正如我们所料位于st2的栈顶,当 st2.pop() 操作执行时,取出 a 元素,符合队列先进先出的特性。此时st1为空,st2顶部元素为b,如下图:

    下一步我们进行第二批插入,将元素 e 执行 push() 操作时,显然 e 需要先进st1中,所以st1中此时不为空,元素e即为st1栈顶元素:

    那么此时我们要不要把st1中的所有元素(这里仅有 e )顺手植入st2中呢?

    1)如果移入st2中:

    现在面临一个问题:如果我们一次性取出st2中的元素,亦或是仅取st2栈顶元素执行 st2.pop() 来获得理想的队列头,却发现得到的结果并不满足我们的设计需求,两轮输入的总序列为 {a, b, c, d, e} ,而取出序列为 {a, e, ... },显然不符合先进先出的原则。

    那是否意味着第二批的输入元素应该先保存在st1中,并非单批输入结束后就将其接着压入st2中,那为什么首批输入的数据就可以直接经st1后全部置入st2呢?

    难道因为开始时候 st2 为空吗?那如果后续其他批次插入时,也遵循 st2 为空后再将 st1 中所有元素置于 st2 中,会不会发生这样的冲突?

    不妨我们按照这样的设计思路进行测试:

    首先给出部分功能的此思路代码:

    1)清空 st1 容器功能代码

    1. template<typename T>
    2. void MyQueue::appendOver()
    3. {
    4. if (st2.empty()) // 只有满足st2为空才清空st1
    5. {
    6. while (!st1.empty())
    7. {
    8. st2.push(st1.top());
    9. st1.pop();
    10. }
    11. }
    12. }

     2)模拟 front() , push() 和 pop() 的函数功能

    1. template<typename T>
    2. T& MyQueue::front()
    3. {
    4. if (!st1.empty())
    5. {
    6. appendOver();
    7. }
    8. assert(!st2.empty());
    9. return st2.top();
    10. }
    11. template<typename T>
    12. void MyQueue::appendTail(const T& n)
    13. {
    14. st1.push(n);
    15. }
    16. template<typename T>
    17. T MyQueue::deleteHead()
    18. {
    19. assert(!empty()); // 不能同时为空
    20. if (st2.empty())
    21. {
    22. appendOver();
    23. }
    24. T tmp_poped = st2.top();
    25. st2.pop();
    26. return tmp_poped;
    27. }

     3)实际测试代码

    1. void test1()
    2. {
    3. MyQueue<int> mq;
    4. // queue.push()模拟尾插
    5. for (int i = 10; i > 0; i--)
    6. {
    7. mq.appendTail(i);
    8. } // 1 2 3 4 5 6 7 8 9 10 右侧栈顶
    9. // queue.front()模拟取首元素
    10. // appendTail()--push()模拟入队列操作
    11. // deleteHead()--pop()模拟删除队列首元素
    12. mq.deleteHead(); // st1中:空 st2中:1 2 3 4 5 6 7 8 9 右侧栈顶
    13. mq.appendTail(100); // st1中:100 st2中:1 2 3 4 5 6 7 8 9 右侧栈顶
    14. // 由于st2不为空,st1中元素不发生迁移
    15. // 经过上面两步 st1中:100 st2中:1 2 3 4 5 6 7 8 9
    16. // 假设我们再进行一组类似操作
    17. mq.deleteHead(); // st1中:100 st2中:1 2 3 4 5 6 7 8 右侧栈顶
    18. mq.appendTail(55); // st1中:55 100 st2中:1 2 3 4 5 6 7 8 右侧栈顶
    19. int val = mq.deleteHead(); // st1中:55 100 st2中:1 2 3 4 5 6 7 右侧栈顶
    20. printf("val = %d\n", val); // 8
    21. PopAndPrintMyQueue<int>(mq);
    22. }

    按理来说结果应该和各行代码后面的理想推测相同,我们看看最后两行执行结果:

    可以看到和我们推测的结果完全一致,我们不仅在上面完成了一组插入删除后,额外执行了一组插入和删除操作,最后打印整个模拟队列中剩余元素时,呈现的结果和传入的顺序也相同,同样可以采用其他各种操作,统计每次取出的元素组成的序列是否满足先进先出的特性,结果终究是符合的。

    三、代码呈现

    下面直接给出代码:

    1. // 用两个栈实现队列
    2. template<typename T>
    3. class MyQueue
    4. {
    5. public:
    6. void appendTail(const T& n);
    7. T deleteHead();
    8. T& front();
    9. bool empty();
    10. private:
    11. void appendOver(); // 将st1中元素的全部移入st2中 --- 前提:st2不为空
    12. stack st1;
    13. stack st2;
    14. };
    15. template<typename T>
    16. void MyQueue::appendTail(const T& n)
    17. {
    18. st1.push(n);
    19. }
    20. template<typename T>
    21. void MyQueue::appendOver()
    22. {
    23. if (st2.empty()) // 只有满足st2为空才清空st1
    24. {
    25. while (!st1.empty())
    26. {
    27. st2.push(st1.top());
    28. st1.pop();
    29. }
    30. }
    31. }
    32. template<typename T>
    33. bool MyQueue::empty()
    34. {
    35. if (st1.empty() && st2.empty())
    36. {
    37. return true;
    38. }
    39. return false;
    40. }
    41. template<typename T>
    42. T MyQueue::deleteHead()
    43. {
    44. assert(!empty()); // 不能同时为空
    45. if (st2.empty())
    46. {
    47. appendOver();
    48. }
    49. T tmp_poped = st2.top();
    50. st2.pop();
    51. return tmp_poped;
    52. }
    53. template<typename T>
    54. T& MyQueue::front()
    55. {
    56. if (st2.empty())
    57. {
    58. appendOver();
    59. }
    60. assert(!st2.empty());
    61. return st2.top();
    62. }
    63. template<typename T>
    64. void PopAndPrintMyQueue(MyQueue& my_q)
    65. {
    66. while (!(my_q.empty()))
    67. {
    68. cout << my_q.front() << "\t";
    69. my_q.deleteHead();
    70. }
    71. cout << endl;
    72. }
    73. void test1()
    74. {
    75. MyQueue<int> mq;
    76. // queue.push()模拟尾插
    77. for (int i = 10; i > 0; i--)
    78. {
    79. mq.appendTail(i);
    80. } // 1 2 3 4 5 6 7 8 9 10 右侧栈顶
    81. // queue.front()模拟取首元素
    82. // appendTail()--push()模拟入队列操作
    83. // deleteHead()--pop()模拟删除队列首元素
    84. mq.deleteHead(); // st1中:空 st2中:1 2 3 4 5 6 7 8 9 右侧栈顶
    85. mq.appendTail(100); // st1中:100 st2中:1 2 3 4 5 6 7 8 9 右侧栈顶
    86. // 由于st2不为空,st1中元素不发生迁移
    87. // 经过上面两步 st1中:100 st2中:1 2 3 4 5 6 7 8 9
    88. // 假设我们再进行一组类似操作
    89. mq.deleteHead(); // st1中:100 st2中:1 2 3 4 5 6 7 8 右侧栈顶
    90. mq.appendTail(55); // st1中:55 100 st2中:1 2 3 4 5 6 7 8 右侧栈顶
    91. int val = mq.deleteHead(); // st1中:55 100 st2中:1 2 3 4 5 6 7 右侧栈顶
    92. printf("val = %d\n", val); // 8
    93. PopAndPrintMyQueue<int>(mq);
    94. }

    值得注意的是,上面对重要功能的安全性检查中,下面两种写法其本质是相同的:

    1. // 1.
    2. if (st2.empty())
    3. {
    4. appendOver();
    5. }
    6. assert(!st2.empty());
    7. // 2.
    8. assert(!empty()); // 不能同时为空
    9. if (st2.empty())
    10. {
    11. appendOver();
    12. }

    总结

            本文详细介绍了如何利用栈来实现队列的先进先出特性。通过使用两个栈,我们可以将插入的顺序和取出的顺序保持一致。文章讨论了具体的实现思路,并通过代码实例进行了测试。通过测试结果,我们验证了模拟队列的各种操作都满足先进先出的特性。这种使用栈实现队列的方法在实际应用中具有重要的意义。

  • 相关阅读:
    Groovy(第九节) Groovy 之单元测试
    虚拟机信息巡检脚本
    手机拍摄全景图并且使用Threejs实现VR全景,超简单
    k8s使用的iptables,具体原理是什么?一学就会
    OA项目之我的审批(查询&会议签字)
    SpringBoot-Mybatis批量插入Oracle数据库数据
    爬虫HTTP代理:获取多种类型数据的神器
    【Linux】exec函数族及进程控制相关
    数据优化 | CnOpenData中国工业企业专利及引用被引用数据
    一到汇报思绪乱? 学会这4个模型,高效表达无废话
  • 原文地址:https://blog.csdn.net/2301_78694061/article/details/134342859