数据结构之—栈和队列

目录

引言

一、栈(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基础上的实现。栈和队列是一个使用上更加严格的线性表,动态数组,链表可以在任意位置进行元素的插入和删除,栈和队列不行,他们是一端插入一端删除。

        线性表:数组、链表、字符串、栈、队列(元素按照一条“直线”排列,线性表这个结构中,一次添加单个元素)

一、栈(stack)

        后进先出的线性表,支持三个核心操作:入栈push出栈pop返回栈顶元素 peek【水杯就是一个天然的栈结构,只能从杯口倒入水,从杯口倒出水】

LIFO:后进先出--Last In First Out

1.栈的应用

1)撤销操作:一般任意编译器中,撤销操作:ctrl+z

2)浏览器的前进后退:如此时页面在C页面,看完C后想返回B页面,点击后退箭头相当于将C出栈,此时栈顶就是B页面。

3)开发中程序的“调用栈”操作系统栈底层就是我们的栈实现。

数据结构之—栈和队列_第1张图片

分析:funA()卡在第二行入栈,funB()卡在第二行入栈,funC入栈,return开始就是出栈了

2.栈的实现

自己实现栈,栈也是一个线性表

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.基础队列的实现

1)基于数组实现的队列--顺序队列

2)基于链表实现的队列--链式队列(优)

        由于队列是队尾入队,队首出队,队首出队如果用数组就是数组的头部删除,时间复杂度为O(n),此时使用链表会更好,在链表尾部插入,头部删除,或者尾删头插。

数据结构之—栈和队列_第2张图片

 头删尾插举例:

/**
 * 将队列设计为接口,泛型接口(支持多种类型)
 * @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。

2.循环队列 数据结构之—栈和队列_第3张图片

由于数组实现队列,所以产生循环队列【环形队列由数组实现】

1)概念

        循环队列的队列是一个“环”,逻辑上成环,物理上还是线性表。head指向当前队列的第一个元素下标,tail指向当前队列的最后一个元素的下一个位置,当tail走到数组末尾时,下一步再次返回数组头部(0)。

数据结构之—栈和队列_第4张图片

1)当tail走到数组末尾时,若数组头部还有空闲位置,tail返回数组头部继续存放元素(比之前的普通队列节省空间)

数据结构之—栈和队列_第5张图片

2)出队时直接将head引用向后移动即可,不需要再搬移元素

eg:此时出队head=head+1;

此时元素1就访问不到了,逻辑删除。此时就解决了数组队列出队时需要来回搬移元素的问题

数据结构之—栈和队列_第6张图片

2)如何判断环形队列为空⭐

1.head==tail

数据结构之—栈和队列_第7张图片

 2.注意

数据结构之—栈和队列_第8张图片

        满员情况head==tail,tail也指向了1。此时就无法区分环形队列到底是满还是空,因此我们在环形队列的数组中,浪费一个空间,来区分数组是否已满

数据结构之—栈和队列_第9张图片

        此时tail已经走到数组末尾,如何让他返回数组头部??⭐⭐【data.length=5;tail=4,当tail到达数组末尾时,如何再次移动返回头部】

取模-对数组长度取模

tail=(tail+1)%data.length;

总结:环形队列是否已满条件(tail+1)%data.length==head;

3.此时head和tail引用向后移动时,不能简单地+1,要+后对数组长度取模(可以返回数组头部继续向后移动)

head=(head+1)%data.length;tail=(tail+1)%data.length;注意:head是队首元素的索引,tail是当前队尾元素的下一个位置的索引。

4.如何获取当前循环队列的最后一个元素的索引?⭐

tail-1(tail指向当前最后一个元素的下一个位置)

特殊:观察如下情况

数据结构之—栈和队列_第10张图片

3)循环队列代码实现:

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);

三、题目:

数据结构之—栈和队列_第11张图片

1.栈和队列的相互转化

        栈和队列本质上是相同的,都只能在线性表的一端进行插入或删除,因此可以互相转换。可以用栈模拟队列,也可以用队列模拟栈。

1)用栈实现队列(leetcode232)

数据结构之—栈和队列_第12张图片

分析:用两个栈模拟队列

数据结构之—栈和队列_第13张图片

        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();
    }
}

2)用队列实现栈(leetcode225)

数据结构之—栈和队列_第14张图片

要达到的目标:队首元素恰好是栈顶元素

数据结构之—栈和队列_第15张图片

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();
    }
}

2.栈的应用

1)括号匹配问题(leetcode20)

        分析:问题的关键在于看右括号是否和左括号匹配,当发现左括号先不处理,等先找右括号。碰到右括号就倒着看与最后的左括号(栈顶)是否匹配成功,匹配成功则左括号出栈,最后栈空则匹配成功。

注意边界问题:

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();
    }
}

2)最小栈

 数据结构之—栈和队列_第16张图片

分析:栈和队列解题套路:双栈或双队列

        双栈,两个栈s1s2。s1:实际存储元素,s2:一直存储当前最小值的栈。去除最小值,直接从s2去取即可。从-2,0,-3入栈为例,-2入两栈;0入s1,0与s2元素比大小,0>-2,不入0,把s2再入一次;-3入s1,与s2栈顶元素比大小,小,入栈s2。getMin()直接取s2栈顶元素即可。

数据结构之—栈和队列_第17张图片

问题:为何入栈时,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存着】
    }
}

3.队列

1)力扣_队列中的最大值

(注意区别最大/最小栈)

/**
 * 队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。
 * 这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。
 * 注意取元素时只能从队尾取,因为队首元素可能大于当前元素。应用到双端队列
 */
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博客

你可能感兴趣的:(数据结构,数据结构,java,队列,栈)