一篇帮你搞定数据结构中的栈和队列(二)

简介: 一篇帮你搞定数据结构中的栈和队列(二)

队列

概念

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

(Head/Front)

2.png

经过后面的学习我们就会知道,上述队列只是一个普通队列.


队列的类型

1:普通队列:也就是只允许我们在一端进行插入数据操作的队列.

2:优先级队列:也就是我们的堆

3:双端队列:双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。

4:循环队列:后面会详细介绍


集合中的队列

2.png

可以看到实体类有两个

一个是LinkedList:LinkedList既可当作双端队列,因为它实现了Deque接口,也可以当作普通队列,因为其也实现Queue接口,LinkedList的底层其实就是我们学过的双向链表,其具备了链表的特性

一个是PriorityQueue:这个是优先级队列,因为它实现了Queue接口,优先级队列的底层其实是二叉树,但是其具备队列的特性.


Queue接口常用方法(普通队列)

2.png


add方法

public class TestDemo5 {
    public static void main(String[] args) {
        //此时是一个普通队列
        Queue<Integer> queue = new LinkedList<>();
        //此时调用add方法,默认是尾部入队.
        queue.add(1);
        //输出普通队列的长度,为1
        System.out.println(queue.size());
    }
}

此时我们调用普通队列中的add方法,默认是尾部入队列

此时 Queue接口发生了向上转型,我们的LinkedList类中重写了Queue接口中的add方法,所以在调用add方法的时候发生了运行时绑定,来看源码:

2.png

这个是LinkedList类中的add方法,可以看到其内部还调用了一个方法叫做linkLast方法,这个方法就是尾插法,如下图所示:

2.png

所以普通队列中的add方法其实默认就是尾插法.


offer方法

同样也是实现我们普通队列插入的一个方法,其效果与add方法是一样的,但是与add方法的区别是add方法当队列满了再插入的时候会抛出异常,而offer方法只返回true或者false,所以一般我们建议用offer方法


remove方法

删除普通队列的队头元素


poll方法

删除普通队列的队头元素,效果与remove方法相同,我们一般还是建议使用poll方法

public class TestDemo5 {
    public static void main(String[] args) {
        //此时是一个普通队列
        Queue<Integer> queue = new LinkedList<>();
        //此时调用add方法,默认是尾部入队.
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        //此处的输出值为1,代表所删除的队头元素
        System.out.println(queue.poll());
        //此处的输出值为【2,3,4】
        System.out.println(queue);
    }
}

element方法

element方法是获取队列的头元素,但不删除


peek方法

peek方法是获取队列的头元素,但不删除,与element方法的效果相同.

public class TestDemo5 {
    public static void main(String[] args) {
        //此时是一个普通队列
        Queue<Integer> queue = new LinkedList<>();
        //此时调用add方法,默认是尾部入队.
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);
        //此处的输出值为1
        System.out.println(queue.peek());
        //此处的输出值为【1,2,3,4】
        System.out.println(queue);
    }
}

Deque接口常用方法(双端队列)

以下方法只是Deque接口中非常常用的一些方法,当然Deque接口总还有很多其他常用的方法,如果大家想要查看的话可以直接打开我们Deque接口的源码,Alt+7之后便可以查看里面的其他实现方法.

2.png

代码示例:

public class TestDemo5 {
    public static void main(String[] args) {
        Deque<Integer> deque = new LinkedList<>();
        //从双端队列的队头插入元素
        deque.addFirst(1);
        //从双端队列的队尾插入元素
        deque.addLast(2);
        //输出结果为:[1, 2]
        System.out.println(deque);
    }
}

自己实现一个普通队列(单链表实现)

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低,时间复杂度会达到O(N)

那么在这里我们就拿单链表实现一个普通队列:来看代码:

因为是单链表,所以其方向是固定的,又因为我们在插入元素的时候的方法是尾插法,所以队尾和队头如下所示:

