栈和队列

简介: 栈和队列

前言知识:


栈(Stack)又名堆栈,它是一种运算受限的线性表,限定仅在表尾进行插入和删除操作的线性表。队列(Queue)也是一种特殊的线性表,特殊之处在于它只允许在表的前端(Front)进行删除操作,而在表的后端(Rear)进行插入操作,和栈一样,队列 的部分操作也会受到限制。栈 的特点是先进后出(FILO),队列 则是先进先出(FIFO),本文将会通过顺序存储的方式模拟实现 栈,以及链式存储的方式模拟实现 队列,两种数据结构都比较简单,一起来看看吧!


一: 栈


在讲栈之前我们必须得了解一些基本概念:


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

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

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


图示:


3e03b3973b8942e594eeb27a621aa9f0.png

这就满足我们先前的概念,先进来的元素后出去,后进来的元素先出去。

比如许多生活中的例子,抗日剧中的三八大盖的子弹也是一种栈。


数组模拟栈


首先介绍 栈 的实现,栈 非常适合通过 顺序表 来模拟实现,因为 顺序表 尾插、尾删的复杂度是O(1),并且 栈 的插入、删除都是在 栈顶 完成的,因此我们可以去除 顺序表 的部分功能,这样就可以得到一个 栈。


a06df3ec06e8441bb14cfe57eb45dcc4.png


此时,入栈就相当于顺序表尾插,出栈就相当于顺序表的尾删。

芝士代码:

class MyStack {
    public int[] elem;
    public int usedSize;//记录使用的容量
    public MyStack() {
        this.elem = new int[10];
    }
}


接下来就是对栈的实现的,在实现之前我们还得看看jdk中所给的集合。

我们在最开始讲数据结构的时候就看过 “ 集合的框架 ” 我们再来看看关于栈的部分:



e8db8beddf51463ab8e69f3d3c4fe681.png



现在在正式的开发过程中已经很少 Vector 更多的使用Stack 创建栈,我们来看看stack中有那些方法。


dfffa011544441eca6f4f62e2ada872b.png


可以看到它的主要方法其实并不是很多,并且默认的构造方法还是空的,那么意味着我们需要模仿的方法只有五个。


入栈(push): 判断是否栈已满,已满后进行扩容;每次使用完一个栈空间usedSize向后移动一位。

3d9c4eb91f184a3eb403e25bc574507c.png


出栈(pop):


判断是否为空,如果为空则抛出异常;否则弹出栈顶元素,并且usedSize--。


图示理解:


d57c57697aa549d0b78a000c096650ca.png


代码:


26ccac3daed64c78ae8b1d0c740721ef.png


获取栈顶元素(peek):


类似于出栈,但是下标位置不发生改变。


1a2ff51a78ce425aaaa48e0a69bd51b2.png


判断栈是否为空(isEmpty):


dbaa135a99a0497d954e389ec2ef0d3f.png


判断栈是否已满(isFull):


0fb24bf30e2f4e819103e34ad999604f.png


因为扩容实在内部扩容的,所以我将其权限置为private。


完整代码:

import java.util.Arrays;
import java.util.Stack;
public class MyStack {
    public int[] elem;
    public int usedSize;
    public MyStack() {
        this.elem = new int[10];
    }
    //入栈
    public void push(int val) {
        if (isFull()) {
            elem = Arrays.copyOf(elem, (int) (1.5*elem.length));
        }
        elem[usedSize++] = val;
    }
    //出栈
    public int pop() {
        if (isEmpty()) {
            throw new EmptyException("是个空栈");
        }
        /*int val = elem[usedSize-1];
        usedSize--;
        return val;*/
       /* usedSize--;
        return elem[usedSize];*/
        return elem[--usedSize];
    }
    //获取栈顶元素
    public int peek() {
        if ( isEmpty() ) {
            throw new EmptyException("是个空栈");
        }
        return elem[usedSize - 1];
    }
    //判断是否为空
    public boolean isEmpty() {
        return usedSize == 0;
    }
    //判断是否已满
    private boolean isFull() { // 判断是否需要扩容
        return usedSize == elem.length;
    }
}

