【刷题之路】LeetCode 225. 用队列实现栈
- 一、题目描述
- 二、解题
- 1、主要思路解析
- 2、先实现栈
- 3、实现各个接口
- 3.1、初始化接口
- 3.2、push接口
- 3.3、pop接口
- 3.4、myStackTop接口
- 3.5、myStackEmpty接口
- 3.6、myStackFree接口
一、题目描述
原题连接: 225. 用队列实现栈
题目描述:
请你仅使用两个队列实现一个后入先出(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
提示:
- 1 <= x <= 9
- 最多调用100 次 push、pop、top 和 empty
- 每次调用 pop 和 top 都保证栈不为空
进阶: 你能否仅用一个队列来实现栈。
二、解题
1、主要思路解析
其实这一题并不能算做一道算法题,它应该是一道练习题,目的就是练习我们对于队列和栈的熟悉度。
所以这道题其实逻辑并不复杂,复杂的是结构,因为我们将要用到很多的结构,还有很多结构套结构的地方。
想要解决这道题,其实主要思路就只有一个,就是想办法使用两个队列来模拟出栈的先进后出模型。
但是要怎么模拟呢?我们可以把注意力放到题目中的一个信息,也就是关于pop接口的:“int pop() 移除并返回栈顶元素。”。
我们知道,正经的栈的实现其实是并不需要返回栈顶元素的,但它这里为什么需要返回呢?
我们知道队列的规则是先进先出:
所以后面进去的数据都在队尾,而我们出数据只能在队头出,也就是所我们不能直接的将后面进去的在队尾的数据直接出,所以我们就不能直接使用一个队列实现栈的后进先出结构。
但我们可以使用两个队列:
然后当我们每次要pop数据的时候,我们先将不为空的队列中的数据出队,并将这些数据入到为空的一个队列中,直到出到只剩一个数据:
如上图,最后我们再将queue1中仅剩的一个数据在出队,但这次我们不把它入到queue2中,我们把它返回。
所以这就是为什么,题目要要求我们将出队的数据返回,其实题目就是通过检查我们返回的数据是否是最后面进去的来判断我们写的栈是否满足后进先出的规则。
而对于,压栈push,我们可以直接将数据入队到不为空的队列:
所以我们这种实现方案在每次执行完push或pop之后,都会有其中一个队列是为空的。
那这就是这道题的主要思路了,其实逻辑并不是很复杂。
2、先实现栈
因为我这里使用的是C语言,而C语言是并没有封装数据结构的,所以我们还得自己造轮子。
那我们就先将队列这个结构实现一下,我这里就直接复制我之前在【数据结构】和栈一样简单的结构——队列所写的队列了:
// 重定义数据类型
typedef int QDataType;
// 定义节点类型
typedef struct QueueNode {
struct QueueNode* next;
QDataType data;
} QueueNode;
// 定义队列类型
typedef struct Queue {
QueueNode* head;
QueueNode* tail;
} Queue;
// 队列的初始化
void QueueInit(Queue* pq);
// 队列的入队
void QueuePush(Queue* pq, QDataType x);
// 队列的出队
void QueuePop(Queue* pq);
// 返回队列的对头元素
QDataType QueueFront(Queue* pq);
// 返回队列的队尾元素
QDataType QueueBack(Queue* pq);
// 返回队列中的节点个数
int QueueSize(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 队列的初始化
void QueueInit(Queue* pq) {
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
// 队列的入队
void QueuePush(Queue* pq, QDataType x) {
assert(pq);
// 创建一个新节点
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (NULL == newNode) {
perror("malloc fail!\n");
exit(-1);
}
newNode->data = x;
if (NULL == pq->head) {
pq->head = newNode;
pq->tail = newNode;
pq->tail->next = NULL;
}
else {
pq->tail->next = newNode;
pq->tail = pq->tail->next;
pq->tail->next = NULL;
}
}
// 队列的出队
void QueuePop(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
// 如果对头为空了,我们也要把队尾也给置空,避免野指针
if (NULL == pq->head) {
pq->tail = NULL;
}
}
// 返回队列的对头元素
QDataType QueueFront(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
// 返回队列的队尾元素
QDataType QueueBack(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
// 返回队列中的节点个数
int QueueSize(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* cur = pq->head;
int size = 0;
while (cur) {
size++;
cur = cur->next;
}
return size;
}
// 判断队列是否为空
bool QueueEmpty(Queue* pq) {
assert(pq);
return pq->head == NULL;
}
// 销毁队列
void QueueDestroy(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* cur = pq->head;
QueueNode* next = cur->next;
while (cur) {
next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}
3、实现各个接口
3.1、初始化接口
初始化接口其实只有一个,那就是myStackCreate,因为oj的后台在测试的时候都是通过调用接口来测试的,所以我们需要将创建栈的接口也写上。
但同时我们也要先把,栈的结构体先写上,其实栈的结构体里就只有两个队列变量:
typedef struct {
Queue queue1;
Queue queue2;
} MyStack;
MyStack* myStackCreate() {
MyStack *stack = (MyStack*)malloc(sizeof(MyStack));
if (NULL == stack) {
perror("malloc fail!\n");
exit(-1);
}
QueueInit(&stack->queue1);
QueueInit(&stack->queue2);
return stack;
}
其实这里的栈的结构体里面的两个队列也可以使用指针的,但这样会比较麻烦一些,因为如果写成指针的话就需要在额外的开辟两个队列的空间,最后也要额外的销毁这两个队列指针,所以这里就直接创建成变量了。
然后在函数myStackCreate中我们要对这两个队列进行初始化。
3.2、push接口
因为我们是要把数据入到不为空的队列中,为了代码不冗余,我们可以先假设其中一个队列不为空,用一个指针NonEmptyQueue来指向。然后再进行判断,如果预先假设的那个队列不为空,就让NonEmptyQueue指针指向另一个队列。最后我们就对NonEmptyQueue指向的这个队列执行入队操作即可,这个操作我们就直接调用我们事先实现好的队列中的pus接口即可:
void myStackPush(MyStack* obj, int x) {
// 压栈我们要把数据入到不为空的队列的队尾
Queue *NonEmptyQueue = &obj->queue1; // 默认queue1队列不为空
Queue *EmptyQueue = &obj->queue2;
if (QueueEmpty(&obj->queue1)) { // 如果队列1为空,我们就要替换一下
NonEmptyQueue = &obj->queue2;
}
// 将数据入到不为空的队列
QueuePush(NonEmptyQueue, x);
}
3.3、pop接口
因为我们事先要把数据从不为空的队列中出队在入队到为空的队列中,所以和push一样,我们还是先假设其中一个队列不为空,然后在经过后面的判断,确保NonEmptyQueue 和EmptyQueue 指向正确的队列。
因为我们这里要操作的是两个队列,所以我们这里要确保这两个指针都指向正确地队列,而对于上面的push,我们就只需要保证一个指针即可。
int myStackPop(MyStack* obj) {
// 弹栈我们要先将不为空的队列先执行出队操作,并将数据入到为空的那个栈中,
// 直到不为空的队列出到队列中只剩一个元素
Queue *NonEmptyQueue = &obj->queue1; // 默认queue1队列不为空
Queue *EmptyQueue = &obj->queue2;
if (QueueEmpty(&obj->queue1)) { // 如果队列1为空,我们就要替换一下
NonEmptyQueue = &obj->queue2;
EmptyQueue = &obj->queue1;
}
while (QueueSize(NonEmptyQueue) > 1) {
// 先保存队头元素的值
int temp = QueueFront(NonEmptyQueue);
QueuePop(NonEmptyQueue);
QueuePush(EmptyQueue, temp);
}
// 先保存要返回的值
int returnVal = QueueFront(NonEmptyQueue);
QueuePop(NonEmptyQueue);
return returnVal;
}
最后在我们pop最后一个数据之前,要先将要返回的数据保存,因为接下来我们就要pop这个数据。
最后返回即可。
3.4、myStackTop接口
这个接口其实没什么好说的。
int myStackTop(MyStack* obj) {
Queue *NonEmptyQueue = &obj->queue1; // 默认queue1队列不为空
Queue *EmptyQueue = &obj->queue2;
if (QueueEmpty(&obj->queue1)) { // 如果队列1为空,我们就要替换一下
NonEmptyQueue = &obj->queue2; // 因为只需要操作一个队列,所以我们这里只需要保证一个指针
}
return QueueBack(NonEmptyQueue);
}
3.5、myStackEmpty接口
这个接口其实也很简单,我们直接返回两个队列是否为空的判断结果即可:
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->queue1) && QueueEmpty(&obj->queue2);
}
3.6、myStackFree接口
因为队列中的元素(节点)也是动态开辟出来的,所以我们再释放栈之前要先将其中一个不为空的队列先给释放掉,然后再释放掉栈。
void myStackFree(MyStack* obj) {
// 先要销毁队列,因为我们始终保持着一个队列为空,所以销毁只需要销毁空的那个队列即可
if (!myStackEmpty(obj)) { // 如果栈不为空,那我们就要先销毁队列
Queue *NonEmptyQueue = &obj->queue1; // 默认queue1队列不为空
Queue *EmptyQueue = &obj->queue2;
if (QueueEmpty(&obj->queue1)) { // 如果队列1为空,我们就要替换一下
NonEmptyQueue = &obj->queue2;
}
QueueDestroy(NonEmptyQueue);
}
// 再销毁栈
free(obj);
}
而对于栈指针的置空,我们这里没有选用二级指针,所以在这个接口内部是置空不了的,但这里是oj,所以我们并不需要。如果不是在oj,那我们就要讲这个操作交给使用者,就像free函数一样。
总结起来,这道题的逻辑操作其实就那一点,较为复杂的就是对结构的运用,因为这里要都是要调用队列中的各个接口来完成操作。
所以这道题其实也是值的我们多做几次的,以熟悉对队列和栈的结构和操作。