1. 栈
栈是限定在表的一端进行插入和删除运算的线性表,通常将插入、删除的一端称为栈顶,另一端称为栈底。不含元素的空表称为空栈。根据定义,每次删除的总是当前栈中最后插入的元素,而最先进栈的元素在栈底,要到最后才能删除。因此,栈又称为后进先出的线性表简称为LIFO(Last In First Out)表。
1.1 栈的运算
栈的基本运算除了在栈顶进行插入和删除运算外,还有栈的初始化、判栈空及取栈顶元素等运算。栈的主要运算有以下几种: (1)置空栈 InitStack(&S):构造一个空栈S。 (2)判栈空 StackEmpty(S):若栈S为空栈,则返回TRUE,否则返回FALSE。 (3)判栈满 StackFull(S):若栈S为满栈,,则返回TRUE,否则返回FALSE。 (4)进栈(又称为入栈)Push(&S, x):将元素x插入S栈的栈顶。 (5)退栈(又称为出栈)Pop(&S):若栈S为非空,则将S的栈顶元素删除,并返回删除前的栈顶元素。 (6)取栈顶元素GetTop(S):若栈S为非空,则返回栈顶元素,但不改变栈的状态。
1.2 栈的存储表示和实现
1.2.1 栈的顺序存储结构
栈的顺序存储结构称为顺序栈。由于栈是运算受限的线性表,因此线性表的存储结构也对栈适用。因为栈底位置是固定不变的,故可以将栈底位置设置在数组的最底端(即下标为0),栈顶位置是随着进栈和退栈操作而变化的,故需要用一个整型变量top来指示当前栈顶位置,通常称top为栈顶指针。
#define StackSize 100 //栈空间的大小应根据实际需要来定义,这里假设为100
typedef char DataType; //DataType的类型可根据实际类型而定,这里假设为char
typedef struct {
DataType data[StackSize] //数组data用来存放表结点
int top; //表示栈顶指针
} SeqStack;
SeqStack S;
S.data[0]是栈底元素,进栈时需要将S.top加1,退栈时需要将S.top减1,S.top < 0表示空栈,S.top = StackSize - 1表示栈满。当栈满时再做进栈运算必定产生空间溢出,简称 “上溢”;当栈空时再做退栈运算也将产生溢出,简称 “下溢”。
1.2.2 顺序栈基本运算的实现
顺序栈基本运算的实现方法有以下六种: (1)置空栈
void InitStack(SeqStack *S)
{
//置空顺序栈。由于C语言数组下标是从0开始,所以栈中元素也是从0开始
//存储,因此空栈时栈顶指针不能是0,可以是-1
S->top = -1;
}
(2)判栈空
int StackEmpty(SeqStack *S)
{
return S->top == -1;
}
(3)判栈满
int StackFull(SeqStack *S)
{
return S->top == StackSize -1;
}
(4)进栈
void Push(SeqStack *S, DataType x)
{
if (StackFull())
{
printf("stack overflow");
}
else
{
S->top = S->top + 1; // 栈顶指针加1
S->data[S->top] = x; // 将x入栈
}
}
(5)退栈
DataType Pop(SeqStack *S)
{
if (StackEmpty())
{
printf("stack underflow");
exit(0);
}
else
{
return S->data[S->top--]; //返回栈顶元素后栈顶指针减1
}
}
(6)取栈顶元素
DataType GetTop(SeqStack *S)
{
if (StackEmpty())
{
printf("stack empty");
exit(0);
}
else
{
return S->data[S->top]; //返回栈顶元素
}
}
由于顺序栈必须预先分配存储空间,因此在应用中要考虑溢出的问题。
1.2.3 栈的链式存储结构及基本操作
为了克服这种由顺序存储分配固定空间所产生的溢出和空间浪费问题,可以用链式存储结构来存储栈。栈的链式存储结构称为链栈,它是运算受限的单链表,其插入和删除操作仅限制在表头位置上(栈顶)进行,因此不必设置头结点。链栈的类型定义如下:
typedef char DataType;
typedef struct stacknode {
DataType data;
struct stacknode * next;
} StackNode;
typedef StackNode * LinkStack;
LinkStack top;
下面给出链栈上的基本运算。 (1)判空栈
int StackEmpty(LinkStack top)
{
return top == NULL;
}
(2)进栈
//将元素x插入栈顶
LinkStack Push(LinkStack top, DataType x)
{
StackNode *p;
p = (StackNode *)malloc(sizeof(StackNode)); //申请新结点
p->data = x;
p->next = top; //将新结点*p插入栈顶
top = p; //使top指向新的栈顶
return top; //返回新栈顶指针
}
(3)退栈
LinkStack Pop(LinkStack top, DataType *x)
{
StackNode *p = top; //保存栈顶指针
if (StackEmpty(top))
{
printf("stack empty");
exit(0);
}
else
{
*x = p->data; //保存删除结点值,并带回
top = p->next; //栈顶指针指向下一个结点
free(p); //删除P指向的结点
return top; //返回删除后的栈顶
}
}
(4)取栈顶元素
DataType GetTop(LinkStack top)
{
if (StackEmpty(top))
{
printf("stack empty");
exit(0);
}
else
{
return top->data;
}
}
1.3 队列
1.3.1 队列的定义及其运算
队列(Queue)也是一种操作受限的线性表,它只允许在表的一端进行元素插入,而在另一端进行元素删除。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。队列的概念与现实生活中的排队相似,新来的成员总是加入队尾,排在队列最前面的总是最先离开队列,即先进先出,因此有又称队列为先进先出(First In First Out,FIFO)表。
队列的操作运算与栈类似,有关队列的基本运算如下: (1)置空队列InitQueue(Q),构造一个空队列Q。 (2)判队空QueueEmpty(Q),若Q为空队列,则返回TRUE,否则返回FALSE。 (3)入队列EnQueue(Q, x), 若队列不满,则将数据x插入到Q的队尾。 (4)出队列DeQueue(Q),若队列不空,则删除队头元素,并返回该元素。 (5)取队头GetFront(Q),若队列不空,则返回队头元素。
1.3.2 顺序循环队列
队列的顺序存储结构称为顺序队列。队列的顺序存储结构也是利用一块连续的存储单元存放队列中的元素的,实际上是一个受限的线性表。由于队列的队头和队尾的位置是变化的,因此需要设置两个指针front和rear分别指示队头和队尾元素在表中的位置,它们的初值在队列初始化时置为0。顺序队列的类型定义如下:
typedef char DataType;
#define QueueSize 100
typedef struct{
DataType data[QueueSize];
int front, rear;
} SeqQueue;
SeqQueue Q;
在上述的结构中,随着入队出队的持续,front和rear指向的位置逐渐接近队尾,存储空间耗尽,再进行插入时会产生上溢,然而出队时空出的一些存储单元却无法使用。为了克服这种情况,可以将数组空间想象成一个环形空间。在这种循环队列中进行入队、出队运算时,头尾指针仍然要加1,只不过当头尾指针指向数组上界(QueueSize - 1)时,如果按照正常的加1运算,数组就会越界溢出,因此需要判断加1后是否超过数组上界,若是则使其指向数组下界0。 在循环队列的运算中,要涉及一些边界条件的处理问题。由于入队时的尾指针Q.rear向前追赶队头指针Q.front,出队时头指针向前追赶尾指针,如果是尾指针追赶上头指针,说明队满,否则若头指针追赶上尾指针,说明队空。因此,无论是队满还是队空,Q.front == Q.rear都成立,仅凭队列的头尾指针是否相等是无法判断队列是空是满。有一种方法是少用一个元素空间,约定入队前,判断尾指针在循环意义下加1后是否等于头指针,若相等则认为队列满,即尾指针Q.rear所指向的单元始终为空。该方法实现的循环队列的存储结构与前面定义的顺序队列基本上是一样的,循环队列的顺序存储类型定义如下
#define QueueSize 100
typedef char DataType;
typedef struct{
DataType data[QueueSize];
int front, rear;
} CirQueue;
CirQueue Q;
循环队列基本运算的各算法描述如下: (1)置空队列
void InitQueue(CirQueue *Q)
{
Q->front = Q->rear = 0;
}
(2)判队空
int QueueEmpty(CirQueue *Q)
{
return Q->rear == Q->front;
}
(3)判队满
int QueueFull(CirQueue *Q)
{
return (Q->rear + 1) % QueueSize == Q->front;
}
(4)入队列
void EnQueue(CirQueue *Q, DataType x)
{
//插入元素x为队列Q新的队尾元素
if (QueueFull())
{
printf("Queue overflow");
}
else
{
Q->data[Q->rear] = x;
Q->rear = (Q->rear + 1) % QueueSize; //循环意义下的加1
}
}
(5)取队头元素
DataType GetFront(CirQueue *Q)
{
//获取Q的队头元素值
if (QueueEmpty())
{
printf("Queue empty");
exit(0);
}
else
{
return Q->data[Q->front]; //返回队头元素值
}
}
(6)出队列
DataType DeQueue(CirQueue *Q)
{
DataType x;
if (QueueEmpty())
{
printf("Queue empty");
exit(0);
}
else
{
x = Q->data[Q->front]; //保存待删除的元素值
Q->front = (Q->front + 1) % QueueSize; //头指针循环意义下加1
return x;
}
}
1.3.3 链队列
队列的链式结构称为链队列,它是一种限制在表头删除和表尾插入操作的单链表。仅有单链表的头指针是不便在表尾作插入操作的,为此再增加一个尾指针,使其指向链表上的最后一个结点。链队列的类型定义如下:
typedef char DataType;
typedef struct qnode{
DataType data;
struct qnode *next;
} QueueNode; //链队列结点类型
typedef struct {
QueueNode *front; //队头指针
QueueNode *rear; //队尾指针
} LinkQueue; //链队列类型
LinkQueue Q; //定义一个链队列Q
为了简化边界处理条件,在队头结点之前也附加一个头结点,并设队头指针指向此结点。下面给出带头结点链队列的基本运算: (1)构造空队列
void InitQueue(LinkQueue *Q)
{
Q->front = (QueueNode *)malloc(sizeof(QueueNode)); //申请头结点
Q->rear = Q->front; //队尾指针也指向头结点
Q->rear->next = NULL;
}
(2)判队空
int QueueEmpty(LinkQueue *Q)
{
return Q->rear == Q->front; //头尾指针相等则队列为空
}
(3)入队列
void EnQueue(LinkQueue *Q, DataType x)
{
//将元素x插入链队列尾部
QueueNode *p = (QueueNode *)malloc(sizeof(QueueNode)); //申请新结点
p->data = x;
p->next = NULL;
Q->rear->next = p; //*p链到原队尾结点之后
Q->rear = p; //队尾指针指向新的队尾结点
}
(4)取队头元素
DataType GetFront(LinkQueue *Q)
{
//取链队列的队头元素值
if (QueueEmpty())
{
printf("Queue empty");
exit(0);
}
else
{
return Q->front->next->data; //返回队头元素值
}
}
(5)出队列 为了简便算法,出队列时把头结点删除,原队头结点变为头结点
DataType DeQueue(LinkQueue *Q)
{
QueueNode *p;
if (QueueEmpty())
{
printf("Queue empty");
exit(0);
}
else
{
p = Q->front; //p指向头结点
Q->front = Q->front->next; //头指针指向原队头结点
free(p); //删除释放原头结点
return Q->front->data; //返回原队头结点的元素值
}