c语言是实现数据结构中的栈

发布于:2022-12-28 ⋅ 阅读:(688) ⋅ 点赞:(0)

什么是栈

栈是一种特殊的线性表,但是他只允许在固定的一段进行插入和删除元素的操作,我们把数据插入和删除的一端称为栈顶,另一端称为栈底,栈中的数据遵守后进先出的LIFO原则。那这个概念我们如何来理解呢?什么叫数据的插入和删除都在一段呢?大家可以脑补一个场景就是我们的电梯,这个电梯的作用范围就是两楼,当我们电梯在第一楼的时候就会有很多人上电梯,先进入电梯的人就会被挤到到电梯的最里面啊,而后进来的人就会在最外面,那这时候后进来的人是不是就会最先出去,而最先进来的人是不是就最后一个出去啊,那这个场景就跟我们的栈十分的相似,紧跟着栈还有两个相关的概念:
1.压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
2.出栈:栈的删除操作叫做出栈。出数据也在栈顶。
我们可以来看一张图片来理解理解:
在这里插入图片描述
大家看到这个栈的时候应该会想到我们在讲函数栈帧的时候也提过栈这个概念,说栈是我们内存中的一个部分,那这个栈和我们这里讲的栈是同一个东西吗?那当然不是的,我们这篇文章讲的栈是数据结构中的栈,但是我们函数栈帧中提的栈是操作系统中的栈,这是两个学科中的不同的概念但是名字相同,那这里大家要注意一下,以免搞混了。

几道小题

那么我们这里通过几个小题来理解一下栈的概念:
第一题:
在这里插入图片描述
这道题比较简单,一开始他的栈的初始状态就是空的,然后我们讲这些数据一次放到我们的栈里面再将其取出,那么这时候我们的出栈的顺序是什么呢?我们说栈是后进先出的,而我们这里的数据是一次性进入栈的里面,所以越靠右他进入栈的顺序就越晚,出栈的顺序也就越早,所以我们这里出栈的顺序就是:D C B A 5 4 3 2 1。
第二题:
在这里插入图片描述
我们这道题求的是不可能的出栈顺序,那么看到这个想必有些小伙伴们就很疑惑,啊难道我们这里还会有多种的出栈情况吗?答案是真的有,我们来看一下这道题的讲解,他说进栈的序列为1,2,3,4但是题目有说是一次性的进栈吗?好像没有吧,那么我们这里可以让1进栈随后就让1出栈,也可以让1进栈之后再让2进栈,随后再让2出栈,再让3进栈,那么这样的话我们的出栈的可能性是不是就会多很多啊,好我们在回到这个题首先来看A选项,说出栈的顺序是1,4,3,2,所以我们就知道这里的第一步就是先让1进栈再让1出栈,这样我们的出栈顺序中的第一个就是1 ,而后面的4 ,3 ,2我们就很熟悉了嘛,就是2 3 4 进栈再依次的出栈,这样我们出栈的顺序就是4 3 2 了,所以我们这个整体的出栈顺序就是1 4 3 2 ,我们再来看第二个B选项:说出栈的顺序是2 3 4 1,那么这里的2是第一个出栈的,所以我们就可以知道这里是1 2进栈然后紧接着就让2出栈了,然后1是最后一个出栈的而且后面的出栈顺序是4 3 ,所以我们这里就知道他后面就让3 4 进栈,然后紧接着就让3 4 出栈了,最后再让 1出栈,这样我们的出栈的顺序就是2 4 3 1,我们再来看D选项说出栈的顺序是:3 4 2 1 ,那么这里是3先出栈所以我们知道入栈的顺序就是先1 2 3 ,然后再让3 出栈,因为出栈的顺序是3紧接着4所以后面又让4 进栈再出栈,再全部出栈,那么我们这里的出栈的顺序就是3 4 2 1 ,好根据排除法来看的话我们这道题的选项就是c我们来看一下c为什么是错的呢?他的出栈顺序是 3 1 4 2 ,那么这里的3是最先出栈的,所以这里入栈的顺序就是1 2 3 ,然后再让3出栈,但是题目这里给的是3出栈之后就是1 出栈,但是我们这里的1前面还有个2啊,所以要想让1出栈那就得先让2出栈,所以我们这里的c就是错的。

栈的意义

可能有小伙伴看到这里还是有点不能明白这里的栈有什么用啊?那么这里大家就想一下我们之前学的函数的递归和函数栈帧,我们每调用一次函数都会在在内存中的栈区开辟一个空间来实现该函数的函数栈帧,但是我们内存中的栈区他是非常小的,如果遇到了一些代码这些代码会递归的非常的深,要调用非常次函数的话,是不是就很可能会照成栈的溢出啊,但是这时有小伙伴们会说啊!这不要紧啊我们可以把递归改成循环啊,比如说我们当时学的斐波那契数列的时候就是讲递归改成了循环然后效率变的嘎嘎高,这不就解决问题了嘛!但是这里大家要知道的一点就是不是所有的递归都能改成循环,那么遇到了这些无法改成循环的递归但是这些递归又会照成栈溢出,那么这时我们就可以用栈来对其进行简化,这就是我们栈的意义之一。

实现栈的准备

我们这里就开始来实现我们的链表,那么我们这里是用链表来实现还是用顺序表来实现呢?那么我们这里不同的小伙伴们就会有不同的选着,但是综合来看的话,我们选着用顺序表来实现的话会更好一点,因为我们顺序表在实现尾插和尾删的时候是非常的方便的而且我们的栈刚好就需要尾插和尾删,那么这么看的话是不是就是一个非常好的有点啊,但是有小伙伴们会说:顺序表会照成空间的浪费和性能的消耗啊这不是一个很明显的缺点嘛,但是相比与链表的不停的使用malloc来开辟空间的话,我们的效率也不见得会比链表低很多吧,所以我们这里采用顺序表的形式来实现我们的栈。我们的顺序表会分成两个,一个是静态的顺序表,另外一个就是动态的顺序表,因为我们静态的顺序表局限性很大,所以我们这里采用动态的顺序表。根据我们之前的学习我们知道了动态顺序表中的每个元素的结构体长的啥样:

