栈和队列的常见题型

一、常见题型如下:

1. 实现一个栈,要求实现Push(出栈)、Pop(入栈)、Min(返回最小值的操作)的时间复杂度为O(1)
2. 使用两个栈实现一个队列

3. 使用两个队列实现一个栈

4. 元素出栈、入栈顺序的合法性。如入栈的序列(1,2,3,4,5),出栈序列为(4,5,3,2,1)

5. 一个数组实现两个栈。

二、解法分析及示例代码

1.第一题前两种操作Push(出栈)、Pop(入栈)比较好实现,也是栈中比较基本的操作,题目没有明确要求所以我们可以直接用STL容器,进行个封装即可,stack中这两个操作的时间复杂度就是O(1),但是第三个操作Min(返回最小值的操作)的时间复杂度为O(1),则要我们自己想办法。思路是用两个栈,一个就是普通的存取数据的栈,另一个为当前栈未知的最小值,因为出栈入栈都会使当前栈的最小值发生变化,所以插入数据和删除数据两个栈都要进行操作,返回当前栈最小值的话,就可以直接对第二个栈操作。

★使用两个栈,s1,s2,先将第一个元素同时对s1,s2入栈,然后接着对s1入栈,将s1入栈的元素和s2.top比较,假如s2.top大于入栈元素,则把该元素对s2也入栈(?s2.top和入栈元素相等时也要入栈S2);否则将下一个元素对主栈S1入栈,依次进行;

★出栈时,把s1出栈的元素和栈顶(s2.top)比较,相等就是最小值;同时s1,s2出栈,否则s1单独出栈。

这种方法在出现大量重复的数据情况下辅助栈也会保存大量重复的最小值数据,这时空间利用率较低。这个时候就想到了另一种方案,就是引用计数,将重复的最小值数据用引用计数来实现存储。实现图示如下:

