文章目录
前言
本文介绍c语言实现栈的相关内容。
(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手------通义,DeepSeek等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)
C语言实现栈的详细解析
栈(Stack)一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端,称为栈顶,另一端称为栈底。具有后进先出 (LIFO, Last In First Out)的特性。它在函数调用、表达式求值、括号匹配等场景有广泛应用。下面我将详细讲解C语言中实现栈的两种主要方式:顺序栈 (基于数组-最常用)和链式栈(基于链表)。
一、栈的基本概念
栈的核心特性
- 后进先出:最后进入的元素最先被取出
- 操作限制 :只能在栈顶进行插入(push)和删除(pop)操作
- 基本操作时间复杂度:均为O(1)
栈的两个经典操作
- 压栈(Push):栈的插入操作,将元素放入栈顶
- 出栈(Pop):栈的删除操作,将栈顶元素移除
二、栈的实现方式
1. 顺序栈(基于数组实现)
结构体定义
c
typedef struct {
int* data; // 存储元素的数组
int size; // 栈的总容量
int top; // 栈顶指针(初始为-1,表示空栈)
} Stack;
关键点:
top初始值为-1表示空栈size限制栈的最大容量- 使用动态数组实现顺序存储
代码实现
初始化栈
c
Stack* initStack(int n) {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->data = (int*)malloc(sizeof(int) * n);
s->size = n;
s->top = -1; // 初始化栈顶指针
return s;
}
入栈操作
c
int push(Stack* s, int val) {
if (s->top == s->size - 1) {
// 栈已满,返回错误
return -1;
}
s->top++;
s->data[s->top] = val;
return 0; // 成功
}
出栈操作
c
int pop(Stack* s, int* val) {
if (s->top == -1) {
// 栈为空,返回错误
return -1;
}
*val = s->data[s->top];
s->top--;
return 0; // 成功
}
判空操作
c
int empty(Stack* s) {
return s->top == -1;
}
获取栈顶元素
c
int top(Stack* s) {
if (empty(s)) {
return -1; // 空栈返回错误值
}
return s->data[s->top];
}
顺序栈的特点
- 优点:实现简单,内存连续,访问速度快
- 缺点:容量固定,可能有空间浪费;满栈时需要扩容(增加复杂性)
完整数组实现
c
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
// 定义栈中存储的数据类型为整型
typedef int STDataType;
// 定义栈结构体
typedef struct Stack
{
STDataType* a; // 动态数组,用于存储栈元素
int top; // 栈顶指针(指向栈顶元素的下一个位置)
int capacity; // 栈的容量(当前分配的数组大小)
}ST;
// 函数声明(栈操作接口)
void STInit(ST* ps); // 初始化栈
void STDestroy(ST* ps); // 销毁栈(释放内存)
void STPush(ST* ps, STDataType x); // 入栈(压栈)
void STPop(ST* ps); // 出栈(弹栈)
STDataType STTop(ST* ps); // 获取栈顶元素
int STSize(ST* ps); // 获取栈中元素个数
bool STEmpty(ST* ps); // 判断栈是否为空
// 初始化栈
void STInit(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 初始化栈成员:
// 1. 将动态数组指针置为NULL(表示尚未分配内存)
// 2. 栈顶指针初始化为0(表示栈为空,栈顶元素在位置-1,但实际存储从0开始)
// 3. 栈容量初始化为0
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
// 销毁栈(释放内存并重置状态)
void STDestroy(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 释放动态数组内存
free(ps->a);
// 重置栈状态(避免野指针)
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
// 入栈操作(压栈)
void STPush(ST* ps, STDataType x)
{
// 确保传入的指针有效
assert(ps);
// 检查栈是否已满(top等于容量表示已无可用空间)
if (ps->top == ps->capacity)
{
// 计算新容量:如果当前容量为0(空栈),则分配4个元素空间
// 否则,容量翻倍(避免频繁扩容,提高效率)
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
// 重新分配内存,扩展栈容量
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
// 检查内存分配是否成功
if (tmp == NULL)
{
perror("realloc fail"); // 打印错误信息
return; // 分配失败,退出函数
}
// 更新栈的数组指针和容量
ps->a = tmp;
ps->capacity = newcapacity;
}
// 将元素放入栈顶位置(top指向的位置)
ps->a[ps->top] = x;
// 栈顶指针后移(指向下一个空位置)
ps->top++;
}
// 出栈操作(弹栈)
void STPop(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 检查栈是否为空(不能从空栈弹出元素)
assert(!STEmpty(ps));
// 栈顶指针前移(相当于移除栈顶元素)
ps->top--;
}
// 获取栈顶元素
STDataType STTop(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 检查栈是否为空
assert(!STEmpty(ps));
// 栈顶元素位于top-1位置(因为top指向下一个空位置)
return ps->a[ps->top - 1];
}
// 获取栈中元素个数
int STSize(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 栈中元素个数 = top(因为top表示已使用的元素数量)
return ps->top;
}
// 判断栈是否为空
bool STEmpty(ST* ps)
{
// 确保传入的指针有效
assert(ps);
// 如果栈顶指针为0,则栈为空
return ps->top == 0;
}
int main()
{
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);
STPush(&s, 3);
int top = STTop(&s);
printf("%d ", top);
STPop(&s);
STPush(&s, 4);
STPush(&s, 5);
while (!STEmpty(&s))
{
int top = STTop(&s);
printf("%d ", top);
STPop(&s);
}
STDestroy(&s);
return 0;
}
主要掌握上面数组实现即可
2. 链式栈(基于链表实现)
结构体定义
c
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* top; // 栈顶指针
int size; // 栈的大小
} Stack;
关键点:
- 栈顶即为链表的头节点
top指向栈顶元素size记录栈中元素个数
代码实现
初始化栈
c
Stack* initStack() {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->top = NULL;
s->size = 0;
return s;
}
入栈操作
c
void push(Stack* s, int val) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = val;
newNode->next = s->top;
s->top = newNode;
s->size++;
}
出栈操作
c
int pop(Stack* s, int* val) {
if (s->top == NULL) {
return -1; // 栈为空
}
Node* temp = s->top;
*val = temp->data;
s->top = temp->next;
free(temp);
s->size--;
return 0;
}
判空操作
c
int empty(Stack* s) {
return s->top == NULL;
}
获取栈顶元素
c
int top(Stack* s) {
if (empty(s)) {
return -1; // 空栈返回错误值
}
return s->top->data;
}
链式栈的特点
- 优点:空间利用率高,无需扩容,动态增长
- 缺点:实现相对复杂,内存不连续,访问速度稍慢
三、两种实现方式的对比
| 特性 | 顺序栈 | 链式栈 |
|---|---|---|
| 实现基础 | 数组 | 链表 |
| 空间利用率 | 低(可能有浪费) | 高(动态分配) |
| 扩容 | 需要扩容(可能有性能开销) | 无需扩容 |
| 插入/删除效率 | O(1) | O(1) |
| 代码复杂度 | 较低 | 较高 |
| 适用场景 | 已知栈大小,需要频繁操作 | 不知道栈大小,需要动态增长 |
四、栈的应用场景
栈在C语言中有以下广泛应用:
-
表达式求值:
- 栈可以用于存储运算符和操作数
- 实现表达式的求值算法,如中缀表达式转后缀表达式并计算结果
-
函数调用:
- 函数调用时,需要保存函数的返回地址、参数和局部变量等信息
- 这些信息使用栈来保存和管理
-
括号匹配:
- 栈可以用于检查括号是否匹配
- 遇到左括号入栈,遇到右括号出栈,最终检查栈是否为空
-
逆波兰表达式求值:
- 逆波兰表达式是一种后缀表达式
- 栈可以实现逆波兰表达式的求值
-
递归算法:
- 递归算法中,每次递归调用时需要保存当前函数的状态
- 这些状态可以使用栈来保存和管理
-
深度优先搜索:
- 栈可以用于实现图的深度优先搜索算法
五、实际应用示例
括号匹配检查
c
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char* data;
int size;
int top;
} Stack;
Stack* initStack(int n) {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->data = (char*)malloc(sizeof(char) * n);
s->size = n;
s->top = -1;
return s;
}
int push(Stack* s, char c) {
if (s->top == s->size - 1) return -1;
s->top++;
s->data[s->top] = c;
return 0;
}
int pop(Stack* s, char* c) {
if (s->top == -1) return -1;
*c = s->data[s->top];
s->top--;
return 0;
}
int empty(Stack* s) {
return s->top == -1;
}
int isMatching(char c1, char c2) {
return (c1 == '(' && c2 == ')') ||
(c1 == '[' && c2 == ']') ||
(c1 == '{' && c2 == '}');
}
int checkBrackets(char* str) {
Stack* s = initStack(100);
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] == '}') {
char c;
if (empty(s) || !isMatching(pop(s, &c), str[i])) {
return 0; // 括号不匹配
}
}
}
return empty(s); // 检查栈是否为空
}
int main() {
char str1[] = "({[()])}";
char str2[] = "({[]})";
printf("str1: %s\n", checkBrackets(str1) ? "匹配" : "不匹配");
printf("str2: %s\n", checkBrackets(str2) ? "匹配" : "不匹配");
return 0;
}
六、总结
-
栈的实现:
- 顺序栈:基于数组,实现简单,但容量固定
- 链式栈:基于链表,空间利用率高,动态增长
-
选择建议:
- 如果已知栈的大小,且对性能要求高,选择顺序栈
- 如果栈的大小不确定,需要动态增长,选择链式栈
-
栈的核心价值:
- 通过后进先出的特性,简化了复杂问题的解决
- 为表达式求值、括号匹配、函数调用等提供了高效解决方案
-
重要原则:
- 所有栈操作必须检查栈是否为空(避免空栈出栈错误)
- 顺序栈需注意容量限制,满栈时需要处理扩容
- 链式栈需注意内存管理,出栈时需释放节点内存
栈作为基础数据结构,理解其原理和实现方式对学习更复杂的数据结构和算法至关重要。希望这篇详细解析能帮助你深入理解C语言中栈的实现和应用。