前提知识
统一用下面的定义队列和栈
//队列
Queue<String> queue = new LinkedList<String>();
*****注意!用add、poll、peek
//栈
Deque<Integer> stack = new ArrayDeque<Integer>();
*****注意!这个方法中的push和add都可以用,但是一定要用push、pop、peek
但是这个也可以实现双端队列
queue.poll()从前面弹出
queue.pollLast()从后面弹出
queue.offer()从后面加入 = queue.offerLast()
queue.offerFirst()从前面加入
//双端队列,一般用
LinkedList<Integer> queue = new LinkedList<Integer>();
题目速览
232. 用栈实现队列
225. 用队列实现栈
20. 有效的括号
1047. 删除字符串中的所有相邻重复项
150. 逆波兰表达式求值
239. 滑动窗口最大值
347. 前 K 个高频元素
题目详解
232. 用栈实现队列
整体思路:
在push数据的时候,只要数据放进输入栈就好。
在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
class MyQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
public MyQueue() {
stack1 = new ArrayDeque<Integer>();
stack2 = new ArrayDeque<Integer>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
注意
在工业级别代码开发中,最忌讳的就是实现一个类似的函数,直接把代码粘过来改一改就完事了。
这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)
所以代码修改成:
class MyQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
public MyQueue() {
stack1 = new ArrayDeque<Integer>();
stack2 = new ArrayDeque<Integer>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
dumpStack1();
return stack2.pop();
}
public int peek() {
dumpStack1();
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
//抽出方法,如果stack2为空,那么将stack1中的元素全部放到stack2中
private void dumpStack1(){
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
225. 用队列实现栈
这道题有很多的思路可以借鉴
法一:存在队列1、队列2,一定有一个队列是保持没数的。
push的时候,向有数的队列中加入队尾。
pop的时候,有数的一方向无数的队列倾倒数,直到有数的一方的size等于1,这时候pop出来
peek的时候,有数的一方向无数的队列倾倒数,直到有数的一方的size等于1,这时候peek出来,保存数,再将数继续倾入另一个队列
判断是不是empty的时候,如果两个都为空就是空。
相关的代码:
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
if (!queue1.isEmpty()){
queue1.add(x);
}else {
queue2.add(x);
}
}
public int pop() {
return dump();
}
public int top() {
int res = dump();
if (!queue1.isEmpty()){
queue1.add(res);
}else {
queue2.add(res);
}
return res;
}
public boolean empty() {
return queue2.isEmpty() && queue1.isEmpty();
}
private int dump(){
if (!queue2.isEmpty()){
while (queue2.size() != 1){
queue1.add(queue2.poll());
}
int res = queue2.poll();
return res;
}else {
while (queue1.size() != 1){
queue2.add(queue1.poll());
}
int res = queue1.poll();
return res;
}
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
法二:存在队列1、队列2,一定有一个队列是保持没数的。
push的时候:存入无数的队列中数,再将有数的队列中的数全部倾倒入无数的队列中,形成反着来的顺序,也就是栈的顺序。
peek的时候,直接peek就行
pop的时候,直接pop就行
判断是不是empty的时候,如果两个都为空就是空。
相关的代码:
class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
if (!queue1.isEmpty()){
queue2.add(x);
while (!queue1.isEmpty()){
queue2.add(queue1.poll());
}
}else {
queue1.add(x);
while (!queue2.isEmpty()){
queue1.add(queue2.poll());
}
}
}
public int pop() {
if (!queue1.isEmpty()){
return queue1.poll();
}else {
return queue2.poll();
}
}
public int top() {
if (!queue1.isEmpty()){
return queue1.peek();
}else {
return queue2.peek();
}
}
public boolean empty() {
return queue2.isEmpty() && queue1.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
法三:一个队列
push的时候,先加入到队尾,再依次从头弹出加入到队尾,弹入的次数是加入新数前的次数
pop的时候,直接pop
peek的时候,直接peek
判断是不是empty的时候,如果两个都为空就是空。
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
int size = queue.size();
queue.add(x);
while (size != 0){
queue.add(queue.poll());
size--;
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
20. 有效的括号
括号匹配是使用栈解决的经典问题。
如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。
再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了)
思路:
分三种情况:
- ((((]]左括号多了
- (([}))中间有不匹配的
- (()))右括号多了
第一种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,最后stack不为空为false
第二种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,发现不成对为false
第三种情况,遇到左括号就填入stack,遇到右括号就弹出比对,是不是成对的,最后stack为空没法匹配为false
代码如下:
class Solution {
public boolean isValid(String s) {
Map<Character,Character> map = new HashMap<>();
map.put(')','(');
map.put(']','[');
map.put('}','{');
Deque<Character> stack = new ArrayDeque<Character>();
char[] arr = s.toCharArray();
for (char a : arr){
if (!map.containsKey(a)){
stack.push(a);
}else {
if (stack.isEmpty() || stack.pop().charValue() != map.get(a)){
return false;
}
}
}
if (stack.isEmpty()){
return true;
}else {
return false;
}
}
}
1047. 删除字符串中的所有相邻重复项
题外话:
在企业项目开发中,尽量不要使用递归!
在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
思路:
利用栈
当栈是空的时候,直接加入。
如果加入的数和栈顶的一样,栈顶弹出。
如果加入的数和栈顶的不一样,加入。
最后,弹出栈的字符,反转
class Solution {
public String removeDuplicates(String s) {
char[] arr = s.toCharArray();
Deque<Character> stack = new ArrayDeque<>();
for (char a : arr){
if (stack.isEmpty()){
stack.push(a);
}else if (stack.peek().charValue() != a){
stack.push(a);
}else {
stack.pop();
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.reverse().toString();
}
}
150. 逆波兰表达式求值
题外话:
我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算法,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦!
那么将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/”, “+”] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。
思路:
遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
相关代码:
class Solution {
public int evalRPN(String[] tokens) {
Set<String> set = new HashSet<>();
set.add("+");
set.add("-");
set.add("*");
set.add("/");
Deque<String> stack = new ArrayDeque<>();
for (String abc : tokens){
if (!set.contains(abc)){
stack.push(abc);
}else {
int second = Integer.parseInt(stack.pop());
int first = Integer.parseInt(stack.pop());
stack.push(String.valueOf(calculate(first, second, abc)));
}
}
return Integer.parseInt(stack.pop());
}
public int calculate(int a, int b, String sign){
if ("/".equals(sign)){
return a / b;
}else if ("+".equals(sign)){
return a + b;
}else if ("*".equals(sign)){
return a * b;
}else {
return a - b;
}
}
}
public static int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
//存储结果值
int[] res = new int[len - k + 1];
int index = 0;
//建立双端对列,维持一个单调递减的
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < len; i++) {
//如果加入的数比最后一个大,因为要优先放编号大的,所以都弹出直到小于
while (!list.isEmpty() && nums[list.peekLast()] <= nums[i]) {
list.pollLast();
}
//****注意,这里加入的是编号
list.addLast(i);
//除去不属于现在k空间范围上的,因为编号一直递增,所以一定会排干净
if (list.peekFirst() == i - k) {
list.pollFirst();
}
//当i + 1 >= k的时候,结果数组就可以存数了
if (i + 1 >= k) {
//注意双端队列中存的是地址,所以nums[]
res[index++] = nums[list.peekFirst()];
}
}
return res;
}
239. 滑动窗口最大值
思路:
像这种滑动窗口,从前加入,从后弹出的,双端队列最好用
双端队列用:LinkedList queue = new LinkedList();
双端队列操作步骤:
- 里面的头值是这个k区间最大的。
- addLast:如果add的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到add元素的数值小于队列入口元素的数值为止
- pollFirst(value):如果窗口头值的元素编号到达临界值编号,那么队列弹出元素,否则不用任何操作
代码:
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
//建立答案个数目的返回数组
int[] res = new int[len - k + 1];
int index = 0;
//双端队列
LinkedList<Integer> list = new LinkedList<>();
for(int i = 0; i < len; i++){
//空的时候,比最后一个值小的时候可以直接进,但是遇到大于,等于(编号越大越好)最后一个值的时候,就需要从后弹出,直到比要加入的大
while(!list.isEmpty() && nums[list.peekLast()] <= nums[i]){
list.pollLast();
}
//注意,这里存的是编号
list.addLast(i);
//因为编号一直是上升的,如果可以满足最大值并且保留在队列中,总会遇到 i - k的时候,所以可以排干净
if(list.peekFirst() == i - k){
list.pollFirst();
}
//遇到完整的【k】就可以依次保存最大值了
if(i + 1 >= k){
//注意双端队列中存的是地址,所以nums[]
res[index++] = nums[list.peekFirst()];
}
}
return res;
}
347. 前 K 个高频元素
//默认是小根堆
PriorityQueue<Interger> heap = new PriorityQueue<>();
//下面试利用Collections里的方法,对集合进行排序
Collections.sort(dictionary, (a, b) -> {
//b在前,倒序; a在前,正序
if (a.length() != b.length()) return b.length() - a.length();
//字典序比较
return a.compareTo(b);
});
思路:
- 借助 哈希表 来建立数字和其出现次数的映射,遍历一遍数组统计元素的频率
- 维护一个元素数目为 k 的最小堆
- 每次都将新的元素与堆顶元素(堆中频率最小的元素)进行比较
- 如果新的元素的频率比堆顶端的元素大,则弹出堆顶端的元素,将新的元素添加进堆中
- 最终,堆中的 k 个元素即为前 k 个高频元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
//得出每一个单词的词频
for (int i : nums){
map.put(i, map.getOrDefault(i, 0) + 1);
}
//定义优先级队列,存储k个空间,按照对应的字频来比较
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(map::get));
for (int i : map.keySet()){
if (priorityQueue.isEmpty() || priorityQueue.size() < k){
priorityQueue.add(i);
}else if (map.get(i) > map.get(priorityQueue.peek())){
priorityQueue.poll();
priorityQueue.add(i);
}
}
int[] res = new int[k];
int index = 0;
while (!priorityQueue.isEmpty()){
res[index++] = priorityQueue.poll();
}
return res;
}
}
总结
栈的理论基础
- C++中stack,queue 是容器么?
- 我们使用的stack,queue是属于那个版本的STL?
- 我们使用的STL中stack,queue是如何实现的?
- stack,queue 提供迭代器来遍历空间么?
一道面试题C++
栈里面的元素在内存中是连续分布的么?
陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的