1. 栈
栈是一种重要的线性结构,是线性表的一种具体表达形式。栈这种后进先出的数据结构应用是非常广泛的。在生活中,例如我们的浏览器,每点击一次“后退”都是退回到最近的一次浏览网页。例如我们Word,Photoshop等的“撤销”功能也是如此。再例如我们C语言的函数,也是利用栈的基本原理实现的。
1.1 栈的定义
官方定义:栈(Stack)是一个后进先出(Last in first out,LIFO)的线性表,它要求只在表尾进行删除和插入操作。
小甲鱼定义:所谓的栈,其实也就是一个特殊的线性表(顺序表、链表),但是它在操作上有一些特殊的要求和限制:
(1) 栈的元素必须“后进先出”。
(2) 栈的操作只能在这个线性表的表尾进行。
(3) 注:对于栈来说,这个表尾称为栈的栈顶(top),相应的表头称为栈底(bottom)。
栈的插入操作(Push),叫做进栈,也称为压栈,入栈。类似子弹放入弹夹的动作。栈的删除操作(Pop),叫做出栈,也称为弹栈。如同弹夹中的子弹出夹。
1.2 栈的顺序存储结构
因为栈的本质是一个线性表,线性表有两种存储形式,那么栈也有分为栈的顺序存储结构和栈的链式存储结构,在这里我们介绍栈的顺序存储结构。最开始栈中不含有任何数据,叫做空栈,此时栈顶就是栈底。然后数据从栈顶进入,栈顶栈底分离,整个栈的当前容量变大。数据出栈时从栈顶弹出,栈顶下移,整个栈的当前容量变小,如下图所示
具体结构如下所示
typedef struct
{
ElemType *base;
ElemType *top;
int stackSize;
}sqStack;
这里定义了一个顺序存储的栈,它包含了三个元素:base,top,stackSize。其中base是指向栈底的指针变量,top是指向栈顶的指针变量,stackSize指示栈的当前可使用的最大容量。
1.2.1 创建一个栈
具体代码如下所示
#define STACK_INIT_SIZE 100
initStack(sqStack *s)
{
s->base = (ElemType *)malloc( STACK_INIT_SIZE * sizeof(ElemType) );
if( !s->base )
exit(0);
s->top = s->base; // 最开始,栈顶就是栈底
s->stackSize = STACK_INIT_SIZE;
}
这是一个较为简单的过程,他首先给栈的分配了一个内存,之后使栈的头指针与尾指针重合,栈的最大容量就是开始定义的 STACK_INIT_SIZE。
1.2.2 入栈操作
入栈操作又叫压栈操作,就是向栈中存放数据。入栈操作要在栈顶进行,每次向栈中压入一个数据,top指针就要+1,知道栈满为止。
具体代码如下所示
#define SATCKINCREMENT 10
Push(sqStack *s, ElemType e)
{
// 如果栈满,追加空间
if( s->top – s->base >= s->stackSize )
{
s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
if( !s->base )
exit(0);
s->top = s->base + s->stackSize; // 设置栈顶
s->stackSize = s->stackSize + STACKINCREMENT; // 设置栈的最大容量
}
*(s->top) = e;
s->top++;
}
上面的代码中不是很好理解的部分是,如果栈满如何追加栈的部分。首先进行判断,如果 if( s->top – s->base >= s->stackSize )
,说明已经溢出了,栈满了要追加,这个时候使用 realloc
进行追加,它不仅仅可以扩大栈的大小还可以将原先栈中的元素复制过来。进行扩展之后得到了新的栈底,然后加上模型容量得到了新的栈顶。有的人会问,在这个时候栈顶与栈底之间的差没有加大啊,那么栈的容量也没有扩大啊。这个实际上存在着一个误区,栈顶减去栈底只是现在的容量是多少,但是它并不是整个栈的最大容量。
1.2.3 出栈操作
出栈操作就是在栈顶取出数据,栈顶指针随之下移的操作。每当从栈内弹出一个数据,栈的当前容量就-1。
具体代码如下所示
Pop(sqStack *s, ElemType *e)
{
if( s->top == s->base ) // 栈已空空是也
return;
*e = *--(s->top);
}
出栈的具体操作是先将指针下移,再将元素取出。因为栈顶总是最上面的元素的上面,所以下移了就相当于从栈中去掉了一个元素。
1.2.4 清空栈操作
所谓清空一个栈,就是将栈中的元素全部作废,但栈本身物理空间并不发生改变(不是销毁)。因此我们只要将s->top的内容赋值为s->base即可,这样s->base等于s->top,也就表明这个栈是空的了。这个原理跟高级格式化只是但单纯地清空文件列表而没有覆盖硬盘的原理是一样的。
具体代码如下所示
ClearStack(sqStack *s){
s->top = s->base;
}
1.2.5 销毁栈操作
销毁一个栈与清空一个栈不同,销毁一个栈是要释放掉该栈所占据的物理内存空间,因此不要把销毁一个栈与清空一个栈这两种操作混淆。
DestroyStack(sqStack *s){
int i, len;
len = s->stackSize;
for( i=0; i < len; i++ ){
free( s->base );
s->base++;
}
s->base = s->top = NULL;
s->stackSize = 0;
}
整个过程相当于由底向上地清空栈中的元素。
1.2.6 计算栈的当前容量
计算栈的当前容量也就是计算栈中元素的个数,因此只要返回s.top-s.base即可。注意,栈的最大容量是指该栈占据内存空间的大小,其值是s.stackSize,它与栈的当前容量不是一个概念哦。
int StackLen(sqStack s)
{
return(s.top – s.base); // 返回的实际上是数字(两者间相隔多少个空位),而不是地址的差
}
1.3 栈的链式存储结构
栈的链式存储结构,简称栈链。(通常我们用的都是栈的顺序存储结构存储,链式存储我们作为一个知识点,大家知道就好!)栈因为只是栈顶来做插入和删除操作,所以比较好的方法就是将栈顶放在单链表的头部,栈顶指针和单链表的头指针合二为一,如下图所示
定义结构的代码如下所示
teypedef struct StackNode
{
ElemType data; // 存放栈的数据
struct StackNode *next;
} StackNode, *LinkStackPtr;
teypedef struct LinkStack
{
LinkStackPrt top; // top指针
int count; // 栈元素计数器
}
1.3.1 进栈操作
对于栈链的Push操作,假设元素值为e的新结点是s,top为栈顶指针,我们得到如下代码:
Status Push(LinkStack *s, ElemType e)
{
LinkStackPtr p = (LinkStackPtr) malloc (sizeof(StackNode));
p->data = e;
p->next = s->top; //将数据压入栈顶
s->top = p; //将现在的顶变为 p
s->count++;
return OK;
}
1.3.2 出栈操作
对于栈链的Push操作,假设元素值为e的新结点是s,top为栈顶指针,我们得到如下代码:
Status Pop(LinkStack *s, ElemType *e)
{
LinkStackPtr p;
if( StackEmpty(*s) ) // 判断是否为空栈
return ERROR;
*e = s->top->data;
p = s->top;
s->top = s->top->next;
free(p);
s->count--;
return OK;
}