数据结构基础:P2.2-线性结构--->堆栈

本文介绍了堆栈的概念、实现方式以及在表达式求值中的应用。堆栈作为一种后入先出的数据结构,可用于处理后缀表达式,简化计算过程。文中通过实例详细解释了后缀表达式的求值策略,并讨论了堆栈的顺序存储和链式存储实现,包括入栈、出栈操作。此外,还探讨了堆栈在函数调用、递归、回溯算法等方面的应用。
摘要由CSDN通过智能技术生成

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表


一、堆栈

1.1 什么是堆栈

堆栈是一种线性结构,也是一个特殊的线性表。堆栈在计算机学科领域里面有广泛的用途,像我们的函数调用、递归、表达式求值都要用到堆栈。那么,到底什么是堆栈呢?我们下面来给大家看一个例子。


1.1.1 后缀表达式

:算数表达式 5 + 6 / 2 − 3 ∗ 4 5+6/2-3*4 5+6/234 在计算机里面是怎么求解的。

分析:
正确理解: 5 + 6 / 2 − 3 ∗ 4 = 5 + 3 − 3 ∗ 4 = 8 − 3 ∗ 4 = 8 − 12 = − 4 5+6/2-3*4 = 5+3-3*4 = 8-3*4 = 8-12 = -4 5+6/234=5+334=834=812=4
①算数表达式由两类对象构成:
----运算数,如2、3、4
----运算符号,如+、-、*、/
②不同运算符号优先级不一样
我们的运算往往是放在两个运算数的当中,所以我们什么时候做计算?我们是看到运算符号的时候我们就希望做计算。而这个时候你只知道一个运算数,另一个运算数还在后面。但是紧挨的这个运算符号后面那个运算数呢又不一定是参与你这个运算的那个运算数。就像我们这里的5+6,你看到的6不一定是拿来做加法运算的,后面还有个优先级更高的除法运算。正是由于表达式是把运算符号放在当中,所以其实这个求值就变得比较困难了。
③反过来碰到运算符号的时候,我们已经知道两个运算数了,那这个计算就比较简单了。这种运算符号是在两个运算数后面的表达式叫做后缀表达式。


后缀表达式与中缀表达式

①中缀表达式:运算符号位于两个运算数之间。如 a+b*c-d/e
②后缀表达式:运算符号位于两个运算数之后。如 abc*+de/-
这两个表达式是等价的。


:我们来看个后缀表达式求值的例子: 62 / 3 − 42 ∗ + = ? 62/3-42*+=? 62/342+=?

计算机一般处理数据的方法是一个个地扫描,一个个地处理。处理过程如下:
①从左往右看,先碰到了一个6,记住 。再碰到了一个2,再记住。然后碰到了一个/,我们知道后续要做除法运算,而这两个除法运算的运算数前面已经有了。所以可以把前面两个数拿来除一下,得到一个结果就是等于3
②接下来我们再往后面看,又看到了一个3,又碰到了一个减号。所以这个时候要做减法,把这两个3减一减 得到一个结果就是0
③接下来我们再往后看,看到了一个4,看到了一个2。那么这个时候呢,再往后看,看到了一个乘法运算。那么我们也知道了 乘法运算是最后保存的两个数相乘,即 4 跟 2 乘,得到的结果是 等于8
④所以我们就得到了一个0一个8。接下来再往后看,看到了一个加法运算,这个时候两个运算数也知道了。0和8加起来,所以我们最后得到的结果就等于8
处理过程用动图表示如下:
在这里插入图片描述


对于后缀表达式求值,有以下总结

①后缀表达式求值策略:
从左向右扫描,逐个处理运算数和运算符号。当碰到运算数的时候,把它记住。当碰到运算符号的时候,就把最近记住的两个数来做对应的运算。
②一些问题:
遇到运算数怎么办?如何记住目前还不未参与运算的数?
遇到运算符号怎么办?对应的运算数是什么?


1.1.2 堆栈的定义

所以按照这样的策略,我们需要有一种数据结构来有效地组织记住我们的运算数。它能按顺序记住我给你的这些数,当我需要运算的时候,你能够把最后两个数拿出来做运算。所以这样的数据结构就有一个特点就是:先放进去的后拿出来,后拿进去的先拿出来做运算。那么这实际上就是堆栈

堆栈的定义:

堆栈(Stack):具有一定操作约束的线性表
只在一端(栈顶,Top)做 插入、删除
插入数据:入栈(Push)
删除数据:出栈(Pop)
后入先出:Last In First Out(LIFO)

堆栈的抽象数据类型描述:

