栈是是一种限定性的线性表,它将线性表的插入和删除限定为仅在表的一端进行。将表中允许插入和删除的一端成为栈顶。所以栈顶的位置是不断动态变化的。它具有“后进先出”的特点。因为栈是由线性表实现的,所以,栈有两种存储结构:顺序存储和链式存储。对应的栈成为顺序栈和链式栈。下面,分别来介绍这两种栈的相关操作。
一,顺序栈
它与顺序表类似,即用一组地址连续的空间存放栈中的元素。之前的顺序表是通过数组来实现,其中数组的长度必须图以前设置为一常数。当元素个数超过数组最大长度时,就会插入失败。
下面,来实现可以扩容的顺序栈。即当元素个数超过设定的最大长度时,可以在申请更大的内存来存放元素。
1. 顺序栈的结构
首先,因为该顺序栈要实现扩容,则不能用数组来实现。因此可以动态申请一块内存,将栈中元素放入其中。通过动态内存申请返回的指针来以数组的形式访问栈中元素。
其次,该动态内存的大小初始时可以设置为已默认值,如果超过默认值时,可以重新申请更大的内存,从而达到扩容的目的。
最后,需要知道栈中实际元素的个数。来记录顺序表中最后一个元素所在的位置下标。以及与默认长度进行对比。
所以,顺序栈的结构定义如下:
typedef char SeqStackType;
//定义顺序栈的结构
typedef struct SeqStack
{
SeqStackType* data;//动态申请顺序栈的内存空间
int size;//顺序栈的实际长度
int capacity;//顺序栈的最大长度
}SeqStack;
2. 初始化顺序栈
顺序栈要存放元素,首先要有一块内存空间,在申请时要指定内存大小,初始时,先默认申请1000个char型的空间大小。该空间由动态申请而得,由栈中的成员data指向。从而对栈中元素进行访问。因为,初始时,栈中没有节点,所以,实际长度size为0。
代码如下:
//初始化顺序栈,stack为顺序栈的的指针
void InitSeqStack(SeqStack* stack)
{
if(stack == NULL)
{
//非法输入
return;
}
stack->size = 0;//初始时实际长度为0
stack->capacity = 1000;//初始使顺序栈的长度设置为1000
stack->data = (SeqStackType*)malloc(sizeof(SeqStackType)*(stack->capacity));//为顺序栈申请动态内存
}
因为栈具有“后进先出”的特点,在出,入栈时,必须在栈的一端进行。如果在顺序表的头部进行入栈,则需要将栈中原有元素依次后移,此时需要遍历整个顺序栈。同理,出栈时,也需要遍历。
所以,采取在顺序栈的尾部进行出,入栈。因为size-1可以用来表示顺序栈的最后一个元素所在的下标,所以在入栈时可以直接将下标为size的位置设置为指定元素。出栈时,只需将下标为size-1处的值删除即可。而无需遍历整个顺序栈。
3. 尾插入栈
(1)首先需要判定顺序栈的长度是否等于默认长度。如果相等,此时需要扩容后,方可入栈。如果不等,直接入栈。
(2)在扩容时,因为原内存空间有限,所以需要动态申请更大的空间,新空间的大小可以自定义。申请成功后,将原空间中内容拷贝到新空间中,然后原空间,最后使顺序栈中的data指向新空间即可。
(3)在入栈时,只需将下标为size的位置处的值设置为指定值,然后顺序栈长度加1即可。
代码如下:
//尾插入栈
void SeqStackPush(SeqStack* stack,SeqStackType value)
{
if(stack == NULL)
{
//非法输入
return;
}
if(stack->size >= stack->capacity)
{
//初始的顺序栈已满,此时需要扩容
stack->capacity = stack->capacity*2 + 1;
//申请扩容后的动态内存
SeqStackType* new_data = (SeqStackType*)malloc(sizeof(SeqStackType)*(stack->capacity));
//将原内存中的内容拷贝到新内存中
int i = 0;
for(;i < stack->size;i++)
{
new_data[i] = stack->data[i];
}
//释放原顺序栈中的内存
free(stack->data);
//将扩容后的内存保存在顺序栈的结构中
stack->data = new_data;
}
//尾插入栈
stack->data[stack->size++] = value;
return;
}
4. 尾删出栈
(1)首先判定顺序栈中是否为空,为空则出栈失败
(2)若不为空,直接将顺序栈长度减1即可。此时,原尾元素已变为无效元素。
代码如下:
//尾删出栈
void SeqStackPop(SeqStack* stack)
{
if(stack == NULL)
{
//非法输入
return;
}
if(stack->size == 0)
{
//空顺序栈
return;
}
//将尾元素设置为无效元素即可
--stack->size;
}
5. 取栈顶元素
(1)判断顺序栈是否为空,为空则失败
(2)不为空,因为顺序栈的尾部为栈顶所在位置,所以只需将下标为size-1处的元素保存下来即可。
代码如下:
//取栈顶元素,返回值:-1代表出错返回,0代表成功返回
int SeqStackTop(SeqStack* stack,SeqStackType* value)
{
if(stack == NULL || value == NULL)
{
//非法输入
return -1;
}
if(stack->size == 0)
{
//空顺序栈
return -1;
}
*value = stack->data[stack->size - 1];
return 0;
}
6. 销毁顺序栈
顺序栈在初始化前并没有申请的内存空间和元素,所以销毁后要将顺序栈恢复为初始化前的状态。即释放data指向的动态申请的内存,将有效长度和实际长度均置为0即可。
代码如下:
//销毁顺序栈
void Destory(SeqStack* stack)
{
stack->size = 0;
stack->capacity = 0;
free(stack->data);
return;
}
二,链式栈
链式栈是通过单链表来实现的。每次入栈一个元素,向链表中添加一个节点,出栈一个元素,释放一个节点。因为栈具有“后进先出”的特点,如果每次在链表的尾部进行插入和删除,就要遍历整个链表来找到尾节点。而在头部进行插入和删除时,只需根据头指针即可找到链表的首元素结点。而无需遍历链表。所以链式栈的出,入栈通过对链表进行头删和头插来实现。
1. 链式栈的结点结构
链式栈是有单链表来实现的,所以与单链表的结点结构相同。由数据域和指向下一个结点的next域组成。
typedef char LinkStackType;
//定义链栈的节点结构
typedef struct LinkStackNode
{
LinkStackType data;
struct LinkStackNode* next;
}LinkStackNode;
2. 链式栈的初始化
与单链表的初始化相同,可以通过查看博客“单链表的基本操作”来详细了解。
//初始化链栈
void LinkStackInit(LinkStackNode** pstack)
{
if(pstack == NULL)
{
//非法输入
return;
}
*pstack = NULL;
}
3. 链式栈的入栈操作
链式栈的入栈是由单链表的头插来实现的。这里也不详细说明。
//创建节点
LinkStackNode* CreateNode(LinkStackType value)
{
LinkStackNode* new_node = (LinkStackNode*)malloc(sizeof(LinkStackNode));
new_node->data = value;
new_node->next = NULL;
return new_node;
}
//头插入栈
void LinkStackPush(LinkStackNode** pstack,LinkStackType value)
{
if(pstack == NULL)
{
//非法输入
return;
}
//创建节点
LinkStackNode* new_node = CreateNode(value);
//将新节点的next指向原来的首原节点来做为新的首原节点
new_node->next = *pstack;
//使头指针指向新的首原节点
*pstack = new_node;
return;
}
4. 链式栈的出栈操作
链式栈的出栈操作是通过单链表的头删来实现的。
(1)首先,判断链表是否为空,为空则出栈失败
(2)不为空,头指针指向第二个节点,使第二个节点作为新的首原节点
(3)释放原来的首原节点
代码如下:
//销毁节点
void DestoryNode(LinkStackNode* node)
{
free(node);
}
//头删出栈
void LinkStackPop(LinkStackNode** pstack)
{
if(pstack == NULL)
{
//非法输入
return;
}
if(*pstack == NULL)
{
//空链栈
return;
}
//保存要删除的首原节点
LinkStackNode* to_delete = *pstack;
//使头指针指向第二个节点
*pstack = to_delete->next;
//释放要删除的节点
DestoryNode(to_delete);
return;
}
5. 取栈顶元素
此时的栈顶位于链表的头部。
(1)如果链表为空,则失败
(2)若不为空,将首原节点的数据域保存下来即可。
代码如下:
//取栈顶元素
int LinkStackTop(LinkStackNode* stack,LinkStackType* value)
{
if(stack == NULL)
{
//空链表
return -1;
}
*value = stack->data;
return 0;
}
6. 链式栈的销毁
初始化前链式栈只有一个头指针,所以销毁后要恢复到初始化前的状态。所以
(1)遍历链表将各个节点进行释放
(2)为避免头指针变成野指针,将头指针置空
代码如下:
//销毁链式栈
void LinkStackDetory(LinkStackNode** stack)
{
if(stack == NULL)
{
//非法输入
return;
}
//遍历链表各节点对其进行释放
LinkStackNode* to_delete = *stack;
while(to_delete != NULL)
{
LinkStackNode* next_node = to_delete->next;
free(to_delete);
to_delete = next_node;
}
//将头指针置空
*stack = NULL;
return;
}