解码数据结构栈
栈的概念与特性
栈是线性结构的特殊形式,其设计初衷是解决 “数据需按特定顺序存取” 的场景(如函数调用、括号匹配),核心遵循 “后进先出”(LIFO,Last In First Out)原则,是计算机领域中最基础的数据结构之一。
关键定义与术语
术语 | 定义 |
---|---|
栈(Stack) | 仅允许在一端进行数据插入(入栈)和删除(出栈)的线性表 |
栈底(Bottom) | 栈的封闭端,数据一旦入栈后,只有栈顶元素全部出栈后才能被访问 |
栈顶(Top) | 栈的开放端,所有入栈、出栈操作均在此端进行,栈顶元素始终是 “最新入栈” 的数据 |
空栈 | 不包含任何元素的栈,此时栈顶指针(或下标)指向栈底外侧(如顺序栈 Top=-1) |
核心操作
栈的基本操作需满足 “仅操作栈顶” 的约束,所有操作的时间复杂度均为O(1)(遍历除外,O (n)),具体如下:
操作名称 | 功能描述 | 关键约束 |
---|---|---|
初始化(Init) | 创建空栈,并为栈的管理结构(如结构体)分配内存,初始化栈底、栈顶、容量等参数 | 确保内存分配成功(避免空指针) |
入栈(Push) | 将数据插入栈顶,更新栈顶指针(或下标) | 入栈前需判满(仅顺序栈需判满) |
出栈(Pop) | 移除栈顶数据,返回该数据,更新栈顶指针(或下标) | 出栈前需判空 |
判空(IsEmpty) | 判断栈是否为空(如顺序栈 Top=-1,链式栈栈顶指针为 NULL) | 无 |
判满(IsFull) | 仅顺序栈需实现,判断栈顶是否达到数组容量上限(Top=Size-1) | 链式栈无需判满(动态分配) |
取栈顶(GetTop) | 返回栈顶数据,但不删除该数据 | 需先判空 |
遍历(Print) | 从栈底到栈顶输出所有元素 | 无 |
销毁(Destroy) | 释放栈占用的内存(顺序栈释放数组 + 管理结构体,链式栈释放所有节点 + 栈顶指针) | 避免内存泄漏 |
操作流程示例
以 “Blue→Green→Red” 入栈出栈为例:
栈的两种实现方式
栈作为线性表,可基于数组(顺序栈) 或单链表(链式栈) 实现。
顺序栈
- 核心原理
- 用连续数组存储数据,数组下标 0 为栈底,栈顶随数据插入 / 删除动态移动(Top 初始化为 - 1,代表空栈)。
- 需用结构体管理关键参数:栈底地址(数组指针)、栈容量(数组大小)、栈顶下标(Top)。
- 定义
typedef int DataType_t;
// 2. 定义顺序栈管理结构体
typedef struct SequenceStack {
DataType_t *Bottom; // 栈底地址(指向数组首元素)
unsigned int Size; // 栈容量(数组最大存储元素个数)
int Top; // 栈顶下标(空栈时为-1,栈满时为Size-1)
} SeqStack_t;
- 初始化
SeqStack_t *SeqStack_Init(unsigned int size) {
// 分配管理结构体内存
SeqStack_t *Manager = (SeqStack_t *)malloc(sizeof(SeqStack_t));
if (Manager == NULL) {
printf("Memory allocation failed for Manager!\n");
return NULL;
}
// 分配栈数组内存
Manager->Bottom = (DataType_t *)malloc(size * sizeof(DataType_t));
if (Manager->Bottom == NULL) {
printf("Memory allocation failed for Stack Array!\n");
free(Manager); // 先释放管理结构体,避免内存泄漏
return NULL;
}
// 初始化参数
Manager->Size = size;
Manager->Top = -1; // 空栈标志
return Manager;
}
- 判空
bool SeqStack_IsEmpty(SeqStack_t *Manager) {
return Manager->Top == -1;
}
- 判满
bool SeqStack_IsFull(SeqStack_t *Manager) {
return Manager->Top == Manager->Size - 1;
}
- 入栈
bool SeqStack_Push(SeqStack_t *Manager, DataType_t data) {
// 先判满
if (SeqStack_IsFull(Manager)) {
printf("Stack is full! Push failed.\n");
return false;
}
Manager->Top++; // 栈顶上移
Manager->Bottom[Manager->Top] = data; // 存入数据
return true;
}
- 出栈
DataType_t SeqStack_Pop(SeqStack_t *Manager) {
// 先判空
if (SeqStack_IsEmpty(Manager)) {
printf("Stack is empty! Pop failed.\n");
return 0; // 此处0为默认值,实际可通过返回bool+输出参数优化
}
DataType_t topData = Manager->Bottom[Manager->Top]; // 保存栈顶数据
Manager->Top--; // 栈顶下移(逻辑删除,无需真删除数据)
return topData;
}
- 取栈顶元素
DataType_t SeqStack_GetTop(SeqStack_t *Manager) {
if (SeqStack_IsEmpty(Manager)) {
printf("Stack is empty! GetTop failed.\n");
return 0;
}
return Manager->Bottom[Manager->Top];
}
- 遍历栈(栈底到栈顶)
void SeqStack_Print(SeqStack_t *Manager) {
if (SeqStack_IsEmpty(Manager)) {
printf("Stack is empty!\n");
return;
}
printf("Stack Elements (Bottom -> Top): ");
for (int i = 0; i <= Manager->Top; i++) {
printf("%d ", Manager->Bottom[i]);
}
printf("\n");
}
- 销毁
// 二级指针销毁函数 避免野指针
void SeqStack_Destroy(SeqStack_t **Manager) {
// 先判断二级指针和目标指针是否有效
if (Manager == NULL || *Manager == NULL) {
return; // 避免对空指针解引用
}
// 释放内部数组
if ((*Manager)->Bottom != NULL) {
free((*Manager)->Bottom);
(*Manager)->Bottom = NULL;
}
// 释放管理结构体
free(*Manager);
*Manager = NULL; // 关键:直接将外部指针置为NULL
}
注:
- 顺序栈的容量固定,若需动态扩容,可在判满后重新分配更大数组(如原容量 2 倍),拷贝原数据后释放旧数组。
- 入栈 / 出栈仅操作栈顶,时间复杂度 O (1);遍历需遍历所有元素,时间复杂度 O (n)。
链式栈
- 核心原理
- 用单链表存储数据,链表头部作为栈顶(头插法入栈、头删法出栈,操作更高效),无需固定容量(动态分配节点)。
- 仅需一个栈顶指针(指向链表首节点),空栈时栈顶指针为 NULL(无需栈底指针,链表尾节点即为栈底)。
- 定义
typedef int DataType_t;
typedef struct LinkStackNode {
DataType_t data; // 节点数据
struct LinkStackNode *next; // 指向下一节点的指针
} LinkStackNode;
- 初始化
LinkStackNode *LinkStack_Init() {
return NULL; // 空栈标志:栈顶指针为NULL
}
- 判空
bool LinkStack_IsEmpty(LinkStackNode *top) {
return top == NULL;
}
- 入栈
// 入栈(头插法:新节点作为新栈顶)
LinkStackNode *LinkStack_Push(LinkStackNode *top, DataType_t data) {
// 创建新节点
LinkStackNode *newNode = (LinkStackNode *)malloc(sizeof(LinkStackNode));
if (newNode == NULL) {
printf("Memory allocation failed for new node!\n");
return top; // 入栈失败,返回原栈顶
}
// 新节点指向原栈顶
newNode->data = data;
newNode->next = top;
// 新节点成为新栈顶
return newNode;
}
- 出栈
// 出栈(头删法:删除栈顶节点,返回新栈顶)
LinkStackNode *LinkStack_Pop(LinkStackNode *top, DataType_t *outData) {
// 先判空
if (LinkStack_IsEmpty(top)) {
printf("Stack is empty! Pop failed.\n");
*outData = 0;
return top;
}
// 保存原栈顶节点和数据
LinkStackNode *oldTop = top;
*outData = oldTop->data;
// 新栈顶为原栈顶的下一节点
top = top->next;
// 释放原栈顶节点
free(oldTop);
oldTop = NULL;
return top;
}
- 取栈顶元素
bool LinkStack_GetTop(LinkStackNode *top, DataType_t *outData) {
if (LinkStack_IsEmpty(top)) {
printf("Stack is empty! GetTop failed.\n");
return false;
}
*outData = top->data;
return true;
}
- 遍历(从栈顶到栈底)
void LinkStack_Print(LinkStackNode *top) {
if (LinkStack_IsEmpty(top)) {
printf("Stack is empty!\n");
return;
}
LinkStackNode *p = top; // 辅助指针
printf("Stack Elements (Top -> Bottom): ");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
- 销毁
LinkStackNode *LinkStack_Destroy(LinkStackNode *top) {
LinkStackNode *p = top;
while (p != NULL) {
LinkStackNode *temp = p; // 保存当前节点
p = p->next; // 移动到下一节点
free(temp); // 释放当前节点
temp = NULL;
}
return NULL; // 销毁后栈顶为NULL
}
顺序栈与链式栈的对比
对比维度 | 顺序栈(数组) | 链式栈(单链表) |
---|---|---|
存储方式 | 连续内存空间(数组) | 不连续内存空间(节点动态分配) |
空间效率 | 可能浪费空间(数组容量固定,未用满) | 无空间浪费(按需分配节点) |
时间效率 | 入栈 / 出栈 O (1),扩容 O (n)(可选) | 入栈 / 出栈 O (1)(头插 / 头删) |
判满需求 | 必须判满(数组容量固定) | 无需判满(动态扩展) |
实现复杂度 | 较简单(数组操作) | 较复杂(节点指针管理,需避免内存泄漏) |
适用场景 | 已知数据规模、需高效存取的场景 | 数据规模不确定、需灵活扩展的场景 |
栈的典型应用
应用 1:十进制转十六进制(处理 0 值)
- 核心逻辑
- 原理:十进制数反复除以 16,余数作为十六进制位(逆序排列,需用栈存储余数,最后出栈即正序)。
- 补充:若输入为 0,需单独处理(原 do-while 循环不执行,需手动入栈 '0')。
代码
void SeqStack_Dec2Hex(SeqStack_t *Manager, unsigned int Data) {
// 处理Data=0的情况(否则do-while不执行,栈为空)
if (Data == 0) {
SeqStack_Push(Manager, '0');
} else {
int remainder;
do {
remainder = Data % 16;
// 余数映射:0-9→'0'-'9',10-15→'A'-'F'
if (remainder < 10) {
SeqStack_Push(Manager, remainder + '0');
} else {
SeqStack_Push(Manager, remainder - 10 + 'A');
}
Data /= 16;
} while (Data != 0);
}
// 出栈输出(栈中为逆序,出栈后为正序)
printf("Hex Result: 0x");
while (!SeqStack_IsEmpty(Manager)) {
printf("%c", (char)SeqStack_Pop(Manager));
}
printf("\n");
}
// 测试:输入0→0x0;输入255→0xFF;输入10→0xA
应用 2:有效括号判断(支持多类型括号)
- 核心逻辑
需求:判断字符串中()
、[]
、{}
是否配对(左括号入栈,右括号与栈顶匹配,最后栈需为空)。
代码
// 有效括号判断(支持()、[]、{})
bool SeqStack_IsValidBracket(SeqStack_t *Manager, const char *str) {
if (str == NULL) return false; // 空字符串无效
const char *p = str;
while (*p != '\0') {
// 左括号:入栈
if (*p == '(' || *p == '[' || *p == '{') {
SeqStack_Push(Manager, *p);
}
// 右括号:判断匹配
else if (*p == ')' || *p == ']' || *p == '}') {
// 若栈空,右括号无匹配左括号→无效
if (SeqStack_IsEmpty(Manager)) {
return false;
}
// 取出栈顶左括号,判断是否匹配
char topBracket = (char)SeqStack_Pop(Manager);
if ((*p == ')' && topBracket != '(') ||
(*p == ']' && topBracket != '[') ||
(*p == '}' && topBracket != '{')) {
return false; // 类型不匹配
}
}
// 非括号字符:忽略(若需求为“仅括号”,可返回false)
p++;
}
// 遍历结束后,栈需为空(所有左括号均闭合)
return SeqStack_IsEmpty(Manager);
}
// 测试:"()[]{}"→true;"([)]"→false;"({})"→true;"("→false
其他应用场景
-
函数调用栈:C 语言中,函数调用时会创建 “栈帧”(存储返回地址、局部变量、寄存器值),函数执行完后栈帧出栈,回到调用处(遵循 LIFO)。
-
表达式求值:将中缀表达式(如
3+4*2
)转为后缀表达式(3 4 2 * +
),用栈处理运算符优先级,再通过栈计算后缀表达式结果。 -
核心转换规则:
- 遇到 操作数(如 3、a、123):直接加入后缀表达式(多字符操作数需连续扫描完整后整体加入,如 123 作为一个整体);
- 遇到 左括号 (:直接压入运算符栈(标记 “括号开始”);
- 遇到 右括号 ):从运算符栈弹出运算符,加入后缀表达式,直到弹出左括号 (为止(左括号弹出后不加入后缀,只作为 “括号结束” 的标志);
- 遇到 其他运算符(+、-、×、/):按优先级压栈(乘除优先级 > 加减,优先级低的运算符要等栈里优先级高的弹完再压;若优先级相同或更低,需先弹出栈顶运算符加入后缀,直到栈空、栈顶是 ( 或当前运算符优先级更高,再压入当前运算符);
- 遇到 表达式扫描结束:将运算符栈中剩余的所有运算符,从栈顶依次弹出并加入后缀表达式。
-
核心计算规则:
- 遇到 操作数(如 3、a、123):直接压入操作数栈(多字符操作数需连续扫描完整后整体入栈,如 123 作为一个整体);
- 遇到 运算符(+、-、×、/):从操作数栈弹出两个操作数(先弹出的是右操作数,后弹出的是左操作数),用该运算符计算后,将结果压回操作数栈;
- 遇到 表达式扫描结束:操作数栈中剩余的唯一元素即为最终计算结果(若栈中元素数量不为 1,说明表达式非法)。
-
函数代码
// 中缀表达式转后缀表达式 void infixToPostfix(char* infix, char* postfix) { int i, j; Stack* stack = createStack(strlen(infix)); for (i = 0, j = 0; infix[i]; ++i) { // 如果是操作数,直接添加到后缀表达式 if (isdigit(infix[i]) || infix[i] == '.') { postfix[j++] = infix[i]; } // 如果是左括号,入栈 else if (infix[i] == '(') { pushOperator(stack, infix[i]); } // 如果是右括号,弹出所有运算符直到遇到左括号 else if (infix[i] == ')') { while (!isEmpty(stack) && peekOperator(stack) != '(') { postfix[j++] = ' '; // 用空格分隔不同的元素 postfix[j++] = popOperator(stack); } popOperator(stack); // 弹出左括号,不加入后缀表达式 } // 如果是运算符 else if (isOperator(infix[i])) { postfix[j++] = ' '; // 用空格分隔不同的元素 // 弹出所有优先级大于等于当前运算符的运算符 while (!isEmpty(stack) && precedence(peekOperator(stack)) >= precedence(infix[i])) { postfix[j++] = popOperator(stack); postfix[j++] = ' '; } pushOperator(stack, infix[i]); } } // 弹出栈中剩余的所有运算符 while (!isEmpty(stack)) { postfix[j++] = ' '; postfix[j++] = popOperator(stack); } postfix[j] = '\0'; free(stack->operatorItems); free(stack->numberItems); free(stack); } // 计算后缀表达式 double evaluatePostfix(char* postfix) { Stack* stack = createStack(strlen(postfix)); double num = 0, decimal = 0, decimalPlace = 0; int i; for (i = 0; postfix[i]; ++i) { // 如果是数字或小数点,解析为数字 if (isdigit(postfix[i]) || postfix[i] == '.') { if (postfix[i] == '.') { decimal = 1; decimalPlace = 1; continue; } if (decimal) { num += (postfix[i] - '0') / pow(10, decimalPlace); decimalPlace++; } else { num = num * 10 + (postfix[i] - '0'); } } // 如果是空格且之前有数字,将数字入栈 else if (postfix[i] == ' ' && (isdigit(postfix[i-1]) || postfix[i-1] == '.')) { pushNumber(stack, num); num = 0; decimal = 0; } // 如果是运算符,弹出两个数字进行运算 else if (isOperator(postfix[i])) { double val1 = popNumber(stack); double val2 = popNumber(stack); double result; switch (postfix[i]) { case '+': result = val2 + val1; break; case '-': result = val2 - val1; break; case '*': result = val2 * val1; break; case '/': if (val1 == 0) { printf("错误:除数不能为零!\n"); return NAN; } result = val2 / val1; break; case '^': result = pow(val2, val1); break; default: result = 0; } pushNumber(stack, result); } } double finalResult = popNumber(stack); free(stack->operatorItems); free(stack->numberItems); free(stack); return finalResult; }
-
浏览器前进后退:用两个栈(
forwardStack
和backStack
),浏览新页面时将当前页面压入backStack
;点击后退时将backStack
栈顶弹出,压入forwardStack
并显示;点击前进时反之。