C语言的队列ADT

在C语言中使用抽象数据类型方法编程包含以下3个步骤。
1.以抽象、通用的方式描述一个类型,包括该类型的操作。
2.设计一个函数接口表示这个新类型。
3.编写具体代码实现这个接口。
前面已经把这种方法应用到简单链表中。现在,把这种方法应用于更复杂的数据类型:

1 定义队列抽象数据类型

队列(queue)是具有两个特殊属性的链表。第一,新项只能添加到链表的末尾。从这方面看,队列与简单链表类似。第二,只能从链表的开头移除项。可以把队列想象成排队买票的人。你从队尾加入队列,买完票后从队首离开。队列是一种“先进先出”(first in , first out,缩写为FIFO)的数据形式,就像排队买票的队伍一样(前提是没有人插队)。接下来,我们建立一个非正式的抽象定义:

2 定义一个接口

接口定义放在queue.h文件中。我们使用C的typedef工具创建两个类型名:Item和Queue。相应结构的具体实现应该是queue.h文件的一部分,但是从概念上来看,应该在实现阶段才设计结构。现在,只是假定已经定义了这些类型,着重考虑函数的原型。 首先,考虑初始化。这涉及改变Queue类型,所以该函数应该以Queue的地址作为参数:

void InitializeQueue (Queue * pq);

接下来,确定队列是否为空或已满的函数应返回真或假值。这里,假设C99的stdbool.h头文件可用。如果该文件不可用,可以使用int类型或自己定义bool类型。由于该函数不更改队列,所以接受Queue类型的参数。但是,传递Queue的地址更快,更节省内存,这取决于Queue类型的对象大小。这次我们尝试这种方法。这样做的好处是,所有的函数都以地址作为参数,而不像List示例那样。为了表明这些函数不更改队列,可以且应该使用const限定符:

bool QueueIsFull(const Queue * pq);
bool QueueIsEmpty (const Queue * pq);

指针pq指向Queue数据对象,不能通过pq这个代理更改数据。可以定义一个类似该函数的原型,返回队列的项数:

int QueueItemCount(const Queue * pq);

在队列末尾添加项涉及标识项和队列。这次要更改队列,所以有必要(而不是可选)使用指针。该函数的返回类型可以是void,或者通过返回值来表示是否成功添加项。我们采用后者:

bool EnQueue(Item item, Queue * pq);

最后,删除项有多种方法。如果把项定义为结构或一种基本类型,可以通过函数返回待删除的项。函数的参数可以是Queue类型或指向Queue的指针。因此,可能是下面这样的原型:

Item DeQueue(Queue q);

然而,下面的原型会更合适一些:

bool DeQueue(Item * pitem, Queue * pq);

从队列中待删除的项存储在pitem指针指向的位置,函数的返回值表明是否删除成功。清空队列的函数所需的唯一参数是队列的地址,可以使用下面的函数原型:

void EmptyTheQueue(Queue * pq);

3 实现接口数据表示

第一步是确定在队列中使用何种C数据形式。有可能是数组。数组的优点是方便使用,而且向数组的末尾添加项很简单。问题是如何从队列的开头删除项。类比于排队买票的队列,从队列的开头删除一个项包括拷贝数组首元素的值和把数组剩余各项依次向前移动一个位置。编程实现这个过程很简单,但是会浪费大量的计算机时间。


C语言的队列ADT

Using an array as a queue

第二种解决数组队列删除问题的方法是改变队列首端的位置,其余元素不动。


C语言的队列ADT

Redefining the front element

解决这种问题的一个好方法是,使队列成为环形。这意味着把数组的首尾相连,即数组的首元素紧跟在最后一个元素后面。这样,当到达数组末尾时,如果首元素空出,就可以把新添加的项存储到这些空出的元素中(见图17.8)。可以想象在一张条形的纸上画出数组,然后把数组的首尾粘起来形成一个环。当然,要做一些标记,以免尾端超过首端。


C语言的队列ADT

A circular queue.


另一种方法是使用链表。使用链表的好处是删除首项时不必移动其余元素,只需重置头指针指向新的首元素即可。由于我们已经讨论过链表,所以采用这个方案。我们用一个整数队列开始测试:

