本篇主要内容如下图大纲所示:
1.用栈实现队列
使用栈实现队列的下列操作:
push(x) – 将一个元素放入队列的尾部。
pop() – 从队列首部移除元素。
peek() – 返回队列首部的元素。
empty() – 返回队列是否为空。
示例:
MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false
思路: 用两个栈来模拟队列的操作
-
入队没啥说的就是进栈 stackIn.push(), 比如 [1,2,3]
-
出队就是返回栈底元素 1, 此时再借助一个栈 stackOut 把 stackIn的元素存进去就是 [3,2,1], 再返回stackOut.pop()就是 1 了。
-
队列第一个元素 1 同理出队
-
判空就是 两个栈同时为空的时候
代码如下:
public 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(){
dumpStackIn();
return stackOut.pop();
}
public int peek(){
dumpStackIn();
return stackOut.peek();
}
public boolean isEmpty(){
return stackIn.isEmpty() && stackOut.isEmpty();
}
public void dumpStackIn(){
if(!stackOut.isEmpty()) return;
while(!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
2.用队列实现栈
使用队列实现栈的下列操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
注意:
- 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
- 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
- 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
思路:定义一个队列 ArrayDeque
-
入栈就是入队,deque.addLast(x), [1,2,3]
-
出栈就是把 3 移除,逻辑是 遍历该队列,把1添加到3后面,2又继续添加到1后面结束,队列变成 [3,2,1],
-
此时再移除队列的首部元素3 deque.pollFirst();
-
栈顶元素3获取 ,就是队列的 最后一个元素 deque.peekLast();
-
判空就是队列是否为空
代码如下:
public class MyStack {
// Deque 接口继承了 Queue 接口
// 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
//一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。
ArrayDeque<Integer> deque;
public MyStack() {
deque = new ArrayDeque<Integer>();
}
public void push(int x) {
deque.addLast(x);
}
public int pop() {
int size = deque.size();
size--;
while (size-- > 0) {
deque.addLast(deque.peekFirst());
deque.pollFirst();
}
return deque.pollFirst();;
}
public int top() {
return deque.peekLast();
}
}
3.有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: “()”
输出: true
示例 2:
输入: “()[]{}”
输出: true
思路:
- 用栈模拟括号匹配,左括号就把对应的右括号入栈,“({[ ]})” 即栈里元素为 )}] ,
- 然后继续遍历拿到 右括号和栈里元素一一匹配,匹配成功则出栈(都匹配成功此时栈为空返回true),没匹配到则返回false(c != stack.peek())
- 还有一种情况是 ) ,匹配过程中栈里为空,返回false
代码如下:
class Solution{
public boolean isValid(String s){
Deque<Character> stack = new LinkedList<>();
for(char c : s.toCharArray()){
if(c == '('){
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else if (stack.isEmpty() || c != stack.peek()) {
return false;
}else {
stack.pop();
}
}
return stack.isEmpty();
}
}
4.删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:“abbaca”
输出:“ca”
解释:例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。
提示:
- 1 <= S.length <= 20000
- S 仅由小写英文字母组成。
思路:其实就是消消乐,跟括号匹配类似,定义一个栈,遍历字符串,相等就出栈,最后留下没匹配上的
代码如下:
class Solution{
public String removeDuplicates(String s){
//ArrayDeque会比LinkedList在除了删除元素这一点外会快一点
//参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist
ArrayDeque<Character> stack = new ArrayDeque<>();
for(char c : s.toCharArray()){
if(stack.isEmpty() || stack.peek() != c){
stack.push(c);
}else{
stack.pop();
}
}
String str = "";
//剩余的元素即为不重复的元素
while (!stack.isEmpty()) {
str = stack.pop() + str;
}
return str;
}
}
5.逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入: ["2", "1", "+", "3", " * "]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 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 + ) * ) 。
逆波兰表达式主要有以下两个优点:
-
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
-
适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
思路:适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
class Solution{
public int evalRPN(String[] tokens){
ArrayDeque<Integer> stack = new ArrayDeque<>();
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 temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
}else{
stack.push(Integer.valueOf(s));
}
}
return stack.pop();
}
}
6.滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
提示:
- 1 <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
- 1 <= k <= nums.length
思路:实现一个单调递减队列,每次滑动窗口维护队列的值,直接返回队首元素就是当前窗口的最大值
代码如下:
class MyQueue {
Deque<Integer> deque = new LinkedList<>();
void poll(int val){
if(!deque.isEmpty() && val == deque.peek()){
deque.poll();
}
}
void add(int val){
while (!deque.isEmpty() && val > deque.getLast()){
deque.removeLast();
}
deque.add(val);
}
int peek(){
return deque.peek();
}
}
class Solution{
public static int[] maxSlidingWindow(int[] nums, int k) {
// nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
if(nums.length == 1) return nums;
int len = nums.length - k + 1;
int[] res = new int[len];//存放结果的数组
int num = 0;
//自定义队列
MyQueue1 myQueue = new MyQueue1();
for(int i = 0; i < k; i++){
myQueue.add(nums[i]); //3,-1添加进去了
}
res[num++] = myQueue.peek(); //3最大
for(int i = k; i < nums.length; i++){ //第二次滑动从-3开始,3,-1,-3;第三次滑动5进来,3,-1,-3全部出队返回5就形成单调递减的队列
myQueue.poll(nums[i - k]);
myQueue.add(nums[i]);
res[num++] = myQueue.peek();
}
return res;
}
}
7.前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
- 输入: nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
示例 2:
- 输入: nums = [1], k = 1
- 输出: [1]
提示:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O ( n log n ) O(n \log n) O(nlogn) , n 是数组的大小。
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
- 你可以按任意顺序返回答案。
思路:用map存储元素和频次,用优先队列(建立小顶堆:队首到队尾从小到大排列的)取前k个高频次,小顶堆是从小到大排列的完全二叉树
遍历map,队列元素小于k时入队,大于k时,比较当前元素和根节点(也就是频次最小的节点)的大小,把小值出队
代码如下:
class Solution{
public static int[] topKFrequent2(int[] nums, int k) {
//[1,1,1,1,2,2,2,3,3,4,4,4] 3 返回1,2,4
Map<Integer,Integer> map = new HashMap<>();//key为数组元素值,val为对应出现次数
for(int num : nums){
map.put(num, map.getOrDefault(num,0) + 1);
}
//1->4 2->3 3->2 4->3
//在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
//出现次数 2,3,4
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2)->(pair1[1] - pair2[1]));
for(Map.Entry<Integer,Integer> entry : map.entrySet()){ //小顶堆只需要维持k个元素有序
if(pq.size() < k){ //小顶堆元素个数小于k个时直接加
pq.add(new int[]{entry.getKey(), entry.getValue()});
}else{
//pq.peek()[1] : peek()队头元素的value值 2
if(entry.getValue() > pq.peek()[1]){ //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(new int[]{entry.getKey(), entry.getValue()});
}
}
}
int[] ans = new int[k];
for(int i = k - 1; i >= 0; i--){ //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
ans[i] = pq.poll()[0];
}
return ans;
}
}