栈和队列

简介: 栈和队列

前言知识:


栈(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就是记录实际长度。

相关文章
|
26天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
122 9
|
17天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
22 1
|
4天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
25 5
|
20天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
23天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
25天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
47 4
|
29天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
数据结构(栈与列队)
数据结构(栈与列队)
20 1
|
2月前
【数据结构】-- 栈和队列
【数据结构】-- 栈和队列
17 0
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
33 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器