typedef int Item;

链表由节点组成,所以,下一步是定义节点:

typedef struct node
{
    Item item;
    struct node * next;
} Node;

​

对队列而言,要保存首尾项,这可以使用指针来完成。另外,可以用一个计数器来记录队列中的项数。因此,该结构应由两个指针成员和一个int类型的成员构成:

typedef struct queue
{
    Node * front;   /* pointer to front of queue */
    Node * rear;    /* pointer to rear of queue  */
    int items;      /* number of items in queue  */
} Queue;

注意,Queue是一个内含3个成员的结构,所以用指向队列的指针作为参数比直接用队列作为参数节约了时间和空间。 接下来,考虑队列的大小。对链表而言,其大小受限于可用的内存量,因此链表不要太大。例如,可能使用一个队列模拟飞机等待在机场着陆。如果等待的飞机数量太多,新到的飞机就应该改到其他机场降落。我们把队列的最大长度设置为10。程序queue.h包含了队列接口的原型和定义。Item类型留给用户定义。使用该接口时,可以根据特定的程序插入合适的定义。

The queue.h Interface Header File

/* queue.h -- interface for a queue */
#ifndef _QUEUE_H_
#define _QUEUE_H_
#include 
​
/* INSERT ITEM TYPE HERE */
/* FOR EXAMPLE, */
typedef int Item;  // for use_q.c
/* OR 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;  /* pointer to front of queue  */
    Node * rear;   /* pointer to rear of queue   */
    int items;     /* number of items in queue   */
} Queue;
​
/* operation:        initialize the queue                       */
/* precondition:     pq points to a queue                       */
/* postcondition:    queue is initialized to being empty        */
void InitializeQueue(Queue * pq);
​
/* operation:        check if queue is full                     */
/* precondition:     pq points to previously initialized queue  */
/* postcondition:   returns True if queue is full, else False   */
bool QueueIsFull(const Queue * pq);
​
/* operation:        check if queue is empty                    */
/* precondition:     pq points to previously initialized queue  */
/* postcondition:    returns True if queue is empty, else False */
bool QueueIsEmpty(const Queue *pq);
​
/* operation:        determine number of items in queue         */
/* precondition:     pq points to previously initialized queue  */
/* postcondition:    returns number of items in queue           */
int QueueItemCount(const Queue * pq);
​
/* operation:        add item to rear of queue                  */
/* precondition:     pq points to previously initialized queue  */
/*                   item is to be placed at rear of queue      */
/* postcondition:    if queue is not empty, item is placed at   */
/*                   rear of queue and function returns         */
/*                   True; otherwise, queue is unchanged and    */
/*                   function returns False                     */
bool EnQueue(Item item, Queue * pq);
​
/* operation:        remove item from front of queue            */
/* precondition:     pq points to previously initialized queue  */
/* postcondition:    if queue is not empty, item at head of     */
/*                   queue is copied to *pitem and deleted from */
/*                   queue, and function returns True; if the   */
/*                   operation empties the queue, the queue is  */
/*                   reset to empty. If the queue is empty to   */
/*                   begin with, queue is unchanged and the     */
/*                   function returns False                     */
bool DeQueue(Item *pitem, Queue * pq);
​
/* operation:        empty the queue                            */
/* precondition:     pq points to previously initialized queue  */
/* postconditions:   the queue is empty                         */
void EmptyTheQueue(Queue * pq);
​
#endif

实现接口函数

接下来,我们编写接口代码。首先,初始化队列为空,这里“空”的意思是把指向队列首项和尾项的指针设置为NULL,并把项数(items成员)设置为0:

void InitializeQueue(Queue * pq)
{
    pq->front = pq->rear = NULL;
    pq->items = 0;
}

这样,通过检查items的值可以很方便地了解到队列是否已满、是否为空和确定队列的项数:

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;
}

把项添加到队列中,包括以下几个步骤:
(1)创建一个新节点;
(2)把项拷贝到节点中;
(3)设置节点的next指针为NULL,表明该节点是最后一个节点;
(4)设置当前尾节点的next指针指向新节点,把新节点链接到队列中;
(5)把rear指针指向新节点,以便找到最后的节点;
(6)项数加1。

