【数据结构基础】之栈与队列介绍,生动形象,通俗易懂,算法入门必看

247 阅读14分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

前言

上期为大家分享了数据结构中链表的相关知识,本文将对 数据结构基础【栈与队列】 的相关知识进行分享,下边具体将对栈的概念与实现队列的概念与实现循环队列的概念与实现双端队列的概念与实现Java中栈和队列等进行详尽介绍~

👉Java全栈学习路线可参考: 【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

👉算法刷题路线可参考: 算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~


一、栈

首先我们先介绍栈的相关知识,包括栈的概念,什么是栈,栈有什么特点,以及栈用Java代码该 怎么实现等 一系列相关知识分享。

1.栈的概念

是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。对于栈中的数据元素的存取需要遵守后进先出LIFO(Last In First Out)的原则。下边来看看压栈和出栈的概念。

  • 压栈: 栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
  • 出栈: 栈的删除操作叫做出栈。出数据在栈顶。

但从文字概念上可能有些小伙伴还不太能理解,下边对于压栈和出栈的示意图就可以很好的帮助我们理解什么是栈以及压栈和出栈是怎么操作的。

1.png

2.栈的实现

上边了解了栈的相关概念,包括压栈与出栈,那么对于栈我们怎么实现呢,以及在 Java代码中该如何编写,下边就来看看吧。

对于栈有两种实现方式,总结如下:

  • (1)利用顺序表实现,即使用尾插 + 尾删的方式实现
  • (2)利用链表实现,则头尾皆可

针对以上两种实现方式,相对来说,顺序表的实现上要更为简单一些,所以我们优先看看用顺序表实现栈的Java代码该怎么写,包括栈的定义,入栈与出栈操作,查看栈顶元素,获取当前栈的长度等。

// 基于数组实现链表
public class Stack<E> {
    private E[] elementData; // 栈中的元素
    private int size; // 当前栈中元素个数
 
    public Stack() {
        elementData = (E[]) new Object[10]; // 默认长度为10
    }
 
    public Stack(int initCap) {
        elementData = (E[]) new Object[initCap]; // 初始长度
    }
 
    // 入栈
    public void push(E value) {
        // 扩容
        if(size == elementData.length) {
            int oldLength = elementData.length;
            int newLength = oldLength << 1;
            elementData = Arrays.copyOf(elementData, newLength);
        }
        // 在数组尾部添加元素
        elementData[size++] = value;
    }
 
    // 出栈,返回原来的栈顶元素
    public E pop () {
        if(getSize() == 0) {
            throw new NoSuchElementException("栈中没有元素!");
        }
        // 得到原来的栈顶元素位置
        E oldVaule = elementData[size - 1];
        size--;
        elementData[size] = null;
        return oldVaule;
    }
 
    // 查看栈顶元素
    public E peek() {
        if(getSize() == 0) {
            throw new NoSuchElementException("栈中没有元素!");
        }
        return elementData[size - 1];
    }
 
    // 获取当前栈的长度
    public int getSize() {
        return size;
    }
 
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        for (int i = 0; i < size; i++) {
            stringBuilder.append(elementData[i]);
            if(i != size - 1) {
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]");
        return stringBuilder.toString();
    }
}

二、队列

以上介绍了栈的概念与实现,下边来看看队列的定义以及相关实现吧。

1.队列的概念

队列它是一种仅支持在表尾进行插入操作、在表头进行删除操作的线性表,插入端称为队尾,删除端称为队首,因为这种数据结构整体类似排队的队伍,所以被称为队列。对于队列中 数据元素的存取操作满足先进先出的性质(FIFO),元素入队即将新元素加在队列的尾,元素出队即将队首元素取出,并且将其后边的一个元素作为新的队首。

下图展示了一个队列以及入队与出队的示意图,还是很直观明了的。

2.png

2.队列的实现

下边我们也来看看队列该如何实现,以及用Java代码该如何编写。

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

3.png

以下是队列的Java代码实现,包含队列的定义,以及入队与出队,查看队首元素等队列相关操作。

/**
 * 基于链表的队列实现
 */
public class LinkedQueue{
    private Node head;
    private Node tail;
    private int size;
    private class Node {
        private int data;
        private Node next;
 
