栈和队列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();
    }
}
相关推荐
Kevinyu_2 分钟前
Maven
java·maven
nickxhuang7 分钟前
【基础知识】回头看Maven基础
java·maven
日月星辰Ace41 分钟前
jwk-set-uri
java·后端
Tomorrow'sThinker1 小时前
Python零基础学习第三天:函数与数据结构
开发语言·windows·python
元媛媛1 小时前
Python - 轻量级后端框架 Flask
开发语言·python·flask
xiao--xin1 小时前
LeetCode100之二叉搜索树中第K小的元素(230)--Java
java·算法·leetcode·二叉树·树的统一迭代法
钢板兽1 小时前
Java后端高频面经——Spring、SpringBoot、MyBatis
java·开发语言·spring boot·spring·面试·mybatis
钢板兽1 小时前
Java后端高频面经——JVM、Linux、Git、Docker
java·linux·jvm·git·后端·docker·面试
爱吃柠檬呀1 小时前
《C陷阱与缺陷》读书笔记(一)
c语言·开发语言·算法·《c陷阱与缺陷》·编写程序
行码棋2 小时前
【Python】omegaconf 用法详解
开发语言·python