数据结构 栈(2)--栈的实现

目录

1.栈的实现


1.栈的实现

关于栈的实现,也是三个文件,头文件,实现文件和测试文件。

1.1 头文件(Stack.h)

cs 复制代码
#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. 头文件包含与预处理

cs 复制代码
#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. 栈的类型定义

cs 复制代码
// 定义栈存储的数据类型
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函数

cs 复制代码
#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函数

cs 复制代码
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 ,之后每次翻倍,平

衡扩容频率和空间效率)。

  1. 扩容使用 realloc :若原数组为 NULL (初始状态),则(等效于 malloc )分配新空间;否

则扩大已有空间。

4. 入栈操作:将元素 x 存入 arr[top] ,然后 top++ (下一次入栈位置后移)。

  • 时间复杂度:

  • 平均情况:O(1)(大部分入栈操作无需扩容,仅执行赋值和自增)。

- 最坏情况:O(n)(当需要扩容时, realloc 可能需要将原数组 n 个元素复制到新空

间, n 为当前元素个数)。

(注:由于扩容策略是"翻倍",整个入栈过程的均摊时间复杂度为 O(1),适合频繁入栈场

景)。

- 空间复杂度:O(1)(仅在扩容时分配新空间,属于必要的存储开销,不计入额外复杂度)

3. 判空函数--StackEmpty函数

cs 复制代码
bool StackEmpty(ST* ps)
{
    assert(ps);  // 确保栈指针非空
    return ps->top == 0;  // top为0时栈空(初始状态或所有元素出栈后)
}
  • 逻辑:

通过 top 是否为 0 判断栈空(结合初始化逻辑, top=0 表示没有元素)。

  • 时间复杂度:O(1)(仅判断条件,无循环或复杂操作)。

  • 空间复杂度:O(1)(无额外空间分配)。

4. 出栈函数--StackPop函数

cs 复制代码
void StackPop(ST* ps)
{
    assert(!StackEmpty(ps));  // 确保栈非空,否则调试报错
    --ps->top;  // 栈顶位置前移(等效于移除栈顶元素,不实际删除内存)
}
  • 逻辑:
  1. 先通过 assert(!StackEmpty(ps)) 确保栈非空(防止空栈出栈导致 top 为负数,后续访问

越界)。

2. 仅将 top 自减 1 :由于栈的"后进先出"特性,出栈只需移动栈顶指针(无需实际删除内存

中的元素,下次入栈会覆盖旧值),效率极高。

  • 时间复杂度:O(1)(仅执行自减操作)。

  • 空间复杂度:O(1)(无额外空间操作)。

5. 取栈顶函数--StackTop函数

cs 复制代码
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函数

cs 复制代码
int StackSize(ST* ps)
{
    return ps->top;  // top的值即元素个数(入栈时top自增,出栈时自减)
}
  • 逻辑:

由于 top 记录了"下一个入栈位置",其值恰好等于当前栈中元素的数量(例如: top=5 表示

已有 5 个元素,分别在 arr[0]~arr[4] )。

  • 时间复杂度:O(1)(直接返回 top 的值)。

  • 空间复杂度:O(1)(无额外操作)。

7. 销毁栈函数--StackDestroy函数

cs 复制代码
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 ),详细拆解测试流程和各部分的关联:

cs 复制代码
#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. 栈的初始化:
cs 复制代码
ST st;          // 定义栈变量(基于Stack.h中struct Stack的类型)
StackInit(&st); // 初始化栈(调用Stack.c中的StackInit实现)
  • 关联 Stack.c 的 StackInit :

执行后, st.arr = NULL , st.top = 0 , st.capacity = 0 (栈处于空状态,等待入栈)。
2. 连续入栈操作:

cs 复制代码
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. 计算栈中元素个数并打印

cs 复制代码
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. 销毁栈

cs 复制代码
StackDestroy(&st);  // 调用Stack.c中的StackDestroy
  • 关联 Stack.c 的 StackDestroy :

执行后, st.arr 指向的动态内存被 free 释放, st.arr=NULL , st.top=0 , st.capacity=0 ,

栈恢复为初始空状态,避免内存泄漏。
5. 注释掉的代码:潜在的扩展测试

测试文件中注释了部分代码,它们是对栈其他功能的验证(若取消注释可执行):

cs 复制代码
// 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 函数:程序入口

cs 复制代码
int main()
{
    test01();  // 调用测试函数
    return 0;
}
  • 作用:作为程序入口,启动测试流程,执行 test01 中的所有栈操作,完成后程序退出。

4. 总结:三者协同关系

  • Stack.h :定义"规则"(栈的类型、函数接口),是 test.c 和 Stack.c 的沟通桥梁。

  • Stack.c :实现"功能"(函数的具体逻辑),为栈的操作提供底层支持。

  • test.c :验证"效果"(通过调用接口,测试栈的初始化、入栈、容量计算、销毁等功能是否符合

预期)。

整个流程清晰展示了"声明→实现→使用"的开发模式,也是数据结构测试的典型思路:通过具体操

作验证接口的正确性,确保栈的逻辑符合设计要求(后进先出、动态扩容等)。

这和我们在之前实现顺序表和单链表时的模式是一样的。

以上便是对于栈部分数据结构的所有内容。对比之前的顺序表和单链表的实现, 实现的代码内容较

少,也比较简单。部分内容也和实现顺序表的内容相似。理解起来也比之前更加简单和容易。如果

有错误的地方也希望大家能够指正出来。最后感谢大家的观看!

相关推荐
玩代码8 分钟前
模板方法设计模式
java·开发语言·设计模式·模板方法设计模式
每一天都要努力^36 分钟前
C++拷贝构造
开发语言·c++
GeminiGlory39 分钟前
从0到1开发网页版五子棋:我的Java实战之旅
java·开发语言
im_AMBER1 小时前
Leetcode 03 java
算法·leetcode·职场和发展
轮到我狗叫了1 小时前
力扣.1312让字符串成为回文串的最少插入次数力扣.105从前序和中序遍历构造二叉树牛客.拼三角力扣.57插入区间编辑
算法·leetcode·职场和发展
搂鱼1145141 小时前
离散与组合数学 杂记
算法
Littlewith1 小时前
Node.js:创建第一个应用
服务器·开发语言·后端·学习·node.js
呆呆的小鳄鱼1 小时前
牛客:HJ24 合唱队[华为机考][最长递增子集][动态规划]
算法·华为·动态规划
梅落几点秋.1 小时前
java-字符串和集合
java·开发语言
weixin_449173651 小时前
基础算法题
算法