2.png

来看我们的实现代码:

class Node {
    public int val;
    public Node next;
    public Node(int val) {
        this.val = val;
    }
}
public class MyQueue {
    //定义队列头
    public Node first;
    //定义队列尾
    public Node last;
    //判断列表是否为空
    public boolean isEmpty() {
        if (this.first == null && this.last == null) {
            return true;
        }
        return false;
    }
    //实现队列中的offer方法,在其源码中默认是尾插法
    public boolean offer(int val) {
        Node node = new Node(val);
        //假如此时队列中还没有元素
        if (this.first == null) {
            this.first = node;
            this.last = node;
        } else {
            //假设队列中已经插入了元素
            this.last.next = node;
            this.last = node;
        }
        return true;
    }
    /**
     * 得到队头元素且删除
     *
     * @return
     * @throws RuntimeException
     */
    public int poll() throws RuntimeException {
        if (isEmpty()) {
            throw new RuntimeException("队列为空");
        }
        int num = this.first.val;
        this.first = this.first.next;
        return num;
    }
    /**
     * 得到队列的头元素但是不删除
     *
     * @return
     */
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空");
        }
        return this.first.val;
    }
    public static void main(String[] args) {
    }
}

对于上述代码在进行入队操作(尾插法)时候的时间复杂度为O(1)

在进行出队(头删法)时候的时间复杂度为O(1).


假设此时我们入队操作采用头插法,时间复杂度为O(1),但是当出队操作采用尾删法,时间复杂度就成了O(N).因为需要遍历链表查找到链表的最后一个元素.


所以当遇见这种情况的时候,我们都是采用双向链表,不用单向链表.


能否使用数组来实现队列?

非常麻烦,出队的时候的找到队头在哪里,入队的时候还需要往空位置插,因为我们普通队列的插入是尾插法,如果此时数组后面几个元素插满了,再进行尾插法会发生数组下标越界异常:例如下所示,此时再往7下标后面插入会发生数组下标越界异常,所以只能往0到3号下标处进行插入.

2.png

此时我们小伙伴们就想啦,假如这个队列能够首尾相接,循环起来,是不是就能够实现插入了呢?小伙伴们的想法非常的好,的确是这样,由此就引入了我们的循环队列的概念,而循环队列底层也就是一个循环数组.相当于当我们实现队列的尾插法的时候,本来应该插在8号下标处的元素插在了0号下标处.所以说用数组是可以实现队列的,下面我们来看具体实现.


循环队列

什么是循环队列?我们先来给出一个示意图:

2.png

此时我们相当于将数组首尾相连形成了一个循环队列,0-7代表数组下标,此时定义两个整形指针,一个是指向队列头元素的头指针front,一个是指向队尾元素的尾指针rear,此时因为队列为空,所以此时front指针和rear指针都指向了0下标的位置.

然后我们给出一组想要插入的数据:


12,23,34,45,56,67,78,89


当我们挨个向队列中插入数据的时候,front指针不动,rear指针开始向后移动,rear指针所指向的位置就代表当前可以插入元素的位置,所以rear指针同样代表当前可以存放数组元素的下标,假如我们要删掉队列中的元素,只需要front++即可,这样队列中的元素在遍历的时候就会被删除掉.


注意事项(结论1)

