目录
2.一个栈的入栈序列为ABCDE,则不可能的出栈序列为(D)
5.用无头单链表存储队列,其头指针指向队头结点,尾指针指向队尾结点,则在进行出队操作时(C)
9.现有一循环队列,其队头指针为front,队尾指针为rear,循环队列长度为N,最多存储N-1个数据。其队内有效长度为(B)
前言:
栈和队列->C语言【数据结构】栈与队列实现_糖果雨滴a的博客-CSDN博客_c语言实现栈和队列
一.OJ题
1.225. 用队列实现栈
(1)描述
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
(2)思路
用队列去实现栈,可以通过两个队列互相存放元素进行实现。当两个队列都为空时,要插入元素时,随便插入在任何一个队列即可,之后插入元素时,要插入在有元素的那个队列。
要删除元素时,因为要模拟实现栈,栈是后进先出,因此要删除的元素是刚刚插入的元素。因此要把不为空的那个队列中的元素放入空队列中,只留下非空的队列中的最后一个元素(队列的最后一个元素,就是刚刚插入的元素),然后把这个元素删掉即可。
要想返回栈顶元素,就直接返回非空队列的最后一个元素即可,判断栈是否为空就判断两个队列是否都为空。
代码:
class MyStack {
public:
queue<int> _q1;
queue<int> _q2;
MyStack() {
}
void push(int x) {
// q1和q2一定有一个不为空
// q1不为空
if(!_q1.empty())
{
_q1.push(x);
}
// q2不为空
else
{
_q2.push(x);
}
}
int pop() {
queue<int>* emptyQ = &_q1;
queue<int>* nonemptyQ = &_q2;
// 与假设不符,交换
if(!_q1.empty())
{
swap(emptyQ, nonemptyQ);
}
// 把非空队列的元素转移到空队列中,只留下队尾的元素
while(nonemptyQ->size() > 1)
{
emptyQ->push(nonemptyQ->front());
nonemptyQ->pop();
}
// 非空队列最后的队尾就相对于栈顶元素
int top = nonemptyQ->front();
nonemptyQ->pop();
return top;
}
int top() {
if(!_q1.empty())
{
return _q1.back();
}
else
{
return _q2.back();
}
}
bool empty() {
return _q1.empty() && _q2.empty();
}
};
2.232. 用栈实现队列
(1)描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
你只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
(2)思路
用栈去实现队列,可以通过一个栈专门去用来插入元素,另外一个栈在删除和返回队头时使用。
在s1中插入数据,当要删除数据或者返回队头数据时,如果s2为空,就先把s1中的数据放入s2中,这时候因为是栈,所以当s1的元素放入s2时,顺序会正好与之前相反,这时候就正好相当于队列,因此top就直接相当于队头,删除即可。取队头元素也是如此。
class MyQueue {
public:
stack<int> _s1;
stack<int> _s2;
MyQueue() {
}
void push(int x) {
_s1.push(x);
}
int pop() {
if(_s2.empty())
{
while(!_s1.empty())
{
_s2.push(_s1.top());
_s1.pop();
}
}
int top = _s2.top();
_s2.pop();
return top;
}
int peek() {
if(_s2.empty())
{
while(!_s1.empty())
{
_s2.push(_s1.top());
_s1.pop();
}
}
return _s2.top();
}
bool empty() {
return _s1.empty() && _s2.empty();
}
};
3.622. 设计循环队列
(1)描述
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1); // 返回 true
circularQueue.enQueue(2); // 返回 true
circularQueue.enQueue(3); // 返回 true
circularQueue.enQueue(4); // 返回 false,队列已满
circularQueue.Rear(); // 返回 3
circularQueue.isFull(); // 返回 true
circularQueue.deQueue(); // 返回 true
circularQueue.enQueue(4); // 返回 true
circularQueue.Rear(); // 返回 4
(2)思路
这里我们用数组来实现这个循环队列,这里也可以用单链表,但是数组要更好一些。
如果我们需要一个k个大小的数组,我们再设两个变量,一个指向第一个元素,一个指向最后一个元素,即头和尾,这时当头和尾相等时,我们不知道是数组满了还是数组为空,因此我们可以再额外开一个空间,即开k+1个空间。
这时当头和尾相等时,说明数组为空,当尾的下一个为头时,说明数组满了。
在写的过程中,要注意一些特殊情况,比如尾在最大空间处,这时尾的下一个就是0了,要控制好这些情况。
class MyCircularQueue {
public:
MyCircularQueue(int k) {
// 多开一个
_v.resize(k + 1);
_head = _tail = 0;
_k = k;
}
bool enQueue(int value) {
// 如果满了就插入失败
if(isFull())
{
return false;
}
// 没满就插入
_v[_tail] = value;
// 尾到最大空间时,要重新回到0的位置
if(_tail == _k)
{
_tail = 0;
}
// 尾正常往后走
else
{
++_tail;
}
return true;
}
bool deQueue() {
// 如果为空,就删除失败
if(isEmpty())
{
return false;
}
// 要删除的头到最大空间时,重新回到0的位置
if(_head == _k)
{
_head = 0;
}
else
{
++_head;
}
return true;
}
int Front() {
// 如果为空,就返回-1
if(isEmpty())
{
return -1;
}
return _v[_head];
}
int Rear() {
// 如果为空,就返回-1
if(isEmpty())
{
return -1;
}
// 尾在0的位置时,要返回前一个,就是k位置的值
if(_tail == 0)
{
return _v[_k];
}
// 正常情况,返回尾的前一个
else
{
return _v[_tail - 1];
}
}
bool isEmpty() {
// 尾和头相同时为空
return _tail == _head;
}
bool isFull() {
// 要注意尾在最后,头在最开始
if(_tail == _k && _head == 0)
{
return true;
}
// 正常情况,尾+1等于头就是满了
else
{
return _tail + 1 == _head;
}
}
private:
vector<int> _v;
int _head;
int _tail;
int _k;
};
4.20. 有效的括号
(1)描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例1:
输入:s = "()" 输出:true
示例2:
输入:s = "()[]{}" 输出:true
示例3:
输入:s = "(]" 输出:false
示例4:
输入:s = "([)]" 输出:false
示例5:
输入:s = "{[]}" 输出:true
(2)思路
利用栈后进先出的特性,如果是左括号就入栈,如果是右括号就与当前栈顶的括号(与右括号最近的括号)进行比较,如果相同,就删掉该栈顶括号,继续往后走,如果不相同,那么就不闭合。同时也要注意,如果在过程中,栈空了,却还有右括号,那么也说明不闭合。
最后,如果字符串全部遍历过了之后,栈内还有元素,说明左括号比右括号多了,这依旧说明不闭合。
如果字符串个数是奇数,那么肯定是不闭合的,只有偶数才可能对应闭合。
不用unordered_map:
class Solution {
public:
bool isValid(string s) {
// 字符串为奇数一定不闭合
if(s.size() % 2 == 1)
{
return false;
}
stack<char> st;
for (auto& e : s)
{
// 左括号,入栈
if (e == '(' || e == '[' || e == '{')
{
st.push(e);
}
// 右括号
else
{
// 栈空了,还有右括号,没闭合
if(st.empty())
{
return false;
}
// 栈顶元素与当前右括号比较,不对应,则不闭合
int top = st.top();
if (e == ')' && top != '('
|| e == ']' && top != '['
|| e == '}' && top != '{')
{
return false;
}
// 对应,就去掉该栈顶元素继续
else
{
st.pop();
}
}
}
// 如果字符串结束,栈还有括号,说明没闭合
if (st.empty())
{
return true;
}
else
{
return false;
}
}
};
用unordered_map:
class Solution {
public:
bool isValid(string s) {
// 字符串为奇数一定不闭合
if(s.size() % 2 == 1)
{
return false;
}
// 存入哈希表map中
unordered_map<char, char> um = { { ')' , '('}, { ']', '['}, { '}', '{'} };
stack<char> st;
for (auto e : s)
{
// 如果是右括号
if (um.count(e))
{
// 栈为空或者栈顶元素不是um中key对应的value(左括号),就不闭合
if (st.empty() || st.top() != um[e])
{
return false;
}
// 对应,就去掉该左括号
st.pop();
}
// 左括号,压栈
else
{
st.push(e);
}
}
// 栈还有括号就没闭合
return st.empty();
}
};
二.选择题
1.下列关于栈的叙述正确的是(B)
A.栈是一种“先进先出”的数据结构
B.栈可以使用链表或顺序表来实现
C.栈只能在栈底插入数据
D.栈不能删除数据
解释:
栈是一种线性结构,任何线性表都可以用来实现栈,只是实现方式有所差异。栈后进先出,只能在栈顶进行插入和删除操作。
2.一个栈的入栈序列为ABCDE,则不可能的出栈序列为(D)
A.ABCDE
B.EDCBA
C.DCEBA
D.ECDBA
解释:
如果是E先出,说明所有的元素都已经全部入栈,此时C不可能先于D出栈。
3.链栈与顺序栈相比,比较明显的优点是(C)
A.插入操作更加方便
B.删除操作更加方便
C.无需扩展空间大小
D.无需缩小空间大小
解释:
A:如果是链栈,一般需要进行头插操作,而顺序栈一般进行尾插操作,此时顺序栈的操作更方便。
B:类似于A,链栈进行头删,顺序栈进行尾删,仍然是顺序栈更方便
C:如果是顺序栈,插入元素要考虑增容问题,但是链栈不需要考虑增容,链栈每插入一个元素就对应开辟一个元素的空间。
D:删除操作,链栈需要释放空间,会有空间的缩小
4.下列关于用栈实现队列的说法中错误的是(B)
A.用栈模拟实现队列可以使用两个栈,一个栈模拟入队列,一个栈模拟出队列
B.每次出队列时,都需要将一个栈中的全部元素导入到另一个栈中,然后出栈即可
C.入队列时,将元素直接往模拟入队列的栈中存放即可
D.入队列操作时间复杂度为O(1)
解释:
一个栈模拟入队列,一个栈模拟出队列,出队列时直接弹出模拟出队列栈的栈顶元素,是当该栈为空时,将模拟入队列栈中所有元素导入即可,而不是每次都需要导入元素,故错误。
5.用无头单链表存储队列,其头指针指向队头结点,尾指针指向队尾结点,则在进行出队操作时(C)
A.仅修改队头指针
B.队头、队尾指针都要修改
C.队头、队尾指针都可能要修改
D.仅修改队尾指针
解释:
与5一样,出队操作,就是头删,一定会修改头指针,如果出队之后,队列为空,需要修改尾指针。
6.以下不是队列的基本运算的是(B)
A.从队尾插入一个新元素
B.从队列中删除队尾元素
C.判断一个队列是否为空
D.读取队头元素的值
解释:
队列只能从队头删除元素,只能头删和尾插
7.下面关于栈和队列的说法中错误的是(AB)
A.队列和栈通常都使用链表实现
B.队列和栈都只能从两端插入、删除数据
C.队列和栈都不支持随机访问和随机插入
D.队列是“先入先出”,栈是“先入后出”
解释:
顺序表的结构相比链表简单,不影响效率的情况下会优先使用顺序表。所以栈一般用顺序表实现,队列一般用链表实现。栈只能在一端插入和删除数据。
8.下列关于顺序结构实现循环队列的说法,正确的是(C)
A.循环队列的长度通常都不固定
B.直接用队头和队尾在同一个位置可以判断循环队列是否为满
C.直接用队头和队尾在同一个位置可以判断循环队列是否为空
D.循环队列是一种非线性数据结构
解释:
A:循环队列的长度是固定的,到最大空间处就会到起始点
B:队头的下一个是队尾时,说明循环队列满了
C:队头和队尾相同时,说明循环队列为空
D:循环队列是线性数据结构,可以用数组或链表实现
9.现有一循环队列,其队头指针为front,队尾指针为rear,循环队列长度为N,最多存储N-1个数据。其队内有效长度为(B)
A.(rear - front + N) % N + 1
B.(rear - front + N) % N
C.(rear - front) % (N + 1)
D.(rear - front + N) % (N - 1)
解释:
有效长度一般是rear-front,但是循环队列中rear有可能小于front,所以需要+N,最大长度为N,所以有效长度不可能超过N,故需要%N。