数据结构 二、栈(Stack)和队列(Queue)

在数据结构中,栈和队列,严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 “一对一” 的数据。

使用栈结构存储数据,讲究“先进后出”,即最先进栈的数据,最后出栈;使用队列存储数据,讲究 “先进先出”,即最先进队列的数据,也最先出队列。

其实栈和队列的原理和实现都很容易,但因为其在我们日常的使用中给我们带来了很大的帮助,不论是算法竞赛还是项目应用,栈和队列都是必不可少的存在,其中我们常见的栈的应用如:括号匹配,表达式的计算。而队列则常用于广度搜索。

在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;
}

运行结果:
在这里插入图片描述
关于利用队列进行广度搜索,这里不再说明,后续的博客中再说明吧。

希望我的分享对你的学习有所帮助,如有错误请及时指正,谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值