目录
1.写在前面的话
在阅读这篇文章之前,我相信,无论你是一个业余代码爱好者,还是职业码农(职业码农有
可能会点开这种文章吗),即便你对栈的具体形式不是非常了解,但我相信你们一定或多或少的听
过这个词。无论是大一的初学C的我懵懂的从各路人马口中听到了“栈”这个词,还是在学大学计算
机基础中听到老师说的栈是计算机底层里动态存储数据的一种数据结构。“栈” 似乎出现在零碎知识
点的各个角落。当然,在系统性的学习数据结构之前,我也是对栈一知半解的。
2.线性表
栈是一种特殊的线性表,在了解栈之前,我们需要先粗略的了解一下线性表的概念
抛开代码层面不谈,我相信你看到线性表这三个字,一定心里能把这个玩意的大致长相在自
己的脑海中脑补出来,唯一的修饰词线性注定了这个表应该是笔直的,不管你们脑海中想的是啥,
反正我的脑海中的线性表是这样的:
我的脑海中浮现的是一个由6个小方块纵向堆叠而形成的“表”。
这个表看起来太空,我们可以试着给他加上一点“信息”。
是的,如你所见,我把一个人的人体结构(误)填在了这个线性表里,这下子他就成功进化
成为了“存储数据信息的线性表”,
这里我为大家引入线性表的定义:线性表是具有相同特性的n个数据元素的有限序列。
此处的数据元素都是对人体信息的描述(string类型)
说完了线性表表的结构,相信大家很快就能想到一个类似的玩意——是的,就是一维数组。
2.1顺序表
已经学过编程语言的我们知道,数组是有顺序的,他在内存中占一段连续的存储区。这里我
们就可以在线性表的基础上引出顺序表的概念:采用顺序存储结构的线性表称为“线性表”。(也就
是有序的线性表),我们在数组上学过的所有知识都能运用在顺序表上,我们甚至可以直接用一维
数组作为顺序表使用。
3.栈的概念
介绍完了线性表和顺序表,我们可以正式进入栈的学习了。
栈即是一种特殊的线性表,他是一种操作受限的线性表。
我们前面讲的顺序表就是受到了顺序的限制,此处的栈具有后进先出(LIFO Last-in and
First-out)的限制,也就是说,后面加入到栈内的元素,会优先从栈内被取出来,为了更好理解后
进先出这个限制,我们拓展一下汉诺塔问题:
3.1后进先出的实例——汉诺塔问题
问题引入: 相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置8个“金盘”。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
难怪说人家印度人程序员为啥多呢,还是有一点“产业背景”的。
咳咳,言归正传,这里我们暂时先不讨论汉诺塔问题的解法,好奇的话可以你们参考其他
CSDN上的文章,我们这里探究A杆上的八个金盘要如何取出来,我们很明显的能看出来,下面“金
盘”是被上面的“金盘”给死死压住的,如果我们要取出自上而下的第n个金盘,就得取出这个金盘上
面的(n-1)个金盘。也就是说,先放入A杆的金盘,只能在这个金盘的上头所有的金盘都被取出来
之后才被取出来。这就是后进先出了。
4.栈的基本操作
我本人在看书的时候是很摒弃去钻研一些概念和性质的,一般来说在概念看得一知半解的基础下,我就会直奔Coding层面、应用层面。和线性表一样,栈这一大类里面也分为很多小类:顺序栈、共享空间栈、链栈等,接下来由我来给大家介绍一下栈的基本操作(以C语言为例)。
4.1顺序栈
利用顺序存储方式实现的栈称为“顺序栈”,类似于顺序表的定义。讲人话就是新入栈的元素必
须紧紧跟随在之前已经入栈的元素之后。根据栈的定义,我们可以预设足够长度的一维数组来实
现: int stack[MAXSIZE](假设栈内元素数据类型为int)。
下面是顺序栈基本操作的代码实现:
//standard input & output基本输入输出头函数,C语言几乎必引入的头函数了
#include <stdio.h>
//要用到malloc函数开辟内存空间,则需要引入这个头文件
#include <stdlib.h>
//用到bool基本数据类型,则需要引入这个头文件
#include <stdbool.h>
//宏定义数据库最大空间
#define MAXSIZE 100
//定义栈
typedef struct{
int data[MAXSIZE];
int top;
}stack;
注:此处用到<stdbool.h>头函数是因为我的个人习惯,将某些原本的void无返回值的函数改成bool
返回值,如果操作成功则返回1,操作失败则返回0。如果觉得麻烦可以删去,并将后续bool返回值
的函数改成void。
//栈的初始化
stack *InitStack(){
stack *s;
//用malloc函数开辟内存空间,注意格式:变量 = (数据类型)malloc(sizeof(类型名称))
s = (stack*)malloc(sizeof(stack));
//将栈顶置-1,-1代表空栈的含义
s->top = -1;
return s;
}
//入栈
bool PushStack(stack *s,int data){
//若栈不为满,则入栈
if (s -> top == MAXSIZE - 1){
printf("Push Error : Full Stack\n");
return 0;
}
//先索引值自增,再入栈
s->data[++s->top] = data;
return 1;
}
//出栈
int PopStack(stack *s){
//若栈不为空,则出栈
if(s -> top == -1){
printf("Pop Error : Empty Stack\n");
return 0;
}
//先出栈,再索引值自减
int data = s->data[s->top--];
return data;
}
注:注意区分Push和Pop,Push是入栈,Pop是出栈,这两个概念容易混淆。为了使代码更加健
壮,可以将判断栈满的条件改成(s -> top >= MAXSIZE - 1 ) ,判断栈空的条件改成(s -> top <=
-1),由于此处我的代码比较简单,涉及到top的操作只有自增和自减,故不用担心跨出 (MAXSIZE
- 1)和 -1这两个边界值。
int main(){
//一些操作实例,有能力的话可以拓展成scanf接收参数自由的去操作栈
stack *s = InitStack();
printf("%d\n",PopStack(s));
PushStack(s, 1);
PushStack(s, 2);
printf("%d\n",PopStack(s));
return 0;
}
如果觉得这个过程很抽象的话,请拿出自己的草稿纸画一下一步一步的过程(不要眼高手低
喂!)或者借用上文中顺序表的图脑补下每一步入栈出栈的操作后顺序栈的模样。
4.2 共享空间栈
如果在平常程序中要用到栈,若采用了顺序栈,我们为了保证不发生上溢错误,就必须令栈
的空间足够(MAXSIZE足够大),这样下去,每个栈都被我们分配了过大的存储空间,而事实上
这些被分配了的空间有很多造成了浪费。于是,为了减少这种的内存浪费,我们可以让多个栈共用
一个足够大的连续空间,利用栈的动态特性使他们的存储空间互补。
其中,最常见的是两栈共享。两栈共享用生活中的例子来描述就好比我们小时候和同桌争课
桌的位置,甚至还规定“三八线”,谁跨过线谁是**之类的。就比如你是坐在桌子左侧的小孩,整个
桌子最左边到三八线就一定是你占据的空间,三八线到桌子的最右边就一定是你同桌占据的空间。
这种情况下就栈满了,因为桌子的所有空间都被占据了。当然如果你和你的同学比较“邻里和睦”,
你们之间没有规定明确的“三八线”,你主动退让出一些空间,他也主动退让出一些空间,中间这部
分空间谁也不占据,到某个人需要的时候,这块空间才会被使用,这就是两栈共享的最一般情况
了。当你们两都放学离开了,这个桌子被空了出来,这时候就是空栈了。
上面的例子中,你的桌子左边界就是左栈栈底,你同桌的右边界就是右栈栈底,只有你的右
边界(lefttop,你坐在左边,此处lefttop指左栈的边界也就是你的右边界)和你同桌的左边界
(righttop,你同桌坐在右边,此处righttop指右栈的边界也就是你同桌的左边界)是动态的。(注
意区分你的边界和栈的边界的区别)
下面是两栈共享基本操作的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXSIZE 100
//定义两栈共享,
typedef struct{
int data[MAXSIZE];
int lefttop;
int righttop;
}stack;
//初始化两栈共享
stack *InitStack(){
stack *s;
s = (stack*)malloc(sizeof(stack));
s -> lefttop = -1;
s -> righttop = MAXSIZE;
return s;
}
//入栈,status = 0时进左栈,status = 1时进右栈
bool PushStack(stack *s,bool status,int data){
if(s->lefttop + 1 == s->righttop){
printf("Push Stack Error : Full Stack\n");
return 0;
}
//Push into right stack
if (status) {
s -> data[--s->righttop] = data;
return 1;
}
//Push into left stack
s -> data[++s->lefttop] = data;
return 1;
}
//出栈,status = 0时,出左栈顶元素,status = 1时,出右栈顶元素
int PopStack(stack *s, bool status){
//Pop from right stack
if(status && s->righttop != MAXSIZE)
return s->data[s->righttop++];
//Pop from left stack
if(!status && s->lefttop != -1)
return s->data[s->lefttop--];
printf("Pop Stack Error : Empty Stack\n");
return 0;
}
int main(){
//一些操作实例
stack *s = InitStack();
printf("%d\n",PopStack(s, 0));
PushStack(s, 0, 1);
PushStack(s, 0, 3);
PushStack(s, 1, 5);
PushStack(s, 1, 4);
PushStack(s, 0, 3);
printf("%d\n",PopStack(s, 0));
printf("%d\n",PopStack(s, 1));
return 0;
}
操作实例演示图:
1. stack *s = InitStack();
这是初始化之后两栈共享的图像。
此时lefttop(左栈边界)和righttop(右栈边界)均在界外,代表栈空。(但由于编译器的原
因栈空位置会自动补0)
2.printf("%d\n",PopStack(s, 0));
打印出左栈出栈的元素,由于lefttop = -1,左栈栈空,故打印出错误信息,返回0。
3.PushStack(s, 0, 1);
将1这个整型常量入左栈,入栈后图像如下:
4.后续一系列PushStack和上面的大同小异,我也就不细做演示了,这里给出入栈结束后的图
像:
5.最后出栈操作后的图像:
特别注意:我这里的演示操作实例没有体现到栈满的情况,此处我再给出一种栈满的图像:
此时左栈边界 = 右栈边界 - 1,由图像可直观的看出栈满,此时将无法进行入栈操作。我们在
写入栈操作的函数时,要注意判断是否栈满的情况,以免造成数据溢出现象。
4.3链栈
无论是前文提到的顺序栈还是共享空间栈,栈的上溢现象似乎都是无可避免的,(即便分配
了很大的内存空间,这个分配了的内存空间也是一个定值,还是可以被超过的)这不由得让我们想
到链表,我们在初学链表的时候,链表一定会被拿来和数组进行比较,其相比数组最大的优点就是
不会出现越界的现象,当我们需要新的空间的时候,只需要执行malloc函数开辟新的结点即可。栈
亦是如此,我们也有链栈这个概念,通俗来说,链栈就是有栈性质的链表。
下面是链栈基本操作的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct StackNode{
int data;
struct StackNode *next;
}LinkStack;
//初始化链栈
LinkStack *InitLinkStack(){
LinkStack *top = (LinkStack*)malloc(sizeof(LinkStack));
//将栈顶置空,以确保初始化后的链栈为空栈
top->next = NULL;
return top;
}
//入栈
bool PushLinkStack(LinkStack *top, int InputData){
//由于开辟的是系统的存储空间,估此处没有判断是否栈满
//但其实也可以进行判断malloc后的p!=NULL,确保代码健壮性
LinkStack *p = (LinkStack*)malloc(sizeof(LinkStack));
p->data = InputData;
//此处插入操作等同于头插法
p->next = top->next;
top->next = p;
return 1;
}
//出栈
int PopLinkStack(LinkStack *top){
LinkStack *p;
//判断是否栈空,若头指针的下一个结点为空则为栈空
if(top->next == NULL){
printf("Pop Error : Empty LinkStack\n");
return 0;
}
//取出第一个结点(即栈顶)
p = top->next;
//将头指针指向第二个结点作为新栈顶
top->next = p->next;
return p->data;
}
int main()
{
//一些操作实例
LinkStack *top = InitLinkStack();
printf("%d\n",PopLinkStack(top));
PushLinkStack(top, 1);
PushLinkStack(top, 2);
printf("%d\n",PopLinkStack(top));
return 0;
}
当你仔细看完这段代码,你会发现,你好像学到了什么,又好像什么也没学到,有这种想法
是没有问题的,因为上面这些基本操作也就是空有链栈的皮囊,实际上完完全全可以换成顺序栈来
实现相同的功能。我们将顺序栈改成了链栈也仅仅只实现了避免栈上溢这个功能。为了实现让链栈
具有链表特性,我们不妨对上面的代码进行“优化”:
我们可以再对栈链进行一次包装,引入一个size变量代表栈链的大小,(也可以不进行包装直
接在栈链结构体上加上size变量,或者使用全局变量)再对Push和Pop函数进行“优化”,多传入一
个参数Position代表要出栈/入栈的位置,相信你们听到这里已经发现了,如果让链栈具有了链表的
特性,那么他就可以完全等价于链表了,任意位置的插入/输出消除了栈本身的特性。
这是“优化”后的代码结果(不建议细看):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct StackNode{
int data;
struct StackNode *next;
}LinkStack;
//再对栈链进行包装,引入size变量
typedef struct linkStack{
//size代表链栈的大小
int size;
LinkStack *top;
}DataForm;
//初始化数据表(数据表即为包装后的栈链)
DataForm *InitDataForm(){
DataForm *dataForm = (DataForm*)malloc(sizeof(DataForm));
dataForm->top = (LinkStack*)malloc(sizeof(LinkStack));
//将栈顶置空,以确保初始化后的链栈为空栈
dataForm->top->next = NULL;
//将栈的大小设置为0,代表此时为空栈
dataForm->size = 0;
return dataForm;
}
//入栈
bool PushLinkStack(DataForm *dataForm, int position, int InputData){
if (position > dataForm->size){
printf("Push Error : Cannot Find the Position");
return 0;
}
LinkStack *p = (LinkStack*)malloc(sizeof(LinkStack));
p->data = InputData;
//若position为0,则为入栈顶
if (position == 0){
p->next = dataForm->top->next;
dataForm->top->next = p;
}else{
int temp;
//由于top位置固定,故需要引入p_temp作为某个其他位置的"top"
LinkStack *p_temp = dataForm->top;
//通过position索引到需要入栈的位置
for(temp = 0; temp < position; ++temp)
p_temp = p_temp->next;
p->next = p_temp->next;
p_temp->next = p;
//上面的代码实现了链表的任意位置插入功能
}
++dataForm->size;
return 1;
}
...//省略后续代码
综上所述,链栈主要的目的就是为了防止栈上溢。
其实我在自己手敲代码的时候是没有马上意识到这个问题的,直到我入栈敲了一半才发现,
优化后的结果不就是链表嘛,有的时候一头钻研进去Coding,还不如坐下来理性分析一下呢。现
在当事人就是非常后悔,觉得自己非常的呆。
5.总结
嘛,总之这些就差不多是栈的入门了,从线性表到顺序表到顺序栈再到两栈共享最后到链
栈,上面的所有文字和代码都是我手打的,包括图片也是我自己做的。(除了某张看起来就是从上
找的)。希望我付出了这么多的心血能让你们有所收获吧,当然了,我自己在写栈的过程中也学到
了很多的新知识(压力马斯内)。后面的链栈可能讲的还是有些深奥,各位看官不妨反复细嚼慢咽
品品。
这里再给到大家一张最终的思维导图。
可以看到图中有一块箭头被我打上了问号,诸君不妨思考一下,能否用链栈实现共享空间栈
呢?或者说用链栈实现共享空间是否有意义呢?
OVER.