栈的实现 —— C语言
每博一文案
有位作家说:”我的人生里,没有那么多时间和精力去揣测别人对我的看法和评论。
因为该做的事,太多了。
李宗盛说过:“每一个努力过的脚印,都是相连的。它一步一步带我到今天
,成功就今天的我,人生没有白走的路,每一步都算数。”
每个阳光下开怀大笑的人,都有一段沉默的时光。
—————— 一禅心灵庙语
文章目录
创建项目的注意事项
- 首先我们要明白一点就是不存在没有完完全全BUG的项目,就像人无完人一样 我们只能不断地对项目进行优化,减少BUG 的出现,但是我们好像并不能完全的消除所有的BUG
- 而消除bug 的最好的方式就是运行测试+调试
- 而我们在运行测试+调试 的时候,需要我们控制一定的量 ,我们需要把握住这个量 ,这一点是十分重要的,这样有助于我们快速的发现问题,也就是bug 的所在,从而提高我们的效率,减少我们的bug 的出现
- 具体的方法,我个人有以下建议:
- 首先我们需要对项目进行合理的分类,那个类,那个文件实现什么样的功能: 比如:一个类是用于定义什么类型,方法的 ,另一个类是用于什么头main 的,再另外一个类是用于对于什么功能上的实现的 ,合理的分类,可以提高我们项目的可读性,让我们自身不会因为自己随着代码量的增加,对应功能上的增加而导致我们越敲越乱,越写越蒙
- 对于量上我们可以,每实现了两三个 功能就运行测试看看,是否存在错误,如果存在出现了错误,我们可以在一定的量(范围内),快速的找出哪里出现了问题,从而对它进行调试 ,解决问题,而不是,把项目整体写完了或者是已经实现了好几十个功能,才开始运行测试项目,结果一运行,bug,警告 冒出一大坨,我们先不说,让不让人,抓头发,就是找出问题,恐怕都需要不时间上的浪费吧
- 对于代码上的注释,没事多写明白一点,或许有一天,你会感想曾经,那个写了注释的自己或同事
- 对于bug 不要害怕,一点一点的调试看看,找出问题的所在,慢慢来,要有耐心,要明白一点现在bug 写的多,以后bug 跟你说拜拜,现在bug ,不解决,bug 天天对你说 明天见
栈的介绍
栈: 一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作一端称为 栈顶 ,另一端称为 栈底 。不含任何数据的栈称为 空栈 ,栈中的数据元素遵守: 后进先出LIFO(Last In First Out) 的原则。
压栈 :栈的插入操作 叫作压栈,也称为进栈,入栈、入数据在栈顶
出栈 :栈的删除操作叫作 出栈,也称为弹栈,出数据也是在栈顶
栈对于顺序表与链表的选择
栈 的实现实现一般可以使用数组或者链表来实现的
使用数组 实现栈 的结构图:
从上面的数组实现栈的话,我们对于进栈可以通过对栈顶的下标位置插入数据,而出栈 我们可以使用栈顶 后移动的方式,对于删除数据不需要移动大量的元素,因为栈 的特点只有栈顶 一端可以进出数据,在时间复杂度上是为O(1)的,
使用链表实现栈
对于使用链表 ,我们可以使用单链表 以及双向链表 ,比较方便
单链表 的实现,结构图:
如果以单链表的尾插中的尾节点 作为 栈顶 ,使用这种单链表实现的话,我们可以十分明显的发现进栈容易出栈难 ,因为我们进栈 的话,只要直接拿到尾节点的地址拼接上就可以了,十分容易,但是,如果我们是出栈 删除数据的话,需要找到前一个节点的地址才可以删除数据的,所以从出栈 删除数据上的时间复杂度为O(n),但是我们可以使用 双向链表 解决这个找到前一个节点的问题:结构图:如下
双向链表就解决了,找到前一个节点的地址的问题了,可以在时间复杂度上为O(1),上解决出栈 问题了
其实我们使用单链表实现栈,在出栈上也是可以到达时间复杂度为O(1) 的
我们这样设计链表栈 ,使用头插法 的方式进栈 ,以头节点 作为栈顶 ,这样对于出栈 ,我们可以将栈顶置为下一个节点就可以了,在时间复杂度上是为 O(1)的,设计结构如图下:
栈的具体代码的实现
这里我们使用动态顺序表 实现栈 ,因为相对而言数组的结构实现更优一些。因为数组在尾上
插入数据的代价比较小
创建栈的结构体类型
这里我们使用了宏 的方式定义了我们需要扩容的倍数 ,方便后续的必要修改,使用关键字 typedef 定义别名我们的栈的数据类型,同样是方便为了后续需要改变存放的数据的类型时,只要修改这里即可,有利于项目的维护管理
#define OCE 2 // 扩容的倍数
typedef int STDatatype; // 设定栈数据的类型
// 创建有关栈的结构体类型
typedef struct Stack
{
STDatatype* data; // 动态顺序表
int top; // 栈顶的标识
int copacity; // 栈容量
}ST;
初始化栈
首先我们需要判断一个该传递过来的栈是否为 NULL ,这里我们为空,使用断言 处理,assert(ps) 意思是:如果 ps == NULL 则断言,中止程序,在该位置处报错告诉你,
这里我们使用的是动态顺序表 ,在堆区 上开辟空间,需要判断动态内存的开辟是否成功,如果失败,使用关键字perror 一个错误报告提醒一下,并中止程序 exit(-1)
函数名: exit()
所在头文件:stdlib.h
功 能: 关闭所有文件,终止正在执行的进程。
exit(1)表示异常退出.这个1是返回给操作系统的。
exit(x)(x不为0)都表示异常退出
exit(0)表示正常退出
exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。
栈顶位置的标记存在着两种方式:一个是在下标为 0 的位置,一个是在下标为 -1 的位置,标记的位置的不同其意义也是不一样的,如下图 所示:
void Stacklnit(ST* ps)
{
assert(ps); // 断言,判断一下栈是否为 NULL,
ps->data = (STDatatype*)malloc(sizeof(STDatatype) * 6); // 动态内存(堆区)开辟空间
// 判断动态内存开辟空间是否成功,
if (NULL == ps->data)
{
// 动态内存开辟失败
perror("ps->data,动态内存开辟失败"); // 错误报告提醒
exit(-1); // -1 程序非正常中止
}
ps->copacity = 6; // 栈的容量
ps->top = 0; // 栈顶位置的标记
}
销毁栈
断言一下,防止栈为 NULL(空),释放动态内存(堆区)开辟的空间使用关键字 free( ) ,并置为 NULL ,防止非法访问,
// 销毁栈
void StackDestor(ST* ps)
{
assert(ps); // 断言,防止栈为 NULL 空
free(ps->data); // 释放 ps->data 内存空间
ps->data = NULL; // 手动置为 NULL,防止非法访问
ps->copacity = 0; // 栈容量置为 0
ps->top = 0; // 栈顶标志也为 0
}
栈顶入栈
同样断言,判断一下,防止栈为NULL(空),在数据入栈 前需要判断容量是否足够存放数据,不够扩容,使用关键字realloc( ),注意在计算类型的大小上,尽管你知道对应类型的大小,还是建议使用操作符 sizeof( ) ,原因提高兼容性,因为类型的大小,在不同的位数的 PC 中,类型的大小是不一样的,如:32位的 int类型 的大小是 2个字节,而64位 的 int类型 的大小是 4个字节,所以存在误差,而如果使用操作符 sizeof( ) 的话无论是 多少位数的字节个数上都是统一的,提高了项目的兼容性,同样对于动态内存(堆区),也是需要判断扩容是否成功的,成功后,移交 扩容后的地址的主权位置,栈容量 增加,还有一般扩容是在原来的基础上增加 1.5倍或 2倍,防止空间上的浪费 ,你扩容了那么多,却只用一点点。栈顶入栈
// 栈顶入栈
void StackPush(ST* ps, STDatatype x)
{
assert(ps); // 断言,防止栈为 NULL,
// 判断栈是否满了,满了扩容
if (ps->top == ps->copacity)
{
// 扩容
STDatatype* tmp = (STDatatype*)realloc(ps->data, ps->copacity * OCE*sizeof(STDatatype));
// 判断扩容是否成功
if (NULL == tmp)
{
perror("tmp 扩容"); // 失败提醒
exit(-1); // 非正常退出程序
}
else
{
// 扩容成功,交换主权
ps->data = tmp;
ps->copacity *= OCE; // 扩容成功,栈容量扩大
}
}
// 数据入栈
ps->data[ps->top] = x;
ps->top++; // 移动栈顶标识
}
栈顶出栈
栈顶出栈,判断是否为(NULL)空栈,栈不可以没有数据了还进行出栈操作,出栈:移动栈顶标识后移,注意: 不可以使用 ps->data[ps->top] = 0 ,将元素数据置为 0 的方式,因为:有可能其中存放的数值就是 0 呢,却被当作出栈处理了
// 栈顶出栈
void StackPop(ST* ps)
{
assert(ps); // 防止栈为 NULL
if (ps->top < 0)
{
printf("栈中没有数据了不可在删除出栈了\n");
return; // 退出程序
}
ps->top--; // 栈顶标志后移一个,出栈
}
取出栈顶元素
判断是否为空栈,空栈无法获取栈顶元素,top 的位置的指向在,初始化栈 中已经详细介绍了是栈顶元素的下一个位置,所以这里的栈顶位置是 top -1 的下标位置
// 栈顶取栈顶元素
STDatatype StackTop(ST* ps)
{
assert(ps); // 防止栈为 NULL
if (ps->top <= 0)
{
printf("栈为空不可以取栈顶元素\n");
exit(-1); // 退出程序
}
else
{
return ps->data[ps->top - 1]; // top 指向的是栈顶元素的下一个位置
}
}
取栈的有效数据个数
判断栈是否为空,直接返回 ps->top 的数值就可以了,因为:数组的下标是从 0 开始的,但是我们计数是从 1 开始的,所以需要 + 1,刚好其数值就是等于 ps->top 的,
// 获取栈中的有效数据的个数
int StackSize(ST* ps)
{
assert(ps); // 防止栈为空
return ps->top;
}
判断栈是否为空栈
这里判断栈是否为空,使用了 布尔值 bool ,需要引入头文件 #include<stdbool.h> ,才可以使用布尔值,其中布尔值 true 真就是数值 1,布尔值 false 假就是数值 0 ,关于C语言 中的布尔值,大家可以移步到 🔜🔜🔜 你真的了解C语言 if - else 、bool(布尔值)、浮点数损失吗 ,栈为空,返回 true,栈不为空,返回 false
// 判断栈是否为空栈
bool StackEmpty(ST* ps)
{
assert(ps); // 断言,防止栈为空
if (ps->top == 0)
{ // 栈为空,返回 true 真就是 1
return true;
}
else
{
// 栈不为空,返回 false 键就是 0
return false;
}
}
栈的实现的完整代码:
这里我们使用的是 分布式文件设计
Stack.h
存放有关栈中的,函数的声明,变量,宏、结构体,声明使用 extern 关键字、声明是不会开辟空间的,定义才会开辟空间,而开辟空间不是目的,存放数据(二进制)才是目的,因为存放数据需要开辟空间
#pragma once
#include<stdio.h>
#include<assert.h> // 断言头文件
#include<stdbool.h> // bool 布尔值头文件
#include<stdlib.h>
#define OCE 2 // 扩容的倍数
typedef int STDatatype; // 设定栈数据的类型
// 创建有关栈的结构体类型
typedef struct Stack
{
STDatatype* data; // 动态顺序表
int top; // 栈顶的标识
int copacity; // 栈容量
}ST;
// 使用 extern 声明函数和变量
extern void Stacklnit(ST* ps); // 初始化栈
extern void StackDestor(ST* ps); // 销毁栈
extern void StackPush(ST* ps, STDatatype x); // 栈顶入栈
extern void StackPop(ST* ps); // 栈顶出栈
extern STDatatype StackTop(ST* ps); // 栈顶取栈顶元素
extern int StackSize(ST* ps); // 获取栈中有效数据的个数
extern bool StackEmpty(ST* ps); // 判断栈是否为空栈
extern void StackTest(ST* ps); // 栈的测试
Stack.c
存放栈中的功能
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
// 初始化栈
void Stacklnit(ST* ps)
{
assert(ps); // 断言,判断一下栈是否为 NULL,
ps->data = (STDatatype*)malloc(sizeof(STDatatype) * 6); // 动态内存(堆区)开辟空间
// 判断动态内存开辟空间是否成功,
if (NULL == ps->data)
{
// 动态内存开辟失败
perror("ps->data,动态内存开辟失败"); // 错误报告提醒
exit(-1); // -1 程序非正常中止
}
ps->copacity = 6; // 栈的容量
ps->top = 0; // 栈顶位置的标记
}
// 销毁栈
void StackDestor(ST* ps)
{
assert(ps); // 断言,防止栈为 NULL 空
free(ps->data); // 释放 ps->data 内存空间
ps->data = NULL; // 手动置为 NULL,防止非法访问
ps->copacity = 0; // 栈容量置为 0
ps->top = 0; // 栈顶标志也为 0
}
// 栈顶入栈
void StackPush(ST* ps, STDatatype x)
{
assert(ps); // 断言,防止栈为 NULL,
// 判断栈是否满了,满了扩容
if (ps->top == ps->copacity)
{
// 扩容
STDatatype* tmp = (STDatatype*)realloc(ps->data, ps->copacity * OCE*sizeof(STDatatype));
// 判断扩容是否成功
if (NULL == tmp)
{
perror("tmp 扩容"); // 失败提醒
exit(-1); // 非正常退出程序
}
else
{
// 扩容成功,交换主权
ps->data = tmp;
ps->copacity *= OCE; // 扩容成功,栈容量扩大
}
}
// 数据入栈
ps->data[ps->top] = x;
ps->top++; // 移动栈顶标识
}
// 栈顶出栈
void StackPop(ST* ps)
{
assert(ps); // 防止栈为 NULL
if (ps->top < 0)
{
printf("栈中没有数据了不可在删除出栈了\n");
return; // 退出程序
}
ps->top--; // 栈顶标志后移一个,出栈
}
// 栈顶取栈顶元素
STDatatype StackTop(ST* ps)
{
assert(ps); // 防止栈为 NULL
if (ps->top <= 0)
{
printf("栈为空不可以取栈顶元素\n");
exit(-1); // 退出程序
}
else
{
return ps->data[ps->top - 1]; // top 指向的是栈顶元素的下一个位置
}
}
// 获取栈中的有效数据的个数
int StackSize(ST* ps)
{
assert(ps); // 防止栈为空
return ps->top;
}
// 判断栈是否为空栈
bool StackEmpty(ST* ps)
{
assert(ps); // 断言,防止栈为空
if (ps->top == 0)
{ // 栈为空,返回 true 真就是 1
return true;
}
else
{
// 栈不为空,返回 false 键就是 0
return false;
}
}
Test.c
main 方法中对于栈的测试
栈只有一端栈顶 ,才可以进栈,入栈的,遵循先进后出 的原则的,取了栈顶元素,还要伴随出栈才行,不然你是无法获取到后面的元素数据的,是单向的
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
int main()
{
ST st; // 定义栈结构体类型
StackTest(&st); // 注意是传地址
return 0;
}
// 栈的测试
void StackTest(ST* ps)
{
Stacklnit(ps); // 初始化栈
StackPush(ps, 1);
StackPush(ps, 2);
StackPush(ps, 3); // 数据入栈
StackPush(ps, 4);
printf("%d\n", StackTop(ps)); // 取栈顶元素
StackPop(ps); // 出栈
StackPush(ps, 5);
StackPush(ps, 6);
StackPush(ps, 7);
printf("栈是否为空:%d\n", StackEmpty(ps)); // 判断栈是否为空栈
printf("栈中的元素个数:%d\n", StackSize(ps)); // 获取栈中的有效可数
while (0 == StackEmpty(ps))
{
printf("%d\n", StackTop(ps)); // 取栈顶元素
StackPop(ps); // 栈顶出栈
}
printf("\n");
/*
//StackPop(ps);
printf("%d\n",StackSize(ps));
printf("%d\n",StackTop(ps));
printf("%d\n",StackEmpty(ps));
*/
StackDestor(ps); // 销毁栈
}
运行结果
最后
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见!