准备知识:单链表的运算
栈的定义
先讲一个小故事,当然了这个故事是我从书上看到的。
这个故事是这样的:早些时候军官都是喜欢用左轮手枪,而非弹夹手枪这是为什么呢?原因是在那个时候的子弹质量是不过关的,有个别的子弹是臭弹——就是有问题的、打不出来的子弹。这会导致什么呢?你正和敌人拼命的时候,结果你的子弹卡壳了,后面的好子弹一颗都打不出来了,我去这不是要了命了吗?如果你运气好一点的话,先把臭弹作为第一个放到弹夹里面,那么这样是不是它就会在最后面才会打出来,当然了在我们这个年代这种情况几乎是不可能出现的,除非…哈哈不扯蛋了🤣🤣🤣!下面开始正式来介绍:
其实呢,我们上面故事中的弹夹就是一个栈,大家想一下那个臭弹是不是第一个进入弹夹的,可是却是最后一个出的,我们把具有这样特性的结构都称为栈 。那么是什么特性呢?就是我是第一个进的,居然是最后一个出来的;我是最后一个进去的,但是第一个就可以出了,具有这样特性的就是栈。
好了,现在我们知道栈的定义了,让我们来想一想有什么这种“倒霉”的结构呢?哈哈,开玩笑了。我们当然是要利用这种结构的特性了。在我们生活中有什么是利用这样的特性的吗?当然了,我们的仓库存取就是具有这种特性的,我们可以把常用的最后在放到仓库里面,这样它们就会排到前面方便拿出来,把一些不常用并且有价值的最先放到仓库的最后面,那么没用的丢掉就好了…。还有我们所用到的浏览器,当我们打开一个网页的时候,可能会点开某个东西,然后一看我去,这么垃圾,这个时候就回到了我们之前的那个页面上。
当然了,上面给出的定义是为了方便大家理解的,现在要给出一个正式的定义:
栈是一种只能在一端进行插入或删除操作的线性表
栈的一些概念
表中允许进行插入、删除操作的称为栈顶栈顶,另外一端叫做栈底,当栈中没有元素就是空栈。栈又称为后进先出的线性表,简称LIFO结构。
元素出栈的顺序
这个问题的前提是如果确定了入栈的顺序,那么请问的出栈的顺序有多少种呢?
我们只要记住栈是先入后出就行了,具体是啥意思呢?这么说吧有1234这么一个数字序列入栈了,他们的入栈顺序就是1234,如果说数字3是第一个出栈,那么能得到什么结论呢?我们知道栈是一个先进后出的一个结构,数字12肯定是比数字3要先入栈,由于先进后出,那么数字12现在就是压栈了对吗;也正是由于先进后出,本来是数字4要比3的优先出栈,结果数字3先出栈,说明什么?说明数字4此时还没入栈,那么现在的状态应该是这样的:
这样大家应该明白了,栈是怎么操作的把?下面来看一些题目:
问:元素的入栈顺序序列是1234,能否得到3142的出栈顺序呢?
答:不能,这不一眼看到就不行么?为什么一眼看上去就不行呢?
让我们来分析下:
现在的情况就是3出栈了,那么12必定是压栈了是不是?
那么2的出栈顺序一定是在1的前面,所以这一看就是错的对吗?
好的,别着急现在我们继续来分析下,加强一下对栈的学习
现在如果是3出栈了,他的下一个可能出栈的是谁?是不是可以是2或者是4呢?
对于2来说是不是就是直接出栈就好了,对于4来说是不是就是先入栈后出栈。
问:设一个栈的输入序列为a,b,c,d,则借助一个栈所得到的输出序列不可能是( )。
A. c,d,b,a
B. d,c,b,a
C. a,c,d,b
D. d,a,b,c
你能非常熟练的找出答案吗?A:c出栈了ab一定压栈,所以b一定是在a 的前面,d也可以在c出栈后入栈再出栈
B:d出栈了abc一定压栈,而且外面没有其他元素,所以出栈的顺序一定是cba
C:a出栈了其实不用分析,他是第一个元素所以直接看c出栈,b是压栈的但是d也可以先入栈出栈
D:d出栈了abc一定是压栈,外面没有其他元素所以出栈序列一定是cba所以D是错的,希望大家一起练习这个过程,可以更加熟悉栈的定义与操作。
一个栈的入栈序列为 1 , 2 , 3 , … , n , 1,2,3,…,n , 1,2,3,…,n,其出栈序列是 p 1 , p 2 , p 3 , … , p n 。 p_1,p_2,p_3,…,p_n。 p1,p2,p3,…,pn。若 p 1 = 3 , p1=3, p1=3,则 p 2 p_2 p2可能取值的个数是多少?(让我们好好来看看)
A. n − 3 n-3 n−3
B. n − 2 n-2 n−2
C. n − 1 n-1 n−1
D. 无法确定
当然了我们可以首先就排除D了是不是😁😁😁😂😂😂(凭借我们多年的经验)
还是来好好看一看这个问题的:p 1 = 3 p_1=3 p1=3 说明第一个出栈的元素是3这没错吧,问 p 2 = ? p_2=? p2=?就是要问3后面出栈的元素是多少对吧?
现在我们开始思考,3出栈了,12就被压栈了,下一个可以是2对吧,但是外面还有多少个元素呢?
12在栈底,3出栈了,当然是有 n − 3 n-3 n−3个元素了,很好理解为什么是 n − 3 n-3 n−3个元素,如图,
是的现在我们知道了外面有 n − 3 n-3 n−3个元素了,但是,我们为什么要知道外面有多少个元素呢?不要着急现在观察一下下面的这个图:
大家发现了什么规律呢?是不是我们可以让 4 4 4~ n n n任意出栈呢?如果大家在上面的练习中下功夫的话,相信计在算栈外面有多少元素的时候就想到了对吗?
所以, 3 3 3后面可以出栈的元素是元素 2 2 2和栈外面所有的元素 n − 3 n-3 n−3个,是吗?
那么 2 2 2后面一共就是 1 + n − 3 1+n-3 1+n−3个出栈的可能,就是 n − 2 n-2 n−2种可能。别着急啊,在上面的推理中,我们知道当3出栈的时候, 12 12 12被压栈了对吧,而且 2 2 2的出栈顺序一定是比 1 1 1要早的这个我们大家都知道,我们还知道栈外面的元素都是可以在 3 3 3的后面出栈的对吧?
你现在是不是被我问的烦了,没错,那么说明你是掌握了上面的知识了。如果你还没感觉到烦,说明你棒,继续往下看吧!
那么现在我们整合一下什么样的元素是可以在 3 3 3后面出栈的,是不是 2 2 2和 4 4 4~ n n n啊,很好。那么什么元素是不可以在 3 3 3后面出栈的呢?是不是 1 1 1呀,我们知道每个元素有俩个状态,一个就是可以在 3 3 3后面出栈,一个就是不可以在 3 3 3后面出栈。如果我们用所有的元素减去不可以在 3 3 3后面出栈的元素是不是就得到了可以在 3 3 3后面出栈的元素了,那么现在问怎么算啊?是 n − 1 n-1 n−1吗,我去怎么不对啊,难道不是所有的减去不能出去的怎么不对呢?那么你是不是忘记减去 3 3 3了呀, 3 3 3已经出去了呀!!!
那么现在答案是不是就是 n − 1 − 1 n-1-1 n−1−1,就是 n − 2 n-2 n−2呀。
一个栈的入栈序列为 1 , 2 , 3 , … , n , 1,2,3,…,n , 1,2,3,…,n, 其出栈序列是 p 1 , p 2 , p 3 , … , p n 。 p_1,p_2,p_3,…,p_n。 p1,p2,p3,…,pn。若 p 2 = 3 , p_2=3, p2=3,则 p 3 p_3 p3可能取值的个数是( )多少?
A.n-3
B.n-2
C.n-1
D. 无法确定那么这个题目是不是和上面的那个题目非常的相似呢?
上面的题目是 p 1 = 3 p_1=3 p1=3,这里是 p 2 = 3 p_2=3 p2=3;上面的题目是说 3 3 3是第一个出栈的,这里说的是 3 3 3是第二个出栈的。
我们知道如果说是第一个出栈的是 3 3 3的话, 12 12 12一定是被压栈的,所以说下一个出栈的元素一定不会是 1 1 1,对吧。可是如果 3 3 3 是第二个出来的,那么谁是第一个出来的呢?
是不是只能是 1 , 2 , 4 1,2,4 1,2,4
那现在思考:
如果 p 1 = 1 p_1=1 p1=1的话是不是 p 3 = 2 , 4 , 5 , 6 , . . . , n p_3=2,4,5,6,...,n p3=2,4,5,6,...,n啊, 1 , 3 1,3 1,3出栈, p 3 ! = 1 , 3 p_3!=1,3 p3!=1,3;
如果 p 1 = 2 p_1=2 p1=2的话是不是 p 3 = 1 , 4 , 5 , 6 , . . . , n p_3=1,4,5,6,...,n p3=1,4,5,6,...,n啊, 2 , 3 出 栈 2,3出栈 2,3出栈, p 3 ! = 2 , 3 p_3!=2,3 p3!=2,3;
如果 p 1 = 4 p_1=4 p1=4的话是不是 p 3 = 2 , 5 , 6 , 7 , . . . , n p_3=2,5,6,7,...,n p3=2,5,6,7,...,n啊, 1 1 1被压栈, 3 , 4 3,4 3,4出栈, p 3 ! = 1 , 3 , 4 p_3!=1,3,4 p3!=1,3,4。也就是说不同的情况下, p 3 p_3 p3不能取到的值是不一样的,如果把 p 3 p_3 p3不能取到的值合起来就是 p 3 ! = 1 , 2 , 3 , 4 p_3!=1,2,3,4 p3!=1,2,3,4对吧,但是我们观察到在不同的情况下 p 3 p_3 p3可以取到的值合起来就是 p 3 = 1 , 2 , 4 , 5 , 6 , 7 , . . . , n p_3=1,2,4,5,6,7,...,n p3=1,2,4,5,6,7,...,n,那么就是说 p 3 ! = 3 p_3!=3 p3!=3对吧。
所以答案是 n − 1 n-1 n−1。
显然,当只强调第二个出栈的值是 m m m时,下一个出栈的元素一定不是 m m m,但是可以是其它任意的元素。
关于这里有一个卡特兰数(Catalan Number): 1 n + 1 C 2 n n \LARGE\frac{1}{n+1}C^n_{2n} n+11C2nn,它是n个不同的元素通过一个栈产生的出栈顺序的个数。
栈的顺序结构存储和一些运算方法
顺序存储结构
我们知道栈就是操作受限的线性表,那么就是说栈的顺序存储就是线性表的顺序存储,那我们先要回忆一下顺序表的顺序存储是如何定义的吧:
#define ElemType int
#define MAXSIZE 50
typedef struct {
ElemType data[MAXSIZE];
int length;//数组里面元素的个数
}SqList;//顺序表
我们可以看到顺序表其实就是一个数组加上一个当前数组的元素个数就是数组的当前长度,它其实和数组没什么区别,只不过在求顺序表长度时不用遍历整个顺序表,直接访问结构体成员length,这样的好处就是降低了时间复杂度。
可是我们定义顺序栈的时候,还需要定义一个数组的长度吗?
当然是不需要的,因为栈已经失去了随机存储的特性!
我们对栈的操作只能在栈顶进行,那么如何体现只能在栈顶操作的这个特性呢?
这里我们采用一个指针top,它所指的位置就是栈顶,我们只能对top所指的位置进行操作,对于其他的位置是什么样的我们并不关心,这样的数据结构就是我们的顺序栈。下面来看看顺序栈是如何定义的吧:
#define ElemType int
#define MAXSIZE 50
typedef struct{
ElemType data[MAXSIZE];//存放结点信息
int top;//由于结点是数组存放的,所以指向数组位置的最好的就是数组的下标
}SqStack;//顺序栈
这里我们把数组的第一个位置就是data[0]当作了栈底,那么就会有以下的性质:
栈空的条件: s — > t o p = = − 1 s—>top==-1 s—>top==−1
栈满的条件: s — > t o p = = M A X S I Z E − 1 s—>top==MAXSIZE-1 s—>top==MAXSIZE−1
入栈要先腾出一个位置: s — > d a t a [ + + t o p ] = e s—>data[++top]=e s—>data[++top]=e
出栈要保留元素在删除位置: e = s — > d a t a [ t o p − − ] e=s—>data[top--] e=s—>data[top−−]
简单的运算不做说明
初始化栈:InitStack(Sqstack *&S)
void InitStack(SqStack *&S){//创建一个空栈
S=(SqStack*)malloc(sizeof(SqStack));
S->top=-1;//空栈的条件
//cout<<"初始化成功!"<<endl;
}
销毁栈:DestroyStack(Sqstack *&S)
void DestroySqStack(SqStack *&S){
free(S);//释放这片连续的地址
//cout<<"销毁成功!"<<endl;
}
判断栈是否为空:EmptyStack(SqStack*S)
bool EmptyStack(SqStack*S){
/*
if(S->top==-1){
cout<<"栈空!"<<endl;
}
else{
cout<<"栈不空!"<<endl;
}
*/
return S->top==-1;
}
入栈:Push(SqStack*&S,ElemType e)
bool Push(SqStack*&S,ElemType e){
if(S->top==MAXSIZE-1){
cout<<"栈满,放入失败!"<<endl;
return false;
}
S->data[++S->top]=e;
//cout<<"放入成功!"<<endl;
return true;
}
对于放入元素的时候我们需要知道:当前栈的状态,还能不能放得下元素。如果栈满了,这个时候就不能在放入元素了。
出栈:Pop(SqStack*&S,ElemType &e)
bool Pop(SqStack*&S,ElemType &e){
if(S->top==-1){
cout<<"栈空取出失败!"<<endl;
return false;
}
e=S->data[S->top--];
//cout<<"取出成功!"<<endl;
return true;
}
和上面放东西的时候一样,如果本来就没有东西,去哪里给你东西取的,要不你把栈拿走吧😂😂😂
显示栈顶的元素:GetTop(SqStack *S,ElemType &e)
bool GetTop(SqStack *S,ElemType &e){
if(S->top==-1){//判断当前栈有无元素非常重要
cout<<"栈的当前状态没有元素!"<<endl;
return false;
}
e=S->data[S->top];
//cout<<"栈顶的当前元素为:"<<e<<endl;
return true;
}
创建一个栈:CreateStack(SqStack *&S,ElemType str[],int n)
void CreateStack(SqStack *&S,ElemType *str,int n){
S=(SqStack*)malloc(sizeof(SqStack));
S->top=-1;
//InitStack(S);等价于上面的操作
for(int i=0;i<n;i++){
Push(S,str[i]);
}
}
顺序栈的一个简单的应用
设计一个算法,利用顺序栈判断一个字符串判断一个字符串是否为对称串。
大家一看这题目要求,我去什么垃圾题目啊,这不明显就是判断回文序列吗?我们刚学C语言的时候就学了它,现在让我们想一想回文有什么特点,比如,上海自来水来自海上,大家从左往右读,从右往左读是不是一样的呀?当时我们是怎么做的呢?对了!是用的循环+数组,对吗?现在让我们来快速的写一次!
bool symmetry_while(char *str){
int length=0;
for(int i=0;str[i]!='\0';i++){
length++;
}
for(int i=0,j=length-1;i<j;i++,j--){
if(str[i]!=str[j]){
cout<<"这不是一个回文字符串!"<<endl;
return false;
}
}
cout<<"恭喜,str是一个回文字符串!"<<endl;
return true;
}
这样是不是完美的解决了这个问题了呢?但是我们希望用栈来解决可以帮助我们熟悉栈的一些特性
我们已经非常了解栈的特性先进后出了,也就是说,如果一次性一个数组的元素全部入栈的话,出栈的顺序一定是唯一的对吧?而且这个顺序恰好和入栈的顺序是相反的,也就是我们可以利用栈得到一个逆序的序列。这样和原来正序的序列一一比较,同样可以得到结果
bool sysmetry_stack(ElemType *str,int n){
SqStack *S=NULL;
ElemType e;
CreateStack(S,str,n);
for(int i=0;i<n;i++){
Pop(S,e);
if(e!=str[i]){
cout<<"这不是一个回文字符串!"<<endl;
return false;
}
}
DestroySqStack(S);
cout<<"恭喜,str是一个回文字符串!"<<endl;
return true;
}
共享栈
我们可以看到用数组定义的顺序栈是非常好用的,可是这样静态的定义是非常不科学的。为什么呢?如果我们处理的数据是非常大的,事前我们是无法预知需要开辟多少空间的。现在有这样一种情况,如果我们需要俩个栈,并且俩个栈的数据类型是一模一样的,那我们是不是可以用一个数组来搞定啊?有人这时就会说了,你把两个数组的内容放到一起,难道不会混肴吗?嗯,这确实是个好问题,现在我要问一个问题,栈的特性是什么?先进后出对吧!好了,我现在要强调的是另外一个特性栈是只允许在一端插入和删除的线性表,那现在如果我要是把这俩个栈的栈顶放到一起,还会出错吗?只要两个指针永远不指向同一个位置或者说一个指针永远在另外一个指针的一侧,我们把这样的结构叫做共享栈顶的共享栈。
共享栈的物理存储
#define ElemType int
#define MAXSIZE 50
typedef struct {
ElemType data[MAXSIZE];
int top1;
int top2;
}SqDoubleStack;
那么我们现在来看下共享栈的一些性质:
栈空的条件:栈1空 t o p 1 = = − 1 top1==-1 top1==−1,栈2空 t o p 2 = = M A X S I Z E top2==MAXSIZE top2==MAXSIZE
栈满的条件: t o p 2 − t o p 1 = = 1 top2-top1==1 top2−top1==1 两个指针相邻
简单的运算不做说明
初始化共享栈:InitDoubleStack(SqDoubleStack *&S)
void InitDoubleStack(SqDoubleStack *&S){
S=(SqDoubleStack*)malloc(sizeof(SqDoubleStack));
S->top1=-1;
S->top2=MAXSIZE;
cout<<"初始化栈成功!"<<endl;
}
显示共享栈的元素:DispStack(SqDoubleStack*S)
void DispStack(SqDoubleStack*S){
cout<<"共享栈当前的状态为:"<<endl;
for(int i=0;i<=S->top1;i++){
cout<<S->data[i]<<" ";
}
cout<<"栈口";
for(int i=S->top2;i<MAXSIZE;i--){
cout<<S->data[i]<<" ";
}
cout<<endl;
}
入栈1或者是栈2:Push(SqDoubleStack*&S,ElemType e,int iNumber)
bool Push(SqDoubleStack*&S,ElemType e,int iNumber){
if(S->top1+1==S->top2){
cout<<"栈满!"<<endl;
return false;
}
if(iNumber==1){
S->data[++S->top1]=e;
return true;
}
else{
S->data[--S->top2]=e;
return true;
}
}
销毁共享栈:DestroySqDoubleStact(SqDoubleStack*&S)
void DestroySqDoubleStact(SqDoubleStack*&S){
free(S);
cout<<"销毁成功!"<<endl;
}
出栈1或者栈2的元素;Pop(SqDoubleStack*&S,ElemType e,int iNumber)
bool Pop(SqDoubleStack*&S,ElemType e,int iNumber){
DispStack(S);
if(S->top1==-1&&S->top2==MAXSIZE){
cout<<"共享栈已空,取出失败!"<<endl;
return false;
}
cout<<"请输入指定取出的栈1还是栈2"<<endl;
cin>>iNumber;
if(iNumber==1){
e=S->data[S->top1--];
}
else{
e=S->data[S->top2--];
}。
cout<<"取出成功!"<<endl;
return true;
}
栈的链式结构——链栈
对于栈的逻辑结构关系是呈线性关系的,所以我们同样可以像线性表的一样采用单链表的方式来存储。我们称这样的用链式存储的栈为:链栈。
我们知道单链表一般都是自带一个表头结点的。而我们知道栈只能在栈顶来做插入和删除的操作,那么栈顶是放在链表的头部还是尾部呢?当然是放在链表的头部了。我们当时给链表一个头结点的目的就是为了统一链表的格式,这样的好处是无论是否是空链表我们都可以进行同一个操作。
链栈的好处就是我们不用事先预料有多少元素,也不用担心栈溢出。
链栈的运算(单链表的运算)
栈的结点定义
typedef struct StackNode{
ElemType data;
struct StackNode *next;
}SqStackNode;
其实是与单链表的定义是一样的,只不过表头结点就是top指针的作用了!
栈链的几个重要的要素:
栈空的条件: S − > n e x t = = N U L L S->next==NULL S−>next==NULL
栈一般来说是不会出现满的情况的!
入栈:malloc一个结点放在表头结点的后面即可!
出栈:取出表头结点后面的元素(栈顶元素),然后free掉栈顶
初始化链栈:InitStackNode(SqStackNode*&S)
void InitStackNode(SqStackNode*&S){
S=(SqStackNode*)malloc(sizeof(SqStackNode));
S->next=NULL;
cout<<"初始化成功!"<<endl;
}
销毁链栈:DestroySqStackNode(SqStackNode*&S)
void DestroySqStackNode(SqStackNode*&S){
SqStackNode*p=S,*q=NULL;
while(p->next!=NULL){
q=p;
p=p->next;
free(q);
}
free(p);
cout<<"销毁成功!"<<endl;
}
判断链栈是否为空:EmptySqStackNode(SqStackNode*S)
bool EmptySqStackNode(SqStackNode*S){
return S->next==NULL;
}
入栈:PushOfSqStackNode(SqStackNode*&S,ElemType e)
void PushOfSqStackNode(SqStackNode*&S,ElemType e){//前插法!
SqStackNode *p=NULL;
p=(SqStackNode*)malloc(sizeof(SqStackNode));
p->data=e;
p->next=S->next;
S->next=p;
//cout<<"放入成功!"<<endl;
}
出栈:PopOfSqStackNode(SqStackNode*&S,ElemType &e)
bool PopOfSqStackNode(SqStackNode*&S,ElemType &e){
if(S->next==NULL){
cout<<"取出失败,栈已空!"<<endl;
return false;
}
SqStackNode*p=S->next;
e=p->data;
S->next=p->next;
free(p);
cout<<"取出成功!取出的元素是:"<<e<<endl;
return true;
}
获取栈顶的元素:GetTopE(SqStackNode*S,ElemType &e)
bool GetTopE(SqStackNode*S,ElemType &e){
if(S->next==NULL){
cout<<"获取失败,栈已空!"<<endl;
return false;
}
e=S->next->data;
cout<<"栈顶元素e为:"<<e<<endl;
return true;
}
打印栈的当前元素:DispSqStackNode(SqStackNode*S)
void DispSqStackNode(SqStackNode*S){
SqStackNode*p=S;
cout<<"动态栈当前的状态:"<<endl;
while(p->next!=NULL){
p=p->next;
cout<<p->data<<" ";
}
cout<<endl;
}