栈和队列2

发布于:2025-03-07 ⋅ 阅读:(22) ⋅ 点赞:(0)

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