java 数据队列_Java 数据结构 - 队列

Java 数据结构 - 队列

我们今天要讲的数据结构是队列,比如 Java 线程池任务就是队列实现的。

1. 什么是队列

和栈一样,队列也是一种操作受限的线性结构。使用队列时,在一端插入元素,而在另一端删除元素。

5c0a34b9c704f00458b5d88dd36a055d.png

1.1 队列的主要特性

队列中的数据元素遵守 "先进先出"(First In First Out)的原则,简称 FIFO 结构。

限定只能在队列一端插入,而在另一端进行删除操作。

1.2 队列的相关概念

入队(enqueue):队列的插入操作。

出队(dequeue):队列的删除操作。

2. 复杂度分析

和栈一样,队列也有两种实现方案,我们简单分析一下这两种队列的复杂度:

动态队列(链表):也叫链式队列。其插入、删除时间复杂度都是 O(1)。

静态队列(数组):也叫顺序队列。当队列队尾指针移到最后时,此时有两种操作:一是进行简单的数据搬移,二是进行队列循环。

关于队列的实现,我们只实现如下的基本操作。

public interface Queue {

Queue enqueue(Object obj); // 入队

Object dequeue(); // 出队

int size(); // 元素个数

}

2.1 链式队列

链式队列的实现非常简单,其插入和删除的时间复杂度都是 O(1)。为了简化代码的实现,我们引入哨兵结点。如下图所示,head 头结点是哨兵结点,不保存任何数据,从 head.next 开始保存数据,tail 结点指向最后一个元素结点。链表队列头结点和尾结点说明:

头结点:head 结点为哨兵结点,不保存任何数据,数据从第二个结点开始。

尾结点:tail 结点指向最后一个数据结点。

d65cee1a70c3b8db30a007138d9f35cb.png

根据上图,我们可以轻松实现一个链表组成的队列,代码也很简单。

public class LinkedQueue implements Queue {

private Node head;

private Node tail;

private int size;

public LinkedQueue() {

head = new Node(null, null);

tail = head;

}

@Override

public Queue enqueue(Object obj) {

tail.next = new Node(obj, null);

tail = tail.next;

size++;

return this;

}

@Override

public Object dequeue() {

Node next = head.next;

if (next == null) {

return null;

}

head = head.next;

size--;

return next.item;

}

@Override

public int size() {

return size;

}

public static class Node {

private Object item;

private Node next;

public Node(Object item, Node next) {

this.item = item;

this.next = next;

}

}

}

2.2 顺序队列

如果是数组实现的队列,则比链表要复杂一些,当尾结点指向数组的最后一个位置时,没有剩余的空间存放数据时,此时该如何处理?通过我们有两种解决方案:

数据搬移:将 head ~ tail 结点的数据搬移到从 0 结点开始。

循环队列:tail 结点从 0 开始循环使用,不用搬移数据。

我们先看一下循环队列

9df1932c484c12ec473dee5c81af11f4.png

如上图所示,当尾结点指向数组最后一个位置,当 tail 指向数组最后位置时,触发数据搬移,将 head ~ tail 结点的数据搬移到从 0 结点开始。数组队列头结点和尾结点说明:

头结点:head 结点指向第一个数据结点。当 head == tail 时说明队列处于队空状态,直接返回 null。

尾结点:tail 结点指向最后一个数据之后的空结点。当 tail == capcity 时说明队列处于队满状态,需要扩容或进行数据搬移。

根据上述理论代码实现如下:

public class ArrayQueue implements Queue {

private Object[] array;

private int capcity;

// head指向第一个数据结点的位置,tail指向最后一个数据结点之后的位置

private int head;

private int tail;

public ArrayQueue() {

this.capcity = 1024;

this.array = new Object[this.capcity];

}

public ArrayQueue(int capcity) {

this.capcity = capcity;

this.array = new Object[capcity];

}

/**

* tail 指向数组最后位置时,需要触发扩容或数组搬移

* 1. head!=0 说明数组还有剩余的空间,将 head 搬运到队列 array[0]

* 2. head==0 说明数组没有剩余的空间,扩容

*/

@Override

public Queue enqueue(Object obj) {

if (tail == capcity) {

if (head == 0) {

resize();

} else {

rewind();

}

}

array[tail++] = obj;

return this;

}

@Override

public Object dequeue() {

if (head == tail) {

return null;

}

Object obj = array[head];

array[head] = null;

head++;

return obj;

}

// 将 head 搬运到队列 array[0]

private void rewind() {

for (int i = head; i < tail; i++) {

array[i - head] = array[i];

array[i] = null;

}

tail -= head;

head = 0;

}

// 扩容

private void resize() {

int oldCapcity = this.capcity;

int newCapcity = this.capcity * 2;

Object[] newArray = new Object[newCapcity];

for (int i = 0; i < oldCapcity; i++) {

newArray[i] = array[i];

}

this.capcity = newCapcity;

this.array = newArray;

}

@Override

public int size() {

return tail - head;

}

}

