Stack和Queue

目录

1. 栈(Stack)

1.1 概念

1.2 栈的使用 

1.3 栈的模拟实现 

1.4 栈的应用场景 

1.5 概念区分 

2. 队列(Queue)

2.1 概念

2.2 队列的使用 

2.3 队列模拟实现 

2.4 循环队列 

3. 双端队列 (Deque)

 


1. 栈(Stack)

1.1 概念

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据在栈顶

1.2 栈的使用 

public static void main(String[] args) {
    Stack<Integer> s = new Stack();
    s.push(1);
    s.push(2);
    s.push(3);
    s.push(4);
    System.out.println(s.size()); // 获取栈中有效元素个数---> 4
    System.out.println(s.peek()); // 获取栈顶元素---> 4
    s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
    System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
    if(s.empty()){
        System.out.println("栈空");
    }else{
        System.out.println(s.size());
    }
}

1.3 栈的模拟实现 

从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。 

// 用单链表实现的栈
public class MyStack {
    private int[] elem;
    private int usedSize;

    public MyStack() {
        this.elem = new int[5];
    }

    // 压栈
    public void push(int val) {
        if (isFull()) {
            // 扩容
            this.elem = Arrays.copyOf(this.elem, this.elem.length*2);
        }
        this.elem[usedSize] = val;
        this.usedSize++;
    }
    // 判断栈是否满了
    public boolean isFull() {
        return usedSize == elem.length;
    }
    // 出栈
    public int pop() {
        if(isEmpty()) {
            throw new StackEmptyException("栈为空!");
        }
        return this.elem[--usedSize];
    }
    // 判断栈是不是为空
    public boolean isEmpty() {
        return usedSize == 0;
    }
    // 获取栈顶元素
    public int peek() {
        if (isEmpty()) {
            throw new StackEmptyException("栈为空!");
        }
        // 开始删除
        return this.elem[usedSize--];
    }
}

1.4 栈的应用场景 

1. 改变元素的序列

1. 若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()

A: 1,4,3,2 B: 2,3,4,1 C: 3,1,4,2 D: 3,4,2,1

2.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺 序是( )。

A: 12345ABCDE     B: EDCBA54321     C: ABCDE12345     D: 54321EDCBA

2. 将递归转化为循环

比如:逆序打印链表

// 递归方式
void printList(Node head){
    if(null != head){
    printList(head.next);
    System.out.print(head.val + " ");
    }
}
// 循环方式
void printList(Node head){
    if(null == head){
        return;
    }
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while(null != cur){
    s.push(cur);
    cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
    System.out.print(s.pop().val + " ");
    }
}

1.5 概念区分 

  1. 概念

    • :栈是一种先进后出(FILO)的数据结构,在Java中可以通过java.util.Stack类来使用,它继承自Vector类。
    • 虚拟机栈:每个线程在创建时都会分配一块私有的内存空间,即虚拟机栈,用于存放栈帧,即方法调用相关的信息。
    • 栈帧:当一个方法被调用时,会在虚拟机栈中创建一个栈帧,用来保存该方法的局部变量表、操作数栈等信息。方法执行结束后,相应的栈帧也会从栈中移除。
  2. 存储内容

    • :存储的是一系列对象,这些对象的进栈和出栈顺序遵循后进先出的原则。
    • 虚拟机栈:主要存储栈帧,每个线程独立拥有一个虚拟机栈,存储该线程执行过程中所需的数据。
    • 栈帧:存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
  3. 生命周期

    • :通常指的是逻辑上的生命周期,与具体的实现相关。
    • 虚拟机栈:它的生命周期与线程相同,线程结束时,对应的虚拟机栈也会销毁。
    • 栈帧:伴随方法调用生成,并随着方法执行完毕而销毁。
  4. 异常情况

    • :如果尝试访问空栈中的元素,会抛出异常。
    • 虚拟机栈:如果栈内空间不足,无法为新的方法调用分配栈帧,则会抛出StackOverflowError
    • 栈帧:不直接关联到特定的错误或异常,但方法的异常退出会导致栈帧的出栈。
  5. 垃圾回收

    • :作为逻辑结构,其元素由程序员控制,不涉及自动垃圾回收。
    • 虚拟机栈:不受垃圾回收器的管理,因为其生命周期与线程同步。
    • 栈帧:局部变量表中可能包含对堆上对象的引用,这些对象是可以被垃圾回收的。
  6. 性能优化关系

    • :通常不需要特殊优化,因为它是由JVM管理的。
    • 虚拟机栈:大小可以调整,过大可能导致StackOverflowError,过小可能影响程序性能。
    • 栈帧:局部变量表的大小会影响栈帧的大小,进而影响方法调用的深度和性能。

