Java中的栈与队列——6千字解析

目录

引言

概念

栈在现实生活中的应用(无处不在的栈的应用)

栈的实现

队列

概念

队列接口

普通LFLO队列

循环队列

双端队列


引言

线性表:一次保存单个同类型元素,多个元素之间逻辑上连续数组,链表,栈,队列,字符串(内部是char[])。

栈和队列其实是操作受限的线性表。

之前的数组、链表,既可以在头部插入和删除,也能在尾部插入和删除,甚至在任意位置都可以插入和删除。而"栈和队列"只能在一端插入元素和删除元素。

概念

只能从一端插入元素,也只能从这一端取出元素(栈顶)。

栈的特点:先进后出,后进先出的线性表。添加元素和删除元素的一端称为栈顶,另—端称为栈底。

栈在现实生活中的应用(无处不在的栈的应用)

1. 无处不在的undo(撤销)操作。
a. 在任何一个编辑器中,输错了一个内容,使用ctrl +z就返回到了上一次输入的内容。

b. 在任何一个浏览器点击 <- 就能返回上—次浏览的网页。

都是栈这个结构的应用。

撤销ctrl +z操作,相当于从栈顶取出这个错误的元素,然后文本编辑器就会取出当前的栈顶元素。


2. 操作系统栈:程序在执行过程中,从A函数调用B函数,从B函数调用C函数,返回执行时如何得知从哪开始继续执行呢,其实背后就是栈这个结构。

栈的实现

栈:只能在栈顶插入元素,在栈顶删除元素的结构。

基于数组实现的栈——顺序栈。


栈的三个核心操作:
push(E e):向栈中添加元素,即入栈(压栈)。
E pop():出栈操作,弹出栈顶元素。
E peek():查看栈顶元素,但不出栈。

package seqlist.stack;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * 基于数组实现的顺序栈
 */
public class MyStack<E> {
    // 记录当前栈的元素个数
    private int size;
    // 实际存储数据的动态数组,此时在栈中只能在数组末尾添加和删除元素
    private List<E> data = new ArrayList<>();
    /**
     * 向当前栈中添加元素,压栈操作
     */
    public void push(E val) {
        //在数组尾部插入元素
        data.add(val);
        size++;
    }
    /**
     * 弹出当前的栈顶元素,返回栈顶元素的值
     */
    public E pop() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("stack is empty!cannot pop!");
        }
        //在数组的末尾删除元素
        E val = data.remove(size - 1);
        size--;
        return val;
    }
    /**
     * 查看当前栈顶元素值,但是不弹出
     */
    public E peek() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("stack is empty!cannot pop!");
        }
        return data.get(size - 1);
    }

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

    private boolean isEmpty() {
        return size == 0;
    }
}
package seqlist.stack;
import java.util.ArrayList;
public class StackTest {
    public static void main(String[] args) {
        MyStack<Integer> stack = new MyStack<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack);
        System.out.println(stack.pop());
        System.out.println(stack);
    }
}

队列

概念

栈和队列是一码事,都是对只能在线性表的一端进行插入和删除。因此,栈和队列可以相互转换。
队列:FIFO,先进先出的数据结构,元素从"队尾"添加到队列中,元素从"队首"出队列。元素的出队顺序和入队顺序保持一致。


现实生活中,有各式各样的"排队"操作。
同样的,队列也有基于数组实现的队列和基于链表实现的队列。出队操作只能在队列的头部进行

若采用数组的方案,每次出队一个元素就得搬移剩下的所有元素向前移动一个单位。此时采用链表的方案更加适合队列的结构。

出队列:删除头节点,添加元素:在链表的尾部添加。

队列接口

package seqlist.queue;

/**
 * 相对于栈来说,队列实现的子类比较多
 * 普通的FIFO队列、双端队列Deque、循环队列LoopQueue
 * 优先级队列PriorityQueue
 */
public interface Queue<E> {
    //入队操作
    void offer(E val);
    //出队操作
    E poll();
    //查看队首元素
    E peek();
    boolean isEmpty();
}

普通LFLO队列

package seqlist.queue.impl;
import seqlist.queue.Queue;
import java.util.NoSuchElementException;

/**
 * 基于链表实现的普通队列
 */
class Node<E> {
    E val;
    Node next;

    public Node(E val) {
        this.val = val;
    }
}

public class LinkedQueue<E> implements Queue<E> {
    //当前元素个数
    int size;
    //当前队列的队首元素
    private Node<E> head;
    //当前队列的队尾元素
    private Node<E> tail;

    @Override
    public void offer(E val) {
        //产生一个新节点,尾插
        Node<E> 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 poll!");
        }
        //删除队首元素,即head
        E val = head.val;
        Node node = head;
        head = head.next;
        node.next = node = null;
        size--;
        return val;
    }

    @Override
    public E peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("queue is empty!cannot poll!");
        }
        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);
            if (x.next!=null) {
                sb.append(", ");
            }
        }
        sb.append("] tail");
        return sb.toString();
    }
}
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedQueue<>();
        queue.offer(1);
        queue.offer(3);
        queue.offer(5);
        queue.offer(7);
        System.out.println(queue);    //front [1, 3, 5, 7] tail
        queue.poll();
        System.out.println(queue);    //front [3, 5, 7] tail
    }