代码示例:

 1 template <class T>
 2 class MyStack
 3 {
 4 public:
 5     void Push(const T& x)
 6     {
 7         _st.push(x);  //数据栈先入栈
 8         if (min_st.empty())  //如果Min栈之前没有数据,则直接入栈
 9         {
10             min_st.push(make_pair(x, 1);  //x入Min栈,并置计数为1
11         }
12         else  {
13             pair<T, count>& _min = min_st.top();
14             if (x < _min.first)  //如果入数据栈的数据小于Min栈顶数据,则也入MIn栈
15             {
16                 min_st.push(make_pair(x, 1));   //x入Min栈,并置计数为1
17             }else if (x == _min.first)  //如果入数据栈的数据等于Min栈顶数据,即出现冗余数据,计数加1
18             {
19                 _min.second++;
20             }
21         }
22     }
23     void Pop()
24     {
25         assert(!_st.empty());
26         pair<x, count>& _min = min_st.top();
27         if (_st.top() == _min.first)
28         {
29             if (--_min.second == 0) //计数为1时,最小值出栈,否则计数减1
30             {
31                 min_st.pop();
32             }
33         }
34         _st.pop();
35     }
36     T Min()
37     {
38         return min_st.top();
39     }
40 private:
41     stack<T> _st;  //数据栈
42     stack<pair<T, count>> min_st;  //最小值栈
43 };

2.第二题思路是:两个栈s1,s2,始终维护s1作为存储空间,以s2作为临时缓冲区。

入队时,将元素压入s1。

出队时,将s1的元素逐个“倒入”(弹出并压入)s2,将s2的顶元素弹出作为出队元素,之后再将s2剩下的元素逐个“倒回”s1。

见下面示意图:

实现代码如下:

 1 template <class T>
 2 class TwoStackQueue
 3 {
 4 public:
 5     void Push(T k)
 6     {
 7         s1.push(k);
 8     }
 9     void Pop()
10     {
11         if (s1.empty() && s2.empty() == false) {
12             while (s1.empty())
13             {
14                 s2.push(s1.top());
15                 s1.pop();
16             }
17             s2.pop();
18             while (s2.empty())
19             {
20                 s1.push(s2.top());
21                 s2.pop();
22             }
23         }
24     }
25 private:
26     stack<T> s1;
27     stack<T> s2;
28 };

这个题还可以优化一下。即:在出队时,将s1的元素逐个“倒入”s2时,原在s1栈底的元素,不用“倒入”s2(即只“倒”s1.Count()-1个),可直接弹出作为出队元素返回。这样可以减少一次压栈的操作。

上述思路,有些变种,如:

入队时,先判断s1是否为空,如不为空,说明所有元素都在s1,此时将入队元素直接压入s1;如为空,要将s2的元素逐个“倒回”s1,再压入入队元素。

出队时,先判断s2是否为空,如不为空,直接弹出s2的顶元素并出队;如为空,将s1的元素逐个“倒入”s2,把最后一个元素弹出并出队。

相对于第一种方法,变种的s2好像比较“懒”,每次出队后,并不将元素“倒回”s1,如果赶上下次还是出队操作,效率会高一些,但下次如果是入队操作,效率不如第一种方法。我有时会让面试者分析比较不同方法的性能。我感觉(没做深入研究),入队、出队操作随机分布时,上述两种方法总体上时间复杂度和空间复杂度应该相差无几(无非多个少个判断)。

真正性能较高的,其实是另一个变种。即:

入队时,将元素压入s1。

出队时,判断s2是否为空,如不为空,则直接弹出顶元素;如为空,则将s1的元素逐个“倒入”s2,把最后一个元素弹出并出队。

这个思路,避免了反复“倒”栈,仅在需要时才“倒”一次。

3.第三题思路是:q1是专职进出栈的,q2只是个中转站

入栈:直接入队列q1即可

出栈:把q1的除最后一个元素外全部转移到队q2中,然后把刚才剩下q1中的那个元素出队列。之后把q2中的全部元素转移回q1中。

代码示例:

 1     template<class T>
 2     class Queue
 3     {
 4     public:
 5         void Push(const T& x)
 6         {
 7             _in.push(x);
 8         }
 9
10         void Pop()
11         {
12             assert(!_in.empty() || !_out.empty());
13
14             if (_out.empty())
15             {
16                 while (!_in.empty())
17                 {
18                     _out.push(_in.top());
19                     _in.pop();
20                 }
21             }
22
23             _out.pop();
24         }
25
26         T& Front()
27         {
28             assert(!_in.empty() || !_out.empty());
29
30             if (_out.empty())
31             {
32                 while (!_in.empty())
33                 {
34                     _out.push(_in.top());
35                     _in.pop();
36                 }
37             }
38
39             return _out.top();
40         }
41
42
43     private:
44         stack<T> _in;
45         stack<T> _out;
46     };

思路二:

q1是专职进出栈的,q2只是个中转站。元素集中存放在个栈中,但不是指定(q1 或 q2)。定义两个指针:pushtmp:指向专门进栈的队列q1; tmp:指向临时作为中转站的另一个栈q2

入栈:直接入pushtmp所指队列即可

出栈:把pushtmp的除最后一个元素外全部转移到队列tmp中,然后把刚才剩下q1中的那个元素出队列

示例代码二:

 1 void Push(Queue *q1, Queue *q2, int k)
 2 {
 3         Queue *pushtmp;
 4         if(!IsQueueEmpty(q1))
 5         {
 6             pushtmp = q1;
 7         }
 8         else
 9         {
10             pushtmp = q2;
11         }
12         EnQueue(pushtmp, k);
13 }
14
15 int  Pop(Queue *q1, Queue *q2)
16 {
17     int tmpvalue;
18     Queue *pushtmp, *tmp;
19     if(!IsQueueEmpty(q1))
20     {
21         pushtmp = q1;
22         tmp = q2;
23     }
24     else
25     {
26         pushtmp = q2;
27         tmp = q1;
28     }
29
30     if(IsQueueEmpty(pushtmp))
31     {
32        printf("Stack Empty!\n");
33     }
34     else
35     {
36         while(SizeOfQueue(pushtmp) != 1)
37         {
38             EnQueue(tmp, DeQueue(pushtmp));
39         }
40         tmpvalue = DeQueue(pushtmp);
41         return tmpvalue;
42     }
43 }

比较: 在第二种思路中,出栈后就不用转移回原来的栈了(图示最后一步),这样减少了转移的次数。

4.第四题是比较常见的判断出栈顺序是否合法性的题目,思路是:

1)用一个 st 栈做入栈序列(1,2,3,4,5)出入栈操作,然后定义指针 in 和 out ,in 指向入栈序列,out 指向需要判断的出栈序列;

2)将入栈序列第一个数据入栈,如果此数据与第一个出栈序列数据相等,则再出栈,再比较两个序列的第二个数据;

3)若不相等,入栈序列第二个数据入栈,再与出栈序列比较;

4)若入栈序列遍历完且临时栈已空,而出栈序列没完,则不合法,若出栈序列也遍历完,则合法。

