第
3 
章栈、队列和数组

教学重点

教学难点

教学目标

教学提示

栈和队列的操作特性;栈和队列基本操作的实现;特殊矩阵的压缩存储
方法及寻址方法

循环队列的存储方法;循环队列中队空和队满的判定条件

(1)解释栈的定义及操作特性,根据入栈序列判断出栈序列是否合法;
(2)描述顺序栈和链栈存储方法,针对实际问题画出存储示意图;
(3)实现顺序栈和链栈的基本操作,比较两种存储结构的优缺点;
(4)在比较队列和栈的基础上,解释队列的定义及操作特性;
(5)辨明循环队列是顺序队列的改进,画出顺序队列和循环队列的存储
示意图,描述循环队列存储方法;
(6)归纳循环队列的队空/队满判定条件,实现循环队列的基本操作;
(7)描述链队列存储方法,画出存储示意图,实现链队列的基本操作;
(8)解释(多维)数组的定义及存储方法,归纳寻址公式;
(9)描述对称矩阵、对角矩阵等特殊矩阵的压缩存储方法,归纳寻址
公式;
(10)描述稀疏矩阵的顺序存储和链表存储方法,画出存储示意图
对于栈和队列要抓住一条明线:逻辑结构
→ 
存储结构
→ 
算法实现
→ 
时
间性能,从栈和队列的定义入手,在与线性表的定义和操作比较的基础
上,得出栈和队列的操作特性和存储方法。注意将顺序栈与顺序表、链
栈和单链表、顺序队列和循环队列、链队列和链表等进行比较。注意不
要直接给出循环队列存储方法,要以提出问题、解决问题的方式逐渐引
入,一方面训练学生的逻辑思维能力,另一方面深刻理解存储结构的
含义。
对于(多维)数组,要把握数组的广义线性特征,基于数组的逻辑结构和
操作特点,得出数组的顺序存储方法。对于特殊矩阵的压缩存储,要根
据矩阵的特点设计压缩存储方法,强调特殊矩阵在压缩存储下仍然具
有随机存取特性,归纳寻址公式。
本章的算法非常简单,但要求熟练掌握,在树结构和图结构中会使用栈
和队列作为辅助数据结构实现遍历等复杂操作


64
数据结构———从概念到
C 
实现(第2版) 

3.引言
1 