函数还要处理两种特殊情况。第一种情况,如果队列为空,应该把front指针设置为指向新节点。因为如果队列中只有一个节点,那么这个节点既是首节点也是尾节点。第二种情况是,如果函数不能为节点分配所需内存,则必须执行一些动作。因为大多数情况下我们都使用小型队列,这种情况很少发生,所以,如果程序运行的内存不足,我们只是通过函数终止程序。EnQueue()的代码如下:

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;           /* item goes to front     */
    else
        pq->rear->next = pnew;      /* link at end of queue   */
    pq->rear = pnew;                /* record location of end */
    pq->items++;                    /* one more item in queue */
​
    return true;
}

CopyToNode()函数是静态函数,用于把项拷贝到节点中:

static void CopyToNode(Item item, Node * pn)
{
    pn->item = item;
}

从队列的首端删除项,涉及以下几个步骤:
(1)把项拷贝到给定的变量中;
(2)释放空出的节点使用的内存空间;
(3)重置首指针指向队列中的下一个项;
(4)如果删除最后一项,把首指针和尾指针都重置为NULL;
(5)项数减1。 下面的代码完成了这些步骤:

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;
}

关于指针要注意两点。第一,删除最后一项时,代码中并未显式设置front指针为NULL,因为已经设置front指针指向被删除节点的next指针。如果该节点是最后一个节点,那么它的next指针就为NULL。第二,代码使用临时指针(pt)存储待删除节点的位置。因为指向首节点的正式指针(pt->front)被重置为指向下一个节点,所以如果没有临时指针,程序就不知道该释放哪块内存。 我们使用DeQueue()函数清空队列。循环调用DeQueue()函数直到队列为空:

void EmptyTheQueue(Queue * pq)
{
    Item dummy;
    while (!QueueIsEmpty(pq))
        DeQueue(&dummy, pq);
}

注意 保持纯正的ADT
定义ADT接口后,应该只使用接口函数处理数据类型。例如,DeQueue()依赖EnQueue()函数来正确设置指针和把rear节点的next指针设置为NULL。如果在一个使用ADT的程序中,决定直接操控队列的某些部分,有可能破坏接口包中函数之间的协作关系。


程序queue.c演示了该接口中的所有函数,包括EnQueue()函数中用到的CopyToNode()函数。

/* queue.c -- the Queue type implementation*/
#include 
#include 
#include "queue.h"
​
/* local functions */
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;           /* item goes to front     */
    else
        pq->rear->next = pnew;      /* link at end of queue   */
    pq->rear = pnew;                /* record location of end */
    pq->items++;                    /* one more item in queue */
​
    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;
}
​
/* empty the queue                */
void EmptyTheQueue(Queue * pq)
{
    Item dummy;
    while (!QueueIsEmpty(pq))
        DeQueue(&dummy, pq);
}
​
/* Local functions                 */
​
static void CopyToNode(Item item, Node * pn)
{
    pn->item = item;
}
​
static void CopyToItem(Node * pn, Item * pi)
{
    *pi = pn->item;
}
​

4 测试队列

在重要程序中使用一个新的设计(如,队列包)之前,应该先测试该设计。测试的一种方法是,编写一个小程序。这样的程序称为驱动程序(driver),其唯一的用途是进行测试。例如,程序useq.c使用一个添加和删除整数的队列。在运行该程序之前,要确保queue.h中包含下面这行代码:

The useq.c Program

/* use_q.c -- driver testing the Queue interface */
/* compile with queue.c                          */
#include 
#include "queue.h"  /* defines 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')   /* ignore other input */
            continue;
        if ( ch == 'a')
        {
            printf("Integer to add: ");
            scanf("%d", &temp);
            if (!QueueIsFull(&line))
            {
                printf("Putting %d into queuen", 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 queuen", temp);
            }
        }
        printf("%d items in queuen", 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!
展开阅读全文

页面更新:2024-05-19

标签:队列   空出   末尾   数组   节点   原型   指针   函数   元素   接口   定义   参数   语言   类型   代码

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top