栈和队列2

1、链表实现栈

上篇文章,我们讲到栈是用数组实现的,它入栈和出栈的时间复杂度都是O(1),那我如果要使用链表来实现栈应该怎么做呢?

那我们先假设使用单链表 能否实现栈:

尾插法:

入栈: 时间复杂度:O(n)(找到链表的倒数第一个元素)。

出栈: 时间复杂度:O(n)(找到链表的倒数第二个元素)。

那么此时,有聪明的同学可能会想到:那我如果给它再加一个尾巴节点时间复杂度又是多少呢? 入栈: 因为有last节点,时间复杂度变为O(1)。

出栈: 时间复杂度仍为O(n),因为是单链表last只能找到last后面的节点无法找到它前面的节点。

头插法:

入栈: 时间复杂度O(1)

出栈: 由于栈先进后出 的特性,时间复杂度也为O(1)。

因此,在使用单链表实现栈时一般采用头插法。

假设双链表 实现栈:

如果使用双链表实现栈:那么无论从那边出入栈时间复杂度都为O(1)。

如下代码所示:就是用链表创建了一个名为stack的对象,它可以正常使用push(),pop(),isEmpty()。

复制代码
        LinkedList<Integer> stack = new LinkedList<>();
        System.out.println(stack.isEmpty());
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.peek());

点进LinkedList中,可以看见LinkedList的底层是实现了这些方法的。

2、队列

2.1 概念

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

我们也可以将链表当作一个队列,入队时使用尾插法,出队时删除头节点即可。 代码如下:

复制代码
        //普通队列
        Queue<Integer> queue1 = new LinkedList<>();
        //入队
        queue1.offer(1);
        queue1.offer(2);
        queue1.offer(3);
        queue1.add(4);
        System.out.println(queue1);
        //出队
        System.out.println(queue1.poll());//1
        //获取队头元素
        System.out.println(queue1.peek());//2

2.2 队列的使用

在Java中, Queue是一个接口,而链表实现了这个接口

那细心的同学就会发现Queue下面还有个Deque,那这个Deque是什么呢?

Deque是双端队列,能够在同一端进出元素。

常用方法有这些: 注意:因为Queue只是一个接口,实例化的时候,必须要实例化一个LinkedList的对象,因为是LinkedList实现了Queue接口 。

在Queue的结构中,不止存在offer( ),poll( )和peek( )还存在add( ),remove( )和element方法,

在Java的Queue接口中,add/offer、remove/poll、element/peek是两组功能类似但行为不同的方法。他们的核心区别在于 **对队列边界条件(满或空)**的处理方式 。

可以看到使用add()方法在队列满时抛出了异常。

在队列未满时,offer( )返回true,队列满了则返回false,不报异常。

在队列为空时:poll( )方法返回空

remove( )方法报异常

在队列为空时:peek( )方法返回空

element( )方法抛异常

3、队列的模拟实现

我们使用双向链表来实现一个队列:

