【刷题之路】LeetCode 232. 用栈实现队列

【刷题之路】LeetCode 232. 用栈实现队列

  • 一、题目描述
  • 二、解题
    • 1、图解主要思路
    • 2、先实现栈
    • 3、实现各个接口
      • 3.1、初始化接口
      • 3.2、入队接口
      • 3.3、出队接口
      • 3.4、取队头接口
      • 3.5、判空接口
      • 3.6、释放接口

一、题目描述

原题连接: 232. 用栈实现队列
题目描述:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:

  1. void push(int x) 将元素 x 推到队列的末尾
  2. int pop() 从队列的开头移除并返回元素
  3. int peek() 返回队列开头的元素
  4. boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  1. 你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  2. 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
[“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

提示:

  1. 1 <= x <= 9
  2. 最多调用 100 次 push、pop、peek 和 empty
  3. 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

进阶:
你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

二、解题

1、图解主要思路

其实这题和[225. 用队列实现栈]一样,也是属于栈和队列的应用练习题。
所以其实现思路也是相似的,也是通过返回出队元素的方法来模拟出队列的先进先出结构。
其实这一题也是要用到两个栈,但是我们怎样设计思路呢?
其实我们可以对这两个栈的任务进行分工,一个栈专门用来入队,一个栈专门用来出栈:
【刷题之路】LeetCode 232. 用栈实现队列_第1张图片
那么对于入队操作,我们就可以直接将数据压入到“入队栈”中:
【刷题之路】LeetCode 232. 用栈实现队列_第2张图片
而对于出队操作,我们就要先检查“出队栈”是否为空,如果为空的话,就要先将“入队栈”中的数据逐一弹出再压入“出队栈”中:
【刷题之路】LeetCode 232. 用栈实现队列_第3张图片
这其实就等于将“入队栈”中的数据给逆置了,然后我们就可以将“出队栈”的栈顶数据弹栈,最后返回了。
而在后面的出队操作中,如果“出队栈”不为空,我们就可以直接将“出队栈”的栈顶数据弹出并返回了,直到“出队栈”再次为空,那我们就要再次将“入队栈”清空。
不知道大家有没有注意到,其实我们可以把这两个栈形象的看成一个一根U型管的两端:
【刷题之路】LeetCode 232. 用栈实现队列_第4张图片
我们可以假设“入队栈”的数据是通过下边的弯的管道“流”过去的,这样即完成了“入队栈”的数据的逆序,并且也符合了先进先出。

好了,其实这一道题的主要实现逻辑就是这一点,剩下来的就是结构的问题了。

2、先实现栈

同样的,因为我这里选择的是C语言实现,所以我们要先把栈实现一下,我这里就直接赋值我之前的【数据结构】简单到有摸鱼负罪感的栈的实现中的栈的实现了:

// 重定义数据类型
typedef int DataType;

// 定义栈结构
typedef struct stack {
	DataType* data;
	int top;
	int capacity;
} Stack;

// 栈的初始化
void StackInit(Stack* ps);

// 压栈
void StackPush(Stack* ps, DataType x);
// 弹栈
void StackPop(Stack* ps);
// 返回栈顶数据
DataType StackTop(Stack* ps);
// 返回栈的数据个数
int StackSize(Stack* ps);
// 判断栈是否为空
bool StackEmpty(Stack* ps);
// 栈的销毁
void DestroyStack(Stack* ps);

// 栈的初始化
void StackInit(Stack* ps) {
	assert(ps);
	ps->data = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

// 压栈
void StackPush(Stack* ps, DataType x) {
	assert(ps);
	// 检查是否需要增容
	if (ps->top == ps->capacity) {
		int newCapacity = ps->capacity == 0 ? 10 : ps->capacity * 2;
		DataType* temp = (DataType*)realloc(ps->data, newCapacity * sizeof(DataType));
		if (NULL == temp) {
			perror("ralloc fail!\n");
			exit(-1);
		}
		ps->data = temp;
		ps->capacity = newCapacity;
	}
	ps->data[ps->top] = x;
	ps->top++;
}

// 弹栈
void StackPop(Stack* ps) {
	assert(ps);
	assert(ps->top > 0);
	ps->top--;
}

// 返回栈顶数据
DataType StackTop(Stack* ps) {
	assert(ps);
	assert(!StackEmpty(ps));
	return ps->data[ps->top - 1];
}

// 返回栈的数据个数
int StackSize(Stack* ps) {
	assert(ps);
	assert(ps->top >= 0);
	return ps->top;
}

// 判断栈是否为空
bool StackEmpty(Stack* ps) {
	assert(ps);
	return ps->top == 0;
}

// 栈的销毁
void DestroyStack(Stack* ps) {
	assert(ps);
	free(ps->data);
	ps->data = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

3、实现各个接口

3.1、初始化接口

在myQueueCreate接口中,我们不仅要将队列开辟好,同时也要将队列中的,栈给初始化了,初始化就直接调用栈中实现的接口即可。

typedef struct {
    Stack headStack; // 队头栈,用于出队
    Stack tailStack; // 队尾栈,用于入队
} MyQueue;

MyQueue* myQueueCreate() {
    MyQueue *queue = (MyQueue*)malloc(sizeof(MyQueue));
    if (NULL == queue) {
        perror("malloc fail!\n");
        exit(-1);
    }
    StackInit(&queue->headStack);
    StackInit(&queue->tailStack);
    return queue;
}

3.2、入队接口

入队没什么好说的,统一将数据压入到“入队栈”中:

void myQueuePush(MyQueue* obj, int x) {
    // 对于入队,我们可以统一把数据压入队尾栈
    StackPush(&obj->tailStack, x);
}

3.3、出队接口

出队就要分两种情况,如果“出队栈”不为空,就直接将“出队栈”的栈顶元素弹出即可,如果为空,这要将“入队栈”清空,将数据压入到“出队栈”中。

int myQueuePop(MyQueue* obj) {
    int returnVal = 0;
    if (!StackEmpty(&obj->headStack)) {
        returnVal = StackTop(&obj->headStack);
        StackPop(&obj->headStack);
    } else {
        // 先将队尾栈的数据全部弹出,压到队尾栈
        while (!StackEmpty(&obj->tailStack)) {
            int temp = StackTop(&obj->tailStack);
            StackPop(&obj->tailStack);
            StackPush(&obj->headStack, temp);
        }
        returnVal = StackTop(&obj->headStack);
        StackPop(&obj->headStack);
    }
    return returnVal;
}

要注意的是,在真正弹栈之前需要先将栈顶元素的值保存,最后返回。

3.4、取队头接口

取队头其实和出队是一样的逻辑,只不过不用将数据真正弹栈:

int myQueuePeek(MyQueue* obj) {
    // 这个操作其实也和Pop一样,如果队头栈为空,就要先队尾栈的数据全都压入到队头栈,返回栈顶
    if (StackEmpty(&obj->headStack)) {
        // 先将队尾栈的数据全部弹出,压到队尾栈
        while (!StackEmpty(&obj->tailStack)) {
            int temp = StackTop(&obj->tailStack);
            StackPop(&obj->tailStack);
            StackPush(&obj->headStack, temp);
        }
    }
    return StackTop(&obj->headStack);
}

3.5、判空接口

判空的话,我们返回两个栈是否同时为空的判断结果即可:

bool myQueueEmpty(MyQueue* obj) {
    return StackEmpty(&obj->headStack) && StackEmpty(&obj->tailStack);
}

3.6、释放接口

如果队列不为空的话,还要将队列里的两个栈先释放掉,如果为空,我们就直接释放掉队列即可。

void myQueueFree(MyQueue* obj) {
    if (!myQueueEmpty(obj)) {
        // 如果队列不为空,我们就要先将栈销毁
        DestroyStack(&obj->headStack);
        DestroyStack(&obj->tailStack);
    }
    free(obj);
}

总结起来,这一题其实和225. 用队列实现栈是一样的,也是逻辑复杂,结构稍复杂一点儿。但也是值得我们多练习几遍,以加深对栈和队列的理解。

你可能感兴趣的:(刷题之路——简单篇,leetcode,算法,数据结构,c语言)