说明: 数组队列出队的时间复杂度始终是 O(1)。但入队时要分为三种情况:

有空间:大多数情况,也是最好时间复杂度 O(1)。

没有空间需要数据搬移:执行 n 后触发一次数据搬移,最坏时间复杂度 O(n)。

没有空间需要扩容:执行 n 后触发一次数据搬移,最坏时间复杂度 O(n)。

如果采用摊还分析法,最好时间复杂度 O(1),最坏时间复杂度 O(n),摊还时间复杂度为 O(1)。虽然,平均时间复杂度还是 O(1),但我们能不能不进行数据搬移,直接循环使用数组呢?

2.3 循环队列

循环队列是一种非常高效的队列,我们需要重点掌握它,要能轻松写出无 BUG 的循环队列。

b4b4a2c1ea8705e012d3435555935b2f.png

数组队列头结点和尾结点说明:

头结点:head 结点指向第一个数据结点。当 head == tail 时说明队列处于队空状态,直接返回 null。否则在元素出队后,需要重新计算 head 值。

尾结点:tail 结点指向最后一个数据之后的空结点。每次插入元素后重新计算 tail 值,当 tail == head 时说明队列处于队满状态,需要扩容。

元素位置:对数组长度取模 (tail + 1) % length ,所以这种数据为了提高效率,都要求数组长度为 2^n,通过位运算取模 (tail + 1) & (length - 1)。

public class ArrayCircularQueue implements Queue {

private Object[] array;

private int capcity;

// 头结点指向

private int head;

private int tail;

public ArrayCircularQueue() {

this.capcity = 1024;

this.array = new Object[this.capcity];

}

public ArrayCircularQueue(int capcity) {

this.capcity = capcity;

this.array = new Object[capcity];

}

@Override

public Queue enqueue(Object obj) {

array[tail] = obj;

if ((tail = (tail + 1) % capcity) == head) {

resize();

}

return this;

}

@Override

public Object dequeue() {

if (head == tail) {

return null;

}

Object obj = array[head];

array[head] = null;

head = (head + 1) % capcity;

return obj;

}

// 不扩容,要先判断能否往数组中添加元素

public Queue enqueue2(Object obj) {

if ((tail + 1) % capcity == head) return this;

array[tail] = obj;

tail = (tail + 1) % capcity;

return this;

}

// 扩容

private void resize() {

// 说明还有空间

if (head != tail) {

return;

}

int oldCapcity = this.capcity;

int newCapcity = this.capcity * 2;

Object[] newArray = new Object[newCapcity];

for (int i = head; i < oldCapcity; i++) {

newArray[i - head] = array[i];

}

for (int i = 0; i < head; i++) {

newArray[capcity - head + i] = array[i];

}

this.capcity = newCapcity;

this.array = newArray;

this.head = 0;

this.tail = oldCapcity;

}

@Override

public int size() {

return tail - head;

}

}

说明: 循环队列关键是判断队空和空满的状态分别进行处理。除开扩容操作,循环队列的入队和出队的时间复杂度都是 O(1),同时也可以充分利用 CPU 缓存,所以说一种高效的数据结构。

2.4 阻塞队列和并发队列

3. 队列在软件工程中应用

如 JDK 线程池,当线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的呢?我们一般有两种处理策略。

非阻塞的处理方式,直接拒绝任务请求;

阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。

每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单片微型计算机(MCU)经过多年的发展,在性能上有很大的进步,在型号上发展到上千种类,已经广泛应用于人类社会生活的各个领域。单片机课程已经成为高校计算机、自动化、测控以及电子信息工程等专业的重要课程。该课程是一门理论性和实践性都很强的课程,在实际教学中,应将理论教学和实验教学紧密结合。学生在掌握理论知识之余,必须通过编写程序、设计硬件电路、仿真、调试这一系列的实验过程,才能更好地掌握单片机的结构原理和应用技能。随着单片机及其接口技术的飞速发展,目前市场上供应的编程仿真实验资源并不能完全满足高校单片机课程教与学的需求,构建低成本、技术先进、源码公开的单片机编程仿真实验系统,对我国单片机课程的教学和单片机领域人才的培养具有重要的现实意义。 本论文结合目前教学中对单片机编程仿真实验系统的实际需求,采用模块化结构设计思想,精心设计和开发了单片机编程仿真实验系统。该单片机编程仿真实验系统由PC机端单片机编程控制软件和单片机编程仿真实验板两部分组成。PC机端的单片机编程控制软件可以自动检测到连接到单片机编程仿真实验板上的单片机,控制单片机编程器擦除、写入、读出、校验目标单片机ROM中的程序,以十六进制文件(.HEX文件)格式显示在控制界面内;单片机仿真实验系统能够把写入单片机的程序实时地运行,并呈现实际运行效果。单片机编程控制软件和单片机仿真实验板组成一个完整的单片机编程仿真实验系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值