栈是一种先进后出的数据结构,先存入的数据后取出来,后存入的数据先取出来。
栈分为以下两种:
1.动态栈。动态栈是基于链表的。
2.静态栈,静态栈是基于数组的。
本文着重讨论动态栈。用C语言去实现。
栈类似于箱子,顶部只有一个口子,栈的操作只能在从顶部操作。压栈和弹栈都是操作的栈顶。
压栈:压栈就是将数据放到栈的顶部。
弹栈:弹栈就是将数据从栈顶部取出。
废话不多说,直接看代码。
动态栈是基于链表的,压入的数据要存到链表的节点中,所以先定义一个节点的数据类型。
typedef struct node
{
int data; //节点的数据域
struct node * pNext;//节点的指针域
}Node,*PNode;//定义节点类型和节点类型的指针类型
//Node 等效于 struct node PNode 等效于 struct node *
空栈具有栈顶指针,栈底指针和一个无效节点。栈顶指针和栈底指针是指向节点类型的指针,无效节点类似于链表的头节点。不存放实际数据,它的存在只是为了方便操作。
typedef struct stack
{
PNode pTop; //指向栈顶的指针
PNode pBottom; //指向栈底的指针
Node s; //栈中无效的节点,为了方便栈操作而定义
}Stack,*PStack;//定义栈类型和栈类型的指针类型
数据类型定义好以后我们就开始写关于栈的API吧。
创建栈
/***********
功能:创建栈
参数:ps是指向栈的指针的指针 如果创建失败*ps的值无效
返回值:创建成功返回true 创建失败返回false
***********/
bool create_stack(PStack * ps)
{
*ps = (PStack)malloc(sizeof(Stack));//申请一个空栈所需要的内存
if (*ps == NULL)
return false;
(*ps)->s.pNext = NULL; //无效节点指针域清空 表示后边再无节点
(*ps)->pBottom = &((*ps)->s); //栈顶指针指向无效节点
(*ps)->pTop = &((*ps)->s); //栈底指针指向无效节点
return true;
}
创建栈就是创建一个空栈。
创建过程:空栈包括一个栈顶指针,一个栈底指针,和一个无效节点。申请空栈所需要的内存,无效节点指针域清空,因为它是最底部的节点。栈顶指针和栈底指针都指向无效节点。
注意参数 PStack * ps 是一个二级指针,也就是常说的指向指针的指针,ps的类型是PStack *类型也就是Stack * *类型。
压栈
/***********
功能:压栈
参数:ps是指向栈的指针的指针 value是压入的值
返回值:压栈成功返回true 压栈失败返回false
***********/
bool push_stack(PStack * ps,int value)
{
PNode p = (PNode)malloc(sizeof(Node));//压栈时先申请节点所需的内存
if (p == NULL)
return false;
p->data = value;//将压栈的值存入节点
p->pNext = (*ps)->pTop;//将新节点的指针域指向栈顶指针(新节点和栈中最上边一个节点连起来)
(*ps)->pTop = p;//栈顶指针指向新节点(栈顶指针上移)
return true;
}
压栈就是将数据放入栈顶。
压栈的具体过程:创建一个新的节点,将要压入的数据放入节点。节点的指针域指向栈顶,栈顶指针指向新的节点(栈顶指针上移)。
判断栈是否为空
/***********
功能:判断栈是否为空
参数:ps是指向的栈的指针
返回值:栈为空返回true 栈非空返回false
***********/
bool stack_is_empty(PStack ps)
{
if (ps->pTop == ps->pBottom)
return true;
else
return false;
}
这个很好理解,当栈顶指针和栈底指针指向同一个节点的时候,此时栈一定是空栈。其实是栈顶指针和栈底指针同时指向无效节点时,栈是一个空栈。
弹栈
/***********
功能:弹栈
参数:ps是指向栈的指针的指针 pvalue指向的地址保存了弹出的值
返回值:出栈成功返回true 出栈失败返回false
***********/
bool pop_stack(PStack * ps,int *pvalue)
{
PNode p = NULL;
if (stack_is_empty(*ps) == true)
return false; //弹栈失败返回false
p = (*ps)->pTop; //获取要弹出节点的地址
*pvalue = p->data; //保存要弹出的值
(*ps)->pTop = p->pNext; //栈顶指针指向下一个节点(栈指针下移)
free(p); //释放要弹出节点的内存
return true; //弹栈成功返回true
}
弹栈就是将数据从栈顶取出。
弹栈的过程:先获取栈顶节点的指针,将栈顶节点的数据域拷贝出来,栈顶指针向下移动。最后释放栈顶节点的内存。一定要释放内存,否则会造成内存泄漏。内存泄漏的后果就是程序运行一段时间堆中的内存被用光,应用程序申请不到内存,程序就无法正常运行了。
清空栈
/***********
功能:清空栈 让栈变为空栈回到创建完成以后的状态
参数:ps是指向栈的指针的指针
返回值:无
***********/
void clear_stack(PStack * ps)
{
int value = 0;
while (stack_is_empty(*ps) != true)//如果栈不为空
{
pop_stack(ps,&value);//弹栈
}
}
清空就很好办了,从栈顶开始将有效节点全部弹出就好了。清空以后的栈和刚创建完成的栈一模一样,栈中只有栈顶指针,栈底指针和一个无效节点。栈顶指针和栈底指针都指向栈中的无效节点。
销毁栈
/***********
功能:销毁栈
参数:ps是指向栈的指针的指针
返回值:无
***********/
void destroy_stack(PStack * ps)
{
int value = 0;
while (stack_is_empty(*ps) != true)//如果栈不为空
{
pop_stack(ps, &value);//弹栈
}
free(*ps);//将创建空栈时动态申请的内存释放
*ps = NULL;//将指向栈指针的指针赋值为NULL
}
销毁过程:将栈中的有效节点全部弹出,然后将空栈所需要的内存释放掉。此时栈已经不复存在。注意与清空栈的区别,清空栈只是将栈变成一个空栈,栈还是存在的。
遍历栈:
/***********
功能:销毁栈
参数:ps是指向栈的指针
返回值:无
***********/
void traverse_stack(PStack ps)
{
PNode p = ps->pTop;//获取栈顶的节点
printf("遍历值:");
while (p != ps->pBottom)//当前遍历的节点不是最后一个节点
{
printf("%d ",p->data);//打印当前所遍历节点的值
p = p->pNext;//遍历节点的指针指向下一个节点(遍历指针下移)
}
printf("\n");
}
遍历栈就是从栈顶开始,把栈中有效节点挨个问候一遍。我这里是将它们输出。
遍历过程:先获取栈顶节点的地址,将栈顶节点的地址保存到一个临时指针变量中,这里不能移动栈顶指针。如果节点不是最后一个节点就将其输出,然后临时指针变量下移,再判断,如此往复。
下面演示一下栈的使用,只是演示栈的操作过程,并不是用栈解决实际问题。
PStack ps = NULL; //定义指向栈的指针
int popvalue = 0; //保存栈中弹出的值
create_stack(&ps); //创建栈
push_stack(&ps, 5);
push_stack(&ps, 9);
push_stack(&ps, 6);
push_stack(&ps, 3);
traverse_stack(ps);
pop_stack(&ps, &popvalue);//演示程序偷懒了,没有判断弹出是否成功
printf("弹出的值:%d\n",popvalue);
pop_stack(&ps, &popvalue);
printf("弹出的值:%d\n", popvalue);
traverse_stack(ps);
clear_stack(&ps);
traverse_stack(ps);
printf("%p\n", ps);
destroy_stack(&ps);
printf("%p\n",ps);
运行结果如下
重点观察清空栈以后指向栈的指针保存的是一个有效值,当销毁栈以后指向栈的指针保存的是0了,无效的地址。因为销毁以后栈已经不复存在了。