🎉welcome🎉
✒️博主介绍:博主大一智能制造在读,热爱C/C++,会不定期更新系统、语法、算法、硬件的相关博客,浅浅期待下一次更新吧!
✈️算法专栏:算法与数据结构
😘博客制作不易,👍点赞+⭐收藏+➕关注
文章目录
概念
在生活中,手枪的弹夹子弹是从最上面一颗一颗出去的,而子弹装进弹夹里面的时候,子弹的顺序和子弹出去的顺序正好是相反的,即子弹是先进后出的, 在数据结构当中,同样有一种结构是先进后出,它就是栈。栈是一种特殊的线性表,它只限定在表尾进行插入和删除,并且只能访问到表尾的数据,表尾可以被访问到的元素被叫做栈顶,插入数据的时候叫做入栈,删除数据的时候叫出栈,元素在栈中时叫做压栈。栈这种先进后出(Last In First Out)的线性表结构,简称LIFO结构。
顺序栈
栈是特殊的线性表,在实现栈的时候,可以使用顺序表实现顺序栈,也可以使用链表变成链栈,而对于顺序栈,栈顶和栈底如何选择?对于顺序栈而言,栈顶如果选在首元素位置即下标0位置,则会导致在入栈和出栈的时候进行大量的挪动数据,时间复杂度在O(N),而当栈底选在首元素位置,栈顶位置放在尾元素位置的时候,只需要记录下尾部下标,就可以在**O(1)**时间进行入栈和出栈。
顺序栈的结构体成员
对于顺序栈而言,选择了尾部作为栈顶,那结构体中就需要一个成员来记录栈顶元素下标发下一个位置即栈内元素个数,同时,对于采用动态内存的栈,使栈没有存储上限,这是还需要个变量来记录容量,这个容量表示栈最多可以放多少元素,当栈顶和容量相当时就会进行扩容。
c
typedef int StDataType;
typedef struct stack
{
StDataType* data; //数据存放
int top; //栈顶下标 or 栈内元素个数
int capacity; //栈容量
};
顺序栈的初始化
对于需要动态内存扩容的顺序栈而言,初始化是开空间外加更改容量和栈顶下标。
c
void StInit(St* st)
{
assert(st);
st->data = (StDataType*)malloc(sizeof(StDataType) * 4);
if (st->data == NULL)
{
perror("malloc");
}
st->top = 0;
st->capacity = 4;
}
顺序栈的销毁
与初始化相对的就是栈的销毁,对于顺序栈而言,因为底层空间为连续空间,则释放空间时直接一次就可以释放完成,而top和capacity只需要置0。
c
void StDestroy(St* st)
{
assert(st);
free(st->data);
st->data = NULL;
st->top = 0;
st->capacity = 0;
}
顺序栈的入栈
顺序栈的栈顶是顺序表的表尾,因为已经记录下栈尾下一个元素的位置,所以入栈可以直接入,不需要去找尾,当元素入栈后,将top更新即可,注意:在进行入栈操作之前,需要先进行判断,判断是否还有位置,如果没有,就需要进行扩容。
c
//扩容
void Stdilata(St* st)
{
assert(st);//断言如果为空则说明传过来的数据有问题
if (st->top == st->capacity)//当我们存放的元素个数和容器最大容量一样的时候,说明需要扩容
{
StDataType* tmp = (StDataType*)realloc(st->data, sizeof(StDataType) * st->capacity * 2);
if (tmp == NULL)
{
perror("");
return;
}
st->data = tmp;//因为realloc开辟的空间的时候可能换到另一个地方
st->capacity *= 2;
}
}
//入栈
void StPush(St* st, StDataType x)
{
assert(st);
Stdilata(st); //扩容函数内部自己检查是否容量已满
st->data[st->top++] = x;//后置++是先使用在++,当插入元素后top才会更新
}
顺序栈的判空
对于顺序栈而言,判空只需要判断top 即可,top为0的时候为空,返回真。
c
bool StEmpty(St* st)
{
assert(st);
return st->top == 0;
}
顺序栈的出栈
出栈时候必须要删除元素吗?并不一定,这个元素出栈,只要无法访问到这个元素即可,那这里可以直接让top 进行减减,对于栈而言,访问元素只能访问到top-1 ,而这时top-- ,那就访问不到在top--前top-1的位置,即原栈顶位置,那原栈顶也属于出栈。
c
void Stpop(St* st)
{
assert(!StEmpty(st));//判空函数内部判断st
st->top--;
}
顺序栈内的元素个数
顺序栈内的元素个数已经用top记录下来,直接返回top
c
int StSize(St* st)
{
assert(st);
return st->top;
}
顺序栈获取栈顶元素
同样已经用top记录下尾部下一个位置,直接返回数组元素第top-1个即可。
c
StDataType StTop(St* st)
{
assert(st);
return st->data[st->top - 1];
}
链式栈
对于链式栈而言,是选择单链表还是其他链表?对于栈而言,只需要操作栈顶元素即可,而单链表对于头删和头插是可以做到O(1)的,所有对于链式栈而言,采用单链表做栈是较为合适的,其他的链表用在栈上有点大材小用了,单链表做栈,拿头部当栈顶,这一点和顺序表是不一样的,同时,对于链表而言,结构体成员当中就可以不用capacity 来记录容量的大小,而top 是否还需要?从顺序栈的各类函数来看,top 是很有必要的,但是top 的作用其实用size来表示是更加合适的,而链式栈和顺序栈的相关函数内部实现和思路是一样的,只是将顺序表换成了链表,入栈和出栈分别对应链表的头插和头删,栈顶元素就是头节点保存的data。
栈的应用
递归
函数的递归就是使用了栈,每个使用函数的时候,这个函数就会入栈,这里的栈指的是系统中的栈区,而函数入栈之后,在运行内部又调用了这个函数,此时这次调用还没有结束,新的调用又压到了这个函数上面,系统就会执行这次新的调用,直到在无新的调用,最上面压栈的函数调用完毕出栈,露出下面的,在接着执行,直到调用完最下面即第一次调用的这个函数,完成递归。
递归的好处
递归写起来很简便,对于递归而言,往往可以将一长串的代码变成几行,递归用的好,往往可以增加代码的可读性,使自己的代码更加通俗易懂。
递归需要注意的地方
当要使用递归的时候,一定要注意有条件可以让这个函数停下来,否则就会出现栈溢出 ,何为栈溢出 ?栈溢出指的是当前调用的东西已经将系统的栈填满了,但是这个时候还在往栈区进行入栈的操作,这里就会有人出现疑问,上面实现的栈是可以进行动态内存申请的,系统的栈难道是静态栈吗?确切的说,不完全是,系统的栈是会随着入栈出栈随机改变的,而对于一些编译器,如:VS、VScode、clion、devc++,它们往往会给栈一个容量的限制,超出这个容量就会造成栈溢出,所以递归一定需要有条件停下来才可以。
递归经典应用------斐波那契数列
斐波那契数列又叫黄金分割数列,同时也叫兔子数列, 斐波那契数列起源于斐波那契提出的一个问题:"如果一个兔子在出生两个月的时候就拥有的生育能力,一对兔子可以在一个月生一对小兔子,假设所有兔子都不会死,一年之后有多少兔子?"当将每个月的数据列出来的时候,就会发现前面两项之和就等于下一项,这也是斐波那契数列的定义,前两项之和等于下一项,那这个和递归有什么关系呢?对比一下采用循环迭代写出的代码和递归写出的代码。
c
int Fib1(int sum)
{
if (sum < 2) return sum == 0 ? 0 : 1;
int x = 1;
int y = 1;
int z = 0;
for (int i = 3; i <= sum; i++)
{
z = x + y;
x = y;
y = z;
}
return z;
}
int Fib2(int sum)
{
if (sum < 2) return sum == 0 ? 0 : 1;//当是0的时候返回0
return Fib2(sum - 1) + Fib2(sum - 2);
}
可以看到下面采用递归的代码对比迭代看起来很简洁,代码少了很多,但是这里的运行效率却是迭代快,所以递归并不完全优于迭代,要看情况决定。
四则运算求表达式
计算机在处理四则运算时就运用到了栈,具体如何处理可以看我这篇博客:计算机是如何计算四则运算表达式的?
🚀专栏:算法与数据结构🙉都看到这里了,留下你们的👍点赞+⭐收藏+📋评论吧🙉