一、用栈实现队列 LeetCode链接
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(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(双端队列)来模拟一个栈,只要是标准的栈操作即可。
示例 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 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
思路
栈是先进后出,队列是先进先出,用两个栈实现队列,一个管输入stackIn,一个管输出stackOut。要实现队列的push(),pop(),peek()和empty()方法,
对于push方法直接把元素压到stackIn栈中即可.
pop()方法是本题关键,队列是先进先出,加入先进入了1,2,3那么出去也是1,2,3.但是在栈中直接输出为3,2,1.那不妨把这三个数依次出栈并压到out栈中,那么out栈中就为1,2,3,栈头为1.此时调用out栈的pop()方法即可。
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
if(stackOut.empty()){ //首先保证out栈为空,否则直接pop即可
// 每次调用pop都得把in栈的元素全都压入out栈,否则数据会混乱
while (!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
public int peek() {
// 求队头元素,先调用本队列的pop方法,保存数据后再压入栈中即可
int elem = this.pop();
stackOut.push(elem);
return elem;
}
public boolean empty() {
if(stackIn.empty() && stackOut.empty())
return true;
else
return false;
//return (stackIn.empty() && stackOut.empty());
}
}
二、用队列实现栈 LeetCode链接
请你仅使用 两/一 个队列实现一个后入先出(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方法和peek方法,直接调用队列的对应方法即可,因为此时队列中的元素排列顺序和用栈操作时一样。
(2)一个队列实现
用一个队列操作,也是模拟数据在栈中的顺序。当队列中没有元素时直接入队,当有多个时,先把数据入队,然后把它前面的数据依次出队并进入这个队列(总共执行size()- 1 次)。此时队列中数据的顺序和用栈操作时一样。
public class MyStack {
Queue<Integer> queue1;
// Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
// queue2 = new LinkedList<>();
}
public void push(int x) {
// queue2.offer(x);
// while (!queue1.isEmpty()){
// queue2.offer(queue1.poll());
// }
// Queue<Integer> q3 = queue1;
// queue1 = queue2;
// queue2 = q3;
queue1.offer(x);
for (int i = 0; i < queue1.size() - 1; i++) {
int elem = queue1.poll();
queue1.offer(elem);
}
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
if (queue1.isEmpty())
return true;
else
return false;
// return queue1.empty();
}
}
三、有效的括号 LeetCode链接
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()" 输出:true
示例 2:
输入:s = "()[]{}" 输出:true
示例 3:
输入:s = "(]" 输出:false
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
思路
本题考虑括号的配对。对于左括号,依次把它们对应的右括号压入栈中,遍历到右括号时,拿它和栈顶元素比较。
return false有好几种情况,(1)左括号多,即字符串遍历结束后栈不为空。(2)右括号多,即当字符串还未遍历完时栈已为空。(3)括号不匹配,当遍历的右括号和栈顶元素不一样时,表明该右括号和其对应位置上的左括号不匹配。
当以上可能都没发生,即字符串遍历结束后栈恰为空,且在比较过程中,遍历的字符和栈顶元素的一样,则返回ture。
public class Brackets {
public boolean isValid(String s) {
if (s.length() % 2 != 0)
return false;
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
if ('(' == s.charAt(i))
stack.push(')');
else if ('[' == s.charAt(i))
stack.push(']');
else if ('{' == s.charAt(i))
stack.push('}');
else if (stack.empty() || !stack.peek().equals(s.charAt(i)))
return false;
else
stack.pop();
}
if (stack.empty())
return true;
else
return false;
}
}
四、删除字符串中的所有相邻重复项 LeetCode链接
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca" 输出:"ca" 解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
思路
本题用栈做很简单,遍历字符串,每次取一个字符,当栈为空时直接入栈,否则就拿该字符和栈顶元素比较,若相等则出栈一次,否则就把该字符压入栈中,直到字符串遍历完。
public class DeleteSame {
public String removeDuplicates(String s) {
Deque<Character> myStack = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
if ( myStack.isEmpty() || myStack.peek() != s.charAt(i)){
myStack.push(s.charAt(i));
}else
myStack.pop();
}
String s1 = "";
while (myStack.size() != 0){
s1 = myStack.pop() + s1;
}
return s1;
}
}
五、 逆波兰表达式求值 LeetCode链接
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入:tokens = ["2","1","+","3","*"] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22
思路
逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
-
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
-
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
本题的做法即为遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
public class EvalRPN {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < tokens.length; i++) {
if ("+".equals(tokens[i]))
stack.push(stack.pop() + stack.pop());
else if ("-".equals(tokens[i]))
stack.push(-stack.pop() + stack.pop());
else if ("*".equals(tokens[i]))
stack.push(stack.pop() * stack.pop());
else if ("/".equals(tokens[i])){
int num1 = stack.pop();
int num2 = stack.pop();
stack.push(num2 / num1);
}
else
stack.push(Integer.valueOf(tokens[i]));
}
return stack.pop();
}
}
六、滑动窗口最大值 LeetCode链接
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1 输出:[1]
思路
我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。每次窗口移动的时候就调用一次pop()和push(),让队列中的元素跟着窗口里的元素一起变化,之后再调用getMaxValue()返回最大值。
然后在分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
(1)自定义单调队列
public class MyQueue {
Deque<Integer> deque = new LinkedList<>();
void push(int val){
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,
// 直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
while (!deque.isEmpty() && val > deque.peekLast()){
deque.pollLast();
}
deque.offerLast(val);
}
void pop(int val){
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,
// 如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
if (!deque.isEmpty() && val == deque.peekFirst()){
deque.pollFirst();
}
}
int getMaxValue(){
return deque.peekFirst();
}
}
(2)题解
public class MaxSlidingWindow {
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length - k + 1;
int[] arr = new int[len];
MyQueue myQueue = new MyQueue();
// 先把前k个元素放入队列中,
// 因为刚开始只需要push和getMaxValue操作,不需要pop
for (int i = 0; i < k; i++) {
myQueue.push(nums[i]);
}
int count = 0;
arr[count++] = myQueue.getMaxVlue();
for (int i = k; i < nums.length; i++) {
myQueue.pop(nums[i - k]);
myQueue.push(nums[i]);
arr[count++] = myQueue.getMaxVlue();
}
return arr;
}
}
总结
1.对于栈和队列来说,要了解清楚其底层代码是如何书写的,这样用的时候才能得心应手。
2.栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
3.要理解单调队列和优先级队列。