目录
栈和队列是计算机科学中常见的数据结构,它们都是一种线性数据结构,可以对元素进行快速的插入、删除和查找操作。栈和队列都可以用于各种不同的应用场景,不过它们的使用方式和特点有所不同。
栈
首先,让我们来了解一下栈的概念。栈是一种具有“后进先出”(Last In First Out,LIFO)特性的数据结构,如图:
只有栈顶元素是可以访问的。新加入的元素会直接放在栈顶,而每次需要访问栈元素时,都会从栈顶开始弹出元素。栈常用于实现函数调用、表达式求值、括号匹配等场景。通常使用 push() 方法将一个元素压入栈中,使用 pop() 方法将栈顶元素弹出。
栈结构包括两类:顺序栈结构和链式栈结构。
顺序栈结构
顺序栈结构使用一组地址连续的内存单元依次保存栈中的数据。在程序中,可以定义一个指定大小的结构数组作为栈,定义一个变量top保存栈顶序号,初始为-1表示空栈。顺序栈的定义和常见操作及代码如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char *data;
int top;
int maxSize;
} Stack;
// 初始化栈
void initStack(Stack *stack, int maxSize) {
stack->data = (char *)malloc(sizeof(char) * maxSize);
stack->top = -1;
stack->maxSize = maxSize;
}
// 判断栈是否为空
int isEmpty(Stack *stack) {
return stack->top == -1;
}
// 判断栈是否已满
int isFull(Stack *stack) {
return stack->top == stack->maxSize;
}
// 入栈
void push(Stack *stack, char element) {
if (isFull(stack)) {
printf("栈已满,无法入栈!\n");
return;
}
stack->data[++stack->top] = element;
}
// 出栈
char pop(Stack *stack) {
if (isEmpty(stack)) {
printf("栈已空,无法出栈!\n");
return 0;
}
return stack->data[stack->top--];
}
// 读取栈顶元素
char peek(Stack *stack) {
if (isEmpty(stack)) {
printf("栈已空,无法读取栈顶元素!\n");
return 0;
}
return stack->data[stack->top];
}
// 清空栈
void clear(Stack *stack) {
stack->top = -1;
}
// 销毁栈
void destroy(Stack *stack) {
free(stack->data);
stack->data = NULL;
stack->top = -1;
stack->maxSize = 0;
}
链式栈结构
链式栈结构使用链表保存栈元素值,链表头部为栈顶,链表尾部为栈底。链式栈的定义和常见操作及代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DATA_SIZE 20
// 链式栈结点结构
typedef struct Node {
char data[DATA_SIZE];
struct Node *next;
} Node, *LinkedStack;
// 初始化栈
void initStack(LinkedStack *top) {
*top = NULL;
}
// 判断栈是否为空
int isEmpty(LinkedStack *top) {
return *top == NULL;
}
// 入栈
void push(LinkedStack *top, char *data) {
//printf("push: %s ", data);
Node *node = (Node *)malloc(sizeof(Node));
strcpy(node->data, data);
node->next = *top;
*top = node;
}
// 出栈
void pop(LinkedStack *top, char *data) {
if (isEmpty(top)) {
printf("栈已空,无法出栈!\n");
return;
}
Node *node = *top;
*top = node->next;
strcpy(data, node->data);
//printf("pop: %s ", data);
free(node);
}
// 读取栈顶元素
char* peek(LinkedStack *top) {
if (isEmpty(top)) {
printf("栈已空,无法读取栈顶元素!\n");
return NULL;
}
return (*top)->data;
}
// 清空栈
void clear(LinkedStack *top) {
while (!isEmpty(top)) {
Node *node = *top;
*top = node->next;
free(node);
}
}
// 销毁栈
void destroy(LinkedStack *top) {
clear(top);
}
// 打印栈
void print(LinkedStack *top) {
if (isEmpty(top)) {
printf("栈已空,无法打印!\n");
return;
}
printf("栈中元素:");
Node *node = *top;
while (node != NULL) {
printf("%s ", node->data);
node = node->next;
}
printf("\n");
}
中缀表达式和后缀表达式
前面提到栈可以用于实现表达式求值,在展示这个案例前,我们先了解一下表达式求值的应用过程。我们平时使用的表达式如“2*(5-1)”是中缀表达式,使用括号来明确运算符的优先级和结合性。后缀表达式(也称为逆波兰表达式),是一种将运算符放置在操作数之后的表示方法,将前面的中缀表达式转换为后缀表达式就是“251-*”。将中缀表达式转换为后缀表达式,可以使表达式更容易计算,而不需要考虑括号和运算符优先级。将中缀表达式 "2*(5-1)" 转换为后缀表达式的过程如下:
- 从左到右遍历中缀表达式 "2*(5-1)";
- 遇到操作数 2,将其添加到后缀表达式列表中;
- 遇到运算符 *,将其入栈;
- 遇到左括号 (,将其入栈;
- 遇到操作数 5,将其添加到后缀表达式列表中;
- 遇到运算符 -,与栈顶运算符 * 比较,栈顶优先级较低,将 - 入栈(如果栈顶运算符优先级高,则弹出并添加到后缀表达式列表中)。
- 遇到操作数 1,将其添加到后缀表达式列表中;
- 遇到右括号 ),将栈顶的运算符 - * 依次弹出并添加到后缀表达式列表中,直到遇到左括号 ( 时停止,左括号丢弃;
- 中缀表达式遍历完成后,如果栈中还有剩余运算符,则将栈中剩余的运算符依次弹出并添加到后缀表达式列表中。
处理后缀表达式求值的过程则相对简单,因为不需要考虑括号和运算符优先级:
- 从左到右遍历后缀表达式中的每个元素;
- 如果遇到操作数,将其压入栈中;
- 如果遇到运算符,从栈中弹出 2 个操作数,根据运算符进行计算,并将结果压入栈中;
- 重复步骤 2 和步骤 3,直到遍历完整个后缀表达式。
- 最后,栈中将只剩下一个元素&#