数据结构:栈

1. 栈的概念和结构

栈(Stack)是一种遵循后进先出(LIFO,Last In First Out)原则的有序集合。在栈中,新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一端就称为栈底。在栈的操作中,最后一个添加进栈的元素会是第一个被移除的。栈也被称为后进先出(LIFO)的线性表

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶

在这里插入图片描述

栈主要有两种存储方式来实现:一是顺序存储结构(数组),二是链式存储结构(链表)

2. 栈的顺序存储实现

栈的顺序存储结构通常由数组和一个记录栈顶元素的下一个位置的变量(下一个入栈的数据插入的位置)组成,但是当数组空间大小不够时还需要扩容,所以还要存储栈的空间大小的变量

用数组实现栈和之前顺序表的实现类似,对顺序表不熟悉的可以看一下我之前的博客:数据结构—顺序表

下面我们来定义一下栈的顺序存储结构和栈具体要实现哪些功能:

//定义栈的结构
typedef int STDataType;
typedef struct Stack {
	STDataType* arr;
	int top;//栈顶指针
	int capacity;//栈的空间大小
}ST;
//初始化
void STInit(ST* ps);
//销毁
void STDestory(ST* ps);
//入栈
void StackPush(ST* ps, STDataType x);
//出栈
void StackPop(ST* ps);
//取栈顶数据
STDataType StackTop(ST* ps);
//判断栈是否为空
bool StackEmpty(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
//打印栈中的数据
void STackPrint(ST* ps);

2.1 初始化

思路:将栈这个变量的地址传过来用一级指针接收即可,这样就实现了形参改变实参,在函数内部将指针置为NULL,指向栈顶元素下一个位置的变量和记录栈的空间大小的变量初始化为0即可。

void STInit(ST* ps)
{
	assert(ps);//ps!=NULL
	ps->arr = NULL;
	ps->capacity = ps->top = 0;
}

2.2 入栈

思路:入栈前首先判断栈的空间是否足够,如果当ps->capacity == ps->top时,就说明栈的空间大小满了,需要对数组进行扩容realloc(因为是在原本空间大小下再开辟更大的空间),再进行入栈操作。如果栈的空间大小没满,直接将数据进行入栈操作。

void StackPush(ST* ps, STDataType x)
{
	assert(ps);//ps!=NULL
	//判断空间是否足够
	if (ps->capacity == ps->top)
	{
		//空间不足就申请新空间
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* tmp = (STDataType*)realloc(ps->arr, sizeof(STDataType)*newCapacity);
		//因为申请空间可能会失败,所以这里使用tmp来接收增容后的空间
		if (tmp == NULL)
		{
			//申请失败
			perror("realloc fail!");//打印提示消息
			exit(1);//异常退出
		}
		//申请成功
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
	ps->arr[ps->top++] = x;
}

2.3 判断栈是否为空

思路:只需要判断指向栈顶元素下一个位置(下一个入栈的数据插入的位置)的top指针是否为0即可,如果为0,表示栈中没有数据即为空,反之,则栈不为空。

bool StackEmpty(ST* ps)
{
	assert(ps);//ps!=NULL
	return ps->top == 0;
}

2.4 出栈

思路:出栈前首先要判断栈是否为空,只有当栈不为空时才能出栈,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将top指针减一即可。

void StackPop(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	ps->top--;
}

2.5 取栈顶数据

思路:取栈顶数据前首先要判断栈是否为空,只有当栈不为空时才能取栈顶数据,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将栈顶数据返回即可,需要注意的是,栈顶数据的下标应该是top-1

STDataType StackTop(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));
	return ps->arr[ps->top - 1];
}

2.6 获取栈中有效元素个数

思路:因为每次入栈时,top都会加一,而出栈时都会减一,所以栈中有效元素的个数就为top

int STSize(ST* ps)
{
	assert(ps);//ps!=NULL
	return ps->top;
}

2.7 打印栈中的数据

思路:首先判断栈中的数据为不为空,如果为空就打印不了,直接报错。如果不为空,只需每次将栈顶元素打印,再出栈即可,当栈为空就停止循环,所以循环条件为栈不为空:!StackEmpty(ps)