示例代码:

 1     bool IsInvalidStack(char* in, char* out)
 2     {
 3         assert(in && out);
 4
 5         stack<char> st;
 6         while (1)
 7         {
 8             while (!st.empty())
 9             {
10                 if (*out && st.top() == *out)
11                 {
12                     st.pop();
13                     ++out;
14                 }
15                 else
16                 {
17                     break;
18                 }
19             }
20
21             if (*in)
22             {
23                 st.push(*in);
24                 ++in;
25             }
26             else
27             {
28                 if (*out)
29                 {
30                     return false;
31                 }
32                 else
33                 {
34                     return true;
35                 }
36             }
37         }
38     }
39     void TestIsInvalidStack()
40     {
41         char* in = "12345";
42         char* out = "45321";
43         cout<<IsInvalidStack(in, out)<<endl;
44     }

5.第5题有多种思路解题

①奇偶位存储:数组的奇数位置存储一个栈的元素,偶数位置存储另一个栈的元素;

示例代码:

 1 template<class T, size_t N>
 2     class DoubleStack
 3     {
 4     public:
 5         DoubleStack()
 6             :_top1(-2)
 7             ,_top2(-1)
 8         {}
 9
10         void Check(int pos)
11         {
12             if (pos >= _a.size())
13             {
14                 _a.resize(pos*2);
15             }
16         }
17
18         void Push1(const T& x)
19         {
20             Check(_top1+2);
21             _a[_top1+2] = x;
22         }
23
24         void Push2(const T& x)
25         {
26             _a[_top2+2] = x;
27         }
28
29     private:
30         vector<T> _a;
31         int _top1;
32         int _top2;
33     };

②左右存储:两个栈分别从数组的中间向两头增长; 数组的中间位置看做两个栈的栈底,压栈时栈顶指针分别向两边移动,当任何一边到达数组的起始位置或是数组尾部,则开始扩容;

实际上方法二与方法一空间利用率相当本质上相同,所以方法二代码就不实现了。

③两端生长:两个栈分别从数组的两头开始增长。 将数组的起始位置看作是第一个栈的栈底,将数组的尾部看作第二个栈的栈底, 压栈时,栈顶指针分别向中间移动,直到两栈顶指针相遇,则扩容;

示例代码:

 1 //两个栈分别从数组的两头开始向中间增长。
 2 template <class T>
 3 class DoubleStack2
 4 {
 5 public:
 6     void Push(int index, T data)
 7     {
 8         if (top1 > top2)  //已满
 9             return;
10         if (index == 0)  {  //对栈1操作
11             top1++;
12             array[top1] = data;
13         }
14         else if(index == 1) {  //对栈2操作
15             top2--;
16             array[top2] = data;
17         }
18     }
19     void Pop(int index)
20     {
21         if (index == 0) {  //对栈1操作
22             if (top1 >= 0)  //栈1不为空时
23                 top1--;
24         }
25         else if (index == 1) {  //对栈2操作
26             if (top2 < MAX)    //栈2不为空时
27                 top2++;
28         }
29     }
30     T& top(int index)
31     {
32         if (index == 0 && top1 > 0) {  //对栈1操作
33             return array[top1 - 1];
34         }
35         if (index ==1 && top2 < MAX) {
36             return array[top2 + 1]
37         }
38     }
39 private:
40     T* array[MAX];
41     int top1;
42     int top2;
43 };

前两种方法在一定的情况下会造成空间的浪费,比如当一个栈存满了而另一个栈还是空的,这时候空间利用率不高,所以建议采用第三种方式完成。当一个栈一直入栈,另一个栈一直出栈时,方法三的空间利用率是比较好的。

时间: 2024-12-13 06:45:01

栈和队列的常见题型的相关文章

栈和队列常见题型(java版)

