栈和队列

简介: 栈和队列

前言知识:


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

相关文章
|
5天前
|
算法 C语言 C++
【practise】栈的压入和弹出序列
【practise】栈的压入和弹出序列
|
3天前
栈的几个经典应用,真的绝了
文章总结了栈的几个经典应用场景,包括使用两个栈来实现队列的功能以及利用栈进行对称匹配,并通过LeetCode上的题目示例展示了栈在实际问题中的应用。
栈的几个经典应用,真的绝了
|
4天前
|
存储 网络协议 Linux
用户态协议栈06-TCP三次握手
用户态协议栈06-TCP三次握手
|
3天前
|
算法
【数据结构与算法】优先级队列
【数据结构与算法】优先级队列
6 0
|
3天前
|
存储 算法
【数据结构与算法】队列(顺序存储)
【数据结构与算法】队列(顺序存储)
5 0
|
4天前
|
存储
全局变量和局部变量在堆和栈的区别
全局变量和局部变量在堆和栈的区别
12 0
|
5天前
|
存储 人工智能 运维
高质量存储力发展问题之浪潮信息发布的大模型智算软件栈的定义如何解决
高质量存储力发展问题之浪潮信息发布的大模型智算软件栈的定义如何解决
8 0
|
5天前
|
设计模式 算法 C语言
【CPP】栈、双端队列、队列、优先级队列与反向迭代器
【CPP】栈、双端队列、队列、优先级队列与反向迭代器
|
5天前
|
存储 算法 C++
【CPP】栈简介及简化模拟实现
【CPP】栈简介及简化模拟实现
|
6天前
【数据结构】用栈实现队列
【数据结构】用栈实现队列