从数据结构角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此,可称为限定性的数据结构。但从数据类型角度看,它们是和线性表大不相同的两类重要的抽象数据类型。由于它们广泛应用在各种软件系统中,因此在面向对象的程序设计中,它们是多型数据类型。
1 栈
1.1 抽象数据类型栈的定义
栈(stack)是限定仅在表尾进行插入或删除操作的线性表。因此,对栈来说,表尾端有其特殊含义,称为栈顶(top),相应地,表头端称为栈底(bottom)。不含元素的空表称为空栈。
假设栈 \(S=(a_1,a_2,\cdots,a_n)\),则称 \(a_1\) 为栈底元素,\(a_n\) 为栈顶元素。栈中元素按 \(a_1,a_2,\cdots,a_n\) 的次序进栈,退栈的第一个元素应为栈顶元素。即栈的修改是按后进先出的原则进行的,因此,栈又称为后进先出(last in first out)的线性表(简称 LIFO 结构)。
栈的抽象数据类型定义如下:
ADT Stack {
数据对象:D = {ai | ai ∈ ElemSet, i = 1, 2, ..., n}
数据对象: R1 = {<a_{i-1}, ai> | a_{i-1}, ai ∈ D, i = 2, 3, ..., n}
约定 an 端为栈顶,a1 端为栈底
基本操作:
InitStack(&S)
操作结果: 构造一个空栈 S
DestroyStack(&S)
初始条件: 栈 S 存在
操作结果: 销毁栈 S
StackEmpty(S)
初始条件: 栈 S 存在
操作结果: 若栈 S 为空栈,则返回 TRUE,否则返回 FALSE
StackLenght(S)
初始条件: 栈 S 存在
操作结果: 返回栈 S 的元素个数
GetTop(S, &e)
初始条件: 栈 S 存在且非空
操作结果: 用 e 返回栈 S 的栈顶元素
Push(&S, e)
初始条件: 栈 S 存在
操作结果: 插入新元素 e 为栈 S 的新的栈顶元素
Pop(&S, &e)
初始条件: 栈 S 存在且非空
操作结果: 删除栈 S 的栈顶元素,并用 e 返回其值
StackTraverse(S, visit())
初始条件: 栈 S 存在
操作结果: 从栈底到栈顶依次对栈 S 中每个元素调用函数 visit()。一旦 visit() 失败,则操作失败
} ADT Stack
1.2 栈的表示与实现
和线性表类似,栈也有两种存储表示方式。
顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针 top
指示栈顶元素在顺序栈中的位置。通常的习惯做法是以 top=0
表示空栈;另一方面,由于栈在使用过程中所需最大空间的大小很难估计,因此,一般来说,在初始化设空栈时不应限定栈的最大容量。一个较合理的做法是:先为栈分配一个基本容量,然后在应用过程中,当栈的空间不够使用时再逐段扩大。为此,可设定两个常量:STACK_INIT_SIZE(存储空间初始分配量)和 STACKINCREMENT(存储空间分配增量),并以下述类型说明作为顺序栈的定义。
typedef struct {
SElemType * base;
SElemType * top;
int stacksize;
} SqStack;
其中,stacksize
指示栈的当前可用最大容量。
栈的初始化操作为:按设定的初始分配量进行第一次存储分配,base
可称为栈底指针,在顺序栈中,它始终指向栈底的位置,若 base
的值为 NULL
,则表明栈结构不存在。称 top
为栈顶指针,其初值指向栈底,即 top=base
可作为栈空的标记,每当插入新的栈顶元素时,指针 top
增 1;删除栈顶元素时,指针 top
减 1,因此,非空栈中的栈顶指针始终在栈顶元素的下一个位置上。
// 栈基本操作的部分算法描述
Status InitStack(SqStack &S) {
// 构造一个空栈
S.base = (SElemType * )malloc(STACK_INIT_SIZE * sizeof(SElemType));
if(!S.base)exit(OVERFLOW); // 存储分配失败
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return OK;
} // InitStack
Status GetTop(SqStack S, SElemType &e) {
// 若栈不空,则用 e 返回 S 的顶栈元素,并返回 OK;否则返回 ERROR
if(S.top == S.base) return ERROR;
e = *(S.top - 1);
return OK;
} // GetTop
Status Push(SqStack &S, SElemType e) {
// 插入元素 e 为新的栈顶元素
if(S.top - S.base >= S.stacksize) {
// 栈满,追加存储空间
S.base = (SElemType * )realloc(S.base, (S.stacksize + STACKINCREMENT) * sizeof(SElemType));
if(!S.base) exit(OVERFLOW); // 存储分配失败
S.top = S.base + S.stacksize;
S.stacksize += STACKINCREMENT;
}
*S.top++ = e;
return OK;
} // Push
Status Pop(SqStack &S, SElemType &e) {
// 若栈不空,则删除 S 的栈顶元素,用 e 返回其值,并返回 OK;否则返回 ERROR
if(S.top == S.base) return ERROR;
e = *--S.top;
return OK;
} // Pop
2 队列
2.1 抽象数据类型队列的定义
和栈相反,队列(queue)是一种先进先出(first in first out,缩写为 FIFO)的线性表。 它只允许在表的一端进行插入,而在另一端删除元素。
在队列中,允许插入的一端叫队尾(rear),允许删除的一端叫队头(front)。在队列 \(q=(a_1,a_2,\cdots,a_n)\) 中,\(a_1\) 时队头元素,\(a_n\) 时队尾元素,队列中元素按照 \(a_1,a_2,\cdots,a_n\) 的顺序进入,推出队列也只能按照这个顺序依次退出。
队列的抽象数据类型定义如下:
ADT Queue {
数据对象: D = {ai | ai ∈ ElemSet, i = 1, 2, ..., n, n ≥ 0}
数据关系: R1 = {<a_{i-1}, ai> | a_{i-1}, ai ∈ D, i = 2,..., n}
约定其中 a1 为队头元素,an 为队尾元素
基本操作:
InitQueue(&Q)
操作结果: 构造一个空队列 Q
DestroyQueue(&Q)
初始条件: 队列 Q 已存在
操作结果: 销毁队列 Q
ClearQueue(&Q)
初始条件: 队列 Q 已存在
操作结果: 将 Q 清为空队列
QueueEmpty(Q)
初始条件: 队列 Q 已存在
操作结果: 若 Q 为空队列,则返回 TRUE,否则返回 FALSE
GueueLength(Q)
初始条件: 队列 Q 已存在
操作结果: 返回 Q 的元素个数,即队列的长度
GetHead(Q, &e)
初始条件: 队列 Q 已存在
操作结果: 若队列不空,则用 e 返回 Q 的队头元素,并返回 OK;否则返回 ERROR
EnQueue(&Q, e)
初始条件: 队列 Q 已存在
操作结果: 插入新元素 e 为 Q 的新的队尾元素
DeQueue(&Q, &e)
初始条件: 队列 Q 已存在
操作结果: 若队列不空,则删除 Q 的队头元素,用 e 返回其值,并返回 OK;否则返回 ERROR
QueueTraverse(Q, visit())
初始条件: 队列 Q 已存在
操作结果: 从队头到队尾依次对 Q 的每个元素调用函数 visit()。一旦 visit() 失败,则操作失败
} ADT Queue
除了栈和队列外,还有一种限定性数据结构是双端队列(deque)。双端队列是限定插人和删除操作在表的两端进行的线性表。这两端分别称做端点 1 和端点 2。在实际使用中,还可以有输出受限的双端队列(即一个端点允许插人和删除,另一个端点只允许插人的双端队列)和输入受限的双端队列(即一个端点允许插人和删除,另一个端点只允许删除的双端队列)。而如果限定双端队列从某个端点插人的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈了。尽管双端队列看起来似乎比栈和队列更灵活,但实际上在应用程序中远不及栈和队列有用。
2.2 链队列
和线性表类似,队列也可以有两种存储表示。
用链表表示的队列简称为链队列。一个链队列显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能惟一确定。这里,和线性表的单链表一样,为了操作方便起见,给链队列添加一个头结点,并令头指针指向头结点。由此,空的链队列的判决条件为头指针和尾指针均指向头结点。
链队列的操作即为单链表的插入和删除操作的特殊情况,只需修改尾指针或头指针。
// 单链队列的存储结构
typedef struct QNode {
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct {
QueuePtr front; // 队头指针
QUeuePtr rear; // 队尾指针
} LinkQueue;
// 基本操作的部分算法描述
Status InitQueue(LinkQueue &Q) {
// 构造一个空队列
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
if (!Q->front) exit(OVERFLOW); // 存储分配失败
Q.front->next = NULL;
return OK;
}
Status DestroyQueue(LinkQueue &Q) {
// 销毁队列 Q
while (Q.front) {
Q.rear = Q.front->next;
free(Q.front);
Q.front = Q.rear;
}
return OK;
}
Status EnQueue(LinkQueue &Q, QElemType e) {
// 插入元素 e 为 Q 的新的队尾元素
p = (QueuePtr)malloc(sizeof(QNode));
if (!p) exit(OVERFLOW); // 存储分配失败
p->data = e;
p->next = NULL;
Q.rear->next = p;
Q.rear = p;
return OK;
}
Status DeQueue(LinkQueue &Q, QElemType &e) {
// 若队列不空,则删除 Q 的队头元素,用 e 返回其值,并返回 OK;否则返回 ERROR
if (Q.front == Q.rear) return ERROR;
p = Q.front->next;
e = p->data;
Q.front->next = p->next;
if (Q.rear == p) Q.rear = Q.front;
free(p);
return OK;
}
注意:在上述模块的算法描述中,应注意删除队列头元素算法中的特殊情况。一般情况下,删除队列头元素时仅需修改头结点中的指针,但当队列中最后一个元素被删后,队列尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)。
2.3 循环队列
循环队列是一种特殊的队列,它的队头和队尾指针可以在队列的存储空间上形成一个环,从而使得队列的存储空间可以重复利用。指针和队列元素之间的关系不变。
循环队列的判空和判满条件为:
- 队空条件:
front == rear
- 队满条件:
(rear + 1) % MAXSIZE == front
其中,MAXSIZE
为循环队列的最大容量。
循环队列的入队和出队操作如下:
Status EnQueue(CirQueue &Q, QElemType e) {
// 插入元素 e 为 Q 的新的队尾元素
if ((Q.rear + 1) % MAXSIZE == Q.front) return ERROR; // 队列满
Q.data[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXSIZE;
return OK;
}
Status DeQueue(CirQueue &Q, QElemType &e) {
// 若队列不空,则删除 Q 的队头元素,用 e 返回其值,并返回 OK;否则返回 ERROR
if (Q.front == Q.rear) return ERROR; // 队列空
e = Q.data[Q.front];
Q.front = (Q.front + 1) % MAXSIZE;
return OK;
}
Reference:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。