前言:本篇文章只是记录王争的数据结构与算法之美的学习笔记,写下来能强迫自己系统的再过一遍,加深理解。这门课
以实际开发中遇到的问题为例
,引入解决问题涉及到的的数据结构和算法,但不会讲的太细,最好结合一本实体书进行学习。
CPU 资源是
有限
的,任务的处理速度和线程个数也不是线性正相关的,相反,过多的线程反而会导致CPU 频繁切换
,处理性能下降。当我们向固定大小的线程池中请求一个线程时,如果请求非常多,线程池没有空闲资源,如何处理这个请求?就需要用到队列了。
1. 队列
队列很好理解,可以理解为排队买票,先来的买完票就走了,后来的在最后排队。
跟栈不同,先进者先出,后进者后出
,这就是典型的"队列",跟栈相同的是队列也是一种操作受限的线性表数据结构
,栈只支持两个基本操作:入栈 Push
和出栈 Pop
,队列也支持两个基本操作:入队 enqueue
,出队 dequeue
,如下图所示:
2. 队列的实现
从队列的概念中,我们知道,队列和栈一样,都是抽象的数据结构,具有先进先出,后进后出的特性。
我们有两种实现方式:
- 用数组实现,则为顺序队列
- 用链表实现,则为链式队列
2.1 数组实现队列
// 用数组实现的队列
public class ArrayQueue {
// 数组:items,数组大小:n
private String[] items;
private int n = 0;
// head表示队头下标,tail表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为capacity的数组
public ArrayQueue(int capacity) {
items = new String[capacity];
n = capacity;
}
// 入队
public boolean enqueue(String item) {
// 如果tail == n 表示队列已经满了
if (tail == n) return false;
items[tail] = item;
++tail;
return true;
}
// 出队
public String dequeue() {
// 如果head == tail 表示队列为空
if (head == tail) return null;
// 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
String ret = items[head];
++head;
return ret;
}
}
队列的数组实现也挺简单,对于栈来说需要一个栈顶指针就行,因为栈的入栈和出栈都是在一个方向进行。队列则是不同的方向入队和出队,所以需要两个指针:head 指针
,指向队头
,tail 指针
,指向队尾
。
如下图,当a b c d 依次入队之后,队列的 head 指针指向下标为 0 的位置,tail 指向下标为 4 的位置:
image.png
当我们调用两次出队操作之后,head 指向下标为 2 的位置,tail 仍然指向下标为 4 的位置:
image.png
如果用数组实现队列的话,会遇到一个问题:插入很多元素之后,队列的空间不够了。
我们可以跟数组一样,用数据搬移,出队一个,就搬移一次,但是这样很耗费时间,想象一下,出队从下标为 0 的位置开始,相当于删除下标为 0 的数据,这样每出队一个数据,就需要搬移整个队列的数据,当然也可以稍微优化一下,就是当 tail 指针指向最后一个位置时,将所有数据都移到下标为 0 开始的位置上,整体进行搬移一次,代码如下:
// 入队操作,将item放入队尾
public boolean enqueue(String item) {
// tail == n表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[I];
}
// 搬移完之后重新更新head和tail
tail -= head;
head = 0;
}
items[tail] = item;
++tail;
return true;
}
从代码中看到,当队列的 tail
移动到数组最右边时,如果有新的数据入队,可以将 head
到tail
之间的数据,整体搬移到数组 0 到 tail-head
的位置:
2.2 链表实现队列
基于链表实现,head
指针指向链表的第一个节点
,tail
指向链表的最后一个节点
。
当入队时,我们需要:
tail->next = new_node;
tail = tail->next;
出队时:
head = head->next
image.png
3. 循环队列
顾名思义,就是一个首位相连的队列,在上面用数组实现的队列时,当 tail==n 时,需要对数据做搬移操作,这样性能会受到影响,我们可以让队列的首尾相连,当tail==n
的时候,又开始从下标为 0 的地方入队,这就是循环队列
,就是一个环了:
从上图中可以看到,队列大小为 8,目前 head = 4
,tail = 7
,按照上面数组的实现,我们需要做数据搬移操作了。但是用循环队列的话,我们可以将新的数据 a 放到下标为 7 的位置,将新的数据 b 放到下标为 0 的位置,这样 tail 就指向下标为 1 的位置,如下图:
这样我们避免了数据搬移操作,但是判断队列为空,和队列满了的条件时就比较困难了:
-
数组实现的非循环队列
- 队空条件:tail == n
- 队满条件:head == tail
-
数组实现的循环队列
- 队空条件:tail == n
- 队满条件:(tail + 1) %n = head (需要自己画图理解一下)
像我图中画的队满的情况,
tail=3,head=4,n=8
,所以总结一下规律就是:(3+1)%8=4
。多画几张队满的图,你就会发现,当队满时,(tail+1)%n=head
。
并且当队列满时,tail 指向的位置其实是空的,所以循环队列会浪费一个数组的存储空间。
4. 阻塞队列
阻塞队列就是在队列为空的时候,从队头取数据会被阻塞,因为队列没数据啊,一直等到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回:
image.png
其实上面的定义就是一个"生产者 - 消费者模型"。基于阻塞队列实现的"生产者 - 消费者模型",可以有效的协调生产和消费的速度。可以多配置几个“消费者”:
image.png
5. 对于线程资源的处理策略
-
非阻塞处理:直接拒绝任务请求
-
阻塞处理:将请求排队,使用队列方式存储,直到有空闲线程时,取出队首位置的任务执行
-
基于链表的实现:
可以实现一个支持无线排队的无界队列,但是可能会导致过多的请求无法处理,请求处理的时间较长,所以针对响应时间比较敏感的系统,基于链表实现的无线排队的线程池是不合适的 -
基于数组的实现:
首先为有界队列,即队列的大小有限,当排队请求超过队列大小时,可以直接拒绝这个请求,对于响应时间敏感的系统来说,相对合理
6. 总结&练习
队列可以用数组实现(顺序队列),可以用链表实现(链式队列),都是先进先出,在数组实现队列的时候,可以实现循环队列,这样就不用数据搬移了。
- 数组实现队列
- 链表实现队列
- 实现循环队列
网友评论