直接上干货..... 栈和队列常见题型: 实现栈和实现队列. 两个栈实现一个队列. 设计栈,使得pop,push和min时间复杂度为O(1). 滑动窗口的最大值. 栈的进出序列. 实现栈和实现队列 主要包括:栈,队列,循环队列. package com.sywyg; /** * 实现栈 * 数组应该是Object类型的 * 注意top的设置影响出栈和获取栈顶元素. * size也可以用top代替 */ class MyStack<E>{ // 栈元素个数 private int size; /

栈和队列总结

一 基础知识 1. 均为线性表,可以由数组或链表实现 栈:先进后出,操作均在栈顶进行 队列:先进先出,队尾进,队首出 2.  STL stack & queue stack 常见操作: s.push(x):入栈 (void类型) s.pop(): 出栈 (void类型,只删除,不返回元素) s.top(): 返回栈顶元素 s.size():返回栈元素个数 s.empty() :判断栈是否为空 queue 常见操作: q.push(x): 入队 q.pop(): 出队 (void类型,只删除,不返

快速记忆数组栈和队列函数push()和shift()

在js中,对数组的操作是比较常见的,有时候,我们需要模拟栈和队列的特性才能实现需求,今天来给大家用通俗易懂.简洁明了的几行文字,来告诉大家栈和队列的几个函数,如何快速记住. 首先,概念还是要知道的: 栈(stack)又名堆栈,它是一种运算受限的线性表.其限制是仅允许在表的一端进行插入和删除运算.这一端被称为栈顶,相对地,把另一端称为栈底.向一个栈插入新元素又称作进栈.入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素:从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻

3-5-表达式求值-栈和队列-第3章-《数据结构》课本源码-严蔚敏吴伟民版

课本源码部分 第3章  栈和队列 - 表达式求值 ——<数据结构>-严蔚敏.吴伟民版        源码使用说明  链接??? <数据结构-C语言版>(严蔚敏,吴伟民版)课本源码+习题集解析使用说明        课本源码合辑  链接??? <数据结构>课本源码合辑        习题集全解析  链接??? <数据结构题集>习题解析合辑        本源码引入的文件  链接? SequenceStack.c        相关测试数据下载  链接? 无数据

3-6-汉诺塔(Hanoi Tower)问题-栈和队列-第3章-《数据结构》课本源码-严蔚敏吴伟民版

课本源码部分 第3章  栈和队列 - 汉诺塔(Hanoi Tower)问题 ——<数据结构>-严蔚敏.吴伟民版        源码使用说明  链接??? <数据结构-C语言版>(严蔚敏,吴伟民版)课本源码+习题集解析使用说明        课本源码合辑  链接??? <数据结构>课本源码合辑        习题集全解析  链接??? <数据结构题集>习题解析合辑        本源码引入的文件  链接? 无外链        相关测试数据下载  链接? 无数

数据结构-栈和队列

栈和队列都是线性表,所以满足-只有一个节点没有前继,只有后继,只有一个节点只有后继没有前继,其他的节点只有一个前继只有一个后继. 栈的定义是先进后出,最典型的例子就是弹夹,最先进去的反而是最后射出来的,在实际的软件开发中会进经常的遇到这种类型的线性表,我们成为LIFO(Last in First out).可以把栈想象成是只有一个出口的容器,最先放进去的东西只能够等其上面的东西呗拿走之后才能够拿出来. 队列则是另外的一种线性表,队列在我们生活中就更常见了,比如排队啊什么的,队列讲的是先进先出,在

用JS描述的数据结构及算法表示——栈和队列(基础版)

前言:找了上课时数据结构的教程来看,但是用的语言是c++,所以具体实现在网上搜大神的博客来看,我看到的大神们的博客都写得特别好,不止讲了最基本的思想和算法实现,更多的是侧重于实例运用,一边看一边在心里隐隐歌颂大神的厉害,然后别人的厉害不是我的,所以到底看得各种受打击+头昏脑涨,写这个系列是希望自己能够总结学到东一块.西一下的知识,因为水平有限+经验不足,所以在此只说最基础的思想,附上我自己的算法实现(肯定还有更优解),如果要想看进阶版的,可以在园里搜“数据结构”,各种语言实现和进阶提升的文章有很

数据结构与算法分析:栈与队列

以下是对数据结构中的栈和队列的一些总结: 一.栈 栈(Stack)是一种特殊的线性表,有后进先出(Last In First Out, LIFO)的性质,且只能从线性表的一段进行插入和删除元素等操作. 栈的常用操作有:进栈.出栈.取栈顶.将栈置空.判断栈是否为空.判断栈是否已满等等. 由于栈也属于线性表,因此线性表的存储结构对栈也适用,因此,使用数组或者单向链表均可以实现栈.这两种存储结构的不同,因此实现栈的方式也有不同,形成的栈的性质也有所不同.一个常见的区别是,在顺序栈中有"上溢"

小猪的数据结构辅助教程——3.1 栈与队列中的顺序栈

小猪的数据结构辅助教程--3.1 栈与队列中的顺序栈 标签(空格分隔): 数据结构 本节学习路线图与学习要点 学习要点 1.栈与队列的介绍,栈顶,栈底,入栈,出栈的概念 2.熟悉顺序栈的特点以及存储结构 3.掌握顺序栈的基本操作的实现逻辑 4.掌握顺序栈的经典例子:进制变换的实现逻辑 1.栈与队列的概念: 嗯,本节要进行讲解的就是栈 + 顺序结构 = 顺序栈! 可能大家对栈的概念还是很模糊,我们找个常见的东西来拟物化~ 不知道大家喜欢吃零食不--"桶装薯片"就可以用来演示栈! 生产的时