        public Node(int data) {
            this.data = data;
        }
    }
 
    // 入队
    public void offer(int value) {
        Node node = new Node(value);
        if(head == null) {
            head = tail = node;
        } else {
            tail.next = node;
            tail = node;
        }
        size++;
    }
 
    // 出队(队首元素出队)
    public int poll() {
        if(size == 0) {
            throw new NoSuchElementException("对列为空!");
        } else {
            int oldValue = head.data;
            Node tempHead = head;
            head = head.next;
            tempHead.next = null;
            size--;
            return oldValue;
        }
    }
 
    // 查看队首元素
    public int peek() {
        if(size == 0) {
            throw new NoSuchElementException("对列为空!");
        }
        return head.data;
    }
 
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("front[");
        Node node = head;
        while (node != null) {
            stringBuilder.append(node.data);
            if(node.next != null) {
                stringBuilder.append(",");
            }
            node = node.next;
        }
        stringBuilder.append("]tail");
        return stringBuilder.toString();
    }
}

3.循环队列的概念

实际中我们有时还会使用一种特殊的队列叫循环队列。循环队列就是队列的头部与尾部连在了一块儿,形成了一个环状。环形队列通常使用数组实现。在顺序队列中,当下标走到队尾后,不能再往后走插入元素,但其实数组中还有位置,这叫做“假溢出”,为了解决这个问题提高数组利用率,就出现了循环队列。

在实现循环队列时我们需要注意:实现循环对列,最重要的是如何判断队列为空还是为满。

下图是一个循环队列,也叫环形队列的示意图: 4.png

4.循环队列的实现

下边我们来详细了解一下循环队列该如何实现,上边我们提出了一个需要注意的问题就是实现循环对列时需要考虑如何判断队列为空还是为满,下边给出了通常的解决方案。 如何区分循环队列的空与满:

  • 通过 size 属性记录当前队列中的元素个数
  • 保留一个位置,这个位置不能存储元素。这样当队满为 front == (rear+ 1)% length,队空为 front ==rear
  • front:指向循环队列的第一个元素下标,rear:指向循环队列的最后一个元素的下一个下标

下边是区分循环队列的空与满的示意图,通过保留一个空位置我们就可以利用它来判断队空和队满了。 5.png

以下是循环队列的Java代码实现,包含循环队列的定义,以及入队与出队,查看队首元素,判断队列是否已满,判断队列是否为空等队列相关操作。

/**
 * 循环队列
 */
public class LoopQueue implements IQueue {
    // 指向循环队列的最后一个元素的下一个位置
    private int tail;
    // 队首元素,指向队列中的第一个元素索引
    private int front;
    // 有效元素个数
    private int size;
    private int[] data;
 
    public LoopQueue(int k) {
        data = new int[k + 1];
    }
 
    // 判断队列是否已满
    public boolean isFull() {
        if ((tail + 1) % data.length == front) {
            return true;
        }
        return false;
    }
 
    // 判断队列是否为空
    public boolean isEmpty() {
//        if (front == tail) {
//            return true;
//        }
//        return false;
        return tail == front;
    }
 
    // 入队
    public void offer(int value) {
        if (isFull()) {
            System.err.println("队列已满!");
            return;
        } else {
            data[tail] = value;
            tail = (tail + 1) % data.length;
            size++;
        }
    }
 
    // 出队
    public int poll() {
        if (isEmpty()) {
            System.err.println("队列为空!");
            return -1;
        } else {
            int value = data[front];
            front = (front + 1) % data.length;
            size--;
            return value;
        }
    }
 
    // 查看队首元素
    public int peek() {
        if (isEmpty()) {
            System.err.println("队列为空!");
            return -1;
        }
        return data[front];
    }
 
    // 查看队尾元素
    public int getTail() {
        if (isEmpty()) {
            System.err.println("队列为空!");
            return -1;
        }
        // 最后一个元素的下标
        int index = tail == 0 ? data.length - 1 : tail - 1;
        return data[index];
    }
 
