在上一章,我们学习了线性表,实际上接下来要讲的栈也属于线性表,只是它是操作受限的线性表,但是得益于它的性质,它们被广泛应用于各类系统中。接下来让我们认识一下栈这个数据结构。
一、定义
1、栈的定义
栈(Stack):是指限定在表尾进行插入或删除操作的线性表
它的定义说明了它所具有的性质,即只能在表尾进行插入删除,其余位置无法进行操作。
有人就会疑惑。如果我坚持访问呢?实际上这是取决与你选择的存储结构的,若你选择了顺序存储结构,那你还是可以访问其余位置的,若你选择了链式存储结构,便只能通过前驱/后继来访问其余位置。但是!既然我们要实现如此性质的一种结构,你若还是随意访问,那你还不是实现了一个顺序表。所以既然要实现它,即使你可以打破它的规则,但你还是应该遵循。
我们把允许插入和删除的一端称为栈顶(Top),另一端称为栈底(Bottom),不含任何数据元素的栈称为空栈。根据栈的性质,我们又将其称为后进先出(Last In First Out)的线性表,简称LIFO结构。
我们还把栈的插入称为入栈/进栈(Push),栈的删除称为出栈(Pop)。《大话数据结构》中形象地把栈比作了枪的弹夹,先压入的子弹最后打出,最后压入的子弹最先打出,根据这个例子大家可以更容易理解栈的这个特性。
《数据结构》给出数学形式的定义为:假设栈 S = ( a 1 , a 2 , a 3 , … , a n ) S=(a_1,a_2,a_3,\dots,a_n) S=(a1,a2,a3,…,an),则称 a 1 a_1 a1为栈底元素, a n a_n an为栈顶元素。栈中元素按 a 1 , a 2 , a 3 , … , a n a_1,a_2,a_3,\dots,a_n a1,a2,a3,…,an的次序进栈,出栈的第一个元素应为栈顶元素。
2、进栈出栈变化形式
大家可能会在考试的时候遇到一个问题,如果此时有1、2、3三个整型数依次进栈,它们会有哪些出栈次序呢?
会有小伙伴十分纳闷,那不就一种吗。正确答案是五种。
- 第一种:1进,2进,3进;3出,2出,1出。出栈次序321。
- 第二种:1进,1出,2进,2出,3进,3出。出栈次序123。
- 第三种:1进,1出,2进,3进,3出,2出。出栈顺序132。
- 第四种:1进,2进,2出,1出,3进,3出。出栈顺序213。
- 第五种:1进,2进,2出,3进,3出,1出。出栈顺序231。
初学的时候可能会有点懵,但仔细研究就会发现,进栈顺序确实一直是123,出栈也一直是按照栈的性质来的,确实是有这么多出栈顺序。
爱找规律的同学可能会思考有没有312这样的出栈顺序呢。答案是并没有。我们来分析一下,第一个出栈的元素为3,则证明1和2都已经入栈(因为3最后入栈)并且1和2均未出栈,那在栈中便只有1,2,3这样的排列了,也就是说,它的出栈顺序只能是321而不可能是312了。
还可能有同学会好奇它的出栈序列方案数有没有规律,这个是有的,它的个数遵循以下规律:
设有 n n n个不同元素进栈,则其出栈顺序有 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^n n+11C2nn种。
这个规律只当作课外知识扩充(一般不会被作为知识点考查),真想了解的同学可以去搜索一下“卡特兰数”。
二、栈的抽象数据类型
ADT 栈(Stack)
Data
线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType,其中
除栈底元素a1外,每个元素有且仅有一个直接前驱元素,除最栈顶元素an外,每个
元素有且仅有一个直接后继元素。数据元素之间的关系是一对一关系。
Operation
InitStack(*S):栈的初始化,建立一个空栈。
DestoryStack(*S):栈的销毁,若栈存在,则将其销毁。
ClearStack(*S):栈的清空,若栈存在,则将其清空。
StackEmpty(S):栈的判空,若栈存在,判断其是否为空栈。
GetTop(S,*e):返回栈顶元素,若栈非空,则用e返回当前栈顶元素。
Push(*S,e):栈的插入,若栈存在,则将元素e插入栈顶。
Pop(*S,*e):栈的删除,若栈非空,则用e返回栈顶元素后删除栈顶元素。
StackLength(S):返回栈的长度(栈种元素个数)。
end ADT
三、栈的顺序存储结构及操作实现
既然栈是属于线性表的,那它的存储方式自然也是有顺序存储方式和链式存储方式,我们先来看顺序存储结构。
1、栈的顺序存储结构
首先我们来看一下,栈数据结构中有哪些必要的工作变量,数据自然是不能少的,我们用数组来实现即可,但栈顶该如何找呢,我们只需要定义一个top
作为游标,让它指向栈顶即可。
需要记忆的是,我们在初始化栈时常把top
置为-1,因为在第一个元素入栈时正好指向数组的第一个元素下标0。
以下是栈的顺序存储结构的结构体定义:
typedef int ElemType;
typedef struct{
ElemType data[MAXSIZE];
int top;
}SqStack;
假设现在有一个栈,其大小为5,以下为其栈空、栈非空、栈满的示意图。
2、进栈操作(Push)
先来看一眼进栈操作的示意图:
根据其特性,不难理解插入操作的代码:
Status Push(SqStack *S,ElemType e)
{
if(S->top==MAXSIZE-1)//栈满情况
{
return ERROR;
}
S->top++; //栈顶指针上移
S->data[S->top]=e; //插入新元素
return OK;
}
3、出栈操作(Pop)
根据出栈操作的示意图
以及顺序存储的特性,也不难写出出栈操作的代码:
Status Pop(SqStack *S,ElemType *e)
{
if(S->top==-1)//栈空情况
{
return ERROR;
}
*e=S->data[S->top]; //将要出栈的元素赋给e
S->top--; //栈顶指针下移
return OK;
}
虽在名义上叫删除,但大家会发现,顺序存储结构上的删除从来都没有执行删除操作,只是将其覆盖(表中元素),或将其孤立出来(表尾元素)。这是因为顺序存储结构的存储空间都是申请好就固定的,不能随意释放,且我们在定义结构的时候都会定义一个类似top
这样的游标变量,这一操作就是为了防止出现还未向表中添加元素,表中却已有元素且可以访问或已被删除的元素仍可以被访问的情况。
实际上,栈的操作和线性表的相差不大,得益于栈的特性,插入和删除没有其他情况需要考虑,均在表尾进行操作,这也使得其插入删除操作时间复杂度均为
O
(
1
)
O(1)
O(1)。
其余操作便不再展开细讲,大家可根据其特性以及顺序表操作自行实现。
四、两栈共享空间
1、结构定义
既然是由顺序存储结构实现的栈,那肯定无法避免其缺点——定长。若我们此时有两个栈,其中一个已经满了,再进栈就会溢出,而另一个还有很多空闲空间。在实际应用中,我们经常需要面临这种问题,因为我们也不清楚在运行过程中哪一个栈插入的元素会更多一点。由此,一些大拿们便想出一种方法,我们直接申请一大块内存,用这一块内存来存两个栈的内容,这样长度不就自由一些了。
如图,我们定义了两栈的指针top1
和top2
,一个从表头(0处)开始,一个从表尾(n-1处)开始,两个栈顶指针向中间靠拢,易知当top1==-1
时栈1为空栈,当top2==n
时栈2为空栈。那么何时栈满呢?其实只要两个栈顶指针不碰面,那就两栈都未满,而栈满是两栈一起满,即top1+1==top2
时栈满。
两栈共享空间的结构的代码如下:
typedef struct{
ElemType data[MAXSIZE];
int top1;
int top2;
}SqDoubleStack;
不同的结构也就有了不同的进栈出栈操作,我们来看一下两栈共享空间的进出栈操作。
2、进栈操作(Push)
对于两栈共享空间的进栈操作,还要判断进哪个栈(StackNumber),其代码如下:
Status Push(SqDoubleStack *S,ElemType e,int StackNumber)
{
if(S->top1+1==S->top2) //栈满情况
{
return ERROR;
}
if(StackNumber==1) //栈1进栈
{
S->top1++;
S->data[S->top1]=e;
}
else if(StackNumber==2) //栈2进栈
{
S->top2--;
S->data[S->top2]=e;
}
return OK;
}
需要注意的是两个栈顶指针的移动,当我们定义为之前图示的结构时,一定是两指针向中靠拢,即两指针移动方向不同。
3、出栈操作(Pop)
同样,在执行出栈操作时,仍需要判断哪个栈出栈,实现代码如下:
Status Pop(SqDoubleStack *S,ElemType *e,int StackNumber)
{
if(StackNumber==1)
{
if(S->top1==-1) //栈1为空
return ERROR;
*e=S->data[S->top1];
S->top1--;
}
else if(StackNumber==2)
{
if(S->top2==MAXSIZE) //栈2为空
return ERROR;
*e=S->data[S->top2];
S->top2++;
}
return OK;
}
事实上,这样的结构通常都是两个栈的空间需求有相反关系时才会使用,即一个栈增长的时候另一个栈在缩短的情况。其余情况就未必可以保证其适用性了。同时,这样的结构还有一个问题,两栈的元素要求为同一数据类型,否则这样的方式反倒会使问题更加复杂。
五、栈的链式存储结构及操作实现
两栈共享空间试图解决顺序存储结构定长的缺点,但它也并未完美地解决,且多数人可能更倾向于用链式存储来解决,那我们接下来看一下栈的链式存储结构,我们把它叫做链栈。
1、结构定义
在单链表的学习中,我们常常会设置一个头结点来拉出链表,在链栈中还需要它吗?思考一下,栈的操作全在栈顶,需要一个栈顶指针,而单链表头结点为表头指针,那我们不妨将它们合在一起,让栈顶指针指向表头,形成如下的结构。
对于链栈,我们便不需要担心其长度,只要硬件允许,其长度不成问题。空栈情况即为top==NULL
的情况。
链栈的结构定义如下:
typedef struct StackNode{
ElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
在《大话数据结构》中,作者还为链栈增加了栈的长度的存储,用到了结构体的嵌套,十分巧妙,其定义内容如下:
typedef struct StackNode{
ElemType data;
struct StackNode *next;
}StackNode,*PNode;
typedef struct{
PNode top;
int length;
}LinkStack;
可以看到,结构体StackNode
为链栈每个结点,而结构体LinkStack
用于存储链栈的栈顶指针和栈长。这样一来,只需要申请一个LinkStack
的结点,其中top指向链栈的栈顶,length存储链栈的栈长,每增加一个栈内元素只需要新开辟一个StackNode
结点即可。
注:考虑到结构体嵌套对初学者理解有一定难度,在下面的操作中,均使用《数据结构》中的链栈结构。
2、进栈操作(Push)
对于以链式结构存储的栈的进栈操作如下图所示:
关键操作的代码也已经给出在图中了,下面我们写出链栈的插入操作:
Status Push(LinkStack S,ElemType e)
{
LinkStack s=(LinkStack)malloc(sizeof(StackNode));
if(!s)
S->data=e;
s->next=S->top;
S->top=s;
return OK;
}
3、出栈操作(Pop)
对于以链式结构存储的栈的出栈操作如下图所示:
同样图中已经给出了关键操作代码,下面我们写出链栈的删除操作:
Status Pop(LinkStack S,ElemType *e)
{
LinkStack p;
if(StackEmpty(S))
return ERROR;
p=S->top;
*e=p->data;
S->top=p->next;
free(p);
return OK;
}
由于栈数据结构把进出栈均限制在了末尾元素(栈顶),故无论进栈还是出栈代码均不会涉及到循环,也就是说它们的时间复杂度均为 O ( 1 ) O(1) O(1)。而在空间性能上,顺序栈还是需要实现开辟空间(定长),链栈则是随用随取,区别同线性表一样,大家可根据实际情况选择。
六、栈的应用
那么为什么要定义这样一种数据结构呢?实际上,这个数据结构的应用也是十分广泛的,例如大家可以想一下浏览器的返回上一页以及Windows资源管理器的返回上一路径,是不是均符合栈的特性。当然,栈的应用绝不止这两种,接下来我们来看一看它的两种实际应用。
1、栈的应用——递归
都已经开始学习数据结构了,我想多数人对递归也不陌生了。但没听过也没关系,我在这还是引用《大话数据结构》中的示例,来为大家介绍递归。
镜子大家应该不陌生了吧,但你试过两面镜子互相照吗?就是把两面镜子互相面对面放着,你往中间一站,是不是两边都出现了无数个自己。这时,A镜子中有B镜子中的像,而B镜子中又有A镜子中的像,就有B镜子中看到A镜子中的B镜子中的A镜子…此时就会产生非常奇妙的“像中像”。这就是一种递归现象。
让我们来看两个数学上面的递归例子,第一个是阶乘运算。
Ⅰ. 阶乘运算
这是一个很简单的例子,我们都知道阶乘运算: n ! = n × ( n − 1 ) × ( n − 2 ) × ⋯ × 3 × 2 × 1 n!=n \times (n-1) \times (n-2) \times \cdots \times 3 \times 2 \times 1 n!=n×(n−1)×(n−2)×⋯×3×2×1在学完C语言循环后我们便能写出如下代码来实现阶乘运算:
int main()
{
int n,result=1;
scanf("%d",&n);
for(;n>0;n--)
result*=n;
printf("%d",result);
return 0;
}
以上便是递归的迭代运算。
但实际上它也有更正经一些的函数写法:
F
(
x
)
=
{
1
x
=
0
x
⋅
F
(
x
−
1
)
x
>
0
,
x
∈
N
∗
F(x)=\begin{cases} 1\;\;x=0 \\x \cdot F(x-1)\;\;x>0,x \in N^* \end{cases}
F(x)={1x=0x⋅F(x−1)x>0,x∈N∗下面我们来试试用C语言的函数来写出这个函数:
int F(int i)
{
if(i==0)
return 1;
return i*F(i-1);
}
int main()
{
int n;
scanf("%d",&n);
printf("%d",F(n));
return 0;
}
大家发现在函数F()
中又出现了对F()
的调用,这种函数自己对自己的调用,称为递归函数。
大家可以根据下图来尝试理解这段代码的运行过程:
它的计算过程如下:
F
(
5
)
F(5)
F(5)
↓
\downarrow
↓
5
×
F
(
4
)
5 \times F(4)
5×F(4)
↓
\downarrow
↓
5
×
4
×
F
(
3
)
5 \times 4 \times F(3)
5×4×F(3)
↓
\downarrow
↓
5
×
4
×
3
×
F
(
2
)
5 \times 4 \times 3 \times F(2)
5×4×3×F(2)
↓
\downarrow
↓
5
×
4
×
3
×
2
×
F
(
1
)
5\times4\times3\times2\times F(1)
5×4×3×2×F(1)
↓
\downarrow
↓
5
×
4
×
3
×
2
×
1
=
120
5\times4\times3\times2\times1=120
5×4×3×2×1=120
接下来是大家耳熟能详的一个例子——斐波那契数列。
Ⅱ. 斐波那契数列(Fibonacci Sequence)
如果每对兔子(一雄一雌)每月能生殖一对小兔子(也是一雄一雌,下同),每对兔子第一个月没有生殖能力,但从第二个月以后便能每月生一对小兔子。假定这些兔子都没有死亡现象,那么从第一对刚出生的兔子开始,12个月以后会有多少对兔子呢?
这是一个由数学家莱昂纳多·斐波那契所提出的一个引入问题。
我们先根据问题来写出每个月的兔子对数如下:
1
,
1
,
2
,
3
,
5
,
8
,
13
,
21
,
34
,
55
,
89
,
144
,
233
,
…
1,1,2,3,5,8,13,21,34,55,89,144,233,\dots
1,1,2,3,5,8,13,21,34,55,89,144,233,… 不难看出,从第3项开始,每项的值都是前两项之和。即这个数列符合如下函数式:
F
(
x
)
=
{
1
x
=
1
o
r
2
F
(
x
−
1
)
+
F
(
x
−
2
)
x
≥
3
,
x
∈
N
∗
F(x)=\begin{cases} 1\;\;x=1\,or\,2\\F(x-1)+F(x-2)\;\;x\geq3,x\in N^* \end{cases}
F(x)={1x=1or2F(x−1)+F(x−2)x≥3,x∈N∗ 有些同学在刚学完C语言便可以简单解决这个问题,也是这题的常规(迭代)解法:
int main()
{
int a[13]={0,1};
int i;
for(i=2;i<13;i++)
a[i]=a[i-1]+a[i-2];
printf("%d",a[12]);
return 0;
}
代码也是十分直白,就是单纯的后一项等于前两项之和。
让我们再看看递归实现:
int Fib(int i)
{
if(i<=2)
return 1;
return Fib(i-1)+Fib(i-2);
}
int main()
{
printf("%d",Fib(12));
return 0;
}
同样为方便理解这个程序运行过程,绘制出下图来形象表示(由于Fib(12)图像过于复杂,只画了Fib(5)的图):
它的计算过程如下:
F
i
b
(
5
)
Fib(5)
Fib(5)
↓
\downarrow
↓
F
i
b
(
3
)
+
F
i
b
(
4
)
Fib(3)+Fib(4)
Fib(3)+Fib(4)
↓
\downarrow
↓
(
F
i
b
(
1
)
+
F
i
b
(
2
)
)
+
(
F
i
b
(
2
)
+
F
i
b
(
3
)
)
\big(Fib(1)+Fib(2)\big)+\big(Fib(2)+Fib(3)\big)
(Fib(1)+Fib(2))+(Fib(2)+Fib(3))
↓
\downarrow
↓
(
F
i
b
(
1
)
+
F
i
b
(
2
)
)
+
(
F
i
b
(
2
)
+
(
F
i
b
(
1
)
+
F
i
b
(
2
)
)
)
\big(Fib(1)+Fib(2)\big)+\bigg( Fib(2)+\Big(Fib(1)+Fib(2)\Big)\bigg)
(Fib(1)+Fib(2))+(Fib(2)+(Fib(1)+Fib(2)))
↓
\downarrow
↓
(
1
+
1
)
+
(
1
+
(
1
+
1
)
)
=
5
(1+1)+\big(1+(1+1)\big)=5
(1+1)+(1+(1+1))=5
我们来对比一下递归和迭代:
迭代使用的是循环结构,而递归使用的是选择结构。这样使得递归更易被理解,即使用递归的代码相较于使用迭代的代码有更好的可读性。但是,递归也是有缺点的,每个递归调用都需要在栈中保存信息,包括局部变量和返回地址。对于深度递归,可能导致大量内存消耗,甚至可能导致栈溢出。且深度递归可能不如迭代高效。
说了这么多,递归和栈有什么关系呢?当一个递归函数被调用时,当前函数的状态(包括局部变量、参数和返回地址)被保存在栈上。随后,函数开始执行新的调用,每次递归调用都会创建一个新的栈帧(栈上的一个记录)。这些栈帧一层层堆叠,直到达到递归的基本情形(退出条件),此时不再进行新的递归调用。但实际上,如今我们在编写递归时不需要考虑这些,这些操作都由系统来代劳了。总之,栈的存在是十分有意义的,它也是十分重要的一个数据结构。
2、栈的应用——四则运算表达式求解
现在我们再来看一看栈的另外一个应用——数学表达式的求值。
这个问题让有些同学感到纳闷,我直接写出它不就好了吗?理论上是这样的,只要程序能跑,很少有人乐意深究其根本,但作为对栈的特性的理解,多学一些也无妨,何况这还是考试有可能会考到的内容。
不知大家小时候有没有用过普通的计算器,注意在此我说的是简单的、那种小商店算账用的计算器,它可以帮我们解决例如
152
÷
13
152 \div 13
152÷13、
1322
×
1532
1322 \times 1532
1322×1532这种简单运算,但是当碰到形如
13
+
(
35
+
755
)
×
2
−
35
÷
2
13+(35 + 755) \times 2-35 \div 2
13+(35+755)×2−35÷2这种算式时只得将其拆分开一部分一部分计算。随着科技发展,科学计算器进入了更多人的生活中,它不仅引入了四则运算,支持括号的优先级判断,甚至到现在都可以让其进行积分、求导等高阶运算了。说了这么多,那些大佬们又是如何解决不同运算符以及括号的优先级问题呢?
Ⅰ. 后缀表达式(逆波兰式)
有这么一位来自波兰的逻辑学家,他叫卢卡西维茨(Jan Lukasiewicz),他提出了一种不使用括号的运算式表达法——逆波兰式(Reverse Polish notation, RPN),为了便于理解我们将其称为后缀表达式,这个表达方法巧妙地解决了程序实现四则运算的问题。我们先来看一看例子。
中缀表达式:
10
+
(
12
+
3
)
×
2
−
50
÷
2
10+(12+3) \times 2-50 \div 2
10+(12+3)×2−50÷2
后缀表达式:
10
12
3
+
2
×
+
50
2
÷
−
10\;12\;3\;+\;2\;\times\;+\;50\;2\;\div\;-
10123+2×+502÷−
可以看到,我们日常使用的就是中缀表达式,而后缀表达式正如其名,其所有的运算符号均在要运算数字后面。看不懂没关系,我们慢慢来了解。
Ⅱ. 后缀表达式运算
要理解后缀表达式运算,我们先来看看计算机是如何应用后缀表达式来计算得结果的。
后缀表达式:
10
12
3
+
2
∗
+
50
2
/
−
10\;12\;3\;+\;2\;*\;+\;50\;2\;/\;-
10123+2∗+502/−
运算规则:从左到右遍历表达式,遇到数字则进栈,遇到符号则将处于栈顶的两个数字出栈并作对应符号的运算,后将结果入栈,直到获得最终结果。
以下是详细步骤:
1)初始化一个空栈,此栈仅用于数字的进出。
2)数字进栈,此时
10
、
12
、
3
10、12、3
10、12、3进栈。
3)接下来轮到“
+
+
+”,栈中
3
3
3和
12
12
12出栈,
12
12
12作为被加数,
3
3
3作为加数进行运算,得到结果
15
15
15进栈。
4)接着数字
2
2
2进栈。
5)轮到“
∗
*
∗”,此时
2
2
2和
15
15
15出栈并进行运算得到结果
30
30
30进栈。
6)轮到“
+
+
+”,将数字
30
30
30和
10
10
10出栈并进行运算得到结果
40
40
40进栈。
7)数字
50
50
50和
2
2
2进栈。
8)符号“
/
/
/”,将
2
2
2和
50
50
50出栈运算得
25
25
25进栈。
9)符号“
−
-
−”,将
25
25
25和
40
40
40出栈运算得到
15
15
15进栈。
10)后缀表达式结束,弹出栈顶结果
15
15
15。
以上,我们便完成了一个后缀表达式的计算,不难发现,这样的逻辑确实很适合电脑的运算逻辑。那我们又该如何将一个中缀表达式转换为后缀表达式呢?
接下来,我们便来探讨如何将日常使用的中缀表达式转换为电脑喜欢的后缀表达式。
Ⅲ. 中缀表达式与后缀表达式的转换
在进行转换前,我们需要先明确运算符号的优先级。“先乘除后加减,遇到括号先计算”,也就是我们正常做计算时的顺序。现在我们来看如何将中缀表达式转换为后缀表达式。
中缀表达式:
10
+
(
12
+
3
)
×
2
−
50
÷
2
10+(12+3) \times 2-50 \div 2
10+(12+3)×2−50÷2
转换规则:从左到右遍历表达式,遇到数字直接输出,遇到符号则先判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号的话则将栈顶元素依次出栈并输出,后将当前符号进栈,一直到最终输出结束。
以下是详细步骤:
1)初始化一个空栈,此栈仅用于符号的进出。
2)数字
10
10
10直接输出,符号“
+
+
+”进栈。
3)接下来轮到“
(
(
(”,放入栈中等待与“
)
)
)”配对,数字
12
12
12输出。
4)接着符号“
+
+
+”进栈,数字
3
3
3输出。
5)轮到符号“
)
)
)”,匹配到栈中的“
(
(
(”,将“
(
(
(”后的栈中元素依次出栈并输出。
6)轮到符号“
×
\times
×”,此时栈中栈顶符号为“
+
+
+”,其优先级别低于“
×
\times
×”,则“
×
\times
×”直接进栈。
7)轮到数字
2
2
2直接输出。
8)符号“
−
-
−”,优先级低于栈顶“
×
\times
×”,栈中元素依次出栈并输出,“
−
-
−”进栈。
9)数字
50
50
50,直接输出。
10)符号“
÷
\div
÷”,优先级大于栈顶“
−
-
−”,则入栈。
11)数字
2
2
2直接输出。
12)中缀表达式结束,将栈中剩余元素依次出栈并输出。
以上,我们成功地将一个中缀表达式: 10 + ( 12 + 3 ) × 2 − 50 ÷ 2 10+(12+3) \times 2-50 \div 2 10+(12+3)×2−50÷2转换为它的后缀表达式形式: 10 12 3 + 2 × + 50 2 ÷ − 10\;12\;3\;+\;2\;\times \; + \; 50 \; 2 \; \div \; - 10123+2×+502÷−
注:若还想了解更多的栈的应用举例,可以参阅《数据结构》,里面给出了更多问题解决中栈的应用。
七、本节总结回顾
至此,我们已经学习完了“栈”数据结构,同样,我们来回顾一下涉及知识点。
首先我们知道了栈是指限定在表尾进行插入或删除操作的线性表。同样学习了它的顺序存储方式和链式存储方式下的实现方式和对应的增(入栈)、删(出栈)操作。作为拓展,我们还了解了顺序存储方式下的两栈共享空间(通常非重点)操作。最后,我们了解了栈在两种实际问题(递归,后缀表达式)中的应用。