2. 队列(Queue)

2.1 概念

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear)

出队列:进行删除操作的一端称为队头(Head/Front)

2.2 队列的使用 

在Java中,Queue是个接口,底层是通过链表实现的。

 

注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。

public static void main(String[] args) {
        Queue<Integer> q = new LinkedList<>();
        q.offer(1);
        q.offer(2);
        q.offer(3);
        q.offer(4);
        q.offer(5); // 从队尾入队列
        System.out.println(q.size());
        System.out.println(q.peek()); // 获取队头元素
        q.poll();
        System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
        if(q.isEmpty()){
            System.out.println("队列空");
        }else{
            System.out.println(q.size());
        }
}

2.3 队列模拟实现 

队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有 两种:顺序结构 和 链式结构。下面是用单链表实现的队列.

// 采用带尾指针的单链表实现的队列
public class MyQueue {
    static class ListNode {
        public int val;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
    public ListNode last;

    private int usedSize;

    public int getUsedSize() {
        return usedSize;
    }

    //入队
    public void offer(int val) {
        ListNode node = new ListNode(val);
        if(head == null) {
            head = node;
            last = node;
        }else {
            last.next = node;
            last = last.next;
        }
        usedSize++;
    }
    //出队
    public int poll() {
        if(head == null) {
            return -1;
        }
        int val = -1;
        if(head.next == null) {
            val = head.val;
            head = null;
            last = null;
            usedSize--;
            return val;
        }
        val = head.val;
        head = head.next;
        usedSize--;
        return val;
    }
    //获得队头元素
    public int peek() {
        if(head == null) {
            return -1;
        }
        return head.val;
    }
}

2.4 循环队列 

实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。 环形队列通常使用数组实现。

我们可以思考几个问题

1. 什么时候循环队列是空的?

答: front == rear的时候

2. 什么时候循环队列是满的?

答: 1) 定义一个usedSize, 当usedSize == length的时候, 循环队列是满的

      2) 浪费一个空间来表示满 (接下来实现的代码采用的是这种方式)

3. rear是怎么从7下标走到0下标的? front是怎么从7下标走到0下标的?

答: (rear + 1) % length

class MyCircularQueue {
    private int[] elem;
    private int front; //队头下标
    private int rear; //队尾下标
    public MyCircularQueue(int k) {
        elem = new int[k];
    }
    //入队
    public boolean enQueue(int value) {
        if(isFull()) {
            return false;
        }
        elem[rear] = value;
        rear = (rear + 1) % elem.length;
        return true;
    }
    // 出队
    public boolean deQueue() {
        if(isEmpty()) {
            return false;
        }
        front = (front + 1) % elem.length;
        return true;
    }
    //获取队头元素
    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        return elem[front];
    }
    //获取队尾元素
    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        int index = (rear == 0)? elem.length - 1 : rear - 1;
        return elem[index];
    }
    
    public boolean isEmpty() {
        return front == rear;
    }
    
    public boolean isFull() {
        return (rear + 1) % elem.length == front;
    }
}

3. 双端队列 (Deque)

双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。 那就说明元素可以从队头出队和入队,也可以从队尾出队和入队 

Deque是一个接口,使用时必须创建LinkedList的对象。

在实际工程中,使用Deque接口是比较多的,栈和队列均可以使用该接口.

Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现

 

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

早点睡觉1.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值