    // 判断有效个数
    public int getSize() {
        return size;
    }
 
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("[");
        // 最后一个元素的位置
        int lastIndex = tail == 0 ? data.length - 1 : tail - 1;
        for (int i = front; i != tail; ) {
            stringBuilder.append(data[i]);
            if (i != lastIndex) {
                stringBuilder.append(",");
            }
            i = (i + 1) % data.length;
        }
        stringBuilder.append("]");
        return stringBuilder.toString();
    }
}

5.双端队列的概念

还有一种特殊的队列叫双端队列,首先来看看双端队列的概念。

双端队列(Deque:double ended queue)就是一个两端都是结尾的队列。队列的每一端都可以插入数据项和移除数据项。相对于普通队列,双端队列的入队和出队操作在两端都可进行。双端队列就是 在队列的两端都可以进行插入与删除操作。事实上还有一些 受限双端队列,比如有一端只可以插入元素,另一端既可以插入也可以删除元素,或有一端只可以删除元素,另一端既可以插入也可以删除元素。这些受限的双端队列在这里就不过多介绍了,下图是一个双端队列的示意图:

6.png

6.双端队列的实现

上边了解了什么是双端队列,那么接下来我们看看双端队列该如何实现吧。

我们使用最常用的顺序结构来存储双端队列,为了节省空间,把它首尾相连,构成循环队列。并且规定left指向左端的第一个元素,right指向右端的下一个位置。那么队空的判断则是left== right ,队满是(left-1+MAX)%MAX== right或者(right-left+MAX)%MAX==MAX。示意图如下:

7.png

以下是双端队列的Java代码实现,包括双端队列的定义,在双端队列头那边添加节点变成新的头结点,在双端队列尾那边添加节点变成新的尾节点,从头结点那边拿出Deque的一个节点,从尾结点那边拿出Deque的一个节点,获取Deque处第一个节点的值,获取Deque上最后一个节点的值等操作。

class ListNode {
    //数据域
    public int val;
    //指针
    public ListNode next;
    //初始化值
    public ListNode(int val) {
        this.val = val;
    }
}
 
public class Deque {
    public ListNode head;//头结点
    public ListNode last;//尾节点
 
    //在双端队列头那边添加节点变成新的头结点
    //在第一个节点处添加一个节点
    public void addFirst(int val) {
        //创建对象初始化值建立新节点
        ListNode node = new ListNode(val);
        //判断尾节点是否为空
        if (this.last == null) {
            //若为空就是头结点尾节点都是这个新创建的节点
            this.head = node;
            this.last = node;
        }
        //node成为新的头节点
        node.next = this.head;
        this.head = node;
    }
 
    //在双端队列尾那边添加节点变成新的尾节点
    //在节点的最后添加一个节点
    public void addLast(int val) {
        //创建对象初始化值建立新节点
        ListNode node = new ListNode(val);
        //判断尾节点是否为空
        if (this.last == null) {
            //若为空就是头结点尾节点都是这个新创建的节点
            this.head = node;
            this.last = node;
        }
        //node成为新的尾节点
        this.last.next = node;
        this.last = node;
    }
 
    //从头结点那边拿出Deque的一个节点
    public int offerFirst() {
        //判断头节点是否为空,如果是就输出!
        if (this.head == null) {
            System.out.println("!");
            return -1;
        }
        //如果不为空,把头结点指向的值拿出来
        int oldValue = this.head.val;
        //判断头结点尾节点是否重合,如果重合就表明双端队列为空
        if (this.head == this.last) {
            this.head = null;
            this.last = null;
        } else {
            //没有重合就接着找下一个节点变成新的头结点
            this.head = this.head.next;
        }
        return oldValue;
    }
 
    //从尾结点那边拿出Deque的一个节点
    public int offerLast() {
        //判断尾节点是否为空,如果就输出!
        if (this.last == null) {
            System.out.println("!");
            return -1;
        }
        // //如果不为空,把尾结点指向的值拿出来
        int oldValue = this.last.val;
        //判断头结点尾节点是否重合,如果重合就表明双端队列为空
        if (this.head == this.last) {
            this.last = null;
            this.head = null;
        } else {
            //遍历找到新的尾节点
            ListNode cur = this.head;
            while (cur.next != last) {
                cur = cur.next;
            }
            //把找到的最后一个节点做为尾节点
            this.last = cur;
            //尾节点.next=null
            this.last.next = null;
        }
        return oldValue;
    }
 