复制代码
//双向链表的节点    
static class ListNode {
        private int val;
        private ListNode prev;
        private ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
        private ListNode front;//队头
        private ListNode rear;//队尾
        private int usedSize;

3.1 offer( )方法

使用头插法,进行入队。

复制代码
 //在头插入元素
    public void offer(int x){
        ListNode listNode = new ListNode(x);
        if(front == null){//第一次插入
            front = rear = listNode;
        }else{
            listNode.next = front;
            front.prev = listNode;
            front = listNode;
        }
        usedSize++;
    }

3.2 poll( )方法

在上面的offer( )方法中我们使用头插法进行插入,根据先进先出的原则,应当在尾巴节点进行删除。

复制代码
//出队列  相当于删除尾巴结点
    public int poll(){
        if(front == null){
            throw new NullElementException("没有元素可被删除");
        }
        if(front == rear){
            int x = front.val;
            front = null;
            rear = null;
            usedSize--;
            return x;
        }
        int ret = rear.val;
        rear = rear.prev;
        rear.next = null;
        usedSize--;
        return ret;
    }

3.3 peek( )方法

复制代码
    public int peek(){
        if(front == null){
            throw new NullElementException("没有元素可被删除");
        }
        return rear.val;
    }

3.4 isEmpty( )

复制代码
    public  boolean isEmpty(){
        return usedSize == 0;
    }

4、双端队列

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

注意:Queue和Queue都是一个接口,使用时必须创建LinkList对象。

可以看到Stack也实现了Deque接口,所以有了如下代码:

复制代码
 Deque<Integer> stack = new LinkedList<>();
        stack.push(1);
        stack.offer(2);

此外,双端队列可以有线性数组实现和链式实现 :

复制代码
        Deque<Integer> deque1 = new ArrayDeque<>();//双端队列的数组实现
        Deque<Integer> deque2 = new LinkedList<>();//双端队列的链式实现

5、 面试题

5.1 622. 设计循环队列 - 力扣(LeetCode) 那么我们要如何实现这个队列呢?首先,我们知道,通过数组也可以实现队列:

队尾进,队头出,插入完后rear++。入队完成后: 出队的时候,只需要让front++即可,但是这么做会浪费出队元素的空间,那个空间无法再进行入队。

这时候我们想到可以让数组循环起来,这样就不会浪费那个空间,那如何让数组循环起来呢?很简单嘛,就像我们玩软尺一样,把数组卷起来不就行了!!!

front表示数组的第一个元素的下标,rear表示当前可插入元素的下标。

这时候如果要元素出队,只需让front往后走就行了,并不会影响后面的元素入队。

由上面所述,我们知道了循环队列通过数组实现,但实现循环队列还有最重要的一点:判断队列是否已满。

判断队列是否已满可以采用3种方法:

1、定义一个Usesized,入队Usesized++,出队Usesized--,当Usesized == 数组长度时,说明数组已满。

2、定义一个flg(Boolean),当第二次rear == front时,说明队列已满,此时flg = true。

3、浪费一个空间来进行区分,当rear的下一个就是front说明队列已满。效果如图所示:

那现在问题又来了,这里数组被卷起来了,如果想让rear往后走还是rear++吗?显然不对,通过上图,7的后面的下标是0,而你++后的下标应该是8。所以,这里让rear往后走应该是(rear + 1)% length(数组长度) ,front同理:(front + 1)% length。

综上所述很容易得到下述代码:

复制代码
 //通过数组实现循环队列
    //判定:循环队列是否已满:1、定义usesized==len
    //2、标记
    //3、浪费一个空间进行区分
    private int[] elem;
    private int front;//队头下标
    private int rear;//队尾下标
    public MyCircularQueue(int k) {
        elem = new int[k+1];
    }
    //入队
    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 = (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;
    }

这里还需要注意:1、若队列不为空时,获取队尾元素时rear==0,说明rear已经走了一圈回来了,不能通过rear-1获取队尾元素,而是通过elem.length-1下标来获取。

2、因为我们需要多一个空间来判断数组是否已满,所以需要申请k+1个空间。

5.2 225. 用队列实现栈 - 力扣(LeetCode)

这里先提出一个问题,一个队列能够实现栈吗?答案是不能,因为队列是先进先出的数据结构;而栈是先进后出的数据结构。一个队列根本实现不了栈,所以我们需要两个队列。 当qu1和qu2都为空时说明,我们模拟实现的栈为空。

出栈:因为栈要出的是栈顶的元素,也就是队列中最后一个出的元素,所以我们可以在不为空的队列中先出size() - 1个元素,最后剩下的那个元素就是我们要出的元素。

入栈:入到不为空的队列即可,如果两个队列都为空就入到qu1。

入栈:

出栈

**top方法:**将不为空的队列的元素全部出队,最后一个出队的元素就是栈顶元素。

复制代码
class MyStack {
  Queue<Integer> queue1;
  Queue<Integer> queue2;
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }
    //入栈:入到不为空的队列,如果都为空,入到qu1即可
    public void push(int x) {
        if(!queue1.isEmpty()){
            queue1.offer(x);
        }else if (!queue2.isEmpty()){
            queue2.offer(x);
        }else{
            queue1.offer(x);
        }
    }
    //出栈时,出不为空的队列,出size-1个,最后那一个就是我要出栈的元素
    public int pop() {
        if (empty()){
            return -1;
        }
        if(!queue1.isEmpty()){
            int size = queue1.size();//这里的队列的长度随着元素的弹出会改变
            for (int i = 0; i < size-1; i++) {
                int x = queue1.poll();
                queue2.offer(x);
            }
            return queue1.poll();
        } else {
            int size = queue2.size();
            for (int i = 0; i < size-1; i++) {
               int x = queue2.poll();
               queue1.offer(x);
           }
           return queue2.poll();
        }
    }
    //peek
    //将所有元素通过tmp记录后入到另一个栈里面,最后tmp记录的元素就是我要peek的元素
    public int top() {
        if (empty()){
            return -1;
        }
        int x = -1;
        if(!queue1.isEmpty()){
            int size = queue1.size();//这里的队列的长度随着元素的弹出会改变
            for (int i = 0; i < size; i++) {
                 x = queue1.poll();
                queue2.offer(x);
            }
            return x;
        } else {
            int size = queue2.size();
            for (int i = 0; i < size; i++) {
                x = queue2.poll();
                queue1.offer(x);
            }
            return x;
        }
    }
    //栈是否为空
    public boolean empty() {
        //两个队列都为空的时候,说明我模拟实现的栈为空
        return queue1.isEmpty() && queue2.isEmpty();
    }
}

需要注意的是因为在出栈的过程中,栈的长度在不断变小,所以需要定义一个变量获取栈的长度,防止出栈出少元素。

5.3 232. 用栈实现队列 - 力扣(LeetCode)

一个队列实现不了栈,那一个栈能实现队列吗?同样不能,所以我们同样需要两个栈来实现队列。

**入队:**直接将所有元素入到s1(第一个栈)。

**出队:**将s1(第一个栈)的元素全部倒到s2(第二个栈)中,再出第二个栈的元素。

入队

出队 :

复制代码
class MyQueue {
   //入队时,把所有元素放到第一个栈中
    //出队时(判断空不空)把第一个栈的所有元素全部倒回第二个栈中,出第二个栈的栈顶元素
    //定义两个栈
    Stack<Integer> s1;
    Stack<Integer> s2;
    //构造方法
    public MyQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }
    //统一入到s1
    public void push(int x) {
        s1.push(x);
    }
    //出的时候统一出s2的
    public int pop() {
        if(s1.empty()){
            return -1;
        }
        while(!s1.empty()){
            s2.push(s1.pop());
        }
        int x = s2.pop();
        while(!s2.empty()){
            s1.push(s2.pop());
        }
        return x;
    }

    public int peek() {
        if(s1.empty()){
            return -1;
        }
        while(!s1.empty()){
            s2.push(s1.pop());
        }
        int x = s2.peek();
        while(!s2.empty()){
            s1.push(s2.pop());
        }
        return x;
    }

    public boolean empty() {
        return s1.empty();
    }
}
相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说9 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json