类型名称 :堆栈(Stack)
数据对象集:一个有0个或多个元素的有穷线性表。
操作集:长度为MaxSize的堆栈 S ∈ S t a c k {\rm{S}} \in {\rm{Stack}} SStack,堆栈元素 i t e m ∈ E l e m e n t T y p e {\rm{item}} \in {\rm{ElementType}} itemElementType
1、Stack CreateStack( int MaxSize ): 生成空堆栈,其最大长度为MaxSize;
2、int IsFull( Stack S, int MaxSize ):判断堆栈S是否已满;
3、void Push( Stack S, ElementType item ):将元素item压入堆栈;
4、int IsEmpty ( Stack S ):判断堆栈S是否为空;
5、ElementType Pop( Stack S ):删除并返回栈顶元素;


我们来看看一些使用这些操作集的例子:

①一些图例
在这里插入图片描述
②Push 和 Pop 可以穿插交替进行。我们再来看一些例子,按照以下操作执行后,堆栈输出是?
----Push(S,A), Push(S,B),Push((S,C),Pop(S),Pop(S),Pop(S)
----CBA
----Push(S,A), Pop(S),Push(S,B),Push((S,C),Pop(S),Pop(S)
----ACB
③如果三个字符按ABC顺序压入堆栈
----ABC的所有排列都可能是出栈的序列吗?
----不可能
----可以产生CAB这样的序列吗?
----不能


1.2 堆栈的顺序存储实现

一个很自然的想法就是把堆栈用一个数组来进行实现。这样的堆栈不仅需要一个数组存储数据,而且还需要有一个地方来记录当前栈顶的元素在哪个位置。

1.2.1 堆栈的结构

我们可以用这样的结构来表示我们的堆栈,这个结构包含了两个分量。

①一个叫Data的数组 ,数组的分量是ElementTpye这个类型。
②用Top 这个变量来指示这个栈顶在哪个位置。这个Top不是一个地址,是一个整型变量,代表了栈顶元素它的数组下标是在哪里。

堆栈定义对应的代码如下:

#define MaxSize <储存数据元素的最大个数>
typedef struct SNode *Stack;
struct SNode{
	ElementType Data[MaxSize];
	int Top;
};

1.2.2 入栈与出栈

栈的初始状态

来看一下,假定说这里有一个堆栈如下图所示:
在这里插入图片描述
当前的Top指向的是A这个元素,那么在这个堆栈里面只有一个元素,Top值等于0。那没元素(栈空)的时候呢?栈空就是Top等于0下面一个位置(就是-1)。因此Top = -1就代表堆栈空。


现在我要开始入栈

①入栈操作首先判别一下堆栈满不满。因为我们是用数组来表示的,最多放MaxSize个元素。因为数组的下标是从0开始的,所以从0一直到MaxSize-1。当你的Top指向MaxSize-1的时候,意味着全部放满了 。那么这个时候就指示栈满,就return。
②如果栈没满,就把我们的item放到Top上面的一个位置,同时让Top值加1。

入栈操作对应代码如下:

void Push( Stack PtrS, ElementType item )
{
	if ( PtrS->Top == MaxSize-1 ) {
		printf("堆栈满"); return;
	}else {
	//这行代码执行了两个操作
	//(PtrS->Top)++;  将Top+1
	//PtrS->Data[PtrS->Top]=item; 在Top+1位置放入item
		PtrS->Data[++(PtrS->Top)] = item;
	return;
	}
}

现在我要准备出栈

经过上面入栈之后,堆栈变成下面这个样子。
在这里插入图片描述
此时Top值就等于1,指向数组的第二个分量。下面我们要把它出栈,要Pop一下。Pop之后就是要把B抛出并return出来 ,然后Top值自己要减掉,指向下面这个位置。所以出栈操作如下:
①来判别一下Top是不是等于-1,等于-1就代表了堆栈是空的,返回一个错误标志。
②如果栈不空,就去取下标为Top这个位置的值并return出来,最后将Top值减1。

出栈操作对应代码如下:

ElementType Pop( Stack PtrS )
{
	if ( PtrS->Top == -1 ) {
		printf("堆栈空");
		return ERROR; /* ERROR是ElementType的特殊值,标志错误 */
	} else
		return ( PtrS->Data[(PtrS->Top)--] );
}

1.2.3 一个数组实现两个堆栈

:请用一个数组实现两个堆栈,要求最大地利用数组空间,使数组只要有空间入栈操作就可以成功。

第一种方案:我把数组对分,左边一半是个堆栈,右边一半是另外一个堆栈。但是我们是样来做的时候会出现以下问题:当其中一个堆栈满了而另外那个堆栈没有满,这时候满了的堆栈也无法存放数据了,尽管这个数组还有空间。具体示意如下图所示:
在这里插入图片描述
我们可以换一种思路:一个数组我们做两个堆栈,分别往当中长,大家都往当中入栈。这样做很明显的好处就是只要你这个数组还有空余空间,那么我的入栈操作就可以完成。具体示意图如下:当两个栈顶相遇了,就说明数组满了。
在这里插入图片描述


定义一个包含两个堆栈的数组代码如下

#define MaxSize <存储数据元素的最大个数>
struct DStack {
	ElementType Data[MaxSize];
	int Top1; /* 堆栈1的栈顶指针 */
	int Top2; /* 堆栈2的栈顶指针 */
} S;
//此时代表两个堆栈是空的
S.Top1 = -1;
S.Top2 = MaxSize;

两个堆栈的入栈操作代码如下

void Push( struct DStack *PtrS, ElementType item, int Tag )
{ /* Tag作为区分两个堆栈的标志,取值为1和2 */
	if ( PtrS->Top2 – PtrS->Top1 == 1) { /*堆栈满*/
		printf("堆栈满"); return ;
	}
	if ( Tag == 1 ) /* 对第一个堆栈操作 */
		PtrS->Data[++(PtrS->Top1)] = item;
	else /* 对第二个堆栈操作 */
		PtrS->Data[--(PtrS->Top2)] = item;
}

两个堆栈的出栈操作代码如下

ElementType Pop( struct DStack *PtrS, int Tag )
{ /* Tag作为区分两个堆栈的标志,取值为1和2 */
	if ( Tag == 1 ) { /* 对第一个堆栈操作 */
		if ( PtrS->Top1 == -1 ) { /*堆栈1空 */
			printf("堆栈1空"); return NULL;
		} else return PtrS->Data[(PtrS->Top1)--];
	} else { /* 对第二个堆栈操作 */
		if ( PtrS->Top2 == MaxSize ) { /*堆栈2空 */
			printf("堆栈2空"); return NULL;
		} else return PtrS->Data[(PtrS->Top2)++];
	}
}

1.3 堆栈的链式存储实现

我们前面讲了怎么用数组来实现一个堆栈,同样的,也可以用链表来实现堆栈。

1.3.1 堆栈Top的设置

我们这里用一个单向链表来实现我们的堆栈,当然由于堆栈的特点,我们的插入和删除操作只能在这条链的一头进行。但单向链表有两个头:链表头和链表尾。我们究竟是用哪一头来作为我们的Top?分析如下图所示:

①单向链表的头作为我们的Top时,在这一头要做插入操作跟删除操作显然是方便的
在这里插入图片描述
②反过来,我们如果把链的尾巴作为我们的Top,我们看一下插入操作没问题,我们可以用当前的这个链表的尾巴指向一个新的结点。但删除就有问题了,删了之后它前一个结点在哪里?因为这是单向链表,找不到前面一个结点。所以链尾是不能作为Top的。
在这里插入图片描述


1.3.2 链表实现堆栈的代码

我们定义一个链表实现的堆栈如下:

typedef struct SNode *Stack;
struct SNode{
	ElementType Data;
	struct SNode *Next;
} ;

我们来看对应的一些初始化操作:堆栈初始化(建立空栈)、判断堆栈是否为空。

首先为了操作方便,我们创建一个堆栈头结点S(不是Top结点),让其Next指向Top结点。
建立空栈时,我们让创建的堆栈头结点S的next先指向一个NULL。
在后续的操作中,这个堆栈头结点S一直存在,指向堆栈的Top。
因此,判断堆栈是否为空只需判断堆栈头结点S的next是否为NULL即可。
在这里插入图片描述

堆栈初始化相应的代码如下:

Stack CreateStack()
{ /* 构建一个堆栈的头结点,返回指针 */
	Stack S;
	S =(Stack)malloc(sizeof(struct SNode));
	S->Next = NULL;
	return S;
}
int IsEmpty(Stack S)
{ /*判断堆栈S是否为空,若为空函数返回整数1,否则返回0 */
	return ( S->Next == NULL );
}

入栈操作

void Push( ElementType item, Stack S)
{ /* 将元素item压入堆栈S */
	struct SNode *TmpCell;
	TmpCell=(struct SNode *)malloc(sizeof(struct SNode));
	TmpCell->Element = item;
	TmpCell->Next = S->Next;
	S->Next = TmpCell;
}

出栈操作

ElementType Pop(Stack S)
{ /* 删除并返回堆栈S的栈顶元素 */
	struct SNode *FirstCell;
	ElementType TopElem;
	if( IsEmpty( S ) ) {
		printf("堆栈空"); return NULL;
	} else {
		FirstCell = S->Next;
		S->Next = FirstCell->Next;
		TopElem = FirstCell ->Element;
		free(FirstCell);
		return TopElem;
	}
}

2.4 堆栈应用:表达式求值

前面我们已经讲了后缀表达式的一种实现方法,也就是用堆栈来进行实现,从左到右读入后缀表达式的各项(运算符或运算数)。

运算数:入栈;
运算符:从堆栈中弹出适当数量的运算数,计算并结果入栈;
最后,堆栈顶上的元素就是表达式的结果值。

然而我们日常生活当中、工作当中经常使用的是中缀表达式,这个时候需要能把中缀表达式转换为后缀表达式再求值。如何进行转换呢?我们先来看看一个例子: a ∗ ( b + c ) / d = ? a*( b+c )/d = ? a(b+c)/d=?

具体流程为:从头到尾读取中缀表达式的每个对象,对不同对象按不同的情况处理。
① 运算数:直接输出;
② 左括号:压入堆栈;
③ 右括号:将栈顶的运算符弹出并输出,直到遇到左括号(出栈,不输出);
④ 运算符:
----若优先级大于栈顶运算符时,则把它压栈;
----若优先级小于等于栈顶运算符时,将栈顶运算符弹出并输出;再比较新的栈顶运算符,直到该运算符大于栈顶运算符优先级为止,然后将该运算符压栈;
⑤ 若各对象处理完毕,则把堆栈中存留的运算符一并输出
操作流程对应的动图如下:
在这里插入图片描述


:将前缀表达式 ( 2 ∗ ( 9 + 6 / 3 − 5 ) + 4 ) (2*(9+6/3-5)+4) 2(9+6/35)+4)修改为后缀表达式输出,详细过程如下图所示:
在这里插入图片描述


堆栈的其他应用:

函数调用及递归实现
在程序设计语言里面,我们从一个函数调用另外一个函数,等那个函数执行完了之后再回过头来。这里需要解决一个问题,就是那边调用完了回到哪里,还有回来的时候原来的状态要能恢复出来。所以这个时候我们需要把调用之前的一些变量的状态以及调用回来之后准备要执行的程序的地址保存起来。
我们从这个函数调用另外一个函数,到那个函数的时候能还会再调下一个函数,下一个函数还可能还会再调下一个函数。如果是有一系列函数的调用过程,我们就需要保存一系列的东西。而函数调用回来的时候是倒过来一步一步地返回,a调用b b调用c,然后c回到b b再回到a。所以我们需要有一个管理的机制,能够把我们调用之前的一些变量状态以及我们准备要返回的地址保留起来。而这个过程可能是一系列执行的,所以你需要一系列地恢复过来。样的一种能够保存这些变量跟返回地址的一种数据结构的组织方式必须要有一个特点:就是按照顺序地存,然后按倒过来的顺序返回,这个特点实际上就是堆栈。
回溯算法
回溯法是算法设计中一种主要思想,以老鼠走迷宫为例。我们给了一个迷宫,老鼠要从一个入口进去要找到一条路径,从一个出口出来。在这个过程当中 ,没有什么特别的算法能做这个事情,只能不断地试探各种可能性。从这一步试探下一步哪个地方可以走,下一步再来试探各种可能性。当到某一步各种可能性试探都走不通的时候,它要回到上一步的位置,然后在上一步的位置进一步地再去试探。所以这个过程要把我们试探的路径保存起来,等到哪一步试探不成功的时候要回到最近一次试探的状态。所以这个实际上也是堆栈的特性,所以有了堆栈我们的回溯算法就可以很容易地实现。
深度优先搜索
④…


小测验

1、借助堆栈将中缀表达式 A − ( B − C / D ) ∗ E A-(B-C/D)*E A(BC/D)E 转换为后缀表达式,则该堆栈的大小至少为:

A. 2
B. 3
C. 4
D. 5

答案:C

2、设 1 、 2 、 … 、 n – 1 、 n 1、2、…、n–1、n 12n1n n n n 个数按顺序入栈,若第一个出栈的元素是 n n n,则第三个出栈的元素是:

A. 3
B. n-2
C. n-3
D. 任何元素均可能

答案:B

3、若用单向链表实现一个堆栈,当前链表状态为:1->2->3。当对该堆栈执行pop()、push(4)操作后,链表状态变成怎样?

(1)4->2->3
(2)1->2->4
A. 只能是(1)
B. 只能是(2)
C.(1)和(2)都有可能
D.(1)和(2)都不可能

答案:A

4、如果一堆栈的输入序列是aAbBc,输出为 abcBA,那么该堆栈所进行的操作序列是什么? 设P代表入栈,O代表出栈。

A. PPPOOPOPOO
B. POOPPPOPOO
C. POPPOPPOOO
D. PPOPPOOOPO

答案:C

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值