目录
0 引言
1 栈在括号匹配中的应用
2 栈在表达式求值中的应用
2.1 算数表达式
2.2 中缀表达式转后缀表达式
2.3 后缀表达式求值
3 栈在递归中的应用
3.1 栈在函数调用中的作用
3.2 栈在函数调用中的工作原理
4 总结
0 引言
栈(Stack)是一种非常基本且重要的数据结构,它们在许多计算机科学和软件工程的应用中都有广泛的用途。
栈:
①括号匹配;
②表达式求值;
③递归函数调用。
1 栈在括号匹配中的应用
表达式中有两种括号:圆括号 ( ) 和 方括号 [ ],嵌套的顺序任意,但应为正确的格式。
例如:( ( [ ] [ ] ) ) 为正确格式。
但如何用算法实现括号匹配问题?
思路如下:
(1)初始一个空栈;
(2)顺序读入括号;
(3)当读入的为左括号,将继续读入括号,直到读入第一个右括号。那将检测与之最近的左括号是否与之相匹配,若匹配,则出栈;若不匹配,则退出程序。当程序结束时,栈为空。反之,则表明括号序列的格式不正确。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_SIZE 100 // 假设栈的最大大小
typedef struct {
char data[MAX_SIZE];
int top;
} Stack;
// 初始化栈
void initStack(Stack *s) {
s->top = -1;
}
// 判断栈是否为空
bool isEmpty(Stack *s) {
return s->top == -1;
}
// 入栈
void push(Stack *s, char c) {
if (s->top >= MAX_SIZE - 1) {
printf("Stack overflow\n");
return;
}
s->data[++s->top] = c;
}
// 出栈
char pop(Stack *s) {
if (isEmpty(s)) {
printf("Stack underflow\n");
return '#'; // 返回一个无效字符,或可以选择抛出一个错误
}
return s->data[s->top--];
}
// 检查两个括号是否匹配
bool isMatch(char c1, char c2) {
if (c1 == '(' && c2 == ')') return true;
if (c1 == '[' && c2 == ']') return true;
if (c1 == '{' && c2 == '}') return true;
return false;
}
// 括号匹配函数
bool isBalanced(char *str) {
Stack s;
initStack(&s);
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
push(&s, str[i]);
} else if (str[i] == ')' || str[i] == ']' || str[i] == '}') {
if (isEmpty(&s)) {
// 栈为空,但遇到了右括号,不匹配
return false;
}
char topChar = pop(&s);
if (!isMatch(topChar, str[i])) {
// 栈顶元素与当前右括号不匹配
return false;
}
}
}
// 如果栈为空,则所有括号都匹配
return isEmpty(&s);
}
int main() {
char str[MAX_SIZE];
printf("Enter a string with brackets: ");
scanf("%s", str);
if (isBalanced(str)) {
printf("The brackets are balanced.\n");
} else {
printf("The brackets are not balanced.\n");
}
return 0;
}
2 栈在表达式求值中的应用
2.1 算数表达式
中缀表达式是人们常用的算术表达式,即操作符以中缀形式处于操作数之间。但在计算机中,中缀表达式相较于前缀和后缀表达式来说,更不易被计算机识别。前缀表达式成为波兰式,后缀表达式又称逆波兰式。
2.2 中缀表达式转后缀表达式
(1)手算方法:
①根据运算顺序对表达式运算符排号;
②根据运算符排号顺序,将运算符及两端的操作数以(左操作数 右操作数 运算符)的顺序重新组合。
例如:( A + B ) * C + ( D - E ) / F 转后缀表达式的过程如下:
(2)算法实现:
①初始一个栈;
②遇到操作数,直接加入后缀表达式;
③遇到界限符,若为左括号直接入栈,若为右括号,则依次弹出栈中的运算符,加入后缀表达式,知道弹出左括号为止。需要注意的是,左括号和右括号直接删除,不加入后缀表达式。
④遇到运算符,则看运算符的优先级,若高于除左括号外的栈顶元素,则直接入栈。反之,则依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,直到遇到低于他的优先级的运算符,才入栈。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX_SIZE 100
typedef struct {
char data[MAX_SIZE];
int top;
} Stack;
// 初始化栈
void initStack(Stack *s) {
s->top = -1;
}
// 判断栈是否为空
bool isEmpty(Stack *s) {
return s->top == -1;
}
// 入栈
bool push(Stack *s, char c) {
if (s->top >= MAX_SIZE - 1) {
return false; // 栈溢出
}
s->data[++s->top] = c;
return true;
}
// 出栈
char pop(Stack *s) {
if (isEmpty(s)) {
return '\0'; // 栈空,返回空字符
}
return s->data[s->top--];
}
// 获取栈顶元素,但不弹出
char peek(Stack *s) {
if (isEmpty(s)) {
return '\0'; // 栈空,返回空字符
}
return s->data[s->top];
}
// 运算符的优先级比较(这里只处理了基本的四则运算)
int precedence(char op) {
if (op == '+' || op == '-') {
return 1;
}
if (op == '*' || op == '/') {
return 2;
}
return 0; // 如果不是运算符,返回0
}
// 将中缀表达式转换为后缀表达式
void infixToPostfix(char *infix, char *postfix) {
Stack s;
initStack(&s);
int i = 0, j = 0;
while (infix[i] != '\0') {
if (infix[i] >= '0' && infix[i] <= '9') {
// 如果是操作数,直接添加到后缀表达式中
postfix[j++] = infix[i++];
postfix[j++] = ' '; // 假设操作数都是个位数,用空格分隔
} else if (infix[i] == '(') {
// 如果是左括号,直接入栈
push(&s, infix[i++]);
} else if (infix[i] == ')') {
// 如果是右括号,则弹出栈中元素直到遇到左括号
while (!isEmpty(&s) && peek(&s) != '(') {
postfix[j++] = pop(&s);
postfix[j++] = ' ';
}
// 弹出左括号,但不加入后缀表达式
pop(&s);
i++;
} else {
// 如果是运算符
while (!isEmpty(&s) && precedence(peek(&s)) >= precedence(infix[i])) {
// 如果栈不为空且栈顶元素优先级高于或等于当前运算符,弹出栈顶元素
postfix[j++] = pop(&s);
postfix[j++] = ' ';
}
// 当前运算符入栈
push(&s, infix[i++]);
}
}
// 弹出栈中剩余的所有运算符
while (!isEmpty(&s)) {
postfix[j++] = pop(&s);
postfix[j++] = ' ';
}
// 添加字符串结束符
postfix[j] = '\0';
}
int main() {
char infix[MAX_SIZE], postfix[MAX_SIZE * 2]; // 后缀表达式可能更长,因此分配更多空间
printf("Enter an infix expression: ");
scanf("%s", infix); // 注意:这里不会处理空格和复杂输入
infixToPostfix(infix, postfix);
printf("Postfix expression: %s\n", postfix);
return 0;
}
2.3 后缀表达式求值
后缀表达式(也称为逆波兰表示法或逆波兰记法)是一种不需要括号来标明运算符的优先级的数学表达式。在这种表示法中,所有的运算符都放在操作数的后面。
求值后缀表达式的基本步骤如下:
- 初始化一个栈,用于存储操作数。
- 从左到右扫描后缀表达式。
- 如果扫描到操作数,则将其压入栈中。
- 如果扫描到运算符,则从栈中弹出两个操作数(先弹出的为右操作数,后弹出的为左操作数),将这两个操作数作为运算符的输入进行运算,然后将结果压回栈中。
- 重复步骤2-4,直到后缀表达式扫描完毕。
- 栈中剩下的元素就是表达式的值。
示例:
后缀表达式:3 4 + 5 *
求值过程:
- 扫描到
3
,压入栈:[3]
- 扫描到
4
,压入栈:[3, 4]
- 扫描到
+
,弹出4
和3
,计算3 + 4
得到7
,压入栈:[7]
- 扫描到
5
,压入栈:[7, 5]
- 扫描到
*
,弹出5
和7
,计算7 * 5
得到35
,压入栈:[35]
- 扫描完毕,栈中元素
35
即为表达式的值。
下面是实现代码(以上述示例为例):
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#define MAX_STACK_SIZE 100
typedef struct {
double data[MAX_STACK_SIZE];
int top;
} Stack;
// 初始化栈
void initStack(Stack *s) {
s->top = -1;
}
// 判断栈是否为空
int isEmpty(Stack *s) {
return s->top == -1;
}
// 压栈操作
void push(Stack *s, double value) {
if (s->top >= MAX_STACK_SIZE - 1) {
printf("Stack overflow\n");
exit(1);
}
s->data[++s->top] = value;
}
// 弹栈操作
double pop(Stack *s) {
if (isEmpty(s)) {
printf("Stack underflow\n");
exit(1);
}
return s->data[s->top--];
}
// 求值后缀表达式
double evaluatePostfix(const char *postfix) {
Stack s;
initStack(&s);
const char *token = strtok((char *)postfix, " "); // 假设操作符和操作数之间用空格分隔
while (token != NULL) {
if (isdigit(token[0])) { // 如果是操作数
double value = atof(token);
push(&s, value);
} else { // 如果是运算符
double rightOperand = pop(&s); // 弹出右操作数
double leftOperand = pop(&s); // 弹出左操作数
switch (token[0]) {
case '+':
push(&s, leftOperand + rightOperand);
break;
case '-':
push(&s, leftOperand - rightOperand);
break;
case '*':
push(&s, leftOperand * rightOperand);
break;
case '/':
if (rightOperand != 0.0) {
push(&s, leftOperand / rightOperand);
} else {
printf("Error: Division by zero\n");
exit(1);
}
break;
default:
printf("Error: Unknown operator\n");
exit(1);
}
}
token = strtok(NULL, " "); // 继续获取下一个token
}
if (!isEmpty(&s)) {
return pop(&s); // 栈中剩下的元素就是表达式的值
} else {
printf("Error: Invalid postfix expression\n");
exit(1);
}
}
int main() {
const char *postfix = "3 4 + 5 *";
double result = evaluatePostfix(postfix);
printf("Result: %lf\n", result);
return 0;
}
3 栈在递归中的应用
3.1 栈在函数调用中的作用
- 参数传递:当调用一个函数时,需要传递参数给该函数。这些参数会被压入栈中,以便函数内部能够访问和使用它们。
- 局部变量分配:函数内部定义的局部变量会在栈上分配空间。这些变量的生命周期与函数的执行周期相同,当函数执行完毕后,这些局部变量所占用的栈空间会被自动释放。
- 保存调用的返回地址:在函数调用时,CPU需要知道函数执行完毕后应该返回到哪个位置继续执行。这个返回地址会被保存在栈中,以便函数执行完毕后能够正确地返回到调用它的位置。
- 保存寄存器以供恢复:在函数调用和返回的过程中,CPU的寄存器状态会发生变化。为了能够在函数返回后恢复原来的寄存器状态,栈会保存这些寄存器的值。
3.2 栈在函数调用中的工作原理
- 函数调用:当调用一个函数时,系统首先会创建一个新的栈帧(stack frame)来保存该函数的执行环境。这个栈帧包含了函数的返回地址、参数、局部变量等信息。然后,系统会将当前程序的执行状态(如返回地址、寄存器状态等)压入栈中,以便在函数执行完毕后能够恢复。
- 函数执行:在函数执行过程中,函数会访问栈帧中的参数和局部变量,并根据需要进行计算和操作。同时,如果函数内部调用了其他函数,系统也会为这些被调用的函数创建新的栈帧,并将当前函数的执行状态压入栈中保存。
- 函数返回:当函数执行完毕或者遇到return语句时,系统会弹出当前函数的栈帧,并根据栈帧中的返回地址返回到调用它的位置继续执行。在返回之前,系统还会恢复调用该函数时的寄存器状态。
下面将给出一个例子:
例如:阶乘,大家可以自行调试;
#include <stdio.h>
int step(int n){
if(n==1)
return 1;
else
return n*step(n-1);
}
int main(){
int n,s;
scanf("%d",&n);
s=step(n);
printf("%d",s);
}
4 总结
在本文中,我们深入探讨了栈这一数据结构及其在各种应用场景中的重要作用。栈作为一种后进先出(LIFO)的数据结构,其独特的操作方式——压栈(push)和弹栈(pop),使得它在计算机科学和软件开发中占据了不可或缺的地位。
详细讨论了栈在多个领域中的应用。其中,后缀表达式的求值是一个经典的栈应用示例。在这个问题中,我们利用栈来存储操作数,并通过操作数的弹出和结果的压入,实现了表达式的正确计算。这种方法不仅简化了表达式的处理流程,而且提高了计算效率。
此外,栈还在函数调用、递归等方面发挥着重要作用。在函数调用中,栈用于存储局部变量和返回地址,确保函数能够正确地返回并继续执行。在递归算法中,栈用于保存递归调用的中间结果,从而避免重复计算。
综上所述,栈作为一种基本而强大的数据结构,在各个领域都有着广泛的应用。通过学习和掌握栈的使用方法和应用场景,我们能够更好地解决实际问题,提高编程效率。