void STackPrint(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	while (!StackEmpty(ps))
	{
		STDataType x = StackTop(ps);
		printf("%d ", x);
		StackPop(ps);
	}
	printf("\n");
}

2.8 销毁

思路:只有当栈不为空时才销毁,再使用free函数将栈的空间释放掉,随后将栈顶指针top和栈的空间大小置为0即可

void STDestory(ST* ps)
{
	assert(ps);//ps!=NULL
	if (ps->arr)
	{
	    //栈不为空,释放空间
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->capacity = ps->top = 0;
}

3. 栈的链式存储实现

栈的链式存储结构为单链表,每次入栈时只需先申请一个新节点再改变指向栈顶的指针,出栈时就先销毁节点再改变指向栈顶的指针即可,所以我们得先定义一个栈的结构体来存放指向栈顶的指针和栈中有效数据个数,然后再定义一个链表节点的结构体来存放数据和指向下一个节点的指针

对链表不熟悉可以看一下我之前的博客:数据结构—单链表

下面我们来定义一下栈的链式存储结构和栈具体要实现哪些功能:

//定义栈的结构
typedef int STDataType;
typedef struct StackNode {
	STDataType data;
	struct StackNode* next;
}StackNode;
typedef struct Stack {
	StackNode* top;//栈顶
	int size;//有效数据的个数
}ST;
//初始化
void STInit(ST* ps);
//入栈
void StackPush(ST* ps, STDataType x);
//判断栈是否为空
bool StackEmpty(ST* ps);
//出栈
void StackPop(ST* ps);
//取栈顶元素
STDataType StackTop(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
//打印栈中的数据
void STackPrint(ST* ps);
//销毁
void STDestory(ST* ps);

3.1 初始化

思路:将栈这个变量的地址传过来用一级指针接收即可,这样就实现了形参改变实参,在函数内部将指针top置为NULL,再将栈中有效数据的个数初始化为0即可。

void STInit(ST* ps)
{
	assert(ps);//ps!=NULL
	ps->top = NULL;
	ps->size = 0;
}

3.2 判断栈是否为空

思路:如果栈顶指针ps->top为空(NULL),则栈为空,否则栈不为空

//判断栈是否为空
bool StackEmpty(ST* ps)
{
	assert(ps);//ps!=NULL
	return ps->top == NULL;
}

3.3 入栈

思路:先申请一个新节点,再调用StackEmpty函数判断栈是否为空,如果栈为空则栈顶就为新节点(将新节点newnode赋给栈顶),如果不为空,先将新节点newnode指向下一个节点的指针next指向栈顶,再更新栈顶指针指向即可,最后栈的有效数据个数再加一

void StackPush(ST* ps, STDataType x)
{
	assert(ps);//ps!=NULL
	//申请一个新节点
	StackNode* newnode = (StackNode*)malloc(sizeof(StackNode));
	if (newnode == NULL)
	{
		//申请失败,打印错误信息
		perror("malloc fail!");
		exit(1);//异常退出
	}
	newnode->data = x;
	if (StackEmpty(ps))
	{
		//如果栈为空,将新节点newnode赋给栈顶
		ps->top = newnode;
		ps->top->next = NULL;
	}
	else
	{
		//如果栈不为空,更新栈顶指针即可
		newnode->next = ps->top;
		ps->top = newnode;
	}
	//每次入栈时有效数据+1
	ps->size++;
}

3.4 出栈

思路:因为要出栈,所以栈不能为空,首先要判断栈为不为空,可以使用断言assert(!StackEmpty(ps)),如果栈为空就报错。然后再判断栈是否只有一个数据,如果只有一个数据,直接将栈顶销毁再将栈顶指针置为空即可如果栈的数据个数大于1,可以先改变栈顶指针指向然后再销毁栈顶空间,也可以先销毁栈顶空间然后再改变栈顶指针指向,两种方法都可以,以下代码是第一种。注意每次出栈时有效数据都要减一

void StackPop(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	if (ps->size == 1 || ps->top->next == NULL)
	{
		//如果栈中只有一个数据,直接销毁栈顶即可
		free(ps->top);
		ps->top = NULL;
	}
	else
	{
		//如果栈的数据个数大于1,先改变栈顶指针指向,再销毁栈顶空间即可
		StackNode* del = ps->top;
		ps->top = ps->top->next;
		free(del);//释放空间
		del = NULL;//指向空
	}
	//每次出栈有效数据减一
	ps->size--;
}

3.5 取栈顶元素

思路:取栈顶数据前首先要判断栈是否为空,只有当栈不为空时才能取栈顶数据,所以这里使用了断言 assert(!StackEmpty(ps)),然后只需将栈顶数据返回即可

STDataType StackTop(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	return ps->top->data;
}

3.6 获取栈中有效元素个数

思路:直接返回栈的有效数据个数size即可

int STSize(ST* ps)
{
	assert(ps);//ps!=NULL
	return ps->size;
}

3.7 打印栈中的数据

思路:首先判断栈中的数据为不为空,如果为空就打印不了,直接报错。如果不为空,只需每次将栈顶元素打印,再出栈即可,当栈为空就停止循环,所以循环条件为栈不为空:!StackEmpty(ps)

void STackPrint(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	while (!StackEmpty(ps))
	{
		STDataType data = ps->top->data;
		printf("%d ", data);
		StackPop(ps);
	}
	printf("\n");
}

3.8 销毁

思路:只有当栈不为空时才销毁,因为栈是由链表来实现的,所以需要将栈中的节点全部销毁掉。可以使用StackPop函数将每个栈顶数据都出栈,一直到栈为空为止,所以我们可以使用循环,而循环的条件为:!StackEmpty(ps)

void STDestory(ST* ps)
{
	assert(ps);//ps!=NULL
	assert(!StackEmpty(ps));//栈不能为空
	while (!StackEmpty(ps))
	{
		StackPop(ps);
	}
}

4. 栈的顺序存储和链式存储的对比

栈的顺序存储(顺序栈)和链式存储(链式栈)是栈的两种主要实现方式,它们在存储结构、操作效率、内存使用等方面存在一定的差异。以下是对这两种存储方式的详细对比:

一. 存储结构

顺序栈:

使用数组来存储栈中的元素。栈底通常固定为数组的起始位置,栈顶则随着元素的入栈和出栈而动态变化。需要一个额外的变量(如top指针)来记录栈顶元素在数组中的位置

链式栈:

使用链表来存储栈中的元素。栈顶元素通常位于链表的头部,这样方便进行入栈和出栈操作。链式栈不需要像顺序栈那样预先分配固定大小的存储空间,每次入栈时申请一个结点空间,然后再改变top指针指向即可,因此更加灵活。

二. 操作效率

顺序栈:

入栈和出栈操作的时间复杂度通常为O(1),因为只需要在数组的末尾进行插入或删除操作。但是,当栈满时,如果数组空间不足,则需要进行扩容操作,这可能会增加额外的时间开销。

链式栈:

入栈和出栈操作的时间复杂度也通常为O(1),因为只需要申请新节点空间或者销毁栈顶节点空间,然后改变top指针指向即可。链式栈不需要扩容操作,因为链表的大小可以动态调整。

三. 内存使用

顺序栈:

内存使用效率较高,因为数组中的元素在内存中连续存储,有利于缓存的利用。
但是,如果分配的数组空间过大,而实际使用的元素较少,则会造成内存空间的浪费

链式栈:

内存使用效率相对较低,因为链表中每个节点除了存储数据外,还需要额外的空间来存储指向下一个节点的指针。但是,链式栈可以根据需要动态分配内存空间,避免了顺序栈中可能存在的内存浪费问题

四. 适用场景

顺序栈:

适用于栈的大小可以事先确定或栈的大小变化不大的场景。
当对栈的操作主要是入栈和出栈时,顺序栈通常具有更好的性能。

链式栈:

适用于栈的大小变化较大或需要频繁进行扩容操作的场景。
当栈的大小无法事先确定或需要动态调整栈的大小时,链式栈更加灵活方便。

综上所述,顺序栈和链式栈各有优缺点,选择哪种存储方式取决于具体的应用场景和需求。在实际应用中,可以根据栈的大小变化、操作频率、内存使用效率等因素来综合考虑选择哪种存储方式。

对以上内容由不同看法的欢迎各位大佬来讨论,希望对大家学习有帮助,多多支持哦!

  • 28
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值