数据结构(Java)实现:栈和队列

文章目录

  • [1. 栈的模拟实现](#1. 栈的模拟实现)
    • [1.1 普通栈的模拟实现](#1.1 普通栈的模拟实现)
    • [1.2 泛型栈的模拟实现](#1.2 泛型栈的模拟实现)
  • [2. 栈的介绍](#2. 栈的介绍)
  • [3. 栈的使用](#3. 栈的使用)
  • [4. 栈的应用场景](#4. 栈的应用场景)
    • [4.1 改变元素的序列](#4.1 改变元素的序列)
    • [4.2 将递归转换为循环](#4.2 将递归转换为循环)
    • [4.3 使用栈解题](#4.3 使用栈解题)
  • [5. 栈的链表实现](#5. 栈的链表实现)
  • [6. 队列概念](#6. 队列概念)
  • [7. 队列的使用](#7. 队列的使用)
  • [8. 模拟队列的实现](#8. 模拟队列的实现)
    • [8.1 链式队列](#8.1 链式队列)
    • [8.2 顺序队列](#8.2 顺序队列)
  • [9. 双端队列](#9. 双端队列)

1. 栈的模拟实现

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

1.1 普通栈的模拟实现

栈的模拟实现没有顺序表和链表难,实现代码和对应的注释如下。

java 复制代码
public class MyStack {
    int[] elem;
    int usedSize=0;
    public MyStack(){
        elem=new int[10];
    }
    //入栈
    public void push(int e){
    	//判满,如果满了扩容
        if(isFull()){
            elem= Arrays.copyOf(elem,elem.length*2);
        }
        //入栈
        elem[usedSize]=e;
        usedSize++;
    }
    //判满
    public boolean isFull(){
        return usedSize==elem.length;
    }
    //出栈
    public int pop(){
    	//判空,如果为空返回-1
        if(empty()){
            return -1;
        }
        //出栈后元素个数减一
        usedSize--;
        return elem[usedSize];
    }
    //查看
    public int peek(){
    	//判空,如果为空返回-1
        if(empty()){
            return -1;
        }
        //peek只返回,不删除
        return elem[usedSize-1];
    }
    //判空
    public boolean empty(){
        return usedSize==0;
    }
}

测试代码

java 复制代码
    public static void main(String[] args) {
        MyStack s=new MyStack();
        s.push(1);
        s.push(2);
        s.push(3);

        System.out.println(s.pop());
        System.out.println(s.peek());
        System.out.println(s.peek());
    }

1.2 泛型栈的模拟实现

要使栈变成泛型,仅需改变数据类型。

java 复制代码
public class MyStack2 <E>{
    public Object[] elem;//泛型的本质是Object类
    public int usedSize = 0;
    public MyStack2(){
        elem=new Object[10];
    }
    public void push(E e){
        if(isFull()){
            elem= Arrays.copyOf(elem,elem.length*2);
        }
        elem[usedSize]=e;
        usedSize++;
    }
    public boolean isFull(){
        return usedSize==elem.length;
    }
    //出栈
    public E pop(){//返回类型为E
        if(empty()){
            return null;
        }
        usedSize--;
        return (E)elem[usedSize];//类型转换
    }
    public E peek(){//返回类型为E
        if(empty()){
            return null;
        }
        return (E)elem[usedSize-1];//类型转换
    }
    public boolean empty(){
        return usedSize==0;
    }
}

测试代码

java 复制代码
    public static void main(String[] args) {
        MyStack2<String > s = new MyStack2<>();
        s.push("hello");
        s.push("zzuli");
        s.push("welcome");
        s.push("to");
        s.push("zzuli");

        System.out.println(s.pop());
        System.out.println(s.pop());
        System.out.println(s.peek());
        System.out.println(s.peek());
    }

2. 栈的介绍

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

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

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


3. 栈的使用

java 复制代码
    public static void main(String[] args) {
        Stack<Integer> s=new Stack<>();
        s.push(1);//入栈
        s.push(2);//入栈
        s.push(3);//入栈
        s.push(4);//入栈
        s.push(5);//入栈

        System.out.println(s.size());//栈的数据个数
        System.out.println(s.pop());//出栈
        System.out.println(s.pop());//出栈
        System.out.println(s.peek());//查看栈顶元素
        System.out.println(s.peek());//查看栈顶元素

        if(s.empty()){//判空
            System.out.println("栈空");
        }else{
            System.out.println("栈不为空");
        }
    }

4. 栈的应用场景

4.1 改变元素的序列


答案:C、B

4.2 将递归转换为循环

当我们逆序打印单向链表时,可以通过递归的方式打印。

java 复制代码
// 递归方式
void printList(Node head){//传入链表的头结点
    if(null != head){
        printList(head.next);//递归
        System.out.print(head.val + " ");
    }
}

我们也可以利用栈的方式,将链表的节点入栈,然后出栈。就可以实现逆序打印。

java 复制代码
    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 + " ");
        }
    }

4.3 使用栈解题

  1. 括号匹配
  2. 逆波兰表达式求值
  3. 出栈入栈次序匹配
  4. 最小栈

上面四题题解链接

5. 栈的链表实现

Java集合类的Stack底层是一个数组。这种栈叫做顺序栈。那么是否可以使用链表来实现栈呢?

如果那单链表来实现栈,最好使用链表头部进行入栈和头栈,这样时间复杂度为O(1)。相反,如果从链表的尾部入栈和出栈,时间复杂度为O(n)(此时需要遍历链表才能实现入栈和出栈)。

如果使用双向链表,从链表的头或者尾进行入栈和出栈,时间复杂度均为O(1)。因此,如果我们要使用链式栈,就可以选用双线链表来操作。
代码示例

java 复制代码
    public static void main(String[] args) {
        LinkedList<Integer> stack=new LinkedList<>();
        stack.push(1);//入栈
        stack.push(2);
        stack.push(3);
        
        stack.pop();//出栈
    }

通过Ctrl查看LInkedListpush方法和pop方法可以发现,其本质是链表的头插和头删。

6. 队列概念

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

入队列:进行插入操作的一端称为队尾(Tail/Rear)

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

7. 队列的使用

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

方法 功能
boolean offer(E e) 入队列
E poll() 出队列
peek() 获取队头元素
int size() 获取队列中有效元素个数
boolean isEmpty() 检测队列是否为空

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

上面是Queue接口的一些方法。add方法和offer方法都是用来新增元素的。remove方法和poll方法都是用来弹出元素的。element方法和peek方法都是用来获取队头元素的。那他们有什么区别呢?

注释翻译:
add方法

如果可以立即插入到此队列中,则在不违反容量限制的情况下插入指定的元素,成功后返回 true,如果当前没有可用空间,则引发 IllegalStateException。

参数:

e -- 要添加的元素

返回:

true(由 Collection.add 指定)

抛出:

IllegalStateException -- 如果由于容量限制而此时无法添加元素

ClassCastException -- 如果指定元素的类阻止将其添加到此队列中

NullPointerException -- 如果指定的元素为 null,并且此队列不允许 null 元素

IllegalArgumentException -- 如果此元素的某些属性阻止将其添加到此队列中
offer方法

如果可以立即插入到此队列中,则将其指定的元素插入到此队列中,而不会违反容量限制。当使用容量受限的队列时,此方法通常比 add 更可取,后者只能通过引发异常来无法插入元素。

参数:

e -- 要添加的元素

返回:

如果元素已添加到此队列中,则为 true,否则为 false

抛出:

ClassCastException -- 如果指定元素的类阻止将其添加到此队列中

NullPointerException -- 如果指定的元素为 null,并且此队列不允许 null 元素

IllegalArgumentException -- 如果此元素的某些属性阻止将其添加到此队列中

注释翻译:
remove方法

检索和删除此队列的头部。此方法与 poll() 的不同之处仅在于如果此队列为空,它会引发异常。

返回:

此队列的头部

抛出:

NoSuchElementException -- 如果此队列为空
poll方法

检索并删除此队列的头部,如果此队列为空,则返回 null。

返回:

此队列的头部,如果此队列为空,则为 null

注释翻译:
element方法

检索但不删除此队列的头部。此方法与 peek 的唯一区别在于,如果此队列为空,它会引发异常。

返回:

此队列的头部

抛出:

NoSuchElementException -- 如果此队列为空
peek方法

检索但不删除此队列的头部,如果此队列为空,则返回 null。

返回:

此队列的头部,如果此队列为空,则为 null

使用代码示例

java 复制代码
    public static void main(String[] args) {
        Queue<Integer> q=new LinkedList<>();

        q.offer(1);//入队列
        q.offer(2);
        q.offer(3);
        System.out.println(q.poll());//出队列
        System.out.println(q.poll());
        System.out.println(q.element());//查看第一个队列元素
        System.out.println(q.element());
        System.out.println(q.isEmpty());//队列判空
    }

8. 模拟队列的实现

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

8.1 链式队列

我们使用双向链表来实现链式队列。实现代码如下:

java 复制代码
public class MyQueue {
    static class ListNode{//队列节点
        public ListNode prev;
        public ListNode next;
        public int val;
        public ListNode(int val){
            this.val=val;
            this.prev=null;
            this.next=null;
        }
    }

    public ListNode head;
    public ListNode last;
	//入队列
    public void offer(int val) {
        ListNode node = new ListNode(val);//创建节点
        //尾插法入队列
        if(head==null){
            head=node;
            last=node;
        }else {
            last.next=node;
            node.prev=last;
            last=node;
        }
    }
	//出队列
    public int poll(){
        if(head==null){
            return -1;
        }
        //头删法出队列
        int ret=head.val;
        if(head.next==null){
            head=last=null;
        }else {
            head=head.next;
            head.prev=null;
        }
        return ret;
    }
	//查看队头元素
    public int peek(){
        if(head==null){
            return -1;
        }
        //如果队头不为空,返回队头的值
        return head.val;
    }
	//判断队列是否为空
    public boolean isEmpty(){
    	//如果头节点为null,队列为空
        return head==null;
    }
}

8.2 顺序队列

使用顺序表来实现队列,如果不移动元素的位置来进行出队列和入队列就会造成一个问题:假设first为队头(可以出数据的下标),last为队尾(可以存放数据的下标),入队过程中last到顺序表的最后一个位置,而first端元素在出队列,这样就会使队列的前一段空出来了而没有存储数据。示例图如下:

如果将last重新回到0下标的空间,这样会使队列不连续。因此,我们通过数组来实现环形队列。

环形队列的连续问题解决了,但是当队尾走到顺序表的尽头,此时我们一直让fist++或者last++可能会造成数组越界,我们可以在每次入队列或者出队列时加一个判断,当first==len或者last==len时让他们的下标设置为0,。我们也可以通过表达式来设置下标,first=(first+1)%len或者last=(last+1)%len,通过这个表达式就不用担心数组越界的问题了。

考虑完数组越界问题后,我们如何区分队列的空与满?单纯依靠头尾相遇并不能解决这个问题,因为环形链表头尾相遇时可能为空,也可能为满。我们有下面三种解决方案。题目:力扣循环队列。

  1. 使用usedSize,当usedSize==len时就为满。
java 复制代码
class MyCircularQueue {
    public int[] elem;//存放队列元素
    public int first;//队列头
    public int last;//队列尾
    public int usedSize;//元素个数
    //构造方法
    public MyCircularQueue(int k) {
        elem=new int[k];
        this.usedSize=0;
        this.first=0;
        this.last=0;
    }
    //入队列 
    public boolean enQueue(int value) {
        if(isFull()){//判满,如果队列满了返回false
            return false;
        }
        //元素入队列
        elem[last]=value;
        //队尾位置往后移一个
        last=(last+1)%elem.length;
        //元素个数加1
        usedSize++;
        //成功入队列,返回true
        return true;
    }
    //出队列
    public boolean deQueue() {
        if(isEmpty()){//判空,如果队列为空返回false
            return false;
        }
        //出队列直接让对头往后移动一个
        first=(first+1)%elem.length;
        //元素个数减1
        usedSize--;
        //出队列成功,返回true
        return true;
    }
    //看队头元素
    public int Front() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        //不为空,first下标即为队头元素
        return elem[first];
    }
    //看队尾元素
    public int Rear() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        int index;
        if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
            index = elem.length-1;
        }else {//否则,队尾元素下标为last-1
            index = last-1;
        }
        //返回对应下标的元素
        return elem[index];
    }
    //判空
    public boolean isEmpty() {
    //如果usedSize为0,队列尾空
        return usedSize==0;
    }
    //判满
    public boolean isFull() {
    //如果usedSize等于数组长度队列为满
        return usedSize==elem.length;
    }
}
  1. 使用一个boolean类型的标记,。
java 复制代码
class MyCircularQueue {
    public int[] elem;//存放队列元素
    public int first;//队列头
    public int last;//队列尾
    public boolean flag;//标记
    //构造方法
    public MyCircularQueue(int k) {
        elem=new int[k];
        this.flag=false;//flag为false时,队列不满
        this.first=0;
        this.last=0;
    }
    //入队列
    public boolean enQueue(int value) {
        if(isFull()){//判满,如果队列满了返回false
            return false;
        }
        //元素入队列
        elem[last]=value;
        //队尾位置往后移一个
        last=(last+1)%elem.length;
        if(last==first){//入队列时,last走完一圈回到first队列就满了
            flag=true;
        }
        //成功入队列,返回true
        return true;
    }
    //出队列
    public boolean deQueue() {
        if(isEmpty()){//判空,如果队列为空返回false
            return false;
        }
        //出队列直接让对头往后移动一个
        first=(first+1)%elem.length;
        flag=false;//出队列时,队列就不满,标志设为false
        //出队列成功,返回true
        return true;
    }
    //看队头元素
    public int Front() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        //不为空,first下标即为队头元素
        return elem[first];
    }
    //看队尾元素
    public int Rear() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        int index;
        if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
            index = elem.length-1;
        }else {//否则,队尾元素下标为last-1
            index = last-1;
        }
        //返回对应下标的元素
        return elem[index];
    }
    //判空
    public boolean isEmpty() {
        //如果last==first且flag为false,队列为空
        return last==first&&!flag;
    }
    //判满
    public boolean isFull() {
        //如果队列为满,flag为true
        return flag;
    }
}
  1. 浪费一个空间,当first+1==last时,为满。
java 复制代码
class MyCircularQueue {

    public int[] elem;//存放队列元素
    public int first;//队列头
    public int last;//队列尾
    //构造方法
    public MyCircularQueue(int k) {
        //因为浪费了一个空间,所以为了保证能够存够k个元素就需要开辟k+1个空间
        elem=new int[k+1];
        this.first=0;//队头
        this.last=0;//队尾
    }
    //入队列
    public boolean enQueue(int value) {
        if(isFull()){//判满,如果队列满了返回false
            return false;
        }
        //元素入队列
        elem[last]=value;
        //队尾位置往后移一个
        last=(last+1)%elem.length;
        //成功入队列,返回true
        return true;
    }
    //出队列
    public boolean deQueue() {
        if(isEmpty()){//判空,如果队列为空返回false
            return false;
        }
        //出队列直接让对头往后移动一个
        first=(first+1)%elem.length;
        //出队列成功,返回true
        return true;
    }
    //看队头元素
    public int Front() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        //不为空,first下标即为队头元素
        return elem[first];
    }
    //看队尾元素
    public int Rear() {
        if(isEmpty()){//判空,如果为空返回-1
            return -1;
        }
        int index;
        if(last==0) {//如果last为0,队尾元素下标就为elem.length-1
            index = elem.length-1;
        }else {//否则,队尾元素下标为last-1
            index = last-1;
        }
        //返回对应下标的元素
        return elem[index];
    }
    //判空
    public boolean isEmpty() {
    //头等于尾时为空
        return last==first;
    }

    public boolean isFull() {
    //浪费一个元素,尾的下一个为头就视为满
        return (last+1)%elem.length==first;
    }
}

9. 双端队列

双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 "double ended queue" 的简称。

那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。

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

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

java 复制代码
//下面这两个类也可以当做链表栈使用,也可以当做队列使用
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现

下面是Deque的一些方法。

栈和队列相关练习题链接

相关推荐
Dylanioucn12 分钟前
【编程底层思考】什么是GC Roots
java·jvm
_晓夏_14 分钟前
【JVM 工具命令】JAVA程序线上问题诊断,JVM工具命令的使用,jstat, jstack,jmap命令的使用
java·开发语言·jvm·jvm命令工具·java程序线上问题定位命令·java程序问题故障排查命令·jvm线上问题故障排查
Ty_110631 分钟前
深入理解Java虚拟机:Jvm总结-虚拟机字节码执行引擎
java·开发语言·jvm
蔚一1 小时前
Java设计模式—面向对象设计原则(一) ----->开闭原则OCP(完整详解,附有代码+案例)
java·后端·设计模式·intellij-idea·开闭原则
cesske1 小时前
java 学习从零到精通之历程
java
爱吃香蕉的猴哥1 小时前
Spring AOP
java·后端·spring
Sebastian Wang1 小时前
【学习归纳自我总结版】尚硅谷学习第一天
java·学习
青灯古佛初照1 小时前
EasyExcel 动态表头+表头合并
java·excel
goldfishsky2 小时前
Gson转换
java·开发语言
小巫医初春2 小时前
【EasyExcel】@ColumnWidth(value = 20) EasyExcel设置列宽不生效
java