课件3-
1 
栈和队列是两种常用的数据结构,广泛应用在操作系统、编译程序等各种软件系统
中。在实际问题的处理过程中,有些数据具有后到先处理或先到先处理的特点,请看下面
几个例子。
例3-
1 
括号匹配问题。C语言对于算术表达式中括号的配对原则是:右括号“)”与
其前面最近的尚未配对的左括号“(”配对。如何判断给定表达式中所含括号是否正确配
对呢? 如果顺序扫描表达式,当扫描到右括号“)”时,需要查找已经扫描过的最后一个尚
未配对的左括号“(”,对于“(”具有最后扫描到最先配对的特点。那么,如何保存已经扫描
过的尚未配对的左括号“(”,并对其实施配对操作呢? 
例3-
2 
函数的嵌套调用。函数的嵌套调用是在函数的执行过程中再调用其他函
数,如图3-1所示,在函数A尚未执行结束时调用函数B,在函数B尚未执行结束时又调
用函数C,那么,当函数C执行结束时,应该返回到什么位置呢? 为保证函数嵌套调用的
正确执行,系统自动设立了工作栈保存函数的调用次序。那么,系统工作栈是如何保证调
用次序的正确性呢? 


图3-
1 
函数的嵌套调用

例3-
3 
银行排队问题。在需要顺序操作但人群众多的场合,排队是现代文明的一
种体现。例如,储户到银行办理个人储蓄业务,需要领取一张排队单,在排队单上打印了
储户的顺序号以及前面的人数,储户只需坐在椅子上等待,窗口会按照先来先服务的原则
顺次叫号。那么,如何实现这种先来先服务的功能呢? 

例3-
4 
打印缓冲区。在计算机系统中,经常会遇到两个设备在处理数据时速度不
匹配的问题。例如,将计算机中的数据传送到打印机上打印输出,显然,打印机的打印速
度远远小于计算机处理数据的速度,如果将数据直接送到打印机上,就会导致计算机处理
完一批数据就要等待打印输出。为了提高计算机的效率,通常设置一个缓冲区,计算机将
处理完的数据送到打印缓冲区中,打印机从打印缓冲区取出数据进行打印。由于打印机
的速度比较慢,来不及打印的数据就在缓冲区排队等待,为了保证正确打印,这个缓冲区
必须按照先来先服务的原则,从而解决了计算机处理速度与打印机输出速度不匹配的
矛盾。

例3-
5 
八皇后问题。八皇后问题是数学家高斯于1850 年提出的。问题是在8×8 
的棋盘上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列


第3 章 栈、队列和数组  65 
或同一斜线上。八皇后问题首先要解决的问题就是如何表示棋盘? 如何获得每个皇后的
位置信息进而判断是否互相攻击? 
3.2 栈
3.2.1 栈的逻辑结构
1.栈的定义 
栈(stack)是限定仅在表的一端进行插入和删除操作的线性表,允许插入和删除的一
图3-2 栈的示意图
端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为
空栈。如
图3-2所示,栈中有3个元素,插入元素(也称为入栈、
进栈、压栈)的顺序是a1、a2、a3,当需要删除元素(也称为出
栈、弹栈)时只能删除a3,换言之,任何时刻出栈的元素都只
能是栈顶元素,即最后入栈者最先出栈,所以栈中元素除了
具有线性关系外,还具有后进先出(lastinfirstout)① 的
特性。在
日常生活中,有很多栈的例子,例如,一叠摞在一起的
盘子,要从这叠盘子中取出或放入一个盘子,只有在其顶部操作才是最方便的;早在计算
机出现之前,会计就使用栈来记账;再如,火车扳道站、单车道死胡同等。
2.栈的抽象数据类型定义
虽然对插入和删除操作的位置限制减少了栈操作的灵活性,但同时也使得栈的操作
更有效更容易实现。其抽象数据类型定义为 
ADT Stack 
DataModel 
栈中元素具有后进先出特性,相邻元素具有前驱和后继关系
Operation 
InitStack 
输入: 无 
功能: 栈的初始化 
输出: 一个空栈 
DestroyStack 
输入: 无 
功能: 栈的销毁 
输出: 释放栈的存储空间
① 需要注意的是,栈只是对线性表的插入和删除操作的位置进行了限制,并没有限定插入和删除操作进行的时
间,也就是说,出栈可随时进行,只要某个元素位于栈顶就可以出栈。例如,假定3个元素按a、b、c 的次序依次进栈, 
且每个元素只允许进一次栈,则所有元素都出栈后,可能的出栈序列有abc、acb、bac、bca、cba5种。
课件3-2

66 数据结构———从概念到C 实现(第2 版) 
Push 
输入: 元素值x 
功能: 入栈操作,在栈顶插入一个元素x 
输出: 如果插入成功,栈顶增加了一个元素,否则返回插入失败信息 
Pop 
输入: 无 
功能: 出栈操作,删除栈顶元素 
输出: 如果删除成功,返回被删除的元素值,否则返回删除失败信息 
GetTop 
输入: 无 
功能: 取栈顶元素,读取当前的栈顶元素 
输出: 若栈不空,返回当前的栈顶元素值,否则返回取栈顶失败信息 
Empty 
输入: 无 
功能: 判空操作,判断栈是否为空 
输出: 如果栈为空,返回1,否则返回0 
endADT 
3.2.2 栈的顺序存储结构及实现
1.顺序栈的存储结构定义 
栈的顺序存储结构称为顺序栈(sequentialstack)。
顺序栈本质上是顺序表的简化,唯一需要确定的是用数组的哪一端表示栈底。通常
把数组中下标为0的一端作为栈底,同时附设变量top指示栈顶元素在数组中的位置①。
设存储栈的数组长度为StackSize,则栈空时栈顶位置top=-1;栈满时栈顶位置top= 
StackSize-1。入栈时,栈顶位置top加1;出栈时,栈顶位置top减1。栈操作的示意图
如图3-3所示。
图3-3 栈操作的示意图
下面给出顺序栈的存储结构定义: 
① 在有些教材中,将top指向栈中第一个空闲位置,如果这样的话,空栈应该表示为top=0。

第3 章 栈、队列和数组  67 
#define StackSize 100 /*假定栈元素最多100 个*/ 
typedef int DataType; /*定义栈元素的数据类型,假设为int 型*/ 
typedef struct 
{ 
DataType data[StackSize]; /*存放栈元素的数组*/ 
int top; /*栈顶位置,栈顶元素在数组中的下标*/ 
} SeqStack; 
2.顺序栈的实现
根据栈的操作定义,容易写出顺序栈基本操作的算法,且时间复杂度均为O(1)。
(1)顺序栈的初始化。初始化一个空的顺序栈只需将栈顶位置top置为-1,算法的
C语言实现如下: 
void InitStack(SeqStack *S) 
{ 
S->top = -1; 
}
(2)顺序栈的销毁。顺序栈是静态存储分配,在顺序栈变量退出作用域时自动释放
顺序栈所占存储单元,因此,顺序栈无须销毁。
(3)入栈操作。在栈中插入元素x 只需将栈顶位置top加1,然后在top的位置填入
元素x。函数Push的返回值表示插入操作是否成功,算法的C语言实现如下: 
int Push(SeqStack *S,DataType x) 
{ 
if (S->top == StackSize-1) {printf("上溢错误,插入失败\n"); return 0;} 
S->data[++S->top]= x; return 1; 
}
(4)出栈操作。出栈操作只需取出栈顶元素,然后将栈顶位置top减1。函数Pop的返
回值表示删除操作是否成功,如果删除成功,被删元素通过指针参数ptr返回,算法的C语
言实现如下: 
int Pop(SeqStack *S,DataType *ptr) 
{ 
if (S->top == -1) {printf("下溢错误,删除失败\n"); return 0;} 
*ptr = S->data[S->top--]; return 1; 
}
(5)取栈顶元素。取栈顶元素只是将top位置的栈顶元素取出,并不修改栈顶位置。
算法的C语言实现如下:

68 数据结构———从概念到C 实现(第2 版) 
int GetTop(SeqStack *S,DataType *ptr) 
{ 
if (S->top == -1) {printf("下溢错误,取栈顶失败\n"); return 0;} 
*ptr = S->data[S->top]; return 1; 
}
(6)判空操作。顺序栈的判空操作只需判断top是否等于-1,算法的C语言实现
如下: 
int Empty(SeqStack *S) 
{ 
if (S->top == -1) return 1; /*栈空则返回1*/ 
else return 0; 
}
3.顺序栈的使用
在定义了顺序栈的存储结构SeqStack并实现了基本操作后,程序中就可以使用
SeqStack类型来定义变量,可以调用实现基本操作的函数来完成相应功能。范例程序
如下: 
#include <stdio.h> 
#include <stdlib.h> 
/*将顺序栈的存储结构定义和各个函数定义放到这里*/ 
int main( ) 
{ 
DataType x; 
SeqStack S; /*定义结构体变量S 为顺序栈类型*/ 
InitStack(&S); /*初始化顺序栈S*/ 
printf("对15 和10 执行入栈操作,"); 
Push(&S, 15); 
Push(&S, 10); 
if (GetTop(&S, &x) == 1) 
printf("当前栈顶元素为: %d\n", x); /*输出当前栈顶元素10*/ 
if (Pop(&S, &x) == 1) 
printf("执行一次出栈操作,删除元素: %d\n ", x); /*输出出栈元素10*/ 
if (GetTop(&S, &x) == 1) 
printf("当前栈顶元素为: %d\n", x); /*输出当前栈顶元素15*/ 
printf("请输入待入栈元素: "); 
scanf("%d", &x); 
Push(&S, x); 
if (Empty(&S) == 1) 
源代码3-1

第3 章 栈、队列和数组  69 
printf("栈为空\n"); 
else 
printf("栈非空\n"); /*栈有2 个元素,输出栈非空*/ 
return 0; 
} 
3.2.3 栈的链接存储结构及实现
1.链栈的存储结构定义 
栈的链接存储结构称为链栈(linkedstack),通常用单链表表示,其结点结构与单链
图3-4 链栈示意图
表的结点结构相同,请参见2.4.1节。因为只能在栈顶执行插
入和删除操作,显然以单链表的头部做栈顶是最方便的,通常
将链栈表示成如图3-4所示的形式。
2.链栈的实现
链栈的基本操作本质上是单链表基本操作的简化,由于插
入和删除操作仅在单链表的头部进行,因此,算法的时间复杂
度均为O(1)。
(1)链栈的初始化。空链栈只有头结点,算法如下: 
Node *InitStack( ) 
{ 
Node *top =(Node *)malloc(sizeof(Node); 
top->next = NULL; return top; 
}
(2)链栈的销毁。链栈是动态存储分配,在链栈变量退出作用域前要释放链栈的存
储空间。算法如下: 
void DestroyStack(Node *top) 
{ 
Node *p = top; 
while (top != NULL) /*依次释放链栈的每一个结点*/ 
{ 
top = top->next; 
free(p); 
p = top 
} 
}
(3)入栈操作。链栈的插入操作只需处理栈顶的情况,其操作示意图如图3-5所

70 数据结构———从概念到C 实现(第2 版) 
示,算法如下: 
void Push(Node *top, DataType x) 
{ 
Node *s =(Node *)malloc(sizeof(Node)); /*申请一个结点s*/ 
s->data = x; 
s->next = top->next; top->next = s; /*将结点s 插在栈顶*/ 
}
(4)出栈操作。链栈的删除操作只需处理栈顶的情况,其操作示意图如图3-6所示。
函数Pop的返回值表示出栈操作是否正确执行,如果出栈正确,被删元素通过指针参数
ptr返回,C语言实现如下: 
int Pop(Node *top, DataType *ptr) 
{ 
Node *p = top->next; 
if (top->next == NULL) {printf("下溢错误,删除失败\n"); return 0; } 
*ptr = p->data; /*存储栈顶元素*/ 
top->next = p->next; /*将栈顶结点摘链*/ 
free(p); 
return 1; 
} 
图3-5 链栈插入操作示意图 
图3-6 链栈删除操作示意图
(5)取栈顶元素。取栈顶元素只需返回栈顶指针top所指结点的数据域,C语言实
现如下: 
int GetTop(Node *top, DataType *ptr) 
{ 
if(top == NULL) {printf("下溢错误,取栈顶失败\n"); return 0; } 
*ptr = top->data; return 1; 
}
(6)判空操作。链栈的判空操作只需判断top是否等于NULL,C语言实现如下:

第3 章 栈、队列和数组  71 
int Empty(Node *top) 
{ 
if(top == NULL) return 1; /*栈空则返回1*/ 
else return 0; 
}
3.链栈的使用
在定义了单链表的结点结构Node并实现了链栈的基本操作后,就可以定义指向
Node的栈顶指针来操作链栈,可以调用实现基本操作的函数来完成相应功能。范例程序
如下: 
#include <stdio.h> 
#include <stdlib.h> 
#include <malloc.h> 
/*将单链表的结点结构定义和链栈的各个函数定义放到这里*/ 
int main( ) 
{ 
DataType x; 
Node *top = NULL; /*定义链栈的栈顶指针并初始化*/ 
top = InitStack(top); /*初始化链栈*/ 
printf("对15 和10 执行入栈操作,"); 
Push(top, 15); 
Push(top, 10); 
if (GetTop(top, &x) == 1) 
printf("当前栈顶元素为: %d\n", x); /*输出当前栈顶元素10*/ 
if (Pop(top, &x) != 0) 
printf("执行一次出栈操作,删除元素: %d\n ", x); /*输出出栈元素10*/ 
if (GetTop(top, &x) == 1) 
printf("当前栈顶元素为: %d\n", x); /*输出当前栈顶元素15*/ 
printf("请输入待插入元素: "); 
scanf("%d", &x); 
Push(top, x); 
if (Empty(top) == 1) 
printf("栈为空\n"); 
else 
printf("栈非空\n"); /*栈有2 个元素,输出栈非空*/ 
DestroyStack(top); 
return 0; 
} 
源代码3-2

72 数据结构———从概念到C 实现(第2 版) 
3.2.4 顺序栈和链栈的比较
顺序栈和链栈基本算法的时间复杂度均为O(1),因此唯一可以比较的是空间性能。
初始时顺序栈必须确定一个固定的长度,所以有存储元素个数的限制和浪费空间的问题。
链栈没有栈满的问题,只有当内存没有可用空间时才会出现栈满,但是每个元素都需要一
个指针域,从而产生了结构性开销。所以当栈的使用过程中元素个数变化较大时,应该采
用链栈,反之,应该采用顺序栈。
3.3 队 列
3.3.1 队列的逻辑结构
1.队列的定义 
队列(queue)是只允许在一端进行插入操作,在另一端进行删除操作的线性表,允许
插入(也称为入队、进队)的一端称为队尾,允许删除(也称为出队)的一端称为队头。
图3-7所示是一个有5个元素的队列,入队的顺序为a1、a2、a3、a4、a5,出队的顺序依然
是a1、a2、a3、a4、a5,即最先入队者最先出队。所以队列中的元素除了具有线性关系外, 
还具有先进先出(firstinfirstout)的特性。
图3-7 队列的示意图
现实世界中有许多问题可以用队列描述,例如,对顾客服务部门(例如银行、电信等) 
的工作往往是按队列方式进行的,这类系统称作排队系统。在程序设计中,经常使用队列
保存按先进先出方式处理的数据,例如键盘缓冲区、操作系统中的作业调度等。
2.队列的抽象数据类型定义 
ADT Queue 
DataModel 
队列中元素具有先进先出特性,相邻元素具有前驱和后继关系
Operation 
InitQueue 
输入: 无 
功能: 队列的初始化 
输出: 一个空队列 
DestroyQueue 
输入: 无 
功能: 队列的销毁 
输出: 释放队列占用的存储空间
课件3-3

第3 章 栈、队列和数组  73 
EnQueue 
输入: 元素值x 
功能: 入队操作,在队尾插入元素x 
输出: 如果插入成功,队尾增加了一个元素,否则返回插入失败信息 
DeQueue 
输入: 无 
功能: 出队操作,删除队头元素 
输出: 如果删除成功,队头减少了一个元素,否则返回删除失败信息 
GetHead 
输入: 无 
功能: 读取队头元素 
输出: 若队列不空,返回队头元素,否则返回取队头失败信息 
Empty 
输入: 无 
功能: 判空操作,判断队列是否为空 
输出: 如果队列为空,返回1,否则返回0 
endADT 
3.3.2 队列的顺序存储结构及实现
1.顺序队列 
队列的顺序存储结构称为顺序队列(sequentialqueue)。
假设队列有n 个元素,顺序队列把队列的所有元素存储在数组的前n 个单元。如
果把队头元素放在数组中下标为0的一端,则入队操作相当于追加,不需要移动元素, 
其时间性能为O(1);但是出队操作的时间性能为O(n),因为要保证剩下的n-1个元
素仍然存储在数组的前n-1个单元,所有元素都要向前移动一个位置,如图3-8(c) 
所示。
图3-8 顺序队列的操作示意图
如果放宽队列的所有元素必须存储在数组的前n 个单元这一条件,就可以得到一种
更为有效的存储方法,如图3-8(d)所示。此时入队和出队操作的时间性能都是O(1),因
为没有移动任何元素,但是队列的队头和队尾都是活动的,因此,需要设置队头、队尾两个

74 数据结构———从概念到C 实现(第2 版) 
位置变量front和rear,并且约定:front指向队头元素的前一个位置,rear指向队尾
元素①。
2.循环队列的存储结构定义
在顺序队列中,随着队列的插入和删除操作,整个队列向数组中下标较大的位置移过
去,从而产生了队列的“单向移动性”。当元素被插入数组中下标最大的位置之后,数组空
间就用尽了,尽管此时数组的低端还有空闲空间,这种现象叫作“假溢出”,如图3-9(a) 
所示。
图3-9 循环队列的假溢出及其解决
解决假溢出的方法是将存储队列的数组看成是头尾相接的循环结构,即允许队列直
接从数组中下标最大的位置延续到下标最小的位置,如图3-9(b)所示,这可以通过取模
操作来实现。队列的这种头尾相接的顺序存储结构称为循环队列(circularqueue)。
下面给出循环队列的存储结构定义: 
#define QueueSize 100 /*定义数组的最大长度*/ 
typedef int DataType; /*定义队列元素的数据类型,假设为int 型*/ 
typedef struct 
{ 
DataType data[QueueSize];/*存放队列元素的数组*/ 
int front, rear; /*下标,队头元素和队尾元素的位置*/ 
} CirQueue; 
3.循环队列的实现
在循环队列中,如何判定队空和队满呢? 如图3-10(a)和(c)所示,队列中只有一
个元素,执行出队操作,队头位置加1后与队尾位置相等,即队空的条件是front= 
rear。
图3-11(a)和(c)所示数组中只有一个空闲单元,执行入队操作,队尾位置加1后与队
头位置相等,即队满的条件也是front=rear。如何将队空和队满的判定条件区分开呢? 
① 这样约定的目的是方便运算,如队尾位置减去队头位置正好等于队列的长度。有些参考书约定队头位置
front指向队头元素,队尾位置rear指向队尾元素;或队头位置front指向队头元素,队尾位置rear指向队尾元素的后
一个位置。

第3 章 栈、队列和数组  75 
可以浪费一个数组元素空间①,把图3-11(a)和(c)所示的情况视为队满,此时队尾位置和
队头位置正好差1,即队满的条件是(rear+1)% QueueSize=front。
图3-10 循环队列队空的判定
图3-11 循环队列队满的判定
根据队列的操作定义,容易写出循环队列基本操作的算法,其时间复杂度均为O(1)。
(1)循环队列的初始化。初始化一个空的循环队列只需将队头front和队尾rear同
时指向数组的某一个位置,一般是数组的高端,C语言实现如下: 
void InitQueue(CirQueue *Q) 
{ 
Q->front =Q->rear =QueueSize-1; 
}
(2)循环队列的销毁。循环队列是静态存储分配,在循环队列变量退出作用域时自
动释放所占内存单元,因此,循环队列无须销毁。
(3)入队操作。循环队列的入队操作只需将队尾位置rear在循环意义下加1,然后
将待插元素x 插入队尾位置。函数EnQueue的返回值表示入队操作是否正确执行,C语
言实现如下: 
① 算法设计有一个重要的原则———时空权衡。一般来说,牺牲空间或其他替代资源,通常可以减少时间代价。
例如,在单链表的开始结点之前附设一个头结点,使得单链表的插入和删除等操作不用考虑表头的特殊情况;在双链
表中,结点设置了指向前驱结点的指针和指向后继结点的指针,增加了指针的结构性开销,减少了查找前驱和后继的
时间代价。

76 数据结构———从概念到C 实现(第2 版) 
int EnQueue(CirQueue *Q, DataType x) 
{ 
if ((Q->rear +1) % QueueSize == Q->front) { 
printf("上溢错误,插入失败\n"); return 0; 
} 
Q->rear =(Q->rear +1) %QueueSize; /*队尾位置在循环意义下加1*/ 
Q->data[Q->rear]= x; /*在队尾处插入元素*/ 
return 1; 
}
(4)出队操作。循环队列的出队操作只需将队头位置front在循环意义下加1,然后
读取并返回队头元素。函数DeQueue的返回值表示出队操作是否正确执行,如果正确出
队,被删元素通过指针参数ptr返回,C语言实现如下: 
int DeQueue(CirQueue *Q,DataType *ptr) 
{ 
if (Q->rear == Q->front) {printf("下溢错误,删除失败\n"); return 0; } 
Q->front =(Q->front +1)%QueueSize; /*队头位置在循环意义下加1*/ 
*ptr = Q->data[Q->front]; /*读取出队前的队头元素*/ 
return 1; 
}
(5)取队头元素。读取队头元素与出队操作类似,唯一的区别是不改变队头位置, 
C语言实现如下: 
int GetHead(CirQueue *Q,DataType *ptr) 
{ 
int i; 
if (Q->rear == Q->front) {printf("下溢错误,取队头失败\n"); return 0; } 
i=(Q->front+1)%QueueSize; /*注意不改变队头位置*/ 
*ptr = Q->data[i]; return 1; 
}
(6)判空操作。循环队列的判空操作只需判断front是否等于rear,C 语言实现
如下: 
int Empty(CirQueue *Q) 
{ 
if(Q->rear == Q->front) return 1; /*队列为空返回1*/ 
else return 0; 
}

第3 章 栈、队列和数组  77 
4.循环队列的使用
在定义了循环队列的存储结构CirQueue并实现了基本操作后,程序中就可以使用
CirQueue类型来定义变量,可以调用实现基本操作的函数来完成相应功能。范例程序如
下: 
#include <stdio.h> 
#include <stdlib.h> 
/*将循环队列的存储结构定义和各个函数定义放到这里*/ 
int main( ) 
{ 
DataType x; 
CirQueue Q; /*定义结构体变量Q 为循环队列类型*/ 
InitStack(&Q); /*初始化循环队列Q*/ 
printf("对5 和8 执行入队操作,"); 
EnQueue(&Q, 5); 
EnQueue(&Q, 8); 
if (GetHead(&Q, &x) == 1) 
printf("当前队头元素为: %d\n", x); /*输出当前队头元素5*/ 
if (DeQueue(&Q, &x) == 1) 
printf("执行一次出队操作,出队元素是: %d\n ", x); /*输出出队元素5*/ 
if (GetHead(&Q, &x) == 1) 
printf("当前队头元素为: %d\n", x); /*输出当前队头元素8*/ 
printf("请输入入队元素: "); 
scanf("%d", &x); 
EnQueue(&Q, x); 
if (Empty(&Q) == 1) 
printf("队列为空\n"); 
else 
printf("队列非空\n"); /*队列有两个元素,输出队列非空*/ 
return 0; 
} 
3.3.3 队列的链接存储结构及实现
1.链队列的存储结构定义 
队列的链接存储结构称为链队列(linkedqueue),通常用单链表表示,其结点结构与
单链表的结点结构相同,请参见2.4.1节。为了使空队列和非空队列的操作一致,链队列
也加上头结点。根据队列的先进先出特性,为了操作上的方便,设置队头指针指向链队列
的头结点,队尾指针指向终端结点,如图3-12所示。
下面给出链队列的存储结构定义: 
源代码3-3

78 数据结构———从概念到C 实现(第2 版) 
图3-12 链队列示意图 
typedef int DataType; /*定义队列元素的数据类型,假设为int 型*/ 
typedef struct Node /*定义链队列的结点结构*/ 
{ 
DataType data; 
struct Node *next; 
} Node; 
typedef struct /*定义链队列*/ 
{ 
Node *front, *rear; 
} LinkQueue; 
2.链队列的实现
链队列基本操作的实现本质上是单链表操作的简化,且时间复杂度均为O(1)。
(1)链队列的初始化。初始化链队列只需申请头结点,然后让队头指针和队尾指针
均指向头结点,算法如下: 
void InitQueue(LinkQueue *Q) 
{ 
Node*s =(Node *)malloc(sizeof(Node)); s->next = NULL; 
Q->front = Q->rear = s; /*队头指针和队尾指针均指向头结点*/ 
}
(2)链队列的销毁。链队列是动态存储分配,需要释放链队列的所有结点的存储空
间,算法如下: 
void DestroyQueue(LinkQueue *Q) 
{ 
Node *p = Q->front,temp; 
while (p != NULL) /*依次释放链队列的结点*/ 
{ 
temp = p-> next; 
free(p); 
p =temp; 
} 
}
(3)入队操作。链队列的插入操作只考虑在链表的尾部进行,由于链队列带头结点,空

第3 章 栈、队列和数组  79 
链队列和非空链队列的插入操作语句一致,其操作示意图如图3-13所示。C语言实现
如下: 
void EnQueue(LinkQueue*Q,DataType x) 
{ 
Node *s =(Node*)malloc(sizeof(Node)); 
s->data = x; s->next = NULL; /*申请一个数据域为x 的结点s*/ 
Q->rear->next = s;Q->rear = s; /*将结点s 插入队尾*/ 
} 
图3-13 链队列的入队操作
(①rear->next=s;②rear=s;) 
(4)出队操作。链队列的删除操作只考虑在链表的头部进行,注意队列长度等于1 
的特殊情况,其操作示意图如图3-14所示。函数DeQueue表示出队操作是否正确执行, 
如果正确出队,被删元素通过指针参数ptr返回,C语言实现如下: 
int DeQueue(LinkQueue *Q,DataType *ptr) 
{ 
Node *p; 
if (Q->rear == Q->front) {printf("下溢错误,删除失败\n"); return 0; } 
p = Q->front->next;*ptr = p->data; /*存储队头元素*/ 
Q->front->next = p->next; /*将队头元素所在结点摘链*/ 
if (p->next == NULL) /*判断出队前队列长度是否为1*/ 
Q->rear = Q->front; 
free(p); 
return 1; 
} 
图3-14 链队列出队操作示意图
(5)取队头元素。取链队列的队头元素只需返回第一个元素结点的数据域,算法

80 数据结构———从概念到C 实现(第2 版) 
如下: 
int GetHead(LinkQueue *Q,DataType *ptr) 
{ 
Node *p = NULL; 
if (Q->rear == Q->front) {printf("下溢错误,取队头失败\n"); return 0; } 
p = Q->front->next; 
*ptr = p->data; 
return 1; 
}
(6)判空操作。链队列的判空操作只需判断front是否等于rear,算法如下: 
int Empty(LinkQueue *Q) 
{ 
if (Q->rear == Q->front) return 1; /*队列为空返回1*/ 
else return 0; 
}
3.链队列的使用
在定义了链队列的存储结构LinkQueue并实现了基本操作后,就可以使用LinkQueue 
类型定义变量,可以调用实现基本操作的函数来完成相应功能。范例程序如下: 
#include <stdio.h> 
#include <stdlib.h> 
#include <malloc.h> 
/*将链队列的存储结构定义和各个函数定义放到这里*/ 
int main( ) 
{ 
DataType x; 
LinkQueue Q; /*定义结构体变量Q 为链队列类型*/ 
InitQueue(&Q); /*初始化链队列Q*/ 
printf("对5 和8 执行入队操作,"); 
EnQueue(&Q, 5); 
EnQueue(&Q, 8); 
if (GetHead(&Q, &x) == 1) 
printf("当前队头元素为: %d\n", x); /*输出当前队头元素5*/ 
if (DeQueue(&Q, &x) == 1) 
printf("执行一次出队操作,出队元素是: %d\n ", x); /*输出出队元素5*/ 
if (GetHead(&Q, &x) == 1) 
printf("当前队头元素为: %d\n", x); /*输出当前队头元素8*/ 
printf("请输入入队元素: "); 
源代码3-4

第3 章 栈、队列和数组  81 
scanf("%d", &x); 
EnQueue(&Q, 5); 
if (Empty(&Q) == 1) 
printf("队列为空\n"); 
else 
printf("队列非空\n"); /*队列有两个元素,输出队列非空*/ 
DestroyQueue(&Q); 
return 0; 
} 
3.3.4 循环队列和链队列的比较
循环队列和链队列基本算法的时间复杂度均为O(1),因此,可以比较的只有空间性
能。初始时循环队列必须确定一个固定的长度,所以有存储元素个数的限制和浪费空间
的问题。链队列没有溢出的问题,只有当内存没有可用空间时才会出现溢出,但是每个元
素都需要一个指针域,从而产生了结构性开销。所以当队列中元素个数变化较大时,应该
采用链队列,反之,应该采用循环队列。如果确定不会发生溢出,也可以采用顺序队列。
3.4 多维数组
3.4.1 数组的逻辑结构
1.数组的定义 
数组(array)是由类型相同的数据元素构成的有序集合,每个数据元素称为一个数组
元素(简称为元素),每个元素受n(n≥1)个线性关系的约束,每个元素在n 个线性关系中
的序号i1,i2,…,in ,称为该元素的下标,并称该数组为n 维数组。可以看出,数组的特点
是数据元素本身可以具有某种结构,但属于同一数据类型。例如,一维数组可以看作一个
线性表;二维数组可以看作元素是线性表的线性表;以此类推。所以,数组是线性表的推
广,如图3-15所示。本章重点讨论二维数组。
图3-15 数组是线性表的推广
2.数组的抽象数据类型定义
数组是一个具有固定格式和数量的数据集合,在数组上一般不能执行插入或删除某
课件3-4

82 数据结构———从概念到C 实现(第2 版) 
个数组元素的操作。因此,数组中通常只有两种基本操作:①读操作:给定一组下标,读
取相应的数组元素;②写操作:给定一组下标,存储或修改相应的数组元素。这两种操
作本质上对应一种操作———寻址,即根据一组下标定位相应的数组元素。下面给出数组
的抽象数据类型定义。 
ADT Matrix 
DataModel 
相同类型的数据元素的有序集合 
每个数据元素受n(n≥1)个线性关系的约束并由一组下标唯一标识
Operation 
InitMatrix 
输入: 数组的维数n 和各维的长度l1, l2, …, ln 
功能: 数组的初始化 
输出: 一个空的n 维数组 
GetMatrix 
输入: 一组下标i1, i2, …, in 
功能: 读操作,读取这组下标对应的数组元素 
输出: 对应下标i1, i2, …, in 的元素值 
SetMatrix 
输入: 元素值e,一组下标i1, i2, …, in 
功能: 写操作,存储或修改这组下标对应的数组元素 
输出: 对应下标i1, i2, …, in 的元素值改为e 
endADT 
3.4.2 数组的存储结构与寻址
由于数组一般不执行插入和删除操作,也就是说,一旦建立了数组,其元素个数和元
素之间的关系就不再发生变动,而且,数组是一种特殊的数据结构,通常要求能够随机存
取,因此,数组采用顺序存储。由于内存单元是一维结构,而多维数组是多维结构,所以, 
采用顺序存储结构存储数组首先需要将多维结构映射到一维结构。常用的映射方法有两
种:以行序为主序(row majororder,即按行优先)和以列序为主序(columnmajororder, 
即按列优先)。例如,C语言中的数组是按行优先存储的。
对于二维数组,按行优先存储的基本思想是先行后列,先存储行号较小的元素,行号
相同者先存储列号较小的元素。设二维数组行下标与列下标的范围分别为[l1,h1]与
[l2,h2],则元素aij的存储地址可由下式确定: 
LOC(aij)= LOC(al1l2)+ [(i-l1)× (h2 -l2 +1)+ (j-l2)]×c (3-1) 
在式(3-1)中,i∈[l1,h1],j∈[l2,h2],且i 与j 均为整数;LOC(aij)是元素aij 的存
储地址;LOC(al1l2)是二维数组中第一个元素al1l2 
的存储地址,通常称为基地址;c 是每
个元素所占存储单元数目。二维数组的寻址过程如图3-16所示。
二维数组按列优先存储的基本思想是先列后行,先存储列号较小的元素,列号相同者
先存储行号较小的元素。任一元素存储地址的计算与按行优先存储类似。