DAY 10 栈和队列1
232 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
class MyQueue {
private Stack<Integer> stack1;
private Stack<Integer> stack2;
public MyQueue() {
stack1 = new Stack<>(); // 入
stack2 = new Stack<>(); // 出
}
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() {
int num = pop();
stack2.push(num);
return num;
}
public boolean empty() {
// if(stack1.size()==0 && stack2.size()==0){
// return true;
// }
// return false;
return stack1.isEmpty() && stack2.isEmpty();
}
}
225 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
两个队实现栈:
利用两个队,将队列里的顺序调整成栈里的顺序
class MyStack {
private Queue<Integer> queue1;
private 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> tem = queue1;
queue1 = queue2;
queue2 = tem;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
一个队实现栈:
class MyStack {
private Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.offer(x);
}
public int pop() {
int size = queue.size();
for(int i=0;i<size-1;i++){
queue.offer(queue.poll());
}
return queue.poll();
}
public int top() {
int num = pop();
queue.offer(num);
return num;
}
public boolean empty() {
return queue.isEmpty();
}
}
DAY 11 栈和队列2 匹配类问题
20 有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
解题思路:
tips:要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。
先找到所有匹配错误情况:
1. 字符串里左括号多余 —— 字符串空了,栈没空
1. 括号没有多余,但是 括号的类型没有匹配上 —— 字符串中右括号和栈中左括号不匹配
1. 字符串里右括号多余 —— 栈空了,字符串没空
遇见左括号直接存入栈,遇见右括号时再判断是否与栈顶的左括号匹配:
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(char c : s.toCharArray()){
if(c=='(' || c=='[' || c=='{') stack.push(c);
else if(stack.isEmpty()) return false; // 右半括号多了
else{
// 左右括号不匹配
char top = stack.pop();
if((c==')' && top != '(') || (c==']' && top != '[') || (c=='}' && top != '{')) return false;
}
}
// 若stack不空,则左半括号多了
return stack.isEmpty();
}
遇见左括号将匹配的右括号存入栈,遇见右括号时直接判断是否与栈顶元素相同:
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
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.pop()) return false; // 右半括号多了 或 括号类型不匹配
}
// 若stack不空,则左半括号多了
return stack.isEmpty();
}
1047 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
借助栈Stack实现:
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
for(char c : s.toCharArray()){
if(!stack.isEmpty() && c==stack.peek()){
stack.pop();
continue;
}
stack.push(c);
}
StringBuilder res = new StringBuilder();
// 注意:栈中顺序和字符串顺序相反,栈顶元素反而在字符串最后
while(!stack.isEmpty()){
res.insert(0,stack.pop());
}
return res.toString();
}
将字符串作为栈实现:
public String removeDuplicates(String s) {
StringBuilder res = new StringBuilder();
for(char c : s.toCharArray()){
int len = res.length();
if(len>0 && c==res.charAt(len-1)) res.deleteCharAt(len-1);
else res.append(c);
}
return res.toString();
}
150 逆波兰表达式求值
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
定义:
逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
思路:
递归就是用栈来实现的。所以栈与递归之间在某种程度上是可以转换的!
**逆波兰表达式相当于是二叉树中的后序遍历:**逆波兰表达式是用后续遍历的方式把二叉树序列化了
在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和 1047.删除字符串中的所有相邻重复项 中的对对碰游戏非常像了。**只不过本题不要相邻元素做消除了,而是做运算!
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<>();
for(String s : tokens){
if(!("*".equals(s) || "/".equals(s) || "+".equals(s)|| "-".equals(s))){
stack.push(Integer.valueOf(s));
}else{
// 注意!!! n1 和 n2 的顺序,n1是减数和除数,n2才是被减数和被除数
int n1 = stack.pop();
int n2 = stack.pop();
int curRes = 0;
if("*".equals(s)) curRes = n2 * n1;
else if("/".equals(s)) curRes = n2 / n1;
else if("+".equals(s)) curRes = n2 + n1;
else curRes = n2 - n1;
stack.push(curRes);
}
}
return stack.pop();
}
DAY 13 栈和队列3
239 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
单调队列
设计单调队列的时候,poll 和 offer 操作要保持如下规则:
队列前端为出口,队列后端为入口
- poll(value):如果滑动窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- offer(value):如果offer的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到offer元素的数值小于等于队列入口元素的数值为止
- getMaxValue():获取队列出口元素。单调队列中出口元素就是窗口里最大元素
实现
**法一:**利用双端队列完成单调队列操作
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> queue = new LinkedList<>();
int[] res = new int[nums.length-k+1];
int max = Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){
// poll: 判断当前队列中队首的值是否有效
if(i>=k && nums[i-k] == queue.peekFirst()) queue.pollFirst();
// offer: 保证队中元素从大到小 如果前面数小则需要依次弹出,直至满足要求
while(!queue.isEmpty() && queue.peekLast()<nums[i]){
queue.pollLast();
}
queue.offerLast(nums[i]);
// getMaxValue: 当窗口长度为k时 保存当前窗口中最大值
if(i+1 >= k) res[i+1-k]=queue.peekFirst();
}
return res;
}
**法二:**自定义一个类实现单调队列
class MyQueue{
Deque<Integer> queue;
MyQueue(){
queue = new LinkedList<>();
}
void poll(int val){
if(!queue.isEmpty() && queue.peekFirst()==val) queue.pollFirst();
}
void offer(int val){
while(!queue.isEmpty() && queue.peekLast()<val){
queue.pollLast();
}
queue.offerLast(val);
}
int getMaxVal(){
return queue.peekFirst();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MyQueue que = new MyQueue();
int[] res = new int[nums.length-k+1];
for(int i=0;i<k;i++){
que.offer(nums[i]);
}
res[0] = que.getMaxVal();
for(int i=k;i<nums.length;i++){
que.poll(nums[i-k]);
que.offer(nums[i]);
res[i-k+1] = que.getMaxVal();
}
return res;
}
}
**Tips:**单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。
347 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
优先级队列
使用不弹出元素(即n个元素)的优先级队列(大or小顶堆),相当于对n个元素进行排序,时间复杂度是O(nlogn)
使用仅含k个元素的优先级队列(大or小顶堆),时间复杂度是O(nlogk):遍历一遍需要O(n)的时间复杂度,每次需要对k个元素排序,时间复杂度是O(logk)
所以,本题使用仅含k个元素的小顶堆(优先级队列)更优
左大于右就会建立小顶堆,左小于右建立大顶堆
实现
java中默认的优先级队列是小顶堆
Queue<T> p = new PriorityQueue<>()
PriorityQueue<T> p = new PriorityQueue<>()
Comparable接口与Comparator接口使用的对比
Comparable是令需要比较大小的类实现接口,一旦设置好之后,可以保证该实现类的对象在任何位置都可以比较大小
Comparator是临时设置的排序方式,什么时候需要比较,就临时创建一个Comparator的实现类调用Comparable中的compareTo方法是用其实现类的对象去调用的,参数是另一个需要对比的对象。name1.compareTo(name2);
调用Comparator中的compare()方法是用其实现类(如Double)去调用的,参数是两个需要对比的对象。Double.compare(d1,d2);Comparator接口说明: 对 o1 - o2
返回负数,即o1<o2,o1排在前面;返回正数,即o1>o2,o2排在前面,即,o1 - o2 从小到大排序若将结果取反,就有o1<o2,o2在前;o1>o2,o1在前
即,-(o1-o2) = o2-o1 从大到小排序
优先级队列 搭配 Comparator 比较器
注意:重写比较器Comparator时,比较器也要明确指定泛型类型
Queue<T> queue = new PriorityQueue<>(new Comparator<T>(){
public int compare(T o1, T o2){
return o1-o2;
}
});
PriorityQueue<T> queue = new PriorityQueue<>( (o1,o2) -> o1-o2 );
具体代码:
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums){
if(map.containsKey(num)) map.put(num, map.get(num)+1);
else map.put(num,1);
}
// Queue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>(){
// public int compare(int[] o1, int[] o2){
// return o1[1]-o2[1];
// }
// });
PriorityQueue<int[]> queue = new PriorityQueue<>( (o1,o2) -> o1[1]-o2[1] );
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
queue.offer(new int[]{entry.getKey(), entry.getValue()});
if(queue.size() > k) queue.poll();
}
int[] res = new int[k];
//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
for(int i=0;i<k;i++){
res[i] = queue.poll()[0];
}
return res;
}
优先级队列 搭配 Comparable接口
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] ret = new int[k];
// 边界条件
if(nums==null || k==0){
return ret;
}
Map<Integer,Integer> map = new HashMap<>();
// 1. 遍历数组,将出现的元素及其频次保存到Map集合中
for (int i = 0; i < nums.length; i++) {
// 哈希表中不含nums[i]为key的键值对
if(!map.containsKey(nums[i])){
map.put(nums[i],1);
}else {
// 哈希表中含nums[i]为key的键值对
int times = map.get(nums[i]);
map.put(nums[i],times+1);
}
}
// 2. 扫描Map集合,将出现频次最高的前k个元素添加到优先级队列中
Queue<Node> queue = new PriorityQueue<>();
for(Map.Entry<Integer,Integer> entry: map.entrySet()){
// 优先级队列中元素个数小于等于k时
if(queue.size()<k){
queue.offer(new Node(entry.getKey(), entry.getValue()));
}else {
// 元素中个数大于k时
Node node = queue.peek();
// 将频次高的换入队列中
if(entry.getValue()> node.value){
queue.poll();
queue.offer(new Node(entry.getKey(), entry.getValue()));
}
}
}
// 将队列里频次高的前k个元素导入ret数组
int i = 0;
while (!queue.isEmpty()){
ret[i] = queue.poll().key;
i++;
}
return ret;
}
}
class Node implements Comparable<Node>{
// 数组中出现的元素
int key;
// 该元素出现的频次
int value;
public Node(int key, int value){
this.key = key;
this.value = value;
}
// 告诉优先级队列Node对象如何比较
@Override
public int compareTo(Node o) {
return this.value-o.value;
}
}