目录
引言
一、栈(stack)
1.栈的应用
2.栈的实现
二、队列
1.基础队列的实现
2.循环队列 编辑
1)概念
2)如何判断环形队列为空⭐
总结:环形队列是否已满条件:(tail+1)%data.length==head;
3)循环队列代码实现:
三、题目:
1.栈和队列的相互转化
1)用栈实现队列(leetcode232)
2)用队列实现栈(leetcode225)
进阶:用一个队列实现
2.栈的应用
1)括号匹配问题(leetcode20)
2)最小栈
接:集合类—List、Map、Set的简单用法_林纾y的博客-CSDN博客_map put列表
内部类和泛型_林纾y的博客-CSDN博客_内部类使用泛型
栈和队列都是线性表,都是基于List基础上的实现。栈和队列是一个使用上更加严格的线性表,动态数组,链表可以在任意位置进行元素的插入和删除,栈和队列不行,他们是一端插入一端删除。
线性表:数组、链表、字符串、栈、队列(元素按照一条“直线”排列,线性表这个结构中,一次添加单个元素)
后进先出的线性表,支持三个核心操作:入栈push;出栈pop;返回栈顶元素 peek【水杯就是一个天然的栈结构,只能从杯口倒入水,从杯口倒出水】
LIFO:后进先出--Last In First Out
1)撤销操作:一般任意编译器中,撤销操作:ctrl+z
2)浏览器的前进后退:如此时页面在C页面,看完C后想返回B页面,点击后退箭头相当于将C出栈,此时栈顶就是B页面。
3)开发中程序的“调用栈”操作系统栈底层就是我们的栈实现。
分析:funA()卡在第二行入栈,funB()卡在第二行入栈,funC入栈,return开始就是出栈了
自己实现栈,栈也是一个线性表
1)基于数组实现的栈--顺序栈【数组尾部添加删除时间复杂度O(1)】,栈顶实际上就是数组末尾
2)基于链表实现的栈--链式栈【尾插和尾删】
代码:
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
/**
* 基于数组的顺序栈实现
* @param
*/
public class MyStack {
//当前栈的数据个数
private int size;
//实际存储数据的动态数组-ArrayList-长度不够自动扩容
private List data=new ArrayList<>();
/**
* 将val入栈
* @param val
*/
public void push(E val){
//默认List集合的add是尾插
data.add(val);
size++;
}
/**
* 弹出栈顶元素,返回栈顶的值
* @return
*/
public E pop(){
if(isEmpty()){
//栈为空,没有栈顶元素,抛出异常
throw new NoSuchElementException("stack is empty!cannot pop!");
}
//删除栈顶元素
E val=data.remove(size-1);
size--;
return val;
//这三行可写成一句:return data.remove(--size);
}
/**
* 返回栈顶元素,但不出栈
* @return
*/
public E peek(){
if(isEmpty()){
throw new NoSuchElementException("stack is empty!cannot peek!");
}
return data.get(size-1);
}
/**
* 返回当前栈顶元素是否为空
* @return
*/
public boolean isEmpty(){
return size==0;
}
/**
* 打印栈的元素
* @return
*/
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(data.get(i));
if(i!=size-1){
sb.append(",");
}
}
sb.append("] top");
return sb.toString();
}
}
测试:
public class Test {
public static void main(String[] args) {
MyStack myStack=new MyStack<>();
myStack.push(1);
myStack.push(3);
myStack.push(5);
System.out.println(myStack.toString());
while(!myStack.isEmpty()){
System.out.println(myStack.pop());
}
}
}
FIFO:在队首出队,在队尾入队,先进先出的数据结构
食堂排队就是队列结构
1)基于数组实现的队列--顺序队列
2)基于链表实现的队列--链式队列(优)
由于队列是队尾入队,队首出队,队首出队如果用数组就是数组的头部删除,时间复杂度为O(n),此时使用链表会更好,在链表尾部插入,头部删除,或者尾删头插。
头删尾插举例:
/**
* 将队列设计为接口,泛型接口(支持多种类型)
* @param
*/
public interface Queue {
//入队
void offer(E val);
//出队
E poll();
//返回队首元素
E peek();
//队列是否为空
boolean isEmpty();
}
实现接口:
import java_1_24.stack_queue.queue.Queue;
import java.util.NoSuchElementException;
/**
* 队列的实现
* 基于链表实现的基础队列
*/
public class MyQueue implements Queue {
//节点--用成员内部类-链表的每个节点
private class Node{
E val;
Node next;
public Node(E val) {
this.val = val;
}
}
//当前队列元素个数
private int size;
//链表队首
private Node head;
//队尾
private Node tail;
//入队-尾插
@Override
public void offer(E val) {
Node node=new Node(val);
if(head==null){
//此时队列为空(头节点都为空尾也肯定为空)
head=tail=node;
}else{
tail.next=node;
tail=node;
}
size++;
}
//出队-头删-返回队首元素(未删除前的)
@Override
public E poll() {
if(isEmpty()){
throw new NoSuchElementException("queue is empty!cannot pull!");
}else{
E val=head.val;
Node node=head;
head=head.next;
node.next=null;//原先头节点脱钩
size--;
return val;
}
}
@Override
public E peek() {
if(isEmpty()){
throw new NoSuchElementException("queue is empty!cannot peek!");
}else{
return head.val;
}
}
@Override
public boolean isEmpty() {
return size==0;
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("front [");
//链表遍历
for (Node x = head; x!=null; x=x.next) {
sb.append(x.val);
if(x.next!=null){
//未走到链表尾部
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
测试:
public class QueueTest {
public static void main(String[] args) {
Queue queue=new MyQueue<>();
queue.offer(1);
queue.offer(3);
queue.offer(5);
System.out.println(queue.toString());
while (!queue.isEmpty()){
System.out.println(queue.poll());
}
}
}
Queue中add()和offer()都是用来向队列中添加元素,在容量已满情况下,add方法会抛出异常,offer方法会返回false。
由于数组实现队列,所以产生循环队列【环形队列由数组实现】
循环队列的队列是一个“环”,逻辑上成环,物理上还是线性表。head指向当前队列的第一个元素下标,tail指向当前队列的最后一个元素的下一个位置,当tail走到数组末尾时,下一步再次返回数组头部(0)。
1)当tail走到数组末尾时,若数组头部还有空闲位置,tail返回数组头部继续存放元素(比之前的普通队列节省空间)
2)出队时直接将head引用向后移动即可,不需要再搬移元素
eg:此时出队head=head+1;
此时元素1就访问不到了,逻辑删除。此时就解决了数组队列出队时需要来回搬移元素的问题
1.head==tail
2.注意
满员情况head==tail,tail也指向了1。此时就无法区分环形队列到底是满还是空,因此我们在环形队列的数组中,浪费一个空间,来区分数组是否已满。
此时tail已经走到数组末尾,如何让他返回数组头部??⭐⭐【data.length=5;tail=4,当tail到达数组末尾时,如何再次移动返回头部】
取模-对数组长度取模⭐
tail=(tail+1)%data.length;
3.此时head和tail引用向后移动时,不能简单地+1,要+后对数组长度取模(可以返回数组头部继续向后移动)
head=(head+1)%data.length;tail=(tail+1)%data.length;注意:head是队首元素的索引,tail是当前队尾元素的下一个位置的索引。
4.如何获取当前循环队列的最后一个元素的索引?⭐
tail-1(tail指向当前最后一个元素的下一个位置)
特殊:观察如下情况
import java_1_24.stack_queue.queue.Queue;
import java.util.NoSuchElementException;
/**
* 基于整形的循环队列
*/
public class LoopQueue implements Queue {
private Integer[] data;
//指向循环队列队首元素
private int head;
//指向当前循环队列队尾的队尾元素的下一个位置
private int tail;
//当前队列中的元素个数(思考如何仅用tail和head来判断当前队列中的元素个数)
private int size;
//n为希望保存的元素个数
public LoopQueue(int n){
//在循环队列中浪费一个空间不能存储元素,来判断是否已满
data=new Integer[n+1];
}
@Override
public void offer(Integer val) {
if(isFull()){
throw new ArrayIndexOutOfBoundsException("queue is full,cannot offer new val");
}
data[tail]=val;
tail=(tail+1)% data.length;
size++;
}
@Override
public Integer poll() {
if(isEmpty()){
throw new NoSuchElementException("queue is empty,cannot poll");
}
Integer val=data[head];//保存队首元素并返回
head=(head+1)%data.length;
size--;
return val;
}
@Override
public Integer peek() {
if(isEmpty()){
throw new NoSuchElementException("queue is empty,cannot poll");
}
return data[head];
}
@Override
public boolean isEmpty() {
return tail==head;
}
public boolean isFull(){
return (tail+1)% data.length==head;
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("front [");
//取得最后一个元素下标
//(tail是最后一个元素下标的下一位,如果最后是满的,最后一个元素下标就是数组长度)
int lastIndex=tail==0? data.length-1 : tail-1;
for (int i = head; i !=tail;) {
sb.append(data[i]);
if(i!=lastIndex){
sb.append(",");
}
i=(i+1)% data.length;//这一步放在打印逗号后
}
sb.append("]tail");
return sb.toString();
}
}
测试:
Queue loopQueue=new LoopQueue(5);
for (int i = 0; i < 5; i++) {
loopQueue.offer(i+1);
}
System.out.println(loopQueue);
System.out.println(loopQueue.poll());
System.out.println(loopQueue);
栈和队列本质上是相同的,都只能在线性表的一端进行插入或删除,因此可以互相转换。可以用栈模拟队列,也可以用队列模拟栈。
分析:用两个栈模拟队列
s1是实际存储元素的栈,每次新增元素时一定要保证s1为空,这样新元素入栈s1恰好保证在栈底,通过s1弹出元素再弹回的这个过程就实现了后进后出。
代码:
import java.util.Stack;
/**
* 用栈实现队列
* 双栈模拟队列
*/
public class Num232_MyQueue {
//实际存储元素的栈
private Stack s1=new Stack<>();
//辅助栈
private Stack s2=new Stack<>();
public Num232_MyQueue() {
}
public void push(int x){
if(s1.isEmpty()){
s1.push(x);
}else{
/**
* 先把s1所有元素弹出放在s2
*/
while(!s1.isEmpty()){
// s2.push(s1.pop());
int val=s1.pop();
s2.push(val);
}
//将新元素直接放入s1,此时新元素就在s1栈底
s1.push(x);
//再将s2所有元素依次弹回s1
while (!s2.isEmpty()){
s1.push(s2.pop());
}
}
}
public int pop(){
return s1.pop();
}
public int peek(){
return s1.peek();
}
public boolean empty(){//注意leetcode给的是empty
return s1.isEmpty();
}
}
要达到的目标:队首元素恰好是栈顶元素
q2添加元素后,q1出队入队,再把q1q2引用名字换掉,直接q1就是最后结果
/**
* 双队列实现栈
*/
public class Num225_MyStack {
public Num225_MyStack() {
}
// q1是存储元素的队列
private Queue q1 =new LinkedList<>();
//q2是辅助队列,添加元素后保证q2永远为空
private Queue q2=new LinkedList<>();
public void push(int x){
//新元素直接入q2,新元素就在q2的队首
q2.offer(x);
while(!q1.isEmpty()){
q2.offer(q1.poll());//q1出队入q2
}
//走到这里q1为空,q2为存储元素的队列,互换引用的指向
//互换指向之后仍然可以保证q1是存储元素的队列,q2为空
Queue temp=q1;
q1=q2;
q2=temp;
}
public int pop(){
return q1.poll();
}
public int top(){
return q1.peek();
}
public boolean empty(){
return q1.isEmpty();
}
}
用队列实现的核心操作:保证新元素一定处在队首,那么,将元素先入队,再将之前的元素依次出队(保证新元素再队首)再入队即可。
代码:
public class Num225_OneQueueToRealizeMyStack {
private Queue queue=new LinkedList<>();
public Num225_OneQueueToRealizeMyStack(){
}
public void push(int x) {
//记录之前的元素个数
int size=queue.size();
//新元素入队
queue.offer(x);
//之前的元素依次出队再入队,新元素恰好在队首位置
for (int i = 0; i < size; i++) {
queue.offer(queue.poll());
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
分析:问题的关键在于看右括号是否和左括号匹配,当发现左括号先不处理,等先找右括号。碰到右括号就倒着看与最后的左括号(栈顶)是否匹配成功,匹配成功则左括号出栈,最后栈空则匹配成功。
注意边界问题:
1)当字符串扫描完毕,栈不为空时,说明左括号多了,没有相应右括号闭合匹配-false
2)扫描到第一个右括号时,发现此时栈空了,右括号是字符串第一个字符,没有相应左括号闭合-false
代码:
public boolean isValid(String s) {
Stack stack=new Stack<>();
//将字符串转为字符输出
for (int i = 0; i < s.length(); i++) {
char c=s.charAt(i);//按索引取字符
//碰到左括号直接入栈
if(c=='{'||c=='['||c=='('){
stack.push(c);
}else{//此时c是右括号
if(stack.isEmpty()){
//右括号是第一个字符,没有相应的左括号匹配-false
return false;
}
char top=stack.pop();//弹出栈顶左括号
if(c==')'){
if(top!='('){
return false;
}
}
/**也可以写成:
* if(c==')'&&top!='('){
* return false;
* }
*/
if(c==']'){
if(top!='['){
return false;
}
}
if(c=='}'){
if(top!='{'){
return false;
}
}
}
}
//此时字符串扫描完毕,判断当前栈中是否为空
return stack.isEmpty();//为空就是匹配,返回true
}
相同题型:合法括号序列判断__牛客网 https://www.nowcoder.com/questionTerminal/d8acfa0619814b2d98f12c071aef20d4
import java.util.*;
public class Parenthesis {
public boolean chkParenthesis(String A, int n) {
// write code here
//字符串和长度n已知
//长度为奇数时一定不是合法的
if(n%2!=0){
return false;
}
//长度为偶数时,用栈存放左括号,遍历到右括号时进行匹配,匹配成功括号出栈继续判断
//遍历字符串途中有字母时一定false
//最后遍历完毕,若栈为空则全部匹配完成,不为空匹配失败
//注意遍历途中栈为空的情况,此时栈中无左括号但是字符串还未遍历完毕,明显左右括号匹配出错,false
Stack stack=new Stack();
//遍历字符串:因为不需要对字符串有任何修改,直接for-each循环遍历即可
for(char ch:A.toCharArray()){
if(ch=='('){
stack.push(ch);
}else if(ch==')'){
//判断右括号存在时栈是否为空
if(stack.isEmpty()){
return false;
}
//此时栈不为空,开始匹配,判断是否栈顶是'('
//其实不用if直接出栈也可以,因为此时栈内只可能放了'('
if(stack.peek()=='('){
stack.pop();
}
}else{
//其他字符出现,直接false
return false;
}
}
return stack.isEmpty();
}
}
分析:栈和队列解题套路:双栈或双队列
双栈,两个栈s1s2。s1:实际存储元素,s2:一直存储当前最小值的栈。去除最小值,直接从s2去取即可。从-2,0,-3入栈为例,-2入两栈;0入s1,0与s2元素比大小,0>-2,不入0,把s2再入一次;-3入s1,与s2栈顶元素比大小,小,入栈s2。getMin()直接取s2栈顶元素即可。
问题:为何入栈时,0比-2大,还要再把-2再次入栈s2一次?
为了保证s1和s2元素个数始终相同(个数相同不至于s1还有值时s2为空)
代码:
import java.util.Stack;
/**
* 最小栈
*/
public class Num155_MinStack {
private Stack s1=new Stack<>();
private Stack s2=new Stack<>();
public Num155_MinStack() {
}
public void push(int val) {
s1.push(val);
if(s2.isEmpty()){
s2.push(val);
}else{
//比较当前val和s2栈顶元素,将较小的入栈s2
int tmp=s2.peek();
s2.push(Math.min(tmp,val));//取最小值入栈s2
}
}
public void pop() {
s1.pop();//题目给出是非空栈,不用判空,直接出栈,s1s2都出
s2.pop();
}
public int top() {
return s1.peek();//返回栈顶元素但不出栈
}
public int getMin() {
return s2.peek();//返回s2栈顶元素【最小值在s2存着】
}
}
(注意区别最大/最小栈)
/**
* 队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。
* 这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。
* 注意取元素时只能从队尾取,因为队首元素可能大于当前元素。应用到双端队列
*/
public class Num59_II_队列的最大值 {
Queue q;
Deque d;//辅助队列,存放队列结果的最大值【最大值从队尾插入从队首出队,中途取出比最大值小的值时从队尾出队取出小值】
public Num59_II_队列的最大值() {
q=new LinkedList<>();
d=new LinkedList<>();
}
public int max_value() {//队列为空时返回-1
if (d.isEmpty()){
return -1;
}
return d.peekFirst();
}
public void push_back(int value) {
q.offer(value);//入队
//将入队元素与双端队列中元素对比,将比value小的元素从双端队列出队,将val入队
while (!d.isEmpty()&&d.peekLast()
栈和队列属于线性表,相关线性表内容见:数据结构之—顺序表和链表_林纾y的博客-CSDN博客