栈
在学习数据结构之前, 栈这个词是我听到过最多的数据结构名称了(其次是链表),大部分技术贴都会在开头写上“技术栈”三个字,然后罗列这个帖子内容中用到的各种技术。
那么真实的栈究竟是什么呢?又是为了解决什么问题呢?这一章给了我答案。
栈的定义
栈本质上是线性表,但是具有后进先出的特点,是限定仅在表尾进行插入和删除操作的线性表。栈相当于在地上挖了一个坑,存放数据就是把数据丢进坑里,先存入的数据在栈底,最后存入的在栈顶。对栈中的数据操作分为进栈和出栈,这些操作都只作用与当前的栈顶。
栈的抽象数据类型
栈的各元素,数据类型都是相同的(因为栈本质上就是线性表),它的删除和插入操作是最为特别的。
InitStack(*S); //初始化一个空栈
DestoryStack(*S);//销毁一个栈
ClearStack(*S);//清空一个栈,还原为原始的空栈
StackEmpty(S);//如果栈为空,返回True
GetTop(S,*e);//若栈非空,用e返回栈顶元素
Push(*S,e);//插入新元素e到栈顶
Pop(*S,*e);//删除栈顶的一个元素,并将数据返回给e
StackLength(S);//返回栈的元素个数
栈的顺序存储
线性表拥有两种存储概念,一个是顺序存储,一个是链式存储,栈也不例外。
顺序存储的栈结构为数组和top标记。
typedef struct{
int data[MAXSIZE];//最大长度为MAXSIZE的数组,这也限制了这个栈最多存储多少数据
int top;
}SqStack;
整数top实时记录着这个栈栈顶位于哪里,如果top为-1,那么这个栈是一个空栈,当top为MAXSIZE-1时,栈就满了。
顺序存储栈的进栈
进栈操作名为Push。
Status Push(SqStack *S,int e){
if(S->top == MAXSIZE-1){//检查是否满栈
return ERROR;
}
S->top++; //更新栈顶
S->data[S->top] = e;
return OK;
}
入栈的算法:
- 先确认这个栈是否已满,满栈就返回错误;
- 插入数据前,先将栈顶指示器更新到原栈顶上方的空白的空间;
- 将数据赋给栈顶指示器现在指向的那个空白空间,完成插入。
看上去挺简单的。
顺序存储栈的出栈
出栈操作名为Pop。
Status Pop(SqStack *S,int *e){
if(S->top == -1){//检查是否为空栈
return ERROR;
}
S->data[S->top] = *e;//将要被出栈的元素返回给e
S->top--;
return OK;
}
出栈的算法:
- 先确认这个栈是否为空栈,空栈是无法出栈,就返回错误;
- 删除前先将要被出栈的数据保存给e
- 将栈顶指示器下移,即可完成出栈,实际上并没有真正的删除,毕竟这个是数组,之后要继续插入,直接赋予新的值就行了。
出栈的操作很有意思,只是将栈顶指示器移动,实际上出栈的数据现在还是存在于数组中的。
两个栈共享空间
顺序存储栈是很简单高效的栈类型,但是也有缺点,也是线性表顺序存储的通病,那就是要先确定好数组的空间大小。预设的空间如果大于需求,就会造成资源的浪费,如果小了,就更不好了。为了解决这个问题,两栈共享空间的概念就产生了。
两个栈共用一个数组空间,这样能够大大提高资源的利用率,结构如下:
typedef struct{
int data[MAXSIZE];
int top1; //栈1的栈顶指示器
int top2; //栈2的栈顶指示器
}SqDoubleStack;
两栈共享结构的入栈出栈操作要复杂一些,主要是多了栈号参数,它作用是告诉程序要对这个共享栈的哪个栈进行操作。
入栈的算法:
- 先确认栈是否已满,也就是top1的下一位是否和top2一致
- 从栈号参数确认要对哪个栈进行入栈。
- 进行简单地入栈操作。
Status Push(SqDoubleStack *S, int e, int StackNumber){
if(S->top1+1 == S->top2) return ERROR; //检查栈是否已满
if(StackNumber == 1){//操作对象是栈1
S->top1++; //更新栈1的栈顶指示器
S->data[top1] = e; //将新元素插入栈1
}
else if(StackNumber == 2){ //操作对象是栈2
S->top2--; //更新栈2的栈顶指示器
S->data[top2] = e; // 将新元素插入栈2
}
return OK;
}
出栈的算法:
- 先确认栈是否已满,也就是top1的下一位是否和top2一致
- 从栈号参数确认要对哪个栈进行入栈。
- 进行简单地入栈操作。
Status Push(SqDoubleStack *S, int *e, int StackNumber){
if (StackNumber == 1){
if(S->top1 == -1) return ERROR; //检查栈1是否为空
*e = data[S->top1];
S->top1--;
}
else if(StackNumber == 2){
if (S->top2 == MAXSIZE) retunr ERROR; //检查栈2是否为空
*e = data[S->top2];
S->top2--;
}
return OK;
}
栈的链式存储
顺序存储栈的长度在创栈的那一刻就被限定了。既然栈本质上是线性表,那么肯定也有相应的链式存储结构,也就是链栈。
链栈自顶向下,每个结点都有一个指针域指向其下面的第一个结点,而结点组成的链栈,本身还有一个指针用来作为栈顶指示器。
typedef struct StackNode{ //链栈的结点
int data;
struct StackNode *next; //指向后继的指针
}StackNode;
typedef struct StackNode * LinkStackPtr; //指向链栈结点的指针
typedef struct{ //整个链栈结构
LinkStackPtr top;//链栈的栈顶指示器
int count;//链栈当前长度
}LinkStack;
跟顺序存储不同,链栈可以存储任意、动态数量的数据,最典型的就是我们平时浏览网页,随着我们点击的链接加载的网页越多,记录着我们浏览过的网页就是链栈,我们点击后退能一直回退到最开始的首页。
链栈的进栈
由于是链式结构,进栈就要创建新的内存空间。
进栈算法:
- 创建一个结点大小的内存空间。
- 将数据存放入这个新创建的节点,并将这个结点的后继设为当前的栈顶
- 更新栈顶,让栈顶指示器指向这个新创建的节点。
- 最后不要忘了更新链栈长度
Status Push(LinkStack *S, int e){
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top;
S->top = s;
S->count++;
return OK;
}
链栈的出栈
由于是链式结构,就要涉及到内存的管理,出栈要及时释放掉被出栈的那块内存。
出栈算法:
- 先检查当前链栈是否为空,并声明一个临时结点指针。
- 将要被出栈的数据保存在变量中
- 临时结点指针存储下要被出栈的结点的地址
- 更新栈顶指示器到要被出栈的结点的下一个结点
- free掉临时结点指针存储的地址处的结点
- 最后不要忘了更新链栈长度
Status Pop(LinkStack *S, int *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;
}
小结
学好了前面的线性表,栈的概念就容易理解了,后面是队列,是个硬骨头,加油!