第4章 栈与队列
第4章 栈与队列
栈是限定仅在表尾进行插入和删除操作的线性表。
队列是只允许在一端进行插入操作、而另一端进行删除操作的线性表。
4.2 栈的定义
4.2.1 栈的定义
栈(stack)是限定尽在表尾进行插入和删除操作的线性表。
允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的插入操作,叫做进栈,也称压栈、入栈。
栈的删除操作,叫做出战,也有的叫作弹栈。
4.2.2 进栈出栈变化形式
1、2、3依次进栈,会有哪些出栈次序
- 1进,2进,3进,3出,2出,1出
- 1进,1出,2进,2出,3进,3出
- 1进,2进,2出,1出,3进,3出
- 1进,1出,2进,3进,3出,2出
- 1进,2进,2出,3进,3出,1出
4.3 栈的抽象数据类型
ADT 栈(stack)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S):初始化操作,建立一个空栈S。
DestoryStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,返回true,否则返回false。
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*S, e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT
4.4 栈的顺序存储结构及实现
4.4.1 栈的顺序存储结构
栈的结构定义:
typedef int SElemType; //SElemType类型根据实际情况而定,这里假设为int
typedef struct
{
SElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack;
栈普通情况、空栈和栈满的情况:
4.4.2 栈的顺序存储结构-进栈操作
对于栈的插入,即进栈操作:
因此对栈进行push
//插入元素e为新的栈顶元素
Status Push(SqStack *S, SElemType e)
{
if(S->top == MAXSIZE - 1) //栈满
{
return ERROR;
}
S->top++; //栈顶指针增加1
S->data[S->top]=e; //将新插入元素赋值给栈顶空间
}
4.4.3 栈的顺序存储结构--出栈操作
出栈操作pop,代码如下:
//若栈不空,则删除S的栈顶元素,并用e返回其值,并返回OK,否则返回ERROR
Status Pop(SqStack *S, SElemType *e)
{
if(S->top == -1)
return ERROR;
*e = S->data[S->top];
S->top--;
return OK;
}
4.5 两栈共享空间
两栈共享空间的结构代码:
//两栈共享空间结构
typedef struct
{
SElemType data[MAXSIZE];
int top1; //栈1 栈顶指针
int top2; //栈2 栈顶指针
}SqDoubleStack;
对于两栈共享空间的push方法,我们除了要插入元素值参数之外,还需要判断是栈1还是栈2的栈号参数stackNumber。
Status Push(SqDoubleStack *S, SElemType e, int stackNumber)
{
if (S->top1 + 1 == S->top2) //栈已满,不能再push元素
return ERROR;
if (stackNumber == 1)
S->data[++S->top1] = e; //若栈1,则先top1+1后给数组元素赋值
else if (stackNumber == 2)
S->data[--S->top2] = e; //若栈2,则先top2-1后给数组元素赋值
return OK;
}
两栈共享空间的pop方法,参数就是判断栈1、栈2的参数stackNumber
Satatus 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.6 栈的链式存储结构及实现
4.6.1 栈的链式存储结构
栈的链式存储结构,称为链栈。
链栈结构代码:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
4.6.2 栈的链式存储结构-进栈操作
对于链栈的进栈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;
}
4.6.3 栈的链式存储结构-出栈操作
//若栈不空,则删除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);
S->count--;
return OK;
}
链栈的进栈push和出栈pop操作都很简单,没有任何循环操作,时间复杂度均为O(1)。
4.7 栈的作用
4.8 栈的应用-递归
4.8.1 斐波那契数列的实现
1、1、2、3、5、8、13...构成一个数列,前面相邻两项之和,构成最后一项,称为斐波那契数列。
公式:
打印前4位的斐波那契数列数,代码
int main()
{
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d", a[0]);
printf("%d", a[1]);
for( i = 2; i < 40; i++)
{
a[i] = a[i - 1] + a[i - 2];
printf("%d ", a[i]);
}
return 0;
}
递归实现:
//斐波那契的递归函数
int Fbi(int i)
{
if (i < 2)
return i == 0 ? 0 : 1;
return Fbi(i-1) + Fbi(i-2);
}
int main()
{
int i;
for (int i = 0; i < 40; i++)
printf("%d", Fbi(i));
return 0;
}
模拟代码中的Fbi(i)函数当i=5的执行过程
4.8.2 递归的定义
把一个直接调用自己或者通过一系列的调用语句间接地调用自己的函数,称为递归函数。
递归定义必须至少有一个条件,满足递归不再进行,即不再引用自身而是返回值退出。
迭代和递归的区别,迭代使用的是循环结构,递归使用的是选择结构。
4.9 栈的应用-四则运算表达式求值
4.9.1 后缀(逆波兰)表示法定义
一种不需要括号的后缀表达法,我们也把它称为逆波兰表示。
例如"9 + ( 3 - 1) x 3 + 10 / 2 "用后缀表示法表示为"9 3 1 - 3 * + 10 2 / +"
4.9.2 后缀表达式计算结果
后缀表达式: 9 3 1 - 3 * + 10 2 / +
规则:从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到符号就将栈顶的两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
-
初始化一个空栈。此战用来对要运算的数字进出使用。
-
后缀表达式中前三个都是数字,所以9、3、1进栈,如图4-9-1
-
接下来是‘-’,所以将栈中的1出栈作为减数,3出栈作为被减数,并运算3-1得到2,再将2进栈,如图4-9-2的左图所示。
-
接着是数字3进栈,如图4-9-2的右图所示。
-
后面是"*",也就意味着栈中3和2出栈,2与3相乘,得到6,并将6进栈,如果4-9-3的左图所示。
-
下面是'+',所以栈中6和9出栈,9与6相加,得到15,将15进栈,如图4-9-3的右图所示。
-
接着是10与2两数字进栈,如图4-9-4左图所示。
-
接下来是符号'/',因此,栈顶的2与10出栈,10与2相除,得到5,将5进栈,如如图4-9-4的右图所示。
-
最后一个符号'+',所以15与5出栈并相加,得到20,将20进栈,如果4-9-5的左图所示。
-
结果20出栈,栈变为空,如图4-9-5的右图所示。
4.9.3 中缀表达式转后缀表达式
我们平时所用的标准四则运算表达式,即"9 + ( 3 - 1) x 3 + 10 / 2"叫作中缀表达式。
中缀表达式"9 + (3 -1) x 3 + 10 / 2"转化为后缀表达式 "9 3 1 - 3 * + 10 2 / +"。
规则:
从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;如是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
-
初始化一个空栈,用来对符号进出栈使用。如图4-9-6的左图所示。
-
第一个字符数字是9,输出9,后面是符号"+",进栈。如图4-9-6的右图所示。
-
第三个字符是"(",依然是符号,因其只是左括号,还未配对,故进栈。如图4-9-7的左图所示。
-
第四个字符是数字3,输出,总表达式9 3,接着是"-",进栈。如图4-9-7的右图所示。
-
接下来是数字1,输出,总表达式为9 3 1,后面符号")",此时,我们需要去匹配此前的"(",所以栈顶依次出栈,并输出,直到"("出栈为止。此时左括号上方只有"-",因此输出"-"。总输出表达式为9 3 1 -。如图4-9-8的左图所示。
-
紧接着是符号"x",因为此时的栈顶符号为"+"号,优先级低于"X",因此不输出,"*"进栈。接着数字3,输出,总表达式为9 3 1 - 3。如图4-9-8的右图所示。
-
之后是符号"+",此时当前栈顶元素"*"比这个"+"的优先级更高,因此栈中元素出栈并输出(没有比"+"更低的优先级,所以全部出栈),总输出表达式为9 3 1 - 3 * +。然后将当前这个符号"+"进栈,也就是说,前6张图的栈底的"+"是指中缀表达式中开头的9后面那个"+",而图4-9-9左图中的栈底(也是栈顶)的"+"是指"9 + (3 - 1) x 3 +"中的最后一个"+"。
-
紧接着数字10,输出,总表达式变为9 3 1 - 3 * + 10。后是符号"/",所以"/"进栈。如图4-9-9的右图所示。
-
最后一个数字2,输出,总的表达式为9 3 1 - 3 * + 10 2。如图4-9-10的左图所示。
-
因已到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为9 3 1 - 3 * + 10 2 / +。如4-9-10的右图所示。
-
让计算机具有处理我们通常的标准(中缀)表达式的能力,最重要的两步:
- 将中缀表达式转化为后缀表达式(栈用来进出运算的符号)。
- 将后缀表达式进行运算得结果(栈用来进出运算得数字)。
4.10 队列得定义
队列是只允许在一端进行插入操作,而在另一端进行删除操作得线性表。
队列是一种先进先出(First In First Out)得线性表,简称FIFO。允许插入得一段称为队尾,允许删除得一端称为队头,队列数据插入只能在队尾进行,删除数据只能在队头进行。
队列结构如图4-10-1所示。
4.11 队列的抽象数据类型
ADT 队列(Queue)
Data
同线性表,元素具有相同类型,相邻元素具有前驱和后继的关系。
Operation
InitQueue(* Q):初始化操作,建立一个空队列Q。
DestroyQueue(* Q):若队列存在, 则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,返回为true,否则返回为false。
GetHead(Q, *e):若队列Q存在且非空,用e返回队列Q的队列头元素。
EnQueue(*Q, e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q, *e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
endADT
4.12 循环队列
线性表有顺序存储结构和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。
4.12.1 队列顺序存储的不足
队列顺序存储,由于是先进先出,每次都需要将下标为0的取出,后面依次迁移,存在效率低下的问题。
4.12.2 循环队列的定义
我们把队列的这种头尾相接的顺序存储结构称为循环队列。
通用的计算队列长度公式:
(rear - front + QueueSize) % QueueSize
typedef int QElemType; //QElemType类型根据实际情况而定,这里假设为int
//循环队列的顺序存储结构
typedef struct
{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针
}SqQueue;
循环队列的初始化代码如下:
//初始化一个空队列
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为新的队尾元素
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];
Q->front = (Q->front + 1) % MAXSIZE; //front指针向后移一个位置,若到最后则转到数组头部
}
4.13 队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
正常链队列:
空队列时,fron和rear都指向头结点,如图4-13-2所示
链队列的结构为:
typedef int QElemType; //QElemType类型根据实际情况而定,这里假设为int
typedef struct QNode //结点结构
{
QElemTYpe data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct //队列的链表结构
{
QueuePtr front, rear; //队头、队尾指针
}LinkQueue;
4.13.1 队列的链式存储结构-入队操作
入队操作时,其实就是在链表尾部插入结点,如图4-13-3所示。
其代码如下:
//插入元素e为Q的新的队尾元素
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
if(!s)
exist(OVERFLOW);
s->data = e;
s->next = NULL;
Q->rear->next = s; //把拥有元素e新结点s赋值给队尾结点的后继
Q->rear = s; //把当前的s设置为队尾结点,rear指向s
return OK;
}
4.13.2 队列的链式存储结构-出队操作
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点,如图4-13-4所示。
代码如下:
//若队列不空,删除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)
Q->rear = Q->front;
free(p);
return OK;
}
总结:可确定队列长度最大值的情况下,建议用循环队列,无法预估队列的长度时,则用链队列。