1. 栈的定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈又称为后进先出(Last In First Out)的线性表,简称 LIFO 结构。
栈首先是一个线性表,栈元素具有线性关系,即前驱后继关系。栈是一种特殊的线性表,特殊之处在于限制了这个线性表的插入和删除位置,他始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫做进栈,也称为压栈、入栈;栈的删除操作,叫做出栈。
栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,只要保证是栈顶元素出栈就可以了。
2. 栈的顺序存储结构
栈的顺序存储其实也是线性表顺序存储的简化,简称为顺序栈。通常把下标为 0 的一端作为栈底比较好,因为首元素都存在栈底,变化最小,所以让他做栈底。可以定义一个 top 变量来指示栈顶元素在数组中的位置,当栈存在一个元素时,top 等于 0,因此把空栈的判定条件定为 top 等于 -1。
栈的结构定义:
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
/* 顺序栈结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
2.1 顺序栈的进栈操作
进栈操作 push,其代码如下:
Status Push(SqStack *S,SElemType e)
{
if(S->top == MAXSIZE -1) /* 栈满 */
return ERROR;
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}
2.2 顺序栈的出栈操作
出栈操作 pop,代码如下:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}
3. 两栈共享空间
对于有两个相同类型的栈,我们可以用一个数组来存储两个栈。做法如下图,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为 0 处,另一个栈为数组的末端,即下标为数组长度 n-1 处。这样,两个栈如果增加元素,就是两端点向中间延伸。
关键思路是:它们是在数组两端,向中间靠拢。只要两个栈的栈顶指针不碰面,两个栈就可以一直使用。当两个栈见面之时,也就是两个指针之间相差 1 时,即** top1 + 1 == top2 为栈满**。
两栈共享空间的结构的代码如下:
/* 两栈共享空间结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top1; /* 栈1栈顶指针 */
int top2; /* 栈2栈顶指针 */
}SqDoubleStack;
对于两栈共享空间的 push 方法,需要一个变量 stackNumber 判断是入栈 1 还是栈 2。插入元素的代码如下:
/* 插入元素e为新的栈顶元素 */
Status Push(SqDoubleStack *S,SElemType e,int stackNumber)
{
if (S->top1+1==S->top2) /* 栈已满,不能再push新元素了 */
return ERROR;
if (stackNumber==1) /* 栈1有元素进栈 */
S->data[++S->top1]=e; /* 若是栈1则先top1+1后给数组元素赋值。 */
else if (stackNumber==2) /* 栈2有元素进栈 */
S->data[--S->top2]=e; /* 若是栈2则先top2-1后给数组元素赋值。 */
return OK;
}
删除栈元素的 pop 方法也类似,代码如下:
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
if (stackNumber==1)
{
if (S->top1==-1)
return ERROR; /* 说明栈1已经是空栈,溢出 */
*e=S->data[S->top1--]; /* 将栈1的栈顶元素出栈 */
}
else if (stackNumber==2)
{
if (S->top2==MAXSIZE)
return ERROR; /* 说明栈2已经是空栈,溢出 */
*e=S->data[S->top2++]; /* 将栈2的栈顶元素出栈 */
}
return OK;
}
事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时也就是一个栈增长而另一个栈在缩短的情况。
4. 栈的链式存储结构及实现
栈的链式存储结构,简称为链栈。通常把栈顶放在单链表的头部,对于栈链来说,是不需要头结点的。栈链基本不存在栈满的情况,但对于空栈来说,链表原定义是头指针指向空,那么链栈的空栈其实就是 top = NULL的时候。
链栈的结构代码如下:
/* 链栈结构 */
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct
{
LinkStackPtr top;
int count;
}LinkStack;
链栈的进栈 push 操作,假设元素值为 e 的新结点是 s,top 为栈顶指针,示意图如下,代码如下:
/* 插入元素e为新的栈顶元素 */
Status Push(LinkStack *S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */
S->top=s; /* 将新的结点s赋值给栈顶指针,见图中② */
S->count++;
return OK;
}
链栈的出栈操作,假设变量 p 用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放 p 即可。示意图和代码如下。
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /* 将栈顶结点赋值给p,见图中③ */
S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中④ */
free(p); /* 释放结点p */
S->count--;
return OK;
}
5. 队列的定义
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为对头。对列的基本操作和线性表的顺序存储结构完全一致。
5.1 循环对列
为了让出对的性能增加,不去限制对列的元素必须存储在数组的前 n 个单元。为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引进两个指针,fornt 指针指向对头元素,rear 指针指向队尾元素的下一个位置。
我们把对列的这种头尾相接的顺序存储结构称为循环队列。
此时的问题是,空队列时,front 等于 rear ,对满时 front 也是等于 rear,那么如何判断此时队列的情况呢?
- 办法一是设置一个标志变量 flag,当 front == rear,且 flag = 0 时为队列空,当 front == rear,且 flag = 1 时为队列满。
- 办法二是当队列空时,条件是 front == rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单位。
当使用第二种办法时,由于 rear 可能比 front 大,也可能比 front 小,所有尽管他们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以若队列的最大尺寸是 Q u e u e S i z e QueueSize QueueSize,那么队列满的条件是 ( r e a r + 1 ) % Q u e u e S i z e = = f r o n t (rear + 1)\%QueueSize == front (rear+1)%QueueSize==front (取模“%”的目的是为了整合 rear 与 front 大小为一个问题)。
通用的计算队列长度公式为:
( r e a r − f r o n t + Q u e u e S i z e ) (rear - front + QueueSize)% QueueSize (rear−front+QueueSize)
循环对列的顺序存储结构代码如下:
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data[MAXSIZE];
int front; /* 头指针 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
循环对列的初始化代码如下:
/* 初始化一个空队列Q */
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
循环对列求队列长度代码如下:
/* 返回Q的元素个数,也就是队列的当前长度 */
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
循环对列的入队列操作代码如下:
/* 若队列未满,则插入元素e为Q新的队尾元素 */
Status EnQueue(SqQueue *Q,QElemType e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 队列满的判断 */
return ERROR;
Q->data[Q->rear]=e; /* 将元素e赋值给队尾 */
Q->rear=(Q->rear+1)%MAXSIZE; /* rear指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
循环对列的出队列操作代码如下:
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
Status DeQueue(SqQueue *Q,QElemType *e)
{
if (Q->front == Q->rear) /* 队列空的判断 */
return ERROR;
*e=Q->data[Q->front]; /* 将队头元素赋值给e */
Q->front=(Q->front+1)%MAXSIZE; /* front指针向后移一位置, */
/* 若到最后则转到数组头部 */
return OK;
}
5.2 对列的链式存储结构
对列的链式存储,其实就是线性表的单链表,只不过它只能尾进头出而已,简称为链队列。把对头指针指向链队列的头结点,而队尾指针指向终端结点。
空对列时,front 和 rear 都指向头结点。
链队列结构代码如下:
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */
typedef struct QNode /* 结点结构 */
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct /* 队列的链表结构 */
{
QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;
出队列操作代码如下:
/* 插入元素e为Q的新的队尾元素 */
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s) /* 存储分配失败 */
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s;/* 把拥有元素e的新结点s赋值给原队尾结点的后继,见图中① */
Q->rear=s; /* 把当前的s设置为队尾结点,rear指向s,见图中② */
return OK;
}
出队列操作代码如下:
/* 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR */
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next; /* 将欲删除的队头结点暂存给p,见图中① */
*e=p->data; /* 将欲删除的队头结点的值赋值给e */
Q->front->next=p->next;/* 将原队头结点的后继p->next赋值给头结点后继 */
/* 见图中② */
if(Q->rear==p) /* 若队头就是队尾,则删除后将rear指向头结点,见图中③ */
Q->rear=Q->front;
free(p);
return OK;
}