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

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

队列

概念

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出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链接

相关文章
|
8月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
264 1
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
131 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
535 77
|
10月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
245 11
|
10月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
11月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
444 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
11月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
249 7
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
1037 9
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
298 59

热门文章

最新文章