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

发布于:2025-07-18 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

1.栈的实现


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 :验证“效果”(通过调用接口,测试栈的初始化、入栈、容量计算、销毁等功能是否符合

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

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

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

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

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

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


网站公告

今日签到

点亮在社区的每一天
去签到