引言
学习了JAVA的基础知识以后,这是本人第一次接触数据结构与算法的知识,为了更高效以及自律的学习,我加入了Datawhale学习小组。
Task04------队列
理论部分:
一、用数组实现一个顺序队列
1.队列的特征
与 “栈” 类似,“队列”也偏向于一种 “工具”,“先进先出(FIFO)“是其特性,队列的基本操作
2.使用单指针的顺序队列
只用一个 rear 指针完成队列的基本操作,只要保证 queArray[0] 的位置都是队首元素即可
class SQueue<T>{
private T[] queArray;
private int maxSize;
private int rear;
SQueue(int max){
maxSize=max;
queArray=(T[])new Object[maxSize];
rear=0; //此时没有元素
}
SQueue(){
this(10);
}
public void insert(T data) {
if(rear<maxSize)
queArray[rear++]=data;
else
System.out.println("队列已满!");
}
public void remove() {
if(rear>0){ //队列中还有元素
for(int i=0;i<rear-1;i++) //队首元素舍弃,依次将队列元素前移
queArray[i]=queArray[i+1];
rear--;
}
else
System.out.println("队列已空!");
}
public T peek() {
if(rear>0)
return queArray[0];
else
return null;
}
}
3.使用双指针的顺序队列(常用)
实际上队列应该使用双指针,一个 front 标志队首,一个 rear 标志队尾。
仅用一个 rear指针时,每次删除一个元素时都需要搬移所有元素,效率并不高,这里介绍一个利用双指针进行删除的方法,基本原理如下:
1.当front与rear不重叠时,直接将指向队首的指针 front 往后移动一个
2.当 front 与 rear 重叠时,即删除最后一个元素,只要将 front 与 rear 全部复位即可
3.只有当 rear==maxSize 时,才将搬移所有元素
以下是代码实现:
class SQueue<T>{
private T[] queArray;
private int maxSize;
private int front;
private int rear;
SQueue(int max){
maxSize=max;
queArray=(T[])new Object[maxSize];
front=0;
rear=0;
}
SQueue(){
this(10);
}
public void insert(T data) {
if(rear<maxSize)
queArray[rear++]=data;
else if((rear-front)==maxSize)
System.out.println("队列已满!");
else {
for(int i=0;i<(rear-front);i++)
queArray[i]=queArray[front+i];
}
}
public void remove() {
if(rear==0)
System.out.println("队列已空!");
else if(front==(rear-1)) {
front=0;
rear=0;
}
else
front++;
}
public T peek() {
if(rear!=0)
return queArray[front];
else
return null;
}
}
二、用数组实现一个循环队列
1.循环队列的特征
循环队列是十分巧妙的,它允许 队首指针front 和 队尾指针rear 在超过maxSize后回到0,其实可以看做把直线的数组给围成一个圆,从而省去了以上两种队列的“搬移”问题。具体看下面的图解:
- 删除一个元素依旧是将 front 后移,不过当front已经指向maxSize时,将它移向0
- 插入一个元素时,同样,将 rear 后移,当rear==maxSize时,rear=0;
那么我们怎么知道队列是否为空?队列是否满了?其实也很简单,当rear追上front时,队列就空了:当front追上rear时,队列就满了。
但是这里有个小插曲,因为rear与front一开始都是指向0,所以判断rear是否追上front时,只能通过比较 (rear+1) 与 front ,否则第一个元素永远无法插入。但是用rear+1进行比较会导致数组始终有一个空间闲置,容量为10的空间实际只能储存9个数据。这其实是以 “空间换时间”,浪费一个空间,省下了搬移的时间。
2.代码实现
class CQueue<T>{
private T[] queArray;
private int maxSize;
private int front;
private int rear;
CQueue(int max){
maxSize=max+1; //由于会浪费一个空间,为了用户考虑,在这里加上
queArray=(T[])new Object[maxSize];
front=0;
rear=0;
}
CQueue(){
this(10);
}
public void insert(T data) {
if((rear+1)%maxSize==front) //判断队尾是否追到队首
System.out.println("队列已满!");
else {
rear%=maxSize; //队尾循环起来
queArray[rear++]=data;
}
}
public void remove() {
if(front==rear) //判断队首是否追到队尾
System.out.println("队列已空!");
else
front=(front+1)%maxSize; //队首也循环起来
}
public T peek() {
if(front==rear)
return null;
else
return queArray[front];
}
}
二、用链表实现一个链式队列
1.链式队列的特点
同链式栈一样,链式队列理论上也是无限容量,只是出入方式为 “先入先出”罢了
2.代码实现(部分功能)
class LinkedQueue<T> {
private SLinkedList<T> list;
LinkedQueue(){
list=new SLinkedList<T>();
}
public int getLength() {
return list.getSize();
}
public void insert(T data) {
list.addLast(data);
}
public T remove() {
return list.remove();
}
public boolean isEmpty() {
return list.getSize()==0;
}
}
练习部分:
一、模拟银行服务
1.要求
目前,在以银行营业大厅为代表的窗口行业中大量使用排队(叫号)系统,该系统完全模拟了人群排队全过程,通过取票进队、排队等待、叫号服务等功能,代替了人们站队的辛苦。
排队叫号软件的具体操作流程为:
-
顾客取服务序号
当顾客抵达服务大厅时,前往放置在入口处旁的取号机,并按一下其上的相应服务按钮,取号机会自动打印出一张服务单。单上显示服务号及该服务号前面正在等待服务的人数。 -
服务员工呼叫顾客
服务员工只需按一下其柜台上呼叫器的相应按钮,则顾客的服务号就会按顺序的显示在显示屏上,并发出“叮咚”和相关语音信息,提示顾客前往该窗口办事。当一位顾客办事完毕后,柜台服务员工只需按呼叫器相应键,即可自动呼叫下一位顾客。
编写程序模拟上面的工作过程.
主要要求如下:
-
程序运行后,当看到“请点击触摸屏获取号码:”的提示时,只要按回车键,即可显示“您的号码是:XXX,您前面有YYY位”的提示,其中XXX是所获得的服务号码,YYY是在XXX之前来到的正在等待服务的人数。
-
用多线程技术模拟服务窗口(可模拟多个),具有服务员呼叫顾客的行为,假设每个顾客服务的时间是10000ms,时间到后,显示“请XXX号到ZZZ号窗口!”的提示。其中ZZZ是即将为客户服务的窗口号。
2.分析
用java实现多线程其实很简单,不过要实现线程同步就需要稍微深入的理解对象锁。此题的难点也就在于 “管理多线程”。在看代码前,首先声明一个对之前的代码稍作修改的地方:
- 之前的所有 remove() 只是删除元素,此次为了方便,remove方法在删除元素的同时返回被删除的元素。
3.代码实现
首先是 链队列类 的实现代码,包括Node结点类、SLinkedList单向链表类以及用单向链表实现的链队列
class Node<T> {}
class SLinkedList<T> {}
class LinkedQueue<T> {}
其次是 服务窗口Server类,这是个实现了Runnable的线程类,其中有两个细节点:
- synchronized(Object obj) 中Object的选取
- 每次服务10s的实现
第一个点,要让服务窗口之间 “互斥”,必须让对象锁唯一,三个方法:一个是利用static 类锁, 第二个是利用 “字符串常量池”,第三个是将队列queue放入(我看好多同学都用的是这个,因为所有服务窗口共享的就是唯一的队列queue)
第二个点,要让 服务窗口在接收用户的时候“秒接”,在接收后停顿10s,我引入了一个判断服务窗口是否工作的boolean working,看看代码就能明白了。
public class Server implements Runnable{
private LinkedQueue<Integer> queue;
static String key=new String("key");
String key="key";
public boolean working=false;
Server(LinkedQueue<Integer> queue){
this.queue=queue;
}
public void run() {
while(true) {
try {
synchronized(key) {
if(!queue.isEmpty()) {
//此处remove方法删除并返回队首ID
System.out.println("请"+queue.remove()+"号客户到"+Thread.currentThread().getName()+"号窗口!");
working=true;
}
}
if(working) {
Thread.sleep(10000);
working=false;
}
else
Thread.sleep(500);
}catch(Exception e) {}
}
}
}
最后就是主方法所在的测试类啦,因为客户端一直输入,就不单独做一个类了。这里也有几个点要注意:
- 实现单个回车就结束输入不能用Scanner,而要用BufferedReader(由于本人是菜鸡还没有细学异常,我的所有的异常全部用Exception且不做处理)
- 有几个服务窗口就要new几个Server对象,不能将一个Server反复装给Thread,那样只是对一个窗口操作。
- ”前面有几个人“这个选项根据个人代码的差异有不同的算法,由于我修改了remove方法,“前面有几个人”直接就是队列长度
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Text {
public static void main(String[] args) {
LinkedQueue<Integer> queue=new LinkedQueue();
int server_num=2;
Server[] server=new Server[server_num];
Thread[] t=new Thread[server_num];
for(int i=0;i<server_num;i++) {
server[i]=new Server(queue);
t[i]=new Thread(server[i]);
t[i].setName(i+1+"");
t[i].start();
}
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
int ID=0;
while(true) {
System.out.println("请点击屏幕取号!");
try {
String res=br.readLine();
} catch (Exception e) {}
System.out.println("您的号码是: "+(++ID)+" ,您前面还有"+queue.getLength()+"人");
queue.insert(ID);
}
}
}
以下是执行后的效果: