目录
1.栈的实现
关于栈的实现,也是三个文件,头文件,实现文件和测试文件。
1.1 头文件(Stack.h)
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义栈的结构
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top; //指向栈顶的位置
int capacity;//栈的容量
}ST;
//初始化
void StackInit(ST* ps);
//销毁
void StackDestroy(ST* ps);
//入栈---栈顶
void StackPush(ST* ps, STDataType x);
//出栈——栈顶
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//获取栈中有效元素个数
int StackSize(ST* ps);
//栈是否为空
bool StackEmpty(ST* ps);
栈的头文件( Stack.h ),包含了栈的类型定义和核心操作函数的声明,是栈实现的基础。下面详
细解释每一部分的作用和设计逻辑:
1. 头文件包含与预处理
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
- #pragma once :
防止头文件被重复包含(比如在多个.c文件中同时包含 Stack.h 时,避免结构体、函数声明重复定
义导致编译错误),是C语言中常用的头文件保护机制(类似 #ifndef + #define + #endif )。
- 包含的标准库:
- stdio.h :提供输入输出函数(如后续实现中可能用到的 printf 调试)。
- stdlib.h :提供动态内存管理函数( malloc 、 realloc 、 free ,栈的动态数组实现依赖这些)。
- assert.h :提供 assert 宏,用于调试阶段检查参数有效性(如避免传入 NULL 指针操作栈)。
- stdbool.h :提供 bool 类型( true / false ),用于 StackEmpty 函数返回栈是否为空的状态。
2. 栈的类型定义
// 定义栈存储的数据类型
typedef int STDataType;
// 栈的结构体定义
typedef struct Stack
{
STDataType* arr; // 动态数组,存储栈的元素
int top; // 栈顶指针(索引),标记栈顶元素的位置
int capacity; // 栈的容量,记录当前分配的内存可存储的最大元素个数
} ST;
这部分是栈的核心设计,决定了栈的存储结构和操作基础:
- STDataType :
用 typedef 定义栈存储的数据类型为 int ,方便后续修改栈存储的数据类型(比如要存 char 或自定
义结构体,只需修改这里为 typedef char STDataType 即可,无需改动所有函数)。
- 结构体 struct Stack (重命名为 ST ):
- arr :动态数组指针,栈的元素实际存储在这片连续的内存中(数组实现栈的核心,相比链表更
高效)。
- top :栈顶索引,表示栈中储存了几个数据,有两种常见设计(需与后续函数实现配合):
- 设计1: top 初始为 -1 ,表示“栈顶元素的索引”(空栈时无元素, top=-1 ;入栈时先 top++ ,再
存数据;栈顶元素为 arr[top] )。
- 设计2: top 初始为 0 ,表示“下一个入栈元素的位置”(空栈时 top=0 ;入栈时先存数据,
再 top++ ;栈顶元素为 arr[top-1] )。
(注:两种设计均可,需在函数实现中保持逻辑一致,这里更可能是设计1,因为 StackSize 函数可
直接通过 top+1 计算元素个数)。
- capacity :记录当前栈的最大容量( arr 指向的数组能存多少个元素),用于判断入栈时是否需
要扩容(避免每次入栈都重新分配内存,提升效率)。
3. 栈的核心操作函数声明
函数声明规定了栈的所有可操作接口,后续在 .c 文件中实现这些函数即可完成栈的功能,这些函数
将在下面的.c实现文件中一一讲解。
1.2 实现文件(Stack.c)
以下是栈的 .c 实现文件中各函数的详细逻辑、时间复杂度和空间复杂度分析(结合头文件的结构
体定义,栈基于动态数组实现):
1. 初始化函数--StackInit函数
#include"Stack.h" void StackInit(ST* ps) { ps->arr = NULL; ps->top = ps->capacity = 0; }
- 逻辑:
将栈的动态数组指针 arr 设为 NULL ,栈顶索引 top 和容量 capacity 均初始化为 0 。这
里 top 的设计是**“下一个入栈元素的位置”**(初始时栈为空,第一个元素将存在 arr[0] ,
入栈后 top 自增为 1 )。
- 时间复杂度:O(1)(仅执行固定次数的赋值操作)。
- 空间复杂度:O(1)(不额外分配空间,仅初始化已有结构体成员)。
2. 入栈函数--StackPush函数
void StackPush(ST* ps, STDataType x) { assert(ps); // 确保栈指针非空,否则调试阶段报错 // 检查是否需要扩容(栈满:top == capacity) if (ps->top == ps->capacity) { // 计算新容量:初始容量为0则设为4,否则翻倍 int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; // 重新分配内存(扩大数组容量) STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType)); if (tmp == NULL) // 内存分配失败处理 { perror("realloc fail!"); // 打印错误信息 exit(1); // 终止程序 } ps->arr = tmp; // 更新数组指针为新内存 ps->capacity = newCapacity; // 更新容量 } // 入栈:将x存入top位置,然后top自增(更新下一个入栈位置) ps->arr[ps->top++] = x; }
- 逻辑:
1. 先通过 assert(ps) 确保栈指针有效(防止传入 NULL 导致崩溃)。2. 检查栈是否已满( top == capacity ):若满则扩容(初始容量为 4 ,之后每次翻倍,平
衡扩容频率和空间效率)。
3. 扩容使用 realloc :若原数组为 NULL (初始状态),则(等效于 malloc )分配新空间;否则扩大已有空间。
4. 入栈操作:将元素 x 存入 arr[top] ,然后 top++ (下一次入栈位置后移)。
- 时间复杂度:
- 平均情况:O(1)(大部分入栈操作无需扩容,仅执行赋值和自增)。
- 最坏情况:O(n)(当需要扩容时, realloc 可能需要将原数组 n 个元素复制到新空间, n 为当前元素个数)。
(注:由于扩容策略是“翻倍”,整个入栈过程的均摊时间复杂度为 O(1),适合频繁入栈场景)。
- 空间复杂度:O(1)(仅在扩容时分配新空间,属于必要的存储开销,不计入额外复杂度)。
3. 判空函数--StackEmpty函数
bool StackEmpty(ST* ps) { assert(ps); // 确保栈指针非空 return ps->top == 0; // top为0时栈空(初始状态或所有元素出栈后) }
- 逻辑:
通过 top 是否为 0 判断栈空(结合初始化逻辑, top=0 表示没有元素)。
- 时间复杂度:O(1)(仅判断条件,无循环或复杂操作)。
- 空间复杂度:O(1)(无额外空间分配)。
4. 出栈函数--StackPop函数
void StackPop(ST* ps) { assert(!StackEmpty(ps)); // 确保栈非空,否则调试报错 --ps->top; // 栈顶位置前移(等效于移除栈顶元素,不实际删除内存) }
- 逻辑:
1. 先通过 assert(!StackEmpty(ps)) 确保栈非空(防止空栈出栈导致 top 为负数,后续访问越界)。
2. 仅将 top 自减 1 :由于栈的“后进先出”特性,出栈只需移动栈顶指针(无需实际删除内存中的元素,下次入栈会覆盖旧值),效率极高。
- 时间复杂度:O(1)(仅执行自减操作)。
- 空间复杂度:O(1)(无额外空间操作)。
5. 取栈顶函数--StackTop函数
STDataType StackTop(ST* ps) { assert(!StackEmpty(ps)); // 确保栈非空 return ps->arr[ps->top - 1]; // 返回栈顶元素(top-1为当前栈顶索引) }
- 逻辑:
1. 检查栈非空(避免访问无效内存)。
2. 返回栈顶元素:由于 top 是“下一个入栈位置”,当前栈顶元素的索引为 top - 1 (例如: top=3 时,栈顶元素在 arr[2] )。
- 时间复杂度:O(1)(直接通过索引访问数组元素)。
- 空间复杂度:O(1)(无额外空间分配)。
6. 取栈中元素个数--StackSize函数
int StackSize(ST* ps) { return ps->top; // top的值即元素个数(入栈时top自增,出栈时自减) }
- 逻辑:
由于 top 记录了“下一个入栈位置”,其值恰好等于当前栈中元素的数量(例如: top=5 表示已有 5 个元素,分别在 arr[0]~arr[4] )。
- 时间复杂度:O(1)(直接返回 top 的值)。
- 空间复杂度:O(1)(无额外操作)。
7. 销毁栈函数--StackDestroy函数
void StackDestroy(ST* ps) { if (ps->arr) // 若动态数组存在,则释放内存 free(ps->arr); ps->arr = NULL; // 避免野指针 ps->top = ps->capacity = 0; // 重置栈状态为初始空栈 }
- 逻辑:
释放动态数组 arr 占用的内存(若存在),并将指针置空;同时重置 top 和 capacity 为0 ,确保栈不再被误用,防止内存泄漏。
- 时间复杂度:O(1)( free 操作不依赖元素数量,仅释放整块内存)。
- 空间复杂度:O(1)(仅释放已有空间,不新增分配)。
1.3 测试文件(test.c)
测试文件(test.c )是对栈功能的实际验证,它通过调用 Stack.h 中声明的函数和 Stack.c 中实现
的逻辑,完整演示了栈从初始化到销毁的全生命周期。下面结合头文件( Stack.h )和实现文件(
Stack.c ),详细拆解测试流程和各部分的关联:
#include"Stack.h"
void test01()
{
ST st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
//StackPop(&st);
//StackPop(&st);
//StackPop(&st);
//StackPop(&st);
//StackPop(&st);
//while (!StackEmpty(&st))
//{
// int top = StackTop(&st);
// printf("%d ", top);
// StackPop(&st);
//}
int size = StackSize(&st);
printf("size:%d\n", size);
StackDestroy(&st);
}
int main()
{
test01();
return 0;
}
1. 文件关联逻辑
- #include"Stack.h" :
测试文件通过这句引入头文件,从而获得栈的结构体 ST 定义和所有函数( StackInit 、
StackPush 等)的声明,确保编译器能识别 ST st; 、 StackInit(&st); 等代码的合法性。
- 编译链接关系:
实际运行时, test.c 会与 Stack.c 编译生成的目标文件链接,最终执行 Stack.c 中实现的函数逻
辑(例如 StackPush 的扩容、入栈操作)。测试文件只负责“调用接口”,不关心内部实现,体现
了“接口与实现分离”的设计思想。
2. test01函数:栈的完整操作流程
1. 栈的初始化:
ST st; // 定义栈变量(基于Stack.h中struct Stack的类型) StackInit(&st); // 初始化栈(调用Stack.c中的StackInit实现)
- 关联 Stack.c 的 StackInit :
执行后, st.arr = NULL , st.top = 0 , st.capacity = 0 (栈处于空状态,等待入栈)。
2. 连续入栈操作:
StackPush(&st, 1); StackPush(&st, 2); StackPush(&st, 3); StackPush(&st, 4); StackPush(&st, 5);
- 每步入栈的具体逻辑(依赖 Stack.c 的 StackPush ):
- 第1次入栈( x=1 ):
- 初始 st.top=0 , st.capacity=0 ,触发扩容( newCapacity=4 ), realloc 分配4个 int 大
小的空间, st.arr 指向新空间, st.capacity=4 。
- 执行 st.arr[0] = 1 , st.top 自增为 1 (此时栈内元素: [1] )。
- 第2次入栈( x=2 ):- st.top=1 < capacity=4 ,无需扩容,直接执行 st.arr[1] = 2 , st.top=2 (元素: [1,2] )。
- 第3次入栈( x=3 ): st.top=2 → 执行后 st.top=3 (元素: [1,2,3] )。
- 第4次入栈( x=4 ): st.top=3 → 执行后 st.top=4 (元素: [1,2,3,4] ,此时 top=4 ==capacity=4 ,栈满)。
- 第5次入栈( x=5 ):
- st.top=4 == capacity=4 ,触发扩容( newCapacity=8 ), realloc 将空间扩大到8个 int , st.capacity=8 。
- 执行 st.arr[4] = 5 , st.top=5 (最终元素: [1,2,3,4,5] )。
- 入栈后栈的状态:
st.arr 指向包含 [1,2,3,4,5] 的动态数组, st.top=5 (元素个数为5), st.capacity=8 (当前容量为8,剩余3个空闲位置)。
3. 计算栈中元素个数并打印
int size = StackSize(&st); // 调用Stack.c中的StackSize printf("size:%d\n", size); // 输出结果:size:5
- 关联 Stack.c 的 StackSize :
函数返回 st.top (此时 st.top=5 ),因此 size=5 ,打印结果验证了入栈操作的正确性(5个元素成功入栈)。
4. 销毁栈
StackDestroy(&st); // 调用Stack.c中的StackDestroy
- 关联 Stack.c 的 StackDestroy :
执行后, st.arr 指向的动态内存被 free 释放, st.arr=NULL , st.top=0 , st.capacity=0 ,栈恢复为初始空状态,避免内存泄漏。
5. 注释掉的代码:潜在的扩展测试
测试文件中注释了部分代码,它们是对栈其他功能的验证(若取消注释可执行):// StackPop(&st); // 连续5次出栈,最终栈空 // while (!StackEmpty(&st)) { ... } // 循环出栈并打印元素(验证后进先出)
- 若取消注释,执行逻辑为:
- 每次 StackPop(&st) 会使 st.top 自减(例如5次出栈后 st.top=0 ,栈空)。
- while 循环中, StackEmpty(&st) 检查栈非空, StackTop(&st) 获取栈顶元素(如5,4,3,2,1 ),打印结果符合栈“后进先出”的特性。
这部分注释掉的代码是小编自己在测试的过程中所写的,并不是固定使用的,大家也可以按
照自己的想法正确的组合各部分函数。
3. main 函数:程序入口
int main() { test01(); // 调用测试函数 return 0; }
- 作用:作为程序入口,启动测试流程,执行 test01 中的所有栈操作,完成后程序退出。
4. 总结:三者协同关系
- Stack.h :定义“规则”(栈的类型、函数接口),是 test.c 和 Stack.c 的沟通桥梁。
- Stack.c :实现“功能”(函数的具体逻辑),为栈的操作提供底层支持。
- test.c :验证“效果”(通过调用接口,测试栈的初始化、入栈、容量计算、销毁等功能是否符合
预期)。
整个流程清晰展示了“声明→实现→使用”的开发模式,也是数据结构测试的典型思路:通过具体操
作验证接口的正确性,确保栈的逻辑符合设计要求(后进先出、动态扩容等)。
这和我们在之前实现顺序表和单链表时的模式是一样的。
以上便是对于栈部分数据结构的所有内容。对比之前的顺序表和单链表的实现, 实现的代码内容较
少,也比较简单。部分内容也和实现顺序表的内容相似。理解起来也比之前更加简单和容易。如果
有错误的地方也希望大家能够指正出来。最后感谢大家的观看!