文章目录
系列文章目录
前言
一、栈
1.定义(逻辑结构)
2. 基本操作(运算)
二、栈的顺序存储实现
1.顺序栈的定义
2. 基本操作
3. 共享栈
三、链栈
1.链栈的定义
2.基本操作
四、栈的应用
1.括号匹配
2.表达式求值
3.递归
总结
前言
一、栈(Stack)
1.定义
栈是只允许在一端进行插入或删除操作的线性表
(栈的英文是stack,意思是(整齐的)一堆,那意味着我们只能从这一堆的上面开始取放。)
空栈:没有数据元素;
栈顶:栈允许插入和删除的一端;
栈底:不允许插入和删除的一端;
栈顶元素
栈底元素
特点:先进后出, Last In First Out (LIFO)
2.栈的基本操作
同上次发文中的线性表的基本操作创销、增删查改。
3.常考点
二、栈的顺序存储实现
1.顺序栈的定义
顺序存储,用静态数组实现,并需要记录栈顶指针
2.基本操作的实现
#define Maxsize 10 //定义栈中元素的最大个数
typedef struct {
ElemType data[Maxsize]; //静态数组存放栈中元素
int top; //栈顶指针,值为栈顶元素的数组下标
}Sqstack;
//初始化栈
void Initstack(Sqstack& S)
{
S.top=-1; //刚开始为空栈,0处无值
//若刚开始设置S.top=0也可,其他操作的判断条件稍作变化即可
}
//判断栈空
bool Stackempty(Sqstack S)
{
if (S.top = -1)
return true;//栈空
else
return false;
}
//进栈
bool Push(Sqstack& S, ElemType x)
{
if (S.top == Maxsize - 1); //栈已满
return false;
S.top = S.top + 1; //指针先加1
S.data[S.top] = x; //新元素再入栈
//等价于S.data[++S.top]=x,但不和S.data[S.top++]=x等价
return true;
}
//出栈
bool Pop(Sqstack& S, ElemType& x)
{
if (S.top == -1)
return false;
x = S.data[S.top]; //栈顶元素先出栈
S.top = S.top - 1; //指针再减1
//同理等价于S.data[S.top--],但不和S.data[--S.top]等价
//注意:数据还残留在内存中,只是逻辑上被删除了
return true;
}
//读栈顶元素
bool GetTop(Sqstack S, ElemType& x)
{
if (S.top = -1)
return false;
x = S.data[S.top];
return true;
}
void text()
{
Sqstack S; //声明一个顺序栈(分配空间)
Initstack(S);
}
以上操作的时间复杂度都为O(1)。
3.共享栈
顺序栈的缺点:栈的大小不可变
解决方法之一:共享栈:两个栈共享一片内存空间,两个栈的栈顶指针分别从所申请存储空间的两头开始存取或删除数据元素。
#define Maxsize 10
typedef struct {
ElemType data[Maxsize];
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//初始化栈
void Initstack(Shstack& S)
{
S.top0 = -1; //初始化栈顶指针
S.top1 = Maxsize;
}
//共享栈栈满的条件为top0+1=top1
三、链栈
1.链栈的定义
用链式存储方式实现的栈,和单链表一样,每一个结点初存放数据元素外还有指向下一个结点的指针。
2.基本操作
其实和单链表差不多,只不过增删(进栈和出栈)只能在表头操作。而且进栈为单链表的前插操作,出栈为对头结点的后删操作。具体的代码实现大家可以参考单链表实现即可。并且可以实现带头结点的和不带头结点(推荐)的两种,判空操作也会略有不同。
初始化
进栈
出栈
查
判空、判满
四、栈的应用
1.栈在括号匹配中的应用
在IDE(可视化编程环境)中,括号如(){} [ ] 都是成双成对出现的
对于{(((()))[ ] ) } 可以用栈来实现该特性。即遇到左括号即入栈,遇到右括号即出栈,最后出现的左括号最先被匹配。而出栈就是每出现一个右括号则匹配最近且未被匹配的左括号。
2.表达式求值
①三种算数表达式
给出几个例子:
中缀 | 后缀 | 前缀 |
a+b | ab+ | +ab |
a+b-c | ab+c- 或 abc-+ | -+abc或 +a-bc |
a+b-c*d | ab+cd*- | -+ab*cd |
注意:同一中缀表达式可能有多种后缀表达式,但是计算机算法实现应该只有一种(算法有确定性)因此注意:左优先原则,只要左边的运算符能先计算。就优先算左边的。
后缀表达式计算为例:可从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。注意操作数是有左右顺序的。
我们注意到操作数与操作符的运算与栈的存取是一致的。
下面看看用栈实现后缀表达式的计算思路:
注意:先出栈的是右操作数
中缀表达式转前缀
右优先原则,只要右边的运算符能先计算,就优先算右边的。
用栈实现前缀表达式的计算思路:
注意:先出栈的为左操作数
3.栈在递归中的调用
先看一个函数调用的过程:
void fun1(int a)
{
int b=0;
fun2(b);//.....
}
void fun2(int b)
{
b = 2;//.....
}
int main()
{
int a=0;
fun1(a);//.....
}
在这段代码中,主函数应该是先调用函数fun1,函数fun1再调用函数fun2,然后fun2将结果返回fun1,函数fun1,再将结果返回主函数。这说明函数调用的特点是:
最后被调用的函数最先执行结束(LIFO),实际上,函数调用时,需要用一个栈来存储,每一个函数的参数,变量,返回地址等就是存在栈中的。
递归调用时,函数调用栈可成为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶;
每退出一层递归,就从栈顶弹出相应信息。
//计算n!
int fun(int n)
{
if (n == 0 || n == 1)
return 1;
else
return n * fun(n - 1);
}
int main()
{
fun(10);
return 0;
}
这个例子,在返回时,由于实在栈中存储实现的,首先返回的应该是n=1时的值,最后返回的才是为n时候的值。类似的还有斐波那契数列等。值得注意的是,太多层递归可能会导致栈溢出的问题。
4.数制转换
将十进制数转换为八进制数
算法如图:
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
typedef struct {
int top;
int data[MAXSIZE];
}Sqstack;
bool Initstack(Sqstack &S)
{
S.top = -1;
return true;
}
bool Push(Sqstack& S, int x)
{
if (S.top >= MAXSIZE)
return false;
S.top++;
S.data[S.top] = x;
return true;
}
bool Pop(Sqstack& S, int& e)
{
if (S.top == -1)
return false;
e = S.data[S.top];
S.top--;
return true;
}
bool StackEmpty(Sqstack S)
{
if (S.top == -1)
return true;
else
return false;
}
int main(void)
{
int num;
int e;
scanf_s("%d", &num);
Sqstack S;
Initstack(S);
while (num)
{
Push(S, num % 8);
num /= 8;
}
while(!StackEmpty(S))
{
Pop(S, e);
printf("%d", e);
}
return 0;
}
总结: