(十七)深入学习 -- 2. 抽象数据类型

本文介绍了抽象数据类型(ADT)的概念,以队列为实例详细阐述了ADT的定义和基本操作,包括创建、删除、入队、出队和获取队列长度。在C语言中,通过void*类型实现队列的通用性。队列的两种实现方式被讨论,一种是基于数组的实现,另一种是使用队头和队尾下标的环形缓冲区实现,后者在Dequeue操作中更具效率。此外,还提到了链表作为队列表示的另一种选择。
摘要由CSDN通过智能技术生成

2. 抽象数据类型

按照它的行为而不是它的表示方式定义的类型叫做抽象数据类型(abstract datatype),其缩写为ADT。


2.1 队列抽象

在程序设计中,模拟一组人排队等候的行为方式的结构叫做队列(queue) 。

基本操作包括:

  • 创建一个新的等候队列。

  • 删除一个已存在的等候队列。

  • 在等候队列末尾加入一个顾客。在程序设计中,在队尾加入一个新项的操作,叫做入队(enqueue)操作。

  • 从队首删除一个顾客。在程序设计中,删除队首项的操作叫做出队(dequeue)操作。

  • 确定一个队列里有多少顾客。


2.2 以队列抽象表示类型

就队列软件包而言,它的职责是在客户提供一个数据时,它该怎样进行入队操作。

当稍后执行一个出队操作时,队列软件包必须能把这个数据完整精确地送回给客户。

因此,为了使队列软件包尽可能通用,就应该让你的客户来控制存储在队列中的数据类型。

在C语言中,最好是用void*这个类型,它能匹配任何指针类型。

如果使用void*,那么队列软件包的客户就能对任何指针类型的数据进行出队、入队操作了。


问题是怎样表示一个等候队列本身。

与顾客不同,队列是实现的一个属性。队列软件包要负责在队尾使新数据入队,在队首使头一个数据出队。

要定义抽象队列类型queueADT,实现部分控制基本的表示。

在C语言中,可以在接口中定义一个类型,使它的内部表示对客户来说不可见。

要做到这一点,必须在接口中包含一个抽象类型的定义,其形式如下面的语法框所示。

例如,为了把queueADT定义为一个抽象类型,接口必须包含下列定义:

typedef struct queueCDT *queueADT

C语言的语法认为, 这一行把queueADT类型定义为指向名为queueCDT的结构的指针。

而queueCDT还没有被定义。因为指针总是有同样的大小,所以C编译器允许你使用结构指针,即使它还不知道结构的任何细节。

一个没有被定义的结构称做不完全类型(incomplete type)。

在实现中,通过写这个结构的定义来完成该类型,如下面的句法框所示。该定义包括用于标识该不完全结构的名称,它被称作结构标记(structure tag)。

类型nameCDT是与接口中定义的抽象类型nameADT相关的具体数据类型。


2.3 queue.h接口

queue.h接口输出类型queueADT,以及该类型的五个操作:
NewQueue、FreeQueue、Enqueue、Dequeue、QueueLength。


2.4 实现队列抽象

一旦完成了接口的设计,下一步就是实现队列软件包。

作为实现的一部分,需要定义等候队列本身的基本表示方式。

最直接的表示是一个数组和一个指示其有效长度的整数。

因此,具体的类型定义如下所示:

struct queueCDT {
    void *array[MaxQueueSize];
    int len;
}

其中MaxQueueSize是一个常量,它指明了在一个队列中最多能保存的元素个数。


要实现函数NewQueue,要做的是为具体的结构分配空间并将队列长度初始化为0。

实现代码如下:

queueADT NewQueue(void){
    queueADT queue;
    queue = New(queueADT);
    queue->len=0;
    return queue
}

当把一个新值加入队列时,它必须被插入队尾,队列长度增1。

这些操作以及一个测试队列长度是否超过最大队列长度的操作,组成了Enqueue的实现代码,如下所示:

void Enqueue(ququeADT, *obj) {
	if (queue->len == MaxQueueSize) {
		Error("Enqueue called on a full queue");
	} 
	queue->array[queue->len++] = obj;
}

例如,假设已经执行下列语句新建了一个队列:

queue = NewQueue();

这个语句的作用是创建一个空队列,其结构如下:

这时如果调用

Enqueue(queue, “A");

字符串"A"(这是一个指针, 因此与void*兼容) 存放在数组的第0个元素中。但其副作用是++运算符将队列长度加1,使队列有如下状态:

然后如果执行以下语句:

Enqueue(queue, "B");
Enqueue(queue, "C");

客户B和C被插入数组的后两个位置,如下图所示:

void *Dequeue(queueADT queue) {
	void *result;
	int i;

	if (queue->len == 0) {
		Error("Dequeue of empty queue");
	}
	result = queue->array[0];
	for (i=1; i < queue->len; i++) {
		queue->array[i - 1] = queue -> array[i];
	}
	queue->len--;
	return result;
}

2.5 队列抽象的另一种实现方法

上一节开发的队列软件包的实现不是唯一的选择。尽管它很简单,但它有以下两个局限性:

(1) 调用Dequeue效率不高。每次调用Dequeue后,该实现需要把队列中剩下的项都向数组头的方向移动。

(2) 队列的最大长度是固定的, 由常量Max Queue size指定。


可以用跟踪两个下标位置的方法解决:

等候队列中的第一项的下标和下一个入队的项将存入的位置的下标值。称这两个位置为队头(head)和
队尾(tail)。

包含这两个下标的具体的类型定义如下:

struct queueCDT {
	void *array[MaxQueueSize];
	int head;
	int tail;
}

tail下标值指出下一个Enqueue操作将数据存储到哪个数组元素中,因此它指示的是一个还没有使用的数组位置,head下标值指出下一个Dequeue操作要返回的元素。

当队列为空时,head下标值等于tail下标值,如下图所示:

在队列中新增一项,涉及到将数据存在tail处,并将tail值加1。

因此,对一个空队列执行语句

Equeue(queue, "A");
Equeue(queue, "B");

将产生下图所示的队列结构:

Dequeue函数返回的是处于head位置的值,然后使head值加1,使它指向下一个位置。

这样,如果继续调用Dequeue(queue),其内部数据结构如下图所示:

使用这种策略,就不需要在Dequeue操作中移动数组中的任何元素。

唯-的问题是很快会达到数组末尾而没有空间再存放新的项了。其状态如下图所示:

如果这时客户K来了会怎样呢?数组末尾没有多余的位置了。

因为前四个位置空出来了。如果小心使用下标的话,在实现队列软件包时,可以使Enqueue操作重新使用Dequeue操作释放的空间。

在当前的例子中,K可以进入数组元素0处,使队列处于如下状态:

在这个设计中,数组末尾“卷回”到开头,这样数组更像一个环而非一个线性表了,如下图中的箭头所示:

因为数组开头与末尾在概念上是互相连接起来的,所以程序员称这种表示为环缓冲区(ring buffer)。环缓冲区常常用于实现队列。


除了用数组保存队列外,还可以设计一种队列表示方法,其中每个队列项包含一个指向下一个队列项的指针。

例如,一个队列包含A、B、C三项,如下图所示:

因为这个队列的每个项包含一个指向下一个项的指针,所以这种结构被称为链表(linked list)。

队列本身的具体数据类型包括指向链表中第一个和最后一个元素的指针。





参考

《C语言的科学和艺术》 —— 17 深入学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值