文章目录
栈与队列
栈
栈的定义
-
允许插入删除一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈
-
栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构
-
栈的插入操作,叫做进栈,也称压栈、入栈
-
栈的删除操作,叫做出栈,也有的叫做弹栈
进栈出栈变化形式
- 最先进栈的元素并不一定最后出栈
栈的抽象数据类型
ADT 栈(stack)
Data
同线性表元素具有相同数据类型,相邻元素具有前驱后继关系
Operation
InitStack(*S):初始化,建立空栈
DestoryStack(*S):若栈 S 存在,则销毁它
ClearStack(*S):清空
StackEmpty(S):空否,是则返回 true 否则返回 false
GetTop(S, *e):若栈存在且非空,用 e 返回 S 的栈顶元素
Push(*S, e):若栈存在,插入新元素 e 成为 S 的栈顶元素
Pop(*S, *e):删除栈顶元素,返回其值 e
StackLength(S):返回栈 S 元素个数
endADT
栈的顺序存储结构及实现
栈的顺序存储结构
- 一个大小为5的栈的不同情况如图所示
- 栈底为下标为 0 的一端,因为首元素在栈底,变化最小
- 栈顶可以变化,但不能超出栈的存储长度 StackSize
- 空栈的判定条件为 top = -1
- 栈的结构定义
#define MAXSIZE 5
typedef int SElemType;
typedef struct {
SElemType data[MAXSIZE];
int top; // 用于栈顶指针
}SqStack;
进栈(入栈、压栈)、出栈(弹栈)
bool Push(SqStack* S, SElemType e) {
if (S->top == MAXSIZE - 1) {
return false;
}
S->top++;
S->data[S->top] = e;
return true;
}
bool Pop(SqStack* S, SElemType* e) {
if (S->top == -1) {
return false;
}
*e = S->data[S->top];
S->top--;
return true;
}
两者均为涉及到循环语句,故时间复杂度为 O(1)
两栈共享空间
- 两个栈的栈底分别是一个数组的两个端点,如果两个栈增加元素,则数据向中间延伸
- 当 top1 = 0 时,栈1 空栈;当 top2 = n 时,栈2 空栈
- 当 top1 + 1 = top2 时,栈满
#define MAXSIZE 100
// 结构
typedef int SElemType;
typedef struct {
SElemType data[MAXSIZE];
int top1; // 栈1 栈顶
int top2; // 栈2 栈顶
}SqDoubleStack;
/* 入栈
* S 栈
* e 待输入的数据元素
* stackNum 为1时为 栈1,为2时为 栈2
*/
bool Push(SqDoubleStack* S, SElemType e, int stackNum) {
if (S->top1 + 1 == S->top2) {
return false;
}
if (stackNum == 1) {
S->data[++S->top1] = e;
}
else
{
S->data[--S->top2] = e;
}
return true;
}
// 出栈
bool Pop(SqDoubleStack* S, SElemType* e, int stackNum) {
if (stackNum == 1) {
if (S->top1 == -1) {
return false;
}
*e = S->data[S->top1--];
}
else
{
if (S->top2 == MAXSIZE) {
return false;
}
*e = S->data[S->top2++];
}
return true;
}
这种结构适用于两个栈的空间结构需求有互补关系时,即当一个栈数据增长,另一个栈相应的缩减的情况。一般情况并不建议使用
栈的链式存储结构及实现
栈的连式存储结构
- 栈的连式存储结构,简称链栈
- 对于链栈,基本不存在栈满的情况(栈满了电脑也崩了)
- 空栈:原定义为头指针为空,链栈空即为 top = NULL
- 链栈结构
typedef int SElemType;
typedef struct StackNode
{
SElemType data;
struct StackNode* next;
}StackNode, * LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int countl
}LinkStack;
入栈、出栈
bool Push(LinkStack* S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e; // 给新栈顶赋值
s->next = S->top; // 新栈顶指向原栈顶
S->top = s; // 栈顶指针更改为新栈顶
S->count++;
return true;
}
bool Pop(LinkStack* S, SElemType* e)
{
LinkStackPtr p;
if (S->top == NULL) {
return false;
}
p = S->top; // 临时指针指向当前栈顶
*e = p->data;
S->top = p->next; // 当前栈顶指针指向下一个栈元素
free(p); // 释放原栈顶内存空间
S->count--;
return true;
}
栈的应用
递归
经典递归:斐波那契数列
// 递归实现斐波那契数列
int Fbi(int i)
{
if (i < 2)
{
return i == 0 ? 0 : 1;
}
return Fbi(i - 1) + Fbi(i - 2); // 返回函数调用
}
int main() {
for (int i = 0; i < 10; i++)
{
cout << Fbi(i) << " ";
}
cout << endl;
return 0;
}
- 以 i = 5 的执行过程为例
递归定义
- 我们把一个直接调用自己或通过一系列语句间接调用自己的函数,称为递归函数
- 每个递归定义必须有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出
迭代与递归的区别:
- 迭代使用循环结构,递归使用选择结构
- 递归能够使程序更清晰,更简洁,更易理解,但是大量的递归调用会建立函数副本,耗费大量时间和空间
- 迭代不需要调用自身和占用额外的内存
四则运算表达式求值
后缀(逆波兰)表示法定义
- 一种不需要括号的后缀表达法,称为逆波兰(Reverse Polish Nation, RPN)表示
9 + (3 - 1) **** 3 + 10 / 2 的后缀表达式*为 9 3 1 - 3 *** + 10 2 / + ,称为后缀的原因是所有的符号都是在要运算的数字的后面出现
后缀表达式运算结果
后缀表达式:9 3 1 - 3 * + 10 2 / +
- 运算规则:从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到是符号,就将处于栈顶的两个数字出栈,进行运算,运算结果进栈,知道获得最终结果
中缀表达式 转 后缀表达式
中缀表达式 9 + (3 - 1) * 3 + 10 / 2 转为 上面的后缀表达式
- 运算规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈,直到输出最终的后缀表达式
总结
- 要使计算机具有处理中缀表达式的能力,有重要的两步:
- 将中缀表达式转换为后缀表达式(栈用于进出运算的符号)
- 将后缀表达式转换为中缀表达式(栈用来进出运算的数字)