栈(Stack):是只允许在一端进行插入或删除的线性表。栈是一种线性表,限定这种线性表只能在某一端进行插入和删除操作。进行操作的这一端称为栈顶。
队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。
目录
- 一.栈与队列的基本操作
- 1.栈
- 2.队列
- 二.题目解决思路和实现代码
- 1.用栈实现队列(232)
- 2.用队列实现栈(225)
- 3.有效的括号(20)
- 4.删除字符串中的所有相邻重复项(1047)
- 5.逆波兰表达式求值(150)
- 6.滑动窗口最大值(239)
- 7.前k个高频元素(347)
一.栈与队列的基本操作
1.栈
创建:
//创建栈,以里面存储整数类型数据为例
Stack<Integer> mys=new Stack();
入栈:
mys.push(1);
出栈:
//将栈顶元素删除并返回栈顶元素
int res=mys.pop();
计算大小:
int size=mys.size();
//经过实验得在栈中存入null,也算栈的有效元素个数(empty也是如此
获取栈顶元素:
//返回栈顶元素的值且并不删除
int value=mys.peek();
判断栈是否为空:
boolean ans=mys.empty();
2.队列
创建:
//创建队列,以里面存储整数类型数据为例
//LinkedList类实现了Queue接口
Queue<Integer> myq=new LinkedList<Integer>();
入队:
myq.offer(1);
出队列:
//将队头元素删除并返回队头元素
int res=myq.poll();
计算大小:
int size=myq.size();
//经过实验得在队列中存入null,也算栈的有效元素个数(isEmpty也是如此
获取队头元素:
//返回队头元素的值且并不删除
int value=myq.peek();
判断队列是否为空:
boolean ans=myq.isEmpty();
二.题目解决思路和实现代码
1.用栈实现队列(232)
题目:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:void push(int x) 将元素 x 推到队列的末尾;int pop() 从队列的开头移除并返回元素;int peek() 返回队列开头的元素;boolean empty() 如果队列为空,返回 true ;否则,返回 false
分析和解决思路:
分析:
1.栈的特性是先入后出,队列的特性是先入先出。
2.考虑实例:第一个栈存入“1,2,3”,则第一个栈弹出是“3,2,1”;第二个栈存入第一个栈弹出的“3,2,1”,第二个栈弹出“1,2,3”(正好与一开始存入顺序相同)。说明两个栈刚好能实现先入先出的元素顺序。
解决思路:
1.创建一个输入栈,一个输出栈。(输入栈负责存入,输出栈负责输出)
2.当实现push函数时:将元素直接存入输入栈。
3.当实现pop函数和peek函数时,若输出栈中有元素,则直接从输出栈中弹出元素或者获取栈顶元素;若输出栈为空,则将输入栈中的元素依次弹出并依次存入(即能实现颠倒顺序)。
4.当实现empty函数时:同时考虑输入栈和输出栈中是否为空(全空则空,与关系)
实现代码:
class MyQueue {
private Stack<Integer> stackIn;
private Stack<Integer> stackOut;
public MyQueue() {
stackIn=new Stack();
stackOut=new Stack(); //无参构造方法
}
public void push(int x) {
stackIn.push(x); //往输入栈中输入元素
}
public int pop() {
emptyInToOut();
return stackOut.pop();
}
public int peek() {
emptyInToOut();
return stackOut.peek();
}
public boolean empty() {
return (stackIn.empty() && stackOut.empty());
}
public void emptyInToOut(){
//若输出栈不为空,则直接将输出栈的栈顶元素输出,直接返回
if(!stackOut.empty()) return;
//如果输出栈为空,则将输入栈的全部元素放至输出栈中
while(!stackIn.empty()){
stackOut.push(stackIn.pop());
}
}
}
关键点:
1.实现 MyQueue 类要素:成员变量(在整个类中全局变量);构造方法(有参或无参,对象创建就是通过构造方法实现,在里面实现成员变量的初始化);成员方法。
2.两个栈的先入后出=先入先出
2.用队列实现栈(225)
题目:
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:void push(int x) 将元素 x 压入栈顶。int pop() 移除并返回栈顶元素。int top() 返回栈顶元素。boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
分析和解决思路:
分析:
1.栈的特性是先入后出,队列的特性是先入先出。
2.用一个队列实现栈的操作。存入的时候照常存入;但是在弹出元素和获取栈顶元素时需要将先进入队列的元素都挪至队列尾,直到取到最后一个进入队列的元素。
解决思路:
1.创建一个先入先出的队列myq
2.当实现push函数时:直接将元素存入队列
3.当实现pop函数和top函数时:将队列中前面(size-1)个元素依次从队头弹出,再依次存入队尾。如果是pop函数,则直接myq.poll();如果是top函数,则记录myq.peek(),再将元素存回队尾;
当实现empty函数时:直接返回队列isEmpty()
实现代码:
class MyStack {
private Queue<Integer> quIn;
public MyStack() {
quIn=new LinkedList<>(); //接口实现LinkedList类
}
public void push(int x) {
quIn.offer(x);
}
public int pop() {
for(int i=0;i<quIn.size()-1;i++){
quIn.offer(quIn.poll());
}
return quIn.poll();
}
public int top() {
// int re;
// for(int i=0;i<quIn.size();i++){
// if(i==quIn.size()-1) {
// re=quIn.peek();
// }
// quIn.offer(quIn.poll());
// }
// return re; //这样会报错:re在定义的时候没有赋初值,而且后面赋值是在判断语句里。编译时认为不一定能有值所以报错
int re;
for(int i=0;i<quIn.size()-1;i++){
quIn.offer(quIn.poll());
}
re=quIn.peek();
quIn.offer(quIn.poll());
return re;
}
public boolean empty() {
return quIn.isEmpty();
}
}
关键点:
1.在top函数中,要先记录re=quIn.peek();再将元素存回队列;最后再return re;(不然直接return quIn.peek()结束函数会丢失元素)
3.有效的括号(20)
题目:
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
分析和解决思路:
分析:
1.考虑字符串不有效的情况:
1)左括号多了
2)右括号多了
3)左右括号顺序不匹配
2.如果一个左括号在最前面,那么对应的右括号应该在最后面,由此联想到栈的特性(先入后出)
解决思路:
1.遍历字符串,如果遇到了左括号,就将对应的右括号存入栈中;
2.如果遇到了右括号,若此时栈是空的则说明是情况2),若取出栈顶元素与该右括号不相等则说明是情况3),返回false;若栈不为空且取出元素相等,则说明该顺序正确,将栈顶元素弹出。
3.最后判断栈是否为空,不为空则说明是情况1),返回false。为空说明避免了全部三种情况这是有效字符串返回true。
实现代码:
class Solution {
public boolean isValid(String s) {
Stack<Character> ss=new Stack();
for(int i=0;i<s.length();i++){
char now=s.charAt(i);
if(now=='('){
ss.push(')');
}else if(now=='{'){
ss.push('}');
}else if(now=='['){
ss.push(']');
}else if(ss.empty() || now!=ss.peek()){
return false; //说明前面没有对应的左括号,直接false
}else{
ss.pop(); //说明有对应的左括号且顺序正确,则弹出对那个的右括号
}
}
return ss.empty();
}
}
关键点:
1.栈的特性非常适合做对称匹配的问题
2.else if(ss.empty() || now!=ss.peek()){return false;}
3.return ss.empty();都是在判断是否是不有效字符串的三种情况
4.删除字符串中的所有相邻重复项(1047)
题目:
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
分析和解决思路:
分析:
1.这个题目可以想象成消消乐
2.利用栈获取和删除栈顶元素(即最后入栈元素)的peek和pop函数,实现在存入时就消消乐的操作!
解决思路:
1.for循环遍历字符串。
2.在单个字符存入栈之前,判断
1)如果是此时栈为空,则直接存入元素;
2)如果栈不为空:栈顶元素等于当前字符,说明重复弹出栈顶元素;不等于当前字符,说明不重复存入元素。
3.最后将剩余元素存成字符串返回(可以直接“”+元素变成字符串)(也可以创建字符数组,new String(字符数组))
实现代码:
class Solution {
public String removeDuplicates(String s) {
Stack<Character> ss=new Stack();
for(int i=0;i<s.length();i++){
char now=s.charAt(i);
if(ss.empty()){
ss.push(now);
}else if(now==ss.peek()){
ss.pop();
}
else{
ss.push(now);
}
}
//剩余的元素即为不重复的元素,将栈中的元素取出组成字符串(两种方法都可)
// String str = "";
// while (!ss.empty()) {
// str = ss.pop() + str;
// }
// return str;
char[] re=new char[ss.size()];
for(int j=ss.size()-1;j>=0;j--){
re[j]=ss.pop();
}
return new String(re);
}
}
5.逆波兰表达式求值(150)
题目:
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
分析和解决思路:
分析:
1.逆波兰记表示法:操作符置于操作数的后面。例如表达“三加四”时,写作“3 4 +”,而不是“3 + 4”。即操作符置于第二个操作数的后面。
2.逆波兰表达式的求值使用堆栈结构很容易实现,能很快求值。解释过程一般是:操作数入栈;遇到操作符时,操作数出栈,求值,将结果入栈;当一遍后,栈顶就是表达式的值。
解决思路:
1.创建栈s,并把运算符放到一个字符串yun中。
2.对tokens字符串进行遍历,对于每一个字符进行判断:
1)如果yun中包含(contains)该字符,说明是运算符,需要进行运算,连续弹出栈顶前两个数字进行运算。然后进行进一步判断,不同运算符进行不同的运算。【需注意!在减法和除法运算中,减数和除数都是第二个操作数(即第一个弹出的数字),需要注意运算顺序。】最后将运算结果存入栈,方便下一次运算。
2)如果yun中不包含(contains)该字符,说明是数字,将该字符通过Integer.valueOf()转换成数字存入栈中,方便后续运算。
3.栈中最后的元素就是运算的结果,直接返回pop元素即可。
实现代码:
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> s=new Stack();
String yun="+-*/";
for(int i=0;i<tokens.length;i++){
if(yun.contains(tokens[i])){
//说明是运算符
int res=0;
if("+".equals(tokens[i])){
res=s.pop()+s.pop();
}else if("-".equals(tokens[i])){
res=-s.pop()+s.pop();
}else if("*".equals(tokens[i])){
res=s.pop()*s.pop();
}else if("/".equals(tokens[i])){
int t1=s.pop(); //后一个
int t2=s.pop();
res=t2/t1;
}
s.push(res);
}else{
s.push(Integer.valueOf(tokens[i]));
}
}
return s.pop();
}
}
关键点:
1.进行减法运算和除法运算时,需要注意运算顺序。可以将先弹出的元素存储下来,防止运算顺序错误。
2.Integer.valueOf(tokens[i]),数据转换-字符转换成整数。
6.滑动窗口最大值(239)
题目:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。
分析和解决思路:
分析:
1.第一反应按照严格的顺序,滑动每次比较k个元素的大小,然后输出。这样可实现,但是运算复杂度太高,数据多会超出时间限制。
2.PriorityQueue(优先队列)是一种队列数据结构实现,其中根据优先级处理对象。在优先队列中,添加的对象根据其优先级从队头排至队尾。默认情况下,优先级由对象的自然顺序决定。同时!队列构建时提供的比较器可以覆盖默认优先级。
3.由于每次输出都是窗口内最大值,自然联想到PriorityQueue【提供比较器降序排列】的peek函数可以直接得到队列的最大值。
4.但同时也要保证此时的队头元素在滑动窗口内,才算有效,所以需要将【元素值和元素下标作为数组元素】一起存入队列中,方便判断。
解决思路:
1.定义优先队列pq,并在队列构建时提供比较器,使得队列中对象按照元素值降序排列。
2.对于第一个滑动窗口,就是存入前k个数组[元素值,元素下标]进入优先队列,最后pq.peek()[0]就是第一个滑动窗口中的最大值。
3.对于后面的滑动窗口:
1)是从下标i=[k,length-1]的的数组元素依次存入队列,实现滑动窗口的向后扩展。
2)而保持滑动窗口的前端界限正确,是通过存入的元素下标判断来保证的。由于每次只返回最大值,所以我们只要保证返回的最大值的下标在滑动窗口内就是正确的。下标不应在的范围如下图所示。若此时队列最大值下标在不应出现的范围内则poll。
实现代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
//定义优先队列并给优先队列定义比较器Comparator
PriorityQueue<int[]> pq=new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] p1,int[] p2){ //需定义队列中存储的是数组{nums[i],i}即元素和下标
//在比较器中,优先比较的一定是元素值,相等的情况才根据下标大小来
return p1[0]!=p2[0]?p2[0]-p1[0]:p2[1]-p1[1];
}
});
int len=nums.length;
int[] res=new int[len-k+1]; //定义返回数组大小
//第一个滑动窗口就是前k个元素存入,取队列头数组的元素值就是最大值
for(int i=0;i<k;i++){
pq.offer(new int[]{nums[i],i}); //
}
res[0]=pq.peek()[0];
//后面的滑动窗口依次存入新的值
for(int i=k;i<len;i++){
pq.offer(new int[]{nums[i],i});
//判断存入之后的最大值下标是否还在滑动窗口内
while(pq.peek()[1]<=i-k){
//说明不在窗口内
pq.poll();
}
//直到找到在窗口内的最大值
res[i-k+1]=pq.peek()[0];
}
return res;
}
}
关键点:
1.构造比较器的代码:
new Comparator<int[]>(){
public int compare(int[] p1,int[] p2){
return p1[0]!=p2[0]?p2[0]-p1[0]:p2[1]-p1[1];
}
}
2.比较器中升序降序问题:牢记return正数的时候,两个对象存入时调换位置!return负数和零时,两个对象存入不调换。
1)若return p1-p2:p1>p2时,此时返回正数存入顺序为p2->p1;p1<=p2时,此时返回负数或0,存入顺序为p1->p2。可以发现始终是小的数在前,则是升序排列。
2)若return p2-p1:p1>=p2时,此时返回负数或0,存入顺序为p1->p2;p1<p2时,此时返回正数,存入顺序为p2->p1。可以发现始终是大的数在前,则是降序排列。
7.前k个高频元素(347)
题目:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
分析和解决思路:
分析:
1.题目中要求返回频率高的元素,那么首先肯定要统计每个元素出现的次数。联想到哈希表统计元素个数的方法。
2.题目中要求返回频率前k高的元素,那么需要将元素按照出现次数排序,联想到PriorityQueue(优先队列)。由于队列取元素是从队头取,那么肯定将目前第k次数多的元素放至队头,方便后续取出和比较,则优先队列是升序排列(由队头至队尾)。
3.遍历完所有的元素后,队列里留下的就是出现频率前k高的元素。
解决思路:
1.HashMap存储键值对(元素,元素出现次数)。实现方法利用map当中key的唯一性,使用put方法进行元素出现次数的更新,同时使用getOrDefault获取当前的value值。
2.创建优先队列并构建比较器,此时是升序排列,则是p1-p2。
3.使用entrySet方法遍历map将键值对以数组元素不断存入优先队列中进行前k个高频元素的更新。当队列中元素不足k个时直接存入;有了k个之后,存入之前每次都和队列的队头元素进行比较,次数更高则更新。
4.遍历结束后,将队列中的元素取出放入数组。
实现代码:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//先统计各元素及其个数
HashMap<Integer,Integer> mymap=new HashMap();
for(int i=0;i<nums.length;i++){
mymap.put(nums[i],mymap.getOrDefault(nums[i],0)+1);
}
//把键值对取出来当作数组元素存入优先队列中,此时优先队列的顺序是升序
PriorityQueue<int[]> pq=new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] p1,int[] p2){
return p1[1]-p2[1]; //实现升序排列
}
});
//遍历map的键值对
int i=0;
for(Map.Entry<Integer, Integer> my : mymap.entrySet()){
int num=my.getKey();
int freq=my.getValue();
if(i<k){
pq.offer(new int[]{num,freq});
}else{
if(freq>pq.peek()[1]){ //此时peek是当前队列k个数中中最小的频数的元素
pq.poll();
pq.offer(new int[]{num,freq});
}
}
i++;
}
//取出最后的次数值
int[] res = new int[k];
for (int j = 0; j < k; ++j) {
res[j] = pq.poll()[0];
}
return res;
}
}
关键点:
1.mymap.getOrDefault(key,defaultvalue)根据key值返回对应的value值,若key不存在与map当中,则返回设置的defaultvalue。
2.优先队列和比较器构造参见上一题。
3.遍历map有三种方法分别是keySet、 entrySet、forEach。参考博客:https://blog.csdn.net/qq_38038143/article/details/95317671
耶!又完结一章!祝大家天天开心~~~(特别是你!别看别人就是你😉,加油噢)
Ps:其中有写的不对的地方请大家多多指正!!