力扣题目链接
使用栈实现队列的下列操作:
push(x) – 将一个元素放入队列的尾部。
pop() – 从队列首部移除元素。
peek() – 返回队列首部的元素。
empty() – 返回队列是否为空。
示例:
MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false
说明:
两个栈实现一个队列,一个栈顺序存放元素,一个栈逆序存放元素,两个栈同时只能一个栈存放元素,要么是顺序存放,要么是逆序存放。
class MyQueue {Stack stack1;Stack stack2;public MyQueue() {//顺序存放元素stack1 = new Stack<>();//逆序存放元素stack2 = new Stack<>();//两个栈同时只能一个栈存放元素,要么顺序存放,要么逆序存放}public void push(int x) {while (!stack2.isEmpty()) {stack1.push(stack2.pop());}stack1.push(x);}public int pop() {while (!stack1.isEmpty()) {stack2.push(stack1.pop());}return stack2.pop();}public int peek() {while (!stack1.isEmpty()) {stack2.push(stack1.pop());}return stack2.peek();}public boolean empty() {return stack1.size() == 0 && stack2.size() == 0;}
}
力扣题目链接
使用队列实现栈的下列操作:
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回 true
;否则,返回 false
。注意:
栈和队列只是一种抽象概念,底层可以用任意数据结构实现。只需保证维护栈和队列基本特征,栈:先进后出
;队列:先进先出
。任何实现都是OK的。
下面就是使用一个队列来实现栈。【双端队列直接调API】
class MyStack {//使用一个队列来实现栈LinkedList queue;public MyStack() {queue = new LinkedList<>();}public void push(int x) {queue.addFirst(x);}public int pop() {return queue.removeFirst();}public int top() {return queue.getFirst();}public boolean empty() {return queue.isEmpty();}
}
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
只需要统一操作规则,实现起来就不算难,例如:我的pop()和top()操作,有着统一的规则:让queue1每次都存放的是栈顶元素
代码实现
public class MyStack2 {/*** 使用两个队列来实现栈*/Queue queue1;Queue queue2;public MyStack2() {queue1 = new LinkedList<>();queue2 = new LinkedList<>();}public void push(int x) {queue1.offer(x);}public int pop() {//queue1只维护一个栈顶元素int size = queue1.size();//size >= 1while (size > 1){queue2.offer(queue1.poll());size--;}if (size == 1){return queue1.poll();}//size = 0while (!queue2.isEmpty()){queue1.offer(queue2.poll());size++;}while (size > 1){queue2.offer(queue1.poll());size--;}return queue1.poll();}public int top() {//queue1只维护一个栈顶元素int size = queue1.size();while (size > 1){queue2.offer(queue1.poll());size--;}if (size == 1){return queue1.peek();}while (!queue2.isEmpty()){queue1.offer(queue2.poll());size++;}while (size > 1){queue2.offer(queue1.poll());size--;}return queue1.peek();}public boolean empty() {return queue1.isEmpty() && queue2.isEmpty();}
Queue的一些API解释
api | 用法 |
---|---|
peek() | 返回但不删除此队列的头部,如果此队列为空,则返回null。 |
element() | 返回但不删除此队列的头部。此方法与peek的区别仅在于如果此队列为空,它将抛出异常。(而不是返回null) |
poll() | 返回并删除此队列的头部,如果此队列为空,则返回null。 |
remove() | 返回并删除此队列的头部。此方法与poll的不同之处仅在于如果此队列为空,则抛出异常 |
offer(E e) | 如果可以在不违反容量限制的情况下立即将指定元素插入此队列,则在成功时返回true,如果当前没有可用空间,不会抛异常,仅不会添加成功返回false。 |
add(E e) | 如果可以在不违反容量限制的情况下立即将指定元素插入此队列,则在成功时返回true,如果当前没有可用空间,则抛出IllegalStateException。 |
力扣题目链接
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
示例 1:
示例 2:
示例 3:
示例 4:
示例 5:
括号匹配是使用栈解决的经典问题。
看代码就能看懂,直接上代码
public boolean isValid(String s) {if (s.length() % 2 != 0) {return false;}// java8新特性–双括号初始化mapMap map = new HashMap(3) {{put(')', '(');put(']', '[');put('}', '{');}};//借助栈Stack stack = new Stack<>();for (char c : s.toCharArray()) {if (map.containsValue(c)) {stack.push(c);} else {//先出现反括号直接返回falseif (stack.isEmpty()) {return false;}//peek返回栈顶元素不删除,pop弹出并返回栈顶元素if (!stack.peek().equals(map.get(c))) {//只要栈顶元素跟该元素不匹配直接返回falsereturn false;} else {//弹出栈顶元素stack.pop();}}}return stack.isEmpty();}
力扣题目链接
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
提示:
操作过程如下:
依旧是栈的经典应用,直接使用栈,最后栈不能直接转成字符串,还需要使用额外空间反转栈元素,费时费空间。
使用StringBuilder来充当栈,不需借助额外空间,到最后能直接返回结果。
public String removeDuplicates(String s) {
// //直接使用栈
// Stack stack = new Stack<>();
// for (char c : s.toCharArray()) {
// if (!stack.isEmpty() && stack.peek().equals(c)) {
// stack.pop();
// } else {
// stack.push(c);
// }
// }
//
// String ans = "";
// //字符串反转
// while (!stack.isEmpty()){
// ans = stack.pop() + ans;
// }
// return ans;//StringBuilder 充当栈StringBuilder sb = new StringBuilder();for (char c : s.toCharArray()) {if (sb.length() > 0 && sb.charAt(sb.length() - 1) == c) {sb.deleteCharAt(sb.length() - 1);} else {sb.append(c);}}return sb.toString();}
力扣题目链接
根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
示例 2:
示例 3:
输入: [“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 + ) * ) 。
逆波兰表达式主要有以下两个优点:
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
。操作完成后,栈中只有一个元素,该元素就是最后结果注意两数字操作前后顺序即可
操作过程如下:
public int evalRPN(String[] tokens) {Stack stack = new Stack<>(); //加法、乘法两个元素先后顺序无所谓,减法、除法两个元素顺序不能被改变for(String s : tokens){if("+".equals(s)){ stack.push(stack.pop() + stack.pop()); }else if("-".equals(s)){stack.push(-stack.pop() + stack.pop()); }else if("*".equals(s)){stack.push(stack.pop() * stack.pop()); }else if("/".equals(s)){int first = stack.pop();stack.push(stack.pop() / first); }else{stack.push(Integer.parseInt(s));}} //到最后栈中只有一个元素,该元素就是最后的结果return stack.pop();}
力扣题目链接
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
示例 2:
提示:
这道题目主要涉及到如下三块内容:
首先统计元素出现的频率,这一类的问题可以使用map来进行统计。(key存元素,value存元素出现次数即可)
然后是对频率进行排序,这里我们可以使用一种 集合就是优先级队列。
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示。当然我们也可以自己指定排序规则。
得益于它的这个构造器public PriorityQueue(Comparator super E> comparator)
,因此我们可以通过实现Comparator接口自定义排序规则
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
对于该题我们可以使用大顶堆也可以使用小顶堆。
大顶堆:
优先级队列中不限制元素数量,将上述统计得到的map中的所有元素放入优先级队列,最后取出优先级队列前k个元素即可。这种做法的时间复杂度是O(n log n),不是最优解,而且我们只需要k个元素,没必要对所有元素进行排序,最后取出前k个。
小顶堆:
使用小顶堆的话,我们可以只在优先级队列中存k个元素,当第k + 1个元素过来时与堆顶元素出现次数比较(堆顶元素出现次数最小),如果该元素出现次数大于堆顶元素,只需移除堆顶元素放入该元素即可,最后队列中元素就是我们需要的k个元素。该种做法的时间复杂度是O(n log k)。当元素个数n很大,需要找到元素个数k很小时,这种做法效率明显比大顶堆高很多。
/*Comparator接口说明:* 默认从小到大升序【队头到队尾】小顶堆* * 返回负数,形参中第一个参数排在前面;返回正数,形参中第二个参数排在前面* 对于队列:排在前面意味着往队头靠* * 对于堆(使用PriorityQueue实现):从队头到队尾按从小到大排就是最小堆(小顶堆),* 从队头到队尾按从大到小排就是最大堆(大顶堆)--->队头元素相当于堆的根节点* (pair1, pair2) -> pair1.getValue() - pair2.getValue() 根据元素出现频率升序,对于堆而言就是小顶堆 * */ public int[] topKFrequent(int[] nums, int k) {int[] ans = new int[k];Map map = new HashMap<>();//使用map统计元素出现次数for (int num : nums) {map.put(num, map.getOrDefault(num, 0) + 1);}//优先级队列,出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)PriorityQueue> priorityQueue = new PriorityQueue<>((pair1, pair2) -> pair1.getValue() - pair2.getValue());for (Map.Entry entry : map.entrySet()) {if (priorityQueue.size() < k) {priorityQueue.add(entry);} else {//队列中只维护k个元素,当当前元素出现个数大于堆顶元素出现个数(小顶堆)时,移除队头元素放入当前元素if (entry.getValue() > priorityQueue.peek().getValue()) {priorityQueue.poll();priorityQueue.add(entry);}}}int index = k - 1;while (!priorityQueue.isEmpty()) {ans[index--] = priorityQueue.poll().getKey();}//基于大顶堆实现,从头到尾出现频率依次递减
// PriorityQueue> priorityQueue = new PriorityQueue<>((pair1, pair2) -> pair2.getValue() - pair1.getValue());
// for (Map.Entry entry : map.entrySet()) {
// priorityQueue.add(entry);
// }
//
// //直接取前k个元素即可
// for (int i = 0; i < k; i++) {
// ans[i] = priorityQueue.poll().getKey();
// }return ans;}
力扣题目链接
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
提示:
这题完全可以用优先级队列来解题,优先级队列中只维护k个元素,可以使用大顶堆,窗口每次移动,堆顶也就是队列头就是我们要的元素,放到结果中,这种解法理论上可行。(但是会超时~)
代码如下:
public int[] maxSlidingWindow(int[] nums, int k) {if (nums.length == 1) {return nums;}int len = nums.length - k + 1;int[] ans = new int[len];//一、大顶堆PriorityQueue queue = new PriorityQueue<>((num1, num2) -> num2 -num1);for(int i = 0; i < len; i++){//每次进来清空队列,让队列中只维护k个元素queue.clear();int temp = i;while(temp < i + k){queue.add(nums[temp++]);ans[i] = queue.peek();}}return ans;}
实际上我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
这个队列应该长这个样子:
class MyQueue {void pop(int val) {}void push(Integer val) {}public int getMax() {}}
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.getMax()就返回我们要的最大值。
实际上Java中没有这样的数据结构,我们需要自己实现这么个队列。
然后再分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。
而且不要以为实现的单调队列就是 对窗口里面的数进行排序,如果仅仅排序的话,那和优先级队列又有什么区别了呢。
设计单调队列的时候,pop,和push操作要保持如下规则:
保持如上规则,每次窗口移动的时候,只要问que.getMax()【直接返回队头元素】就可以返回当前窗口的最大值。
动画过程:
首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题。
单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。
不要以为本题中的单调队列实现就是固定的写法。
(自定义单调队列以及直接使用双端队列来实现单调队列)
public int[] maxSlidingWindow(int[] nums, int k) {if (nums.length == 1) {return nums;}int len = nums.length - k + 1;int[] ans = new int[len];//二、自定义单调栈:单调队列只维护可能是最大值的数
// MyQueue queue = new MyQueue();
// int index = 0;
// //先push前k个元素
// for (int i = 0; i < k; i++) {
// queue.push(nums[i]);
// }
// ans[index++] = queue.getMax();
//
// for (int i = k; i < nums.length; i++) {
// //弹出最开始的元素
// queue.pop(nums[i - k]);
// queue.push(nums[i]);
// ans[index++] = queue.getMax();
// }//直接使用双端队列实现单调栈Deque deque = new LinkedList<>();int index = 0;for (int i = 0; i < nums.length; i++) {//先pop 弹出开始元素if (!deque.isEmpty() && i - k >= 0 && nums[i - k] == deque.peekFirst()) {deque.removeFirst();}//再pushwhile (!deque.isEmpty() && deque.getLast() < nums[i]) {deque.removeLast();}deque.add(nums[i]);//最后取出最大元素[队头元素],从前k个元素开始if (i >= k - 1) {ans[index++] = deque.peekFirst();}}return ans;}/*** 自定义单调队列* 队列中元素从头到尾依次递减,* pop元素时,开始元素与队头元素相等时,pop出队头元素* push元素时,队尾元素比当前元素小将该元素弹出(因此需要双端队列)直到队尾元素大于等于当前元素,再把当前元素添加到队尾* 每次pop、push结束之后,获取队列中最大值(即队头元素)getMax*/static class MyQueue {Deque deque = new LinkedList<>();void pop(int val) {//只有开始值与队头元素相等,才将队头元素弹出if (!deque.isEmpty() && deque.getFirst() == val) {deque.removeFirst();}}void push(Integer val) {//只要队尾值比当前元素小,直接从队尾弹出while (!deque.isEmpty() && deque.getLast() < val) {deque.removeLast();}deque.addLast(val);}//返回队头最大值public int getMax() {return deque.peek();}}
详情见代码随想录