    //获取Deque处第一个节点的值
    public int peekFirst() {
        //判断头结点是否为空,是就输出!
        if (this.head == null) {
            System.out.println("!");
            return -1;
        }
        //返回头结点值
        return this.head.val;
    }
 
    //获取Deque上最后一个节点的值
    public int peekLast() {
        //判断尾结点是否为空,是就输出!
        if (this.last == null) {
            System.out.println("!");
            return -1;
        }
        //返回尾结点值
        return this.last.val;
    }
 
    //Check whether the Deque is empty
    public boolean empty() {
        return this.head == null;
    }
 
    public void display(){
        ListNode cur =head;
        while (cur!=last) {
            System.out.println(cur.val);
            cur = cur.next;
        }
        System.out.println(cur.val);
    }
}

三、java 中的栈和队列

事实上Java中也提供了栈与队列相关的集合类供我们直接使用。

1.Java集合框架

首先我们需要了解一下Java中的集合框架,以便于我们了解哪些集合类是实现了栈或者队列的。 下图是Java集合框架图:

8.png

2.Java中的Stack(栈)

Java中就直接提供了Stack类,里边封装了所有栈相关操作的方法。

Stack的方法及说明(Stack继承了Vecter,自己独有的方法只有以下5个)总结如下:

方法解释
Object push(Object item)将元素推送到堆栈顶部
Object pop()移除并返回堆栈的顶部元素;如果我们在调用堆栈为空时调用pop(),则抛出’EmptyStackException’异常
Object peek()返回堆栈顶部的元素,但不删除它
boolean empty()如果堆栈为空,即顶部没有任何内容,则返回true;否则,返回false
int search(Object element)确定对象是否存在于堆栈中,如果找到该元素,它将从堆栈顶部返回元素的位置;否则,它返回-1

栈的主要操作就是入栈,出栈,返回栈顶元素,判断栈空,查找元素。

3.Java中的Queue(队列)

Java中也直接提供了Queue类,里边封装了所有队列相关操作的方法,包括入队与出队。

Queue的方法及说明总结如下:

方法解释
offer()将指定的元素插入此队列,插入成功返回 true;否则返回 false
add() 将指定的元素插入此队列,当使用有容量限制插入失败时抛出一个 IllegalStateException异常
poll() 获取并移除此队列的头,如果此队列为空,则返回 null
remove() 获取并移除此队列的头,如果此队列为空,则抛出NoSuchElementException异常
peek() 返回队列的头元素但不删除,如果此队列为空,则返回 null
element()返回队列的头元素但不删除,如果此队列为空,方法会抛出NoSuchElementException异常

4.Java中的Deque(双端队列)

Java中也提供了Deque(双端队列)类,里边封装了所有双端队列相关操作的方法,包括向队头插入元素,向队尾插入元素,返回队头元素,返回队尾元素等。

Deque的方法及说明总结如下:

方法解释
addFirst()向队头插入元素,如果元素为空,则发生NPE(空指针异常)
addLast()向队尾插入元素,如果为空,则发生NPE
offerFirst()向队头插入元素,如果插入成功返回true,否则返回false
offerLast()向队尾插入元素,如果插入成功返回true,否则返回false
removeFirst()返回并移除队头元素,如果该元素是null,则发生NoSuchElementException
removeLast()返回并移除队尾元素,如果该元素是null,则发生NoSuchElementException
pollFirst()返回并移除队头元素,如果队列无元素,则返回null
pollLast()返回并移除队尾元素,如果队列无元素,则返回null
getFirst()获取队头元素但不移除,如果队列无元素,则发生NoSuchElementException
getLast()获取队尾元素但不移除,如果队列无元素,则发生NoSuchElementException
peekFirst()获取队头元素但不移除,如果队列无元素,则返回null
peekLast()获取队尾元素但不移除,如果队列无元素,则返回null

后记

以上呢为大家分享了 数据结构基础【栈与队列】 的相关知识,包含栈的概念与实现队列的概念与实现循环队列的概念与实现双端队列的概念与实现Java中栈和队列等,希望能使大家有所收获。如果你想深入学习数据结构与算法或Java的知识与技术,可以参考:

👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~