队列是一种“先进先出”(first in,first out,缩写为FIFO)的数据形式,就像排队买票的队伍一样(前提是没有人插队)。接下来,我们建立一个非正式的抽象定义:
类型名: 队列
类型属性: 可以储存一系列项
类型操作: 初始化队列为空
确定队列为空
确定队列已满
确定队列中的项数
在队列末尾添加项
在队列开头删除或恢复项
清空队列
有了的关于ADT之链表基础,直接上代码:
定义接口:
/* queue.h -- Queue的接口 */
#ifndef _QUEUE_H_
#define _QUEUE_H_
#include <stdbool.h>
// 在这里插入Item类型的定义,例如
typedef int Item; // 用于use_q.c
// 或者 typedef struct item {int gumption; int charisma;} Item;
#define MAXQUEUE 10
typedef struct node {
Item item;
struct node *next;
} Node;
typedef struct queue {
Node *front; /* 指向队列首项的指针 */
Node *rear; /* 指向队列尾项的指针 */
int items; /* 队列中的项数 */
} Queue;
/* 操作: 初始化队列 */
/* 前提条件: pq 指向一个队列 */
/* 后置条件: 队列被初始化为空 */
void InitializeQueue(Queue *pq);
/* 操作: 检查队列是否已满 */
/* 前提条件: pq 指向之前被初始化的队列 */
/* 后置条件: 如果队列已满则返回true,否则返回false */
bool QueueIsFull(const Queue *pq);
/* 操作: 检查队列是否为空 */
/* 前提条件: pq 指向之前被初始化的队列
*/
/* 后置条件: 如果队列为空则返回true,否则返回false */
bool QueueIsEmpty(const Queue *pq);
/* 操作: 确定队列中的项数 */
/* 前提条件: pq 指向之前被初始化的队列
*/
/* 后置条件: 返回队列中的项数 */
int QueueItemCount(const Queue *pq);
/* 操作: 在队列末尾添加项 */
/* 前提条件: pq 指向之前被初始化的队列 */
/* item是要被添加在队列末尾的项 */
/* 后置条件: 如果队列不为空,item将被添加在队列的末尾, 该函数返回true;否则,队列不改变,该函数返回false*/
bool EnQueue(Item item, Queue *pq);
/* 操作: 从队列的开头删除项 */
/* 前提条件: pq 指向之前被初始化的队列
*/
/* 后置条件: 如果队列不为空,队列首端的item将被拷贝到*pitem中并被删除,且函数返回true; */
/* 如果该操作使得队列为空,则重置队列为空 */
/* 如果队列在操作前为空,该函数返回false */
bool DeQueue(Item *pitem, Queue *pq);
/* 操作: 清空队列 */
/* 前提条件: pq 指向之前被初始化的队列 */
/* 后置条件: 队列被清空 */
void EmptyTheQueue(Queue *pq);
#endif
接口实现:
/* queue.c -- Queue类型的实现 */
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
/* 局部函数 */
static void CopyToNode(Item item, Node *pn);
static void CopyToItem(Node *pn, Item *pi);
void InitializeQueue(Queue *pq) {
pq->front = pq->rear = NULL;
pq->items = 0;
}
bool QueueIsFull(const Queue *pq) {
return pq->items == MAXQUEUE;
}
bool QueueIsEmpty(const Queue *pq) {
return pq->items == 0;
}
int QueueItemCount(const Queue *pq) {
return pq->items;
}
bool EnQueue(Item item, Queue *pq) {
Node *pnew;
if (QueueIsFull(pq))
return false;
pnew = (Node*) malloc(sizeof(Node));
if (pnew == NULL) {
fprintf(stderr, "Unable to allocate memory!\n");
exit(1);
}
CopyToNode(item, pnew);
pnew->next = NULL;
if (QueueIsEmpty(pq))
pq->front = pnew; /* 项位于队列的首端 */
else
pq->rear->next = pnew; /* 链接到队列的尾端 */
pq->rear = pnew; /* 记录队列尾端的位置 */
pq->items++; /* 队列项数加1 */
return true;
}
bool DeQueue(Item *pitem, Queue *pq) {
Node *pt;
if (QueueIsEmpty(pq))
return false;
CopyToItem(pq->front, pitem);
pt = pq->front;
pq->front = pq->front->next;
free(pt);
pq->items--;
if (pq->items == 0)
pq->rear = NULL;
return true;
}
/* 清空队列 */
void EmptyTheQueue(Queue *pq) {
Item dummy;
while (!QueueIsEmpty(pq))
DeQueue(&dummy, pq);
}
/* 局部函数 */
static void CopyToNode(Item item, Node *pn) {
pn->item = item;
}
static void CopyToItem(Node *pn, Item *pi) {
*pi = pn->item;
}
运用:
/* use_q.c */
#include <stdio.h>
#include "queue.h" /* 定义Queue、Item */
int main(void) {
Queue line;
Item temp;
char ch;
InitializeQueue(&line);
puts("Testing the Queue interface. Type a to add a value,");
puts("type d to delete a value, and type q to quit.");
while ((ch = getchar()) != 'q') {
if (ch != 'a' && ch != 'd') /* 忽略其他输出 */
continue;
if (ch == 'a') {
printf("Integer to add: ");
scanf("%d", &temp);
if (!QueueIsFull(&line)) {
printf("Putting %d into queue\n", temp);
EnQueue(temp, &line);
} else
puts("Queue is full!");
} else {
if (QueueIsEmpty(&line))
puts("Nothing to delete!");
else {
DeQueue(&temp, &line);
printf("Removing %d from queue\n", temp);
}
}
printf("%d items in queue\n", QueueItemCount(&line));
puts("Type a to add, d to delete, q to quit:");
}
EmptyTheQueue(&line);
puts("Bye!");
return 0;
}
Testing the Queue interface. Type a to add a value,
type d to delete a value, and type q to quit.
|a
Integer to add: |40
Putting 40 into queue
1 items in queue
Type a to add, d to delete, q to quit:
|a
Integer to add: |20
Putting 20 into queue
2 items in queue
Type a to add, d to delete, q to quit:
|a
Integer to add: |55
Putting 55 into queue
3 items in queue
Type a to add, d to delete, q to quit:
|d
Removing 40 from queue
2 items in queue
Type a to add, d to delete, q to quit:
|d
Removing 20 from queue
1 items in queue
Type a to add, d to delete, q to quit:
|d
Removing 55 from queue
0 items in queue
Type a to add, d to delete, q to quit:
|d
Nothing to delete!
0 items in queue
Type a to add, d to delete, q to quit:
|q
Bye!
最后给出一个具体的应用实例:
许多现实生活的情形都涉及队列。例如,在银行或超市的顾客队列、机场的飞机队列、多任务计算机系统中的任务队列等。我们可以用队列包来模拟这些情形。
假设Sigmund Landers在商业街设置了一个提供建议的摊位。顾客可以购买1分钟、2分钟或3分钟的建议。为确保交通畅通,商业街规定每个摊位前排队等待的顾客最多为10人(相当于程序中的最大队列长度)。假设顾客都是随机出现的,并且他们花在咨询上的时间也是随机选择的(1分钟、2分钟、3分钟)。那么 Sigmund 平均每小时要接待多少名顾客?每位顾客平均要花多长时间?排队等待的顾客平均有多少人?队列模拟能回答类似的问题。
首先,要确定在队列中放什么。可以根据顾客加入队列的时间和顾客咨询时花费的时间来描述每一位顾客。因此,可以这样定义Item类型。
typedef struct item {
long arrive; /* 一位顾客加入队列的时间 */
int processtime; /* 该顾客咨询时花费的时间 */
} Item;
要用队列包来处理这个结构,必须用typedef定义的Item替换上一个示例的int类型。这样做就不用担心队列的具体工作机制,可以集中精力分析实际问题,即模拟咨询Sigmund的顾客队列。
这里有一种方法,让时间以1分钟为单位递增。每递增1分钟,就检查是否有新顾客到来。如果有一位顾客且队列未满,将该顾客添加到队列中。这涉及把顾客到来的时间和顾客所需的咨询时间记录在Item类型的结构中,然后在队列中添加该项。然而,如果队列已满,就让这位顾客离开。为了做统计,要记录顾客的总数和被拒顾客(队列已满不能加入队列的人)的总数。
接下来,处理队列的首端。也就是说,如果队列不为空且前面的顾客没有在咨询 Sigmund,则删除队列首端的项。记住,该项中储存着这位顾客加入队列的时间,把该时间与当前时间作比较,就可得出该顾客在队列中等待的时间。该项还储存着这位顾客需要咨询的分钟数,即还要咨询 Sigmund多长时间。因此还要用一个变量储存这个时长。如果Sigmund 正忙,则不用让任何人离开队列。尽管如此,记录等待时间的变量应该递减1。
注意,时间的表示比较粗糙(1分钟),所以一小时最多60位顾客。下面是一些变量和函数的含义。
min_per_cus 是顾客到达的平均间隔时间。
newcustomer() 使用C的rand()函数确定在特定时间内是否有顾客到来。
turnaways 是被拒绝的顾客数量。
customers 是加入队列的顾客数量。
temp 是表示新顾客的Item类型变量。
customertime() 设置temp结构中的arrive和processtime成员。
wait_time 是Sigmund完成当前顾客的咨询还需多长时间。
line_wait 是到目前为止队列中所有顾客的等待总时间。
served 是咨询过Sigmund的顾客数量。
sum_line 是到目前为止统计的队列长度。
使用标准函数rand()、srand()和 time()来产生随机数。
下面程序清单演示了模拟商业街咨询摊位队列的完整代码:
// mall.c -- 使用 Queue 接口
// 和 queue.c 一起编译
#include <stdio.h>
#include <stdlib.h> // 提供 rand() 和 srand() 的原型
#include <time.h> // 提供 time() 的原型
#include "queue.h" // 更改 Item 的 typedef
#define MIN_PER_HR 60.0
bool newcustomer(double x); // 是否有新顾客到来?
Item customertime(long when); // 设置顾客参数
int main(void) {
Queue line;
Item temp; // 新的顾客数据
int hours; // 模拟的小时数
int perhour; // 每小时平均多少位顾客
long cycle, cyclelimit; // 循环计数器、计数器的上限
long turnaways = 0; // 因队列已满被拒的顾客数量
long customers = 0; // 加入队列的顾客数量
long served = 0; // 在模拟期间咨询过Sigmund的顾客数量
long sum_line = 0; // 累计的队列总长
int wait_time = 0; // 从当前到Sigmund空闲所需的时间
double min_per_cust; // 顾客到来的平均时间
long line_wait = 0; // 队列累计的等待时间
InitializeQueue(&line);
srand((unsigned int) time(0)); // rand() 随机初始化
puts("Case Study: Sigmund Lander's Advice Booth");
puts("Enter the number of simulation hours:");
scanf("%d", &hours);
cyclelimit = MIN_PER_HR * hours;
puts("Enter the average number of customers per hour:");
scanf("%d", &perhour);
min_per_cust = MIN_PER_HR / perhour;
for (cycle = 0; cycle < cyclelimit; cycle++) {
if (newcustomer(min_per_cust)) {
if (QueueIsFull(&line))
turnaways++;
else {
customers++;
temp = customertime(cycle);
EnQueue(temp, &line);
}
}
if (wait_time <= 0 && !QueueIsEmpty(&line)) {
DeQueue(&temp, &line);
wait_time = temp.processtime;
line_wait += cycle - temp.arrive;
served++;
}
if (wait_time > 0)
wait_time--;
sum_line += QueueItemCount(&line);
}
if (customers > 0) {
printf("customers accepted: %ld\n", customers);
printf(" customers served: %ld\n", served);
printf(" turnaways: %ld\n", turnaways);
printf("average queue size: %.2f\n", (double) sum_line / cyclelimit);
printf(" average wait time: %.2f minutes\n", (double) line_wait / served);
} else
puts("No customers!");
EmptyTheQueue(&line);
puts("Bye!");
return 0;
}
// x是顾客到来的平均时间(单位:分钟)
// 如果1分钟内有顾客到来,则返回true
bool newcustomer(double x) {
if (rand() * x / RAND_MAX < 1)
return true;
else
return false;
}
// when是顾客到来的时间
// 该函数返回一个Item结构,该顾客到达的时间设置为when,
// 咨询时间设置为1~3的随机值
Item customertime(long when) {
Item cust;
cust.processtime = rand() % 3 + 1;
cust.arrive = when;
return cust;
}
该程序允许用户指定模拟运行的小时数和每小时平均有多少位顾客。模拟时间较长得出的值较为平均,模拟时间较短得出的值随时间的变化而随机变化。下面的运行示例解释了这一点(先保持每小时的顾客平均数量不变)。注意,在模拟80小时和800小时的情况下,平均队伍长度和等待时间基本相同。但是,在模拟 1 小时的情况下这两个量差别很大,而且与长时间模拟的情况差别也很大。这是因为小数量的统计样本往往更容易受相对变化的影响。
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|80
Enter the average number of customers per hour:
|20
customers accepted: 1633
customers served: 1633
turnaways: 0
average queue size: 0.46
average wait time: 1.35 minutes
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|800
Enter the average number of customers per hour:
|20
customers accepted: 16020
customers served: 16019
turnaways: 0
average queue size: 0.44
average wait time: 1.32 minutes
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|1
Enter the average number of customers per hour:
|20
customers accepted: 20
customers served: 20
turnaways: 0
average queue size: 0.23
average wait time: 0.70 minutes
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|1
Enter the average number of customers per hour:
|20
customers accepted: 22
customers served: 22
turnaways: 0
average queue size: 0.75
average wait time: 2.05 minutes
然后保持模拟的时间不变,改变每小时的顾客平均数量:
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|80
Enter the average number of customers per hour:
|25
customers accepted: 1960
customers served: 1959
turnaways: 3
average queue size: 1.43
average wait time: 3.50 minutes
Case Study: Sigmund Lander's Advice Booth
Enter the number of simulation hours:
|80
Enter the average number of customers per hour:
|30
customers accepted: 2376
customers served: 2373
turnaways: 94
average queue size: 5.85
average wait time: 11.83 minutes
注意,随着每小时顾客平均数量的增加,顾客的平均等待时间迅速增加。在每小时20位顾客(80小时模拟时间)的情况下,每位顾客的平均等待时间是1.35分钟;在每小时25位顾客的情况下,平均等待时间增加至3.50分钟;在每小时30位顾客的情况下,该数值攀升至11.83分钟。而且,这3种情况下被拒顾客分别从0位增加至3位最后陡增至94位。Sigmund可以根据程序模拟的结果决定是否要增加一个摊位。