异常:

public class EmptyException extends RuntimeException{
    public EmptyException() {
    }
    public EmptyException(String message) {
        super(message);
    }
}



二: 队列


同样,在了解队列之前我们先来了解队列的概念:


队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front)



a8f5e564687d4dfa8c1bbf95cba0033a.png

frist in


6d170237fef148299949ac560f79339f.png


first out


我们再来看看在jdk给出的集合:


001a7841f2c345fda17d949c6c9be5bd.png


Queue是个接口,底层是通过链表实现的。


我们可以通过栈,顺序链或者链表实现,还可以通过PriorityQueue实现,这个后面单独介绍。


我们用LinkedList举例,它主要有如下几种主要方法:


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


也可以在集合中看到所提供的方法:


0a9c9abc42344115b6f79a86c52e9fc8.png


接下来开始写代码:


单链表模拟队列:


public class MyQueue {
    //单链表模拟实现
    static class Node {
        public int val;
        public Node next;
        public Node(int val) {
            this.val = val;
        }
    }
    public Node head;
    public Node last;
    public int usedSize;
}



我们需要设置一个usedSize来记录实际的队列长度,head作为队头,last作为队尾。


那么我们的判断队列是否为空就很好判断:


2aa90c1d1bbc4fbfbb67ed8ad27c0ed6.png


另外我们在多添加一个方法,用来获取实际队列长度:

c4814339359346de8e3e7ffa764aea3a.png

有了Empty方法peek方法就很好写了:



9d918c887a384272bac3dc3ef3c8b64c.png

判断是否为空,为空抛出自定义异常,不为空直接返回队头的val值。


入队:


75ca2b7472d1424493d334adfbe6582a.png


入队比较复杂,第一步需要确定是否为空队,如下图:


6c84862b2343467e9b65979ab3ce88e6.png


是为空队就需要head、last 均指向该元素,并且usedSize ++ 。


23e0eb8b33a64f10a3dbb3a48fe43a11.png


不为空那么lsat就需要先链接新入队的元素,在last指向新入队的元素,并且usedSize ++。


出队:


1d91d76651264d37b45ffc334a46a276.png


出队也很好理解:


f154494aefda4e599fb3203909756a28.png


同样分情况考虑,如果为空抛出异常,不为空正常出队

82e80fb0a3554386bd199e6660734c0e.png



 用一个临时变量保存val保留head的值,head指向head.next,并且usedSize -- ;最终返回val。


自定义异常:


c47f0c05b39c4b2dba9f1a79f24d0c47.png


完整代码:

public class EmptyException extends RuntimeException{
    public EmptyException() {
    }
    public EmptyException(String message) {
        super(message);
    }
}
public class MyQueue {
    //单链表模拟实现
    static class Node {
        public int val;
        public Node next;
        public Node(int val) {
            this.val = val;
        }
    }
    public Node head;
    public Node last;
    public int usedSize;
    public void offer(int val) {
        //入队
        Node node = new Node(val);
        if (Empty()) {
            head = node;
        } else {
            last.next = node;
        }
        last = node;
        usedSize++;
    }
    public int poll() {
        //出队
        if (Empty()) {
            throw new EmptyException("空队");
        }
        int val = head.val;
        head = head.next;
        if (head == null) {
            //如果只有一个结点那么last也要制空
            last = null;
        }
        usedSize--;
        return val;
    }
    public int peek() {
        //peek
        if (Empty()) {
            throw new EmptyException("空队");
        }
        return head.val;
    }
    public boolean Empty() {
        //判断空队
        return usedSize == 0;
    }
    public int getUsedSize() {
        return usedSize;
    }
}

当然用数组模拟大致思路是很类似的,但是有一点不同。


数组模拟队列:


用单链表我们用head记录队头,last记录队尾这很容易找到队头和队尾,我们用数组模拟队,front作为队头,rear作为队尾。

因为队列的第一个元素并非就存储在下标为0的位置处,那么我们就无法直接通过下标去访问队头,所以无法用下标访问的方式去获取元素队头和队尾。