当在删除循环队列中的元素的时候,我们的rear指针其实是不动的,只有front指针一直在动,所以最终front指针与rear指针终会相遇,这时我们的循环队列为空`,所以总结出一个结论,当front和rear相遇的时候,队列为空,如下所示:此时front指针遍历过的元素:12,23,34,45全部都出队列了,此时两个指针相遇代表队列为空

2.png


欧克现在回到我们向队列中插入元素的时候,此时front指针回到我们0下标的位置,然后继续开始插入我们的元素,rear指针继续向下遍历,直到rear指针与我们front指针的第二次重合:


此时有些同学就开始纳闷了:首先当rear走到7号下标的时候,它怎么能再走到front所在的0号下标呢,难道不会发生数组下标越界异常吗?其次就算走到了,之前我们总结到说当front和rear相遇的时候,队列为空,但此时队列竟然满了?这不是悖论吗?如下所示:

2.png

所以接下来当我们将要重点解决同学们的两个疑惑:

问题1:front和rear相遇,此时到底队列为空,还是队列为满?

问题2:front和rear当都走到7号下标的时候,都会面临一个问题:

front+1此时数组下标越界了,rear+1此时数组下标也越界了

那么首先我们来解决问题1:

2.png

假设此时还剩最后一个元素89未插入到队列当中,我们在这里需要牺牲一个空间,来判断当前队列是否是满的,也就是判断rear的下一个是否是front

所以当front和rear相遇的时候,队列为空结论正确.

那么接下来我们来解决第二个问题:

rear此时的下标为7,front此时的下标为0,rear+1=7+1=8,8是永远不可能等于0的,所以此时我们需要用到这个表达式来判断当前队列是否是满的:(rear+1)%len==front,例如(7+1)%8=0,说明此时队列满了


注意事项(结论2)

判断循环队列是否满就使用(rear+1)%len==front这个式子来判断.适用于所有情况.


自己实现一个循环队列(数组实现)

经过前面的分析我们可以知道的是,我们的循环队列的底层其实就是一个数组,代码实现如下:

public class MyCircularQueue {
    private int front;
    //rear表示当前可以插入元素的数组的下标
    private int rear;
    private int[] elem;
    public MyCircularQueue(int k) {
        this.elem = new int[k];
    }
    /**
     * 向循环队列插入一个元素。如果成功插入则返回真。
     *
     * @param value
     * @return
     */
    public boolean enQueue(int value) {
        //此时代表队列已满,无法再进行插入
        if (isFull()) {
            return false;
        }
        //放到数组的rear下标
        this.elem[this.rear] = value;
        //rear指针往后走,取余是为了循环起来
        this.rear = (this.rear + 1) % this.elem.length;
        return true;
    }
    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        }
        //只需要挪动front这个下标就好了,取余是为了循环起来
        this.front = (this.front + 1) % this.elem.length;
        return true;
    }
    /**
     * 得到队头元素
     *
     * @return
     */
    public int Front() {
        if (isEmpty()) {
            return -1;
        }
        return this.elem[this.front];
    }
    /**
     * 得到队尾元素
     * 注意特殊情况
     *
     * @return
     */
    public int Rear() {
        if (isEmpty()) {
            return -1;
        }
        int index = -1;
        if (this.rear == 0) {
            index = this.elem.length - 1;
        } else {
            index = this.rear - 1;
        }
        return this.elem[index];
    }
    /**
     * 队列是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return this.rear == this.front;
    }
    /**
     * 队列是否为满
     *
     * @return
     */
    public boolean isFull() {
        return (this.rear + 1) % this.elem.length == this.front;
    }
}

在这里要重点强调两点:

第一点:当我们不管是插入还是删除队列中的元素的时候,我们rear指针和front指针的遍历语句都是取余语句,同时在进行插入和删除的时候不要忘记分别对队列进行判满和判空操作.

第二点:在我们获得队尾元素的时候是有特殊情况的,不能直接写return this.elem[this.rear-1]这条语句,例如如下图所示:当我们将12这个元素出队列的时候,此时front指向了1下标处,rear指针指向了0下标处,如果直接返回rear[-1]的话,会发生数组下标越界异常,所以此处我们需要对rear的位置进行判断,分成两种不同的情况进行探讨,这块还需要大家着重注意下.以下是第二点中特殊情况的示意图

2.png


面试题汇总

括号匹配问题。OJ链接

用队列实现栈。OJ链接

用栈实现队列。OJ链接

实现一个最小栈。OJ链接

设计循环队列。OJ链接

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