typedef int STDataType;
typedef struct stack
{
	STDataType* phead;
	int top;
	int capacity;
}ST;
void StackInit(ST* ps);

那么这里的top就是指向我们的元素的顶部,capacity就是这个栈当前的容量那么接下来我们就要实现栈的各个函数。

栈的初始化

在使用这个栈之前我们得先对这个栈进行一下初始化,我们这里先创建了一个结构体,然后我们就要用这个函数来对我们这个结构体来进行一下初始化,那我们这里的初始化就是这里的top和capacity都赋值为0,将这里指向动态内存的指针也初始化为NULL,那么我们的代码就如下:

void StackInit(ST* ps)
{
	ps->capacity = ps->top = 0;
	ps->phead = NULL;
}

栈的销毁

当我们用完这个栈之后我们就应该将这个栈进行销毁这样我们就可以防止内存的浪费和内存泄漏,那么我们这里的销毁就是将这个结构体动态开辟的空间进行释放,并且将该指针初始化为NULL。然后将这个结构体里面的top和capcity给初始化为0,这就是我们销毁的过程。我们来看看代码:

void StackDestory(ST* ps)
{
	assert(ps);
	ps->capacity = ps->top = 0;
	free(ps->phead);
	ps->phead = NULL;
}

栈的插入

实现完了初始化和销毁我们这里就来插入数据,我们这里的插入数据就有一个特点就是只能尾部来实现插入,因为我们这里的top是初始化为0,所以我们这里每次在插入数据的时候,这里的top都刚好指向空着的位置这样的话我们就可以根据top来插入我们的数据,但是我们在插入数据之前得先看一下我们的空间还有没有所以我们这里得先进行一下检查,如果top的值等于capacity的话我们就得进行扩容,那么我们这里的扩容就有一个规律每次都扩容原来数量的两倍,但是我们一开始的容量是0,这里再扩容两倍的话得到的结果还是0,所以我们这里就得分情况,当capacity的值等于的时候我们扩容就将其值初始化为4,不是0的时候我们就扩容其两倍,然后就根据top的值来实现插入,再让top的值加一这样我们的函数就实现了,那么我们的代码就如下:

void StackInsert(ST* ps, STDataType x)
{
	assert(ps);
	if (ps->capacity == ps->top)
	{
		ps->capacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		ST*cur = (ST*)realloc(ps->phead, sizeof(ST) * ps->capacity);
		if (cur == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		ps->phead = cur;
	}
	ps->phead[ps->top] = x;
	ps->top++;
}

判断栈是否为空

我们这里判断栈是否为空的话,是不是就只用看一下我们这里的top的值是否为0啊,如果为0的话我们这里栈就为空,那么我们的代码就如下:

int StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

如果为空我们就返回其他值,如果不为空我们就返回0。

链表的删除

既然有插入那么我们这里就得有删除,在实现删除的功能之前我们得检查一下我们这里的链表是否为空,如果为空的话我们这里就不能再执行删除了。好判断完是否为空之后我们这里就再来实现删除,我们这里的删除就很简单,因为我们是根据top的值来进行插入的,那么我们这里就直接将top的值减1不就可以了吗,而且我们还不用管原来的那个数据,因为我们这里是尾插所以我们再再插入数据的时候就会将原来数据进行覆盖,但是有小伙伴们可能会担心就是我们这里的删除不把原来的数据改成其他值的话,那么我们要是打印的时候一不小心将其打印出来,那该怎么办啊,那么我们这里就不要担心,我们的打印也是根据top的值来打印的,我们这里将top的值减一,那么他打印的时候打印出数据的个数也会少一个,那么我们这里的代码就如下:

void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	ps->top--;
}

显示栈的头部的值

我们这个函数就是实现栈的头部的值,那么我们这里就可以直接通过top来看看栈顶的元素,然后再将该值作为函数的返回值,但是这里大家要注意的一点就是我们的这里栈顶元素的下标为top-1而不是top,并且我我们还得判断一下这个栈是否为空,如果为空的话我们调用这个函数就多少有点不妥·,那么我们的代码就如下:

void StackTop(ST* ps)
{
	assert(ps);
	return ps->phead[ps->top - 1];
}

栈的长度

我们这个函数的功能是求得这个栈中含有多少个元素,那么我们这里就可以直接将这个结构体里面的top的值作为返回值,就可以实现该函数的功能,但是有小伙伴们看到这个就感觉十分的疑惑啊,说这么简单的功能我们直接在main函数里面自己操作不就可以了吗?为何还要专门调用一个函数呢?那么这里大家要知道的一点就是这个函数是我们实现的我们知道top的值就是链表的长度,但是使用者知道我们是怎么实现的吗?他不知道,如果他冒然的自己调用这里的top的值是不是很有可能会出错啊,比如说我们这里初始化的时候我们不将top的值初始化为0,而是初始化为-1,然后我们插入的时候不是先插入再++,而是先++再插入的话,我们这里top的值是不是就会比我们上面实现的哪种方法的值要小1啊,那么这时你不用函数你自己调用这里结构体的值的话,是不是就会出错啊,那么这里就是我们调用函数的原因,我们的代码如下:

void StackSize(ST* ps)
{
	return ps->top;
}
本文含有隐藏内容,请 开通VIP 后查看