在数据结构中,栈和队列,严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 “一对一” 的数据。
使用栈结构存储数据,讲究“先进后出”,即最先进栈的数据,最后出栈;使用队列存储数据,讲究 “先进先出”,即最先进队列的数据,也最先出队列。
其实栈和队列的原理和实现都很容易,但因为其在我们日常的使用中给我们带来了很大的帮助,不论是算法竞赛还是项目应用,栈和队列都是必不可少的存在,其中我们常见的栈的应用如:括号匹配,表达式的计算。而队列则常用于广度搜索。
在C++中,栈和队列都有封装好的模块,可以直接调用。
一、栈(Stack)
1.1栈的实现
形象理解就是一个杯子,你先加进去的东西,想取出来只有只有把上层的东西取出来才行,在C++中可以直接调用stl库里的stack函数,具体用法为:
#include<iostream>
#include<stack>//声明栈的调用
using namespace std;
int main()
{
//定义一个int型的栈,名字为q
stack<int>q;
cout<<"输入5个元素:";
for(int i=1;i<=5;i++)
{
int x;
cin>>x;
//将数据压入栈
q.push(x);
}
cout<<"输出栈内元素:";
//判断栈不为空
while(!q.empty())
{
//显示栈顶元素
cout<<q.top()<<" ";
//弹出栈顶元素,让第二个元素显示
q.pop();
}
return 0;
}
运行结果:
可以理解为逆序储存了我们输入的元素,栈的基本使用就输入输出,当然还有
q.size()//用来判断栈内元素个数,返回一个int型数值
在我们前面数据结构一中提到的链表的操作,我们尝试用链表进行栈的构建。
和创建链表的用法一样,先定义一个结构体,当然这个结构体我们要命名为Stack
//声明数据类型
typedef int Datatype;
typedef struct Stack{
Datatype data;
//定义指针
struct Stack *next;
};
然后来考虑链表的创建,有没有注意到一个有趣的事,在上一章节,我们提到链表的两种储存数据方式:顺序储存,逆序储存。
而我们的栈恰好是逆序储存,所以针对栈的使用,我们直接复现一下链表的逆序储存方式即可:
另外吸取上一章的教训,上一章我主要实现的是无空头链表,这样的链表某些操作很麻烦(主要LeetCode题目写多了…),当然无空头链表和有空头链表实现差别很小,就在无空头链表的基础上多加了一个空节点作为头部就行,其他部分实现都一样的,于是这一章或者往后的章节我都用有空头链表进行操作。
区别于:
无空头链表头指针定义:
struct List *head=NULL;
有空头链表定义为:
struct Stack *head=(struct Stack*)malloc(sizeof(struct Stack));
head->next=NULL;
然后我们实现插入部分,即Push函数,代码为:
void Push(struct Stack *head,Datatype data)
{
//定义游标指针p,指向第一个非空节点
struct Stack *p=head->next;
//定义新加入的节点
struct Stack *temp=(struct Stack *)malloc(sizeof(struct Stack));
if(temp==NULL)
{
printf("内存申请失败\n");
exit(0);
}
temp->data=data;
//逆序插入
temp->next=p;
head->next=temp;
}
实现了插入,我们来考虑怎么弹出,也就是删除首部节点(注意不是头节点,头节点是空的),Pop函数,代码如下:
void Pop(struct Stack *head)
{
//如果没有元素,返回
if(head->next==NULL)
return;
//定义游标指针p,指向第一个节点
struct Stack *p=head->next;
//改变连接方式,跳过p指向的节点
head->next=p->next;
//释放p的内存资源
free(p);
}
然后我们还差个类似于empty一样判断是否为空的函数,其实对链表而言,直接head->next是否为空就行了,不用刻意写成函数,于是基本的栈的功能,我们就用链表进行了实现。
完成代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//声明数据类型
typedef int Datatype;
typedef struct Stack{
Datatype data;
//定义指针
struct Stack *next;
};
void Push(struct Stack *head,Datatype data)
{
//定义游标指针p,指向第一个非空节点
struct Stack *p=head->next;
//定义新加入的节点
struct Stack *temp=(struct Stack *)malloc(sizeof(struct Stack));
if(temp==NULL)
{
printf("内存申请失败\n");
exit(0);
}
temp->data=data;
//逆序插入
temp->next=p;
head->next=temp;
}
void Pop(struct Stack *head)
{
//如果没有元素,返回
if(head->next==NULL)
return;
//定义游标指针p,指向第一个节点
struct Stack *p=head->next;
//改变连接方式,跳过p指向的节点
head->next=p->next;
//释放p的内存资源
free(p);
}
int main()
{
//定义有空头链表
struct Stack *head=(struct Stack*)malloc(sizeof(struct Stack));
head->next=NULL;
printf("输入5个元素:");
for(int i=1;i<=5;i++)
{
int data;
scanf("%d",&data);
Push(head,data);
}
//判断是否为空
while(head->next!=NULL)
{
printf("栈首元素=%d\n",head->next->data);
printf("弹出栈首元素\n");
Pop(head);
}
return 0;
}
运行结果:
1.2栈的应用
简单说一下栈的应用:括号匹配和表达式的计算。
这里由于C++的stl库比较简单,所以我用链表结构实现下。
括号匹配
最常见的问题描述为:输入一个计算式,问里面的括号规则是否匹配。
如:sin(x+y)+(3+7)*5,括号是否符合匹配原则
因为只考虑括号,所以其他非括号元素直接丢弃,利用栈实现的过程为:扫描输入的字符串,遇到操作数跳过,遇到左括号压入栈内,遇到右括号,弹出栈里一个左括号进行匹配,当执行结束后栈内元素为空,匹配成功,当栈不为空,匹配错误。或者当执行时遇到遇到右括号但是栈内没有左括号与之匹配,也是错误。
实现代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//声明数据类型为char
typedef char Datatype;
typedef struct Stack{
Datatype data;
//定义指针
struct Stack *next;
};
void Push(struct Stack *head,Datatype data)
{
//定义游标指针p,指向第一个非空节点
struct Stack *p=head->next;
//定义新加入的节点
struct Stack *temp=(struct Stack *)malloc(sizeof(struct Stack));
if(temp==NULL)
{
printf("内存申请失败\n");
exit(0);
}
temp->data=data;
//逆序插入
temp->next=p;
head->next=temp;
}
void Pop(struct Stack *head)
{
//如果没有元素,返回
if(head->next==NULL)
return;
//定义游标指针p,指向第一个节点
struct Stack *p=head->next;
//改变连接方式,跳过p指向的节点
head->next=p->next;
//释放p的内存资源
free(p);
}
int main()
{
//定义有空头链表
struct Stack *head=(struct Stack*)malloc(sizeof(struct Stack));
head->next=NULL;
char s[1000];
while(true)
{
printf("输入exit退出,不然输入表达式:");
//清空字符串
memset(s,0,sizeof(s));
scanf("%s",s);
if(strcmp(s,"exit")==0)
break;
//清空栈
while(head->next!=NULL)
Pop(head);
bool flag=true;
//扫描字符串
for(int i=0;i<strlen(s);i++)
{
if(s[i]=='(')
Push(head,s[i]);
else if(s[i]==')')
{
if(head->next==NULL)
{
flag=false;
break;
}
else Pop(head);
}
}
if(head->next!=NULL)
flag=false;
if(flag==true)
puts("匹配成功");
else puts("匹配错误");
}
return 0;
}
运行结果:
表达式的计算
首先需要知道:
栈对表达式的计算采用的是后缀表达式。
后缀表达式的优点:无视了运算符(操作符)的优先级,使得栈可以更好更快的计算出表达式结果。
中缀表达式,也就是我们常见常用的表达式一般形式:
比如 6+7*(2+3)+10/5
而后缀表达式,则是在中缀表达式基础上,进行一定规则的转换:
1、按运算符优先级对所有运算符和它的运算数加括号,(原本的括号不用加)
2、把运算符移到对应的括号后
3、去掉括号
比如将6+7*(2+3)+10/5转换成后缀表达式:
1.加括号处理:
6+(7*(2+3))+10/5
6+(7*(2+3))+(10/5)
(6+(7*(2+3)))+(10/5)
(6+(7*((2+3))))+(10/5)
((6+(7*((2+3))))+(10/5))
2.右移操作符:
((6+(7*((2+3))))+(10 5)/)
((6+(7*((2+3)))) (10 5)/)+
((6+(7*((2 3)+))) (10 5)/)+
((6+(7 ((2 3)+))*) (10 5)/)+
((6 (7 ((2 3)+))*)+ (10 5)/)+
3.去除括号:
6 7 2 3+*+ 10 5/+
于是得到后缀表达式:6 7 2 3 + * + 10 5 / +
栈对后缀表达式的运算过程为:扫描输入的数据,遇到操作数压入栈内,遇到操作符,先从栈内弹出两个元素进行该操作符的运算,然后将运算结果再压入栈内,最后栈内只有一个运算结果就是答案。
输入用空格空格,并且要区分内容为操作符还是操作数,于是我们采用文件读取结束符进行操作,每个操作数或者操作符用字符串单独读入再判断。
实现代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//声明数据类型为int
typedef int Datatype;
typedef struct Stack{
Datatype data;
//定义指针
struct Stack *next;
};
void Push(struct Stack *head,Datatype data)
{
//定义游标指针p,指向第一个非空节点
struct Stack *p=head->next;
//定义新加入的节点
struct Stack *temp=(struct Stack *)malloc(sizeof(struct Stack));
if(temp==NULL)
{
printf("内存申请失败\n");
exit(0);
}
temp->data=data;
//逆序插入
temp->next=p;
head->next=temp;
}
void Pop(struct Stack *head)
{
//如果没有元素,返回
if(head->next==NULL)
return;
//定义游标指针p,指向第一个节点
struct Stack *p=head->next;
//改变连接方式,跳过p指向的节点
head->next=p->next;
//释放p的内存资源
free(p);
}
int Get_number(char *s)
{
int ans=0;
for(int i=0;i<strlen(s);i++)
{
ans*=10;
ans+=(s[i]-'0');
}
return ans;
}
int main()
{
//定义有空头链表
struct Stack *head=(struct Stack*)malloc(sizeof(struct Stack));
head->next=NULL;
char s[100];
//清空栈
while(head->next!=NULL)
Pop(head);
while(~scanf("%s",s))
{
if(s[0]=='+')
{
int a=head->next->data;
Pop(head);
int b=head->next->data;
Pop(head);
Push(head,a+b);
}
else if(s[0]=='-')
{
int a=head->next->data;
Pop(head);
int b=head->next->data;
Pop(head);
Push(head,a-b);
}
else if(s[0]=='*')
{
int a=head->next->data;
Pop(head);
int b=head->next->data;
Pop(head);
Push(head,a*b);
}
else if(s[0]=='/')
{
int a=head->next->data;
Pop(head);
int b=head->next->data;
Pop(head);
Push(head,b/a);
}
else {
//为数字,存入栈内
Push(head,Get_number(s));
}
memset(s,0,sizeof(s));
}
printf("运算结果:%d\n",head->next->data);
return 0;
}
运行结果:
关于中缀表达式转后缀表达式,可能某些人学习需要,我也抽空跑去写了一下(就在刚才),包括具体实现代码。但是考虑篇幅太长就没有放这篇博客里,如果有需要请查看这篇博客看具体实现代码:中缀表达式转后缀表达式C++实现代码
二、队列(Queue)
队列就是先进先出,直接就是链表的顺序储存方式,同样定义结构体,和栈的定义一样,不过储存数据方式不一样。
同样先看看C++的stl库的直接调用:
#include<iostream>
#include<queue>//声明调用的库函数
using namespace std;
int main()
{
//定义一个int型队列q
queue<int>q;
cout<<"输入5个元素:";
for(int i=1;i<=5;i++)
{
int data;
cin>>data;
//将数据压入队列
q.push(data);
}
cout<<"队列内容:";
while(!q.empty())
{
//输出队首元素
cout<<q.front()<<" ";
//弹出队首元素
q.pop();
}
return 0;
}
运行结果:
就是顺序储存数据,然后看看我们的链表怎么实现,同样先定义一个结构体:
//声明数据类型为int
typedef int Datatype;
typedef struct Queue{
Datatype data;
//定义指针
struct Queue *next;
};
然后是储存数据部分,Push函数采用我们之前说到的顺序储存方式,这里采用有空头链表。
代码:
struct Queue *Push(struct Queue *rear,Datatype data)
{
struct Queue *temp=(struct Queue*)malloc(sizeof(struct Queue));
if(temp==NULL)
{
printf("申请内存失败\n");
exit(0);
}
temp->next=NULL;
temp->data=data;
//插入尾部
rear->next=temp;
//更新尾指针
rear=rear->next;
return rear;
}
这里需要不断更新尾指针,利用尾指针是为了优化时间,不用每次插入数据都得从头遍历。然后是弹出的部分,弹出的代码和栈的写法一致。
代码:
void Pop(struct Queue *head)
{
if(head->next==NULL)
return;
struct Queue *p=head->next;
head->next=p->next ;
free(p);
}
完整实现代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//声明数据类型为int
typedef int Datatype;
typedef struct Queue{
Datatype data;
//定义指针
struct Queue *next;
};
struct Queue *Push(struct Queue *rear,Datatype data)
{
struct Queue *temp=(struct Queue*)malloc(sizeof(struct Queue));
if(temp==NULL)
{
printf("申请内存失败\n");
exit(0);
}
temp->next=NULL;
temp->data=data;
//插入尾部
rear->next=temp;
//更新尾指针
rear=rear->next;
return rear;
}
void Pop(struct Queue *head)
{
if(head->next==NULL)
return;
struct Queue *p=head->next;
head->next=p->next ;
//释放内存资源
free(p);
}
int main()
{
struct Queue *head=(struct Queue*)malloc(sizeof(struct Queue));
struct Queue *rear=NULL;
head->next=NULL;
rear=head;
printf("输入5个元素:");
for(int i=1;i<=5;i++)
{
int data;
scanf("%d",&data);
rear=Push(rear,data);
}
while(head->next!=NULL)
{
printf("队首元素=%d\n",head->next->data);
printf("弹出队首元素\n");
Pop(head);
}
return 0;
}
运行结果:
关于利用队列进行广度搜索,这里不再说明,后续的博客中再说明吧。
希望我的分享对你的学习有所帮助,如有错误请及时指正,谢谢~