JDK内置的队列实现

java.util.Queue;

java.util.LinkedList;

    public static void main(String[] args) {
        //JDK的Queue实现,子类LinkedQUEUE
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(1);
        queue.offer(3);
        queue.offer(5);
        System.out.println(queue);
        queue.poll();
        System.out.println(queue);
    }

 

循环队列

概念

循环队列:基本都是使用长度固定的数组来实现。数组在实现队列时,若从数组头部删除元素需要频繁的移动后面的元素,带来效率比较低。
出队和入队操作,使用两个引用:一个head,一个tail。添加元素在数组尾部添加,删除元素只需要移动head引用指向的地址即可(逻辑删除)。
循环队列就是使用长度固定的数组来实现,数组头部就是队首(head),数组的尾部就是队尾(tail)。数组[head,tail)是循环队列的有效元素。

应用场景:操作系统的生产消费者模型,MySQL数据库的InnoDB存储引擎中的redo日志。

head永远指向循环队列的第一个元素,tail永远指向循环队列有效元素的后一个位置。

所谓的循环队列指的就是当head或者tail引用走到数组末尾时,下一次再继续向后移动,其实返回数组的头部继续操作。

循环队列在删除元素时,不需要进行数据的搬移。循环队列出队一个元素,当有新的元素在添加时就会覆盖掉之前的元素。

我们此时数组为空和数组满,都会使head == tail,因此没法区分当前循环队列到底是空还是满。

修改方案:


1. 在循环队列中浪费一个空间,判断队列是否已满。

若此时(tail + 1) % n == head; 认为此时循环队列已满。

2. head和tail的移动不能简单的+1,应该使用取模操作,%数组长度n。

tail = (tail + 1) % n;
对数组长度取模的本质:
head和tail走到数组最后—个索引位置时,下一次要返回数组头部,就必须使用+1对n取模。

3. 若head == tail; 认为队列为空。

代码实现

package seqlist.queue.impl;
import seqlist.queue.Queue;
import java.util.NoSuchElementException;

/**
 * 基于整型的循环队列
 */
public class LoopQueue implements Queue<Integer> {
    // 定长数组
    private Integer[] data;
    // 指向队首元素
    private int head;
    // 指向队尾元素的下一个索引
    private int tail;


    public LoopQueue(int size) {
        // 因为循环队列中要浪费一个空间判断是否已满
        data = new Integer[size + 1];
    }

    @Override
    public void offer(Integer val) {
        if (isFull()) {
            throw new ArrayIndexOutOfBoundsException("loopQueue is full!cannot offer");
        }
        data[tail] = val;
        tail = (tail + 1) % data.length;
    }

    @Override
    public Integer poll() {
        if (isEmpty()) {
            throw new NoSuchElementException("loopQueue is empty!cannot pool");
        }
        //移动队首元素
        Integer val = data[head];
        //移动 +1再&n
        head = (head + 1) % data.length;
        return val;
    }

    @Override
    public Integer peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("loopQueue is empty!cannot peek");
        }
        return data[head];
    }

    @Override
    public boolean isEmpty() {
        return head == tail;
    }

    public boolean isFull() {
        return (tail + 1) % data.length == head;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("front [");
        //最后一个有效元素的索引
        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();
    }
}

测试

package seqlist.queue.impl;
import seqlist.queue.Queue;
public class LoopQueueTest {
    public static void main(String[] args) {
        Queue<Integer> queue = new LoopQueue(4);
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        System.out.println(queue);
        queue.poll();
        System.out.println(queue);
    }
}

数组越界情况

没有找到元素情况

双端队列

双端队列:Deque——>Queue的子接口。

这个队列,既可以从尾插,头出也可以从头插,尾出。

package seqlist.queue.impl;
import java.util.ArrayList;
import java.util.LinkedList;

/**
 * 双端队列
 */
public class DequeTest {
    public static void main(String[] args) {
        Deque<Integer> stack = new LinkedList<>();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack);
        System.out.println(stack.pop());
        System.out.println(stack);
        System.out.println(stack.peek());
    }
}


package seqlist.queue.impl;
import java.util.ArrayList;
import java.util.LinkedList;

/**
 * 双端队列
 */
public class DequeTest {
    public static void main(String[] args) {
        Deque<Integer> queue = new ArrayDeque<>();
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        System.out.println(queue);
        System.out.println(queue.poll());
        System.out.println(queue);
        System.out.println(queue.peek());
    }
}

以后无论使用的是栈还是接口,统—使用双端队列接口。
不推荐使用Stack这个类,其效率很低,都是同步操作。
双端队列的一个常用子类就是LinkedList。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

瘦皮猴117

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

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

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

打赏作者

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

抵扣说明:

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

余额充值