同时还存在另一个问题,如果我们的队头的下标是数组的尾部,我们如何继续存储元素?

这是我们急迫需要解决的问题。


举例:


97499443b0354568bd6d6dfba2d7e1c4.png


此时我们元素5需要入队的位置在下标为0处,怎么从array.length处跑到0处呢?


我们将队头和队尾加上:



fc57443897db4b0d8d04a8e1a08d080a.png

按照单链表的形势,每次添加一个元素就++一次,明显不再使用于数组形式,我们需要改变记录方法;这里有个很好的想法:


ee5d0ff6ddcf467ba27e037ede1fa07b.png


这个问题的唯一的困难点在于rear处于array.length处。


按照上图,每次 rear 都需要重新赋值,rear 就等于上一次添加元素的下标,每次下标加一再取模数组的长度,如果位于array.length 处取模的值就为0,于是回到了数组开头。


同样出队列时也是用同样的思路去思考front 的值:

da73964770a84b66b45e588c6eb8af9a.png


具体的我就不再一步步介绍,具体的可以参考我的代码:


完整代码:


public class MyQueue {
    //循环队列
    //用数组模拟
    private int[] elem;
    private int usedSize;
    private int front;//表示队列的头
    private int rear;//表示队列的尾
    public MyQueue(int k) {
        this.elem = new int[k + 1];
    }
    /**
     * 入队
     * @param val 值
     * @return
     */
    public boolean enQueue(int val) {
        if (isFull()) {
            return false;
        }
        elem[rear] = val;
        rear = (rear+1) % elem.length;
        //这里不能使用rear++;因为是个循环,rear有可能是最后数组的最后位置
        usedSize++;
        return true;
    }
    /**
     * 出队列
     * @return
     */
    public boolean deQueue() {
        if (Empty()) {
            return false;
        }
        front = (front+1) % elem.length;
        usedSize--;
        //这里不能使用front++;因为是个循环,front有可能是最后数组的最后位置
        return true;
    }
    /**
     * 得到队头元素
     * @return
     */
    public int getFront()  {
        if (Empty()) {
            return -1;
        }
        return elem[front];
    }
    /**
     * 得到队尾元素
     * @return
     */
    public int getRear() {
        if (Empty()) {
            return -1;
        }
        int index = (rear == 0) ? elem.length-1 : rear-1;//??
        return elem[index];
    }
    public boolean isFull() {
        //判断队是否满了
        return (rear + 1) % elem.length == front;
    }
    public boolean Empty() {
        //判断空队
        return rear == front;
    }
}

还可以添加一个获取队列实际长度,代码中usedSize就是记录实际长度。

相关文章
|
3天前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
16 5
【数据结构】优先级队列(堆)从实现到应用详解
|
8天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
10天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
12天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
14 0
【数据结构】栈和队列的深度探索,从实现到应用详解
|
16天前
|
Linux C++ Windows
栈对象返回的问题 RVO / NRVO
具名返回值优化((Name)Return Value Optimization,(N)RVO)是一种优化机制,在函数返回对象时,通过减少临时对象的构造、复制构造及析构调用次数来降低开销。在C++中,通过直接在返回位置构造对象并利用隐藏参数传递地址,可避免不必要的复制操作。然而,Windows和Linux上的RVO与NRVO实现有所不同,且接收栈对象的方式也会影响优化效果。
|
1月前
|
存储 安全 编译器
缓冲区溢出之栈溢出(Stack Overflow
【8月更文挑战第18天】
55 3
|
18天前
crash —— 获取内核地址布局、页大小、以及栈布局
crash —— 获取内核地址布局、页大小、以及栈布局
|
18天前
|
存储 程序员 C语言
堆和栈之间有什么区别
【9月更文挑战第1天】堆和栈之间有什么区别
89 0
|
27天前
|
机器学习/深度学习 消息中间件 缓存
栈与队列的实现
栈与队列的实现
37 0
|
1月前
|
算法 C语言 C++
【practise】栈的压入和弹出序列
【practise】栈的压入和弹出序列