【数据结构】栈与队列的基本操作及其应用


前言

栈和队列是两种重要的线性结构,从数据结构的角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表的子集。他们是操作受限的线性表,因此,可称为限定性的数据结构。但从数据类型角度看,他们是和线性表大不相同的两类重要的的抽象数据类型。

​笔者本周粗略学习了栈与队列的知识及其应用,特此总结记录自己的经验。
(学完之后cpu烧了/(ㄒoㄒ)/~~)


提示:以下是本篇文章正文内容,下面案例可供参考

一、栈

1.栈的定义

栈(Stack):

  1. 一种可以实现“先进后出”的存储结构。可以将其想象为箱子,先放进箱子的东西后拿出来。

  2. 一种只允许在一段进行插入或删除的线性表。首先栈是一种线性表,但限定这种线性表只能在某一段进行插入和删除操作。

补:线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。

(笔者还未具体学习过线性表,此段为百度copy)

栈顶(top):线性表允许进行插入与删除的那一端。

栈底(bottom):固定的,不允许进行插入与删除。

空栈:不含任何元素的空表。

简称: LIFO结构(last in first out)

分类:分为静态栈与动态栈。还可分为链栈,顺序栈,共享栈

附:链栈的实现操作与链表大同小异,因此在此不过多讲述, 可参考以下博客C语言实现链栈的创建、入栈、出栈、取栈顶、遍历…等基本操作(小白版详解)

在学习栈遇到的困难:博客上与网课的讲述内容例如创建栈方法有冲突,不知选择何种方法才是最优。

2.顺序栈的基本操作

1.顺序栈的初始化

首先我们需要定义一个栈所需要的变量。

typedef struct
{
	int* top;//栈顶指针
	int* base;//栈底指针
	int StackSize;//顺序栈的容量
}Sqstack;

然后我们开始初始化一个栈

void InitStack(Sqstack* s, int n)
{
	s->base = (int*)malloc(sizeof(int) * n);//n的含义是栈长度
	if (!s->base)//未初始化完成
	{
		return 0;
	}
	s->top = s->base;//使栈顶与栈底相同
	s->StackSize = n;

许多参考资料上对于栈的容量是在开头定义了一个全局变量。笔者在此处的方法是可以让栈的长度可以根据我们的需求改变。

2.判断是否为空栈

int EmptyStack(Sqstack* s)
{
	return s->top == s->base;//如果栈顶等于栈底则为空栈
	return 1;
}

3.入栈

int PushStack(Sqstack* s, ElemType e)//赋值e到顺序栈中,再让
{
	if (s->top - s->base >= s->stack_size)//指针与指针相减,得出的结果为:两者之间的距离!
	{
		printf("栈满");
	}
	*s->top = e;//先赋值
	s->top++;//使栈顶指针始终指向栈顶元素的下一个位置上
	return 1;
}

需要始终记得一个原则:非空栈中的栈顶指针始终在栈顶元素的下一个位置上。(这点对于理解栈很重要)
当然,创建栈的方式有许多,我们在使用单调栈时,将栈顶指针指向栈顶元素是更加方便的,这里笔者只是单独写出了自己写入栈代码时的习惯
此处我们还需注意:指针与指针之间的相减。如果两个指针指向同一个数组,它们就可以相减,其结果为两个指针之间的元素数目。

4.出栈

void PopStack(Sqstack* s, int* e)//可以将进栈与出栈联合记忆:进栈先赋值后加,出栈与遍历先减后输出
{
	if (s->top == s->base)//栈空
	{
		return 0;
	}
	s->top--;//先将栈顶指针移动到有元素的地方
	*e = s->top; 栈顶指针先下移,后赋值
}

5.取栈顶元素

//获得栈顶的元素,参数e用来存放栈顶的元素
int GetTopStack(Sqstack* s, ElemType* e)
{
	if (s->base == s->top)
		return 0;
	*e = *(s->top - 1);//top实际上是一个数组,*(s.top-1)表示的是栈顶元素,因为top指针始终指向栈顶元素的下一个位置
	return 1;
}

取栈顶元素与出栈元素的不同就是取栈顶元素不需要改变栈顶指针,只需要移动位置

6.销毁栈

//销毁栈,释放栈空间,将栈顶栈底指针设置为NULL,长度置为0
int DestoryStack(Sqstack* s)
{
	free(s->base);
	s->base = s->top = NULL;
	s->stack_size = 0;
	return 1;
}

7.遍历栈

//遍历栈,依次打印每个元素
int StackTraverse(Sqstack* s)
{
	if (s->top == s->base)
	{
		printf("空栈");
		return 0;
	}
	ElemType* p;
	p = s->top;//设立一个新指针从栈顶开始往下遍历
	while (p > s->base)//因为在循环里面是先减的,所以不能是等于,只能是大于,最后一次循环时已经是栈顶指针下标为0
	//base与top指针实质上是指向同一个数组
	{
		--p;
		printf("%d ", *p);
	}
}

8.使用上述基本操作创建并输出一个栈

int main()
{
	Sqstack s;
	int n;
	int e;
	printf("请输入栈的长度:");
	scanf_s("%d", &n);
	InitStack(&s, n);
	for(int i = 0; i < n; ++i)
	{
		scanf_s("%d", &e);
		PushStack(&s, e);
	}
	PopStack(&s, &e);//出栈栈顶元素
	StackTraverse(&s);
}

3.单调栈(栈的应用)

1.单调栈的定义

我们对于单调栈可以将其分为两种
第一种:单增栈
从栈底到栈顶的元素逐步递增
第二种:单减栈
从栈底到栈顶的元素逐步递减

2.单调栈的伪代码

为了更好地理解单调栈,笔者在此给出单增栈的伪代码

int *num = int *(malloc)(sizeof(int)*len);//首先定义一个存放元素数组,len为其长度
int *s = int *(malloc)(sizeof(int)*len);//在c语言中,我们定义一个数组来模拟单调栈会更加方便,但在其他语言如c++中对于栈有着特定的实现方式。目前牧者只学习了c语言,后续学习其他语言将会不借用数组的单调栈的用法
//在这里我们需要明白数组与栈分别存放的是什么值。
//对于num,我们存放的是数组的元素,在此时也许读者并不明白其含义,在下面我将会用例题讲解
//对于单调栈s,我们存放的是数组num的下标
int top = -1//初始化栈
s[++top] = 0;//将第一个数组元素的下标入栈
int 
for(int i=1; i<n; ++i)
{
	if(当前元素大于等于栈顶元素)
	{
		将当前元素入栈;
	}
	else
	{
		while(当前栈不为空栈并且当前元素小于或等于栈顶元素)
		{
			栈顶元素出栈;
			更新结果;
		}
		当前元素入栈;
	}
}

3. 单调栈的例题

下面笔者将使用单调栈的一道例题讲解单调栈的具体使用,如有不足,请不吝指出
题目: 柱状图中最大的矩形(题目地址

首先我们需要明白我们需要创建一个单增栈
为什么是一个单增栈呢?

在我们不断将元素入栈时,假如有元素破坏了单调栈的单调性,那么我们需要对于出栈的元素依次向左与向右遍历,直到找到第一个比它小的值。
注意:对于c语言来说,我们入栈的元素是数组的下标

在我们做此题过程中,我们可以将其分为两大步来做

第一大步:
我们依次将不破坏单调栈的数组下标入栈,当出现会破坏单调栈的情况时,笔者用以下文字说明:

因为我们维护了一个单增栈,所以出栈元素的左边第一个元素一定比它小,而右边第一个元素的一定是即将入栈的i, 因为是数组下标为i的破坏了单调栈的单调性
注意:在这里的大小比较实际上是对于高度的大小比较

由此我们可以得到计算每个出栈的元素的面积的公式:
s = (i - stack[top-1] - 1) heights[top]
在此处:
i表示右边的一个比出栈元素大的值的下标
stack[–top]表示左边第一个比出栈元素小的值的下标
height[top]表示当前出栈元素所得长方体的高

以下粘贴代码说明

for (int i = 1; i < heightsSize; i++)//遍历数组
	{
		if (heights[i] >= heights[stack[top]])//入栈
		{
			stack[++top] = i;//先运算后赋值
		}
		else
		{
			while (top != -1 /*这是栈不是空栈的意思*/ && heights[i] < heights[stack[top]])//出栈并计算面积,维护递增性,需要对小于的元素全部出栈
			{
				if (top - 1 == -1)//最后一个栈顶元素,出栈计算面积需要包含一下前面和后面,因为矩形可以延伸,这里需要好好想一想
				{
					max = fmax(max, i * heights[stack[top]]);
				}
				else
				{//stack[top-1]是出栈元素左边第一个比他小的元素下标  i是出栈元素右边第一个比他小的元素下标,二者之间的距离再减一便是宽

					max = fmax(max, (i - stack[top - 1] - 1) * heights[stack[top]]);//栈中元素,计算面积与需要延伸,能延伸多长就延伸多长

				}//出栈的元素的高就是要计算的长方形的高,出栈元素的左边一定是一定是比他小的值,右边比他小的元素一定是打破单调栈的新元素,因为我们维护了一个单增栈
				--top;//将大于当前元素的弹出栈
			}
			stack[++top] = i;//当小于当前元素的栈元素全部都弹出后放入新的栈元素下标
		}//将数组下标赋值给新元素。   尤其需要记得进入栈中的元素是下标,出栈的元素也是下标
	}

第二大步:
当所有数组下标全部入栈后,我们可以开始依次将剩余的元素出栈,并计算其面积

while (top != -1)//数组元素全部遍历完了,单是栈还有元素,进行清空栈
	{
		if (top - 1 == -1)
		{
			max = fmax(max, (heightsSize)*heights[stack[top]]);//最小的元素向右延申面积的宽就是数组长度
		}
		else
		{
			max = fmax(max, (heightsSize - 1 - stack[top - 1]) * heights[stack[top]]);
		}
		--top;
	}

对于if语句的情况,因为此时他是所有数组元素中最小的元素,所以它可以任意向左向右延申计算面积,因此宽为heightsSzie
在这里插入图片描述

对于else语句的情况,heightSize实际上就表示在出栈元素右边第一个比出栈元素小的下标值,stack[–top]依旧表示左边第一个比出栈元素小的下标

在最后附上完整实现的代码

int main(void)
{
	int heightsSize;
	printf("请输入数组长度");
	scanf_s("%d", &heightsSize);
	int* heights = (int*)malloc(sizeof(int) * heightsSize);
	for (int i = 0; i < heightsSize; ++i)
	{
		printf("请输入第%d个元素", i + 1);
		scanf_s("%d", &heights[i]);
	}
	int* stack = (int*)malloc(sizeof(int) * heightsSize);
	int top = -1;
	stack[++top] = 0;//将第一个元素入栈

	int max = -1;
	for (int i = 1; i < heightsSize; i++)//遍历数组
	{
		if (heights[i] >= heights[stack[top]])//入栈
		{
			stack[++top] = i;//先运算后赋值
		}
		else
		{
			while (top != -1 /*这是栈不是空栈的意思*/ && heights[i] < heights[stack[top]])//出栈并计算面积,维护递增性,需要对小于的元素全部出栈
			{
				if (top - 1 == -1)//最后一个栈顶元素,出栈计算面积需要包含一下前面和后面,因为矩形可以延伸,这里需要好好想一想
				{
					max = fmax(max, i * heights[stack[top]]);
				}
				else
				{//stack[top-1]是出栈元素左边第一个比他小的元素下标  i是出栈元素右边第一个比他小的元素下标,二者之间的距离再减一便是宽

					max = fmax(max, (i - stack[top] + stack[top] - stack[top - 1] - 1) * heights[stack[top]]);//栈中元素,计算面积与需要延伸,能延伸多长就延伸多长

				}//出栈的元素的高就是要计算的长方形的高,出栈元素的左边一定是一定是比他小的值,右边比他小的元素一定是打破单调栈的新元素,因为我们维护了一个单增栈
				--top;//将大于当前元素的弹出栈
			}
			stack[++top] = i;//当小于当前元素的栈元素全部都弹出后放入新的栈元素下标
		}//将数组下标赋值给新元素。   尤其需要记得进入栈中的元素是下标,出栈的元素也是下标
	}
	while (top != -1)//数组元素全部遍历完了,单是栈还有元素,进行清空栈
	{
		if (top - 1 == -1)
		{
			max = fmax(max, (heightsSize)*heights[stack[top]]);//最小的元素向右延申面积的宽就是数组长度
		}
		else
		{
			max = fmax(max, (heightsSize - 1 - stack[top - 1]) * heights[stack[top]]);
		}
		--top;
	}
	printf("%d", max);

}

4.对于例题的总结

//总结:此题可以将他分为两种情况
//第一种:在维护单调栈的过程中有元素弹出,那么需要不断计算弹出元素的最大面积。
//因为我们维护的是一个单增栈,所以弹出元素的左边的所有元素的值都是比他大的,又因为我们入栈的元素是一个下标,这样便可得到出栈的元素的最大面积的宽,而长便是出栈元素本身的数值
//第二种:当维护单调栈过程中并无元素弹出或是所有元素已经遍历完,那么需要将每个元素依次弹出计算最大面积

//top是栈的下标,i是数组的下标, 栈中元素实际上是数组的下标(!!!!重点)

5.后记

笔者对于单调栈的问题理解并不深刻,后续将会补上更多关于单调栈的例题,如接雨水等问题

二、队列

1.队列的定义

队列不同于栈,是一种先进先出的线性表,简称FIFO,允许插入的一段成为队尾,允许删除的一端称为队头

对于栈的现实举例:

当电脑死机时,我们将他重启,电脑会将我们重启前未执行的操作执行一遍,这其实是因为操作系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。

又如移动联通等客服电话占线时的等待,在有客服空置时,最先等待的客户会最先得到客服的回应。

2.队列的基本操作

InitQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给x。


3.队列的顺序存储结构

队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针。

队列的顺序存储类型可描述为:

typedef struct//尾指针永远在新元素要放置的下一个位置上
{
	int data[MAXSIZE];
	int front;//头指针
	int rear;//尾指针
}SqQueue;

初始状态(队空条件):Q->front == Q->rear== 0。
进队操作:队不满时,先送值到队尾,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1
我们使用队尾指针进栈,头指针出栈。

注意:
尾指针永远在新元素要放置的下一个位置上

但是顺序队列有缺点:用以下图来解释:

在这里插入图片描述

如图d,队列出现“上溢出”,然而却又不是真正的溢出,所以是一种“假溢出”。

我们用通俗的语言来讲解,那么就像是前面有空座位,但是后排坐满了,你不可能不坐前排而下车,一定会将空的位置坐满。

4.循环队列

1.循环队列的定义

解决假溢出的方法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。

当队首指针Q->front = MAXSIZE-1后,再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。(对于队尾指针的原理也相同)

初始时:Q->front = Q->rear=0。
队首指针进1:Q->front = (Q->front + 1) % MAXSIZE。
队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE。
队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE。

出队入队时,指针都按照顺时针方向前进1,如下图所示:
在这里插入图片描述

2.判断循环队列栈空与栈满

那么循环队列判断栈空与栈满的条件是什么呢?
如果我们入队的速度快于出队的速度,那么栈满的条件也是Q->front == Q->rare。那么便会产生冲突,为了避免这种情况,我们有两个方法取避免。
办法一:设置一个标志变量flag,当front == rare时,且flag0栈为空,当frontrear,且flag为1时为队列满。

办法二:当队列空时,条件就是front == rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,我们还有一个空闲单元。
通俗的理解便是我们在上公交车时如果发现只剩一个座位了,我们不会去坐。当然现实中这种情况并不会出现,这样举例是为了更好地理解。
此时我们便认为队列已满。

以下笔者重点来讨论第二种方法 :
由于rear既可能比front大,也可能比front小,所以他们只相差一个位置时便是满的情况,但也可能相差整整一圈。在这里我们需要理解尾指针总是指向下一个要执行操作的位置,所以在进行操作时可以先判断栈空或栈满。

所以
队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear

接下来我们计算队列元素的个数,因为尾指针初始总是指向下一个需要执行操作的位置,头指针初始,总是指向当前操作元素的位置,在计算过程中rear与front的大小关系也无法确定,那么我们便可以加上数组长度来避免出现有rear-front可以为正也可以负的情况,所以我们得到:

队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize

有了这些讲解,我们接下来开始实现循环队列

3.循环队列的基本操作

1.循环队列的初始化
typedef struct//尾指针永远在新元素要放置的下一个位置上
{
	int data[MAXSIZE];
	int front;//头指针
	int rear;//尾指针
}SqQueue;

int InitQueue(SqQueue* Q)//初始化队列
{
	Q->front = 0;
	Q->rear = 0;
	return 1;
}
2.循环队列求队列长度
int QueueLength(SqQueue* Q)
{
	return (Q->rear - Q->front + MAXSIZE) % MAXSIZE;
}
3.循环队列入队
int EnQueue(SqQueue* Q, int e)
{
	if ((Q->rear + 1) % MAXSIZE == Q->front)//判断栈满的条件
	{
		return 0;
	}
	Q->data[Q->rear] = e;
	Q->rear = (Q->rear + 1) % MAXSIZE;
	return 1;
}
4. 循环队列出队
int DeQueue(SqQueue* Q, int* e)//int *e 用于返回其值
{ 
	if (Q->front == Q->rear)
	{
		return 0;
	}
	*e = Q->data[Q->front];
	Q->front = (Q->front + 1) % MAXSIZE;
	return 1;
}
5. 利用上述操作实现一个简单的循环队列
int main(void)
{
	SqQueue Q;
	int e ;
	int* q = &e;
	InitQueue(&Q);
	for (int i = 0; i < 5; i++)
	{
		printf("请输入第%d个队列元素", i + 1);
		scanf_s("%d", &e);
		EnQueue(&Q, e);
	}
		DeQueue(&Q, &e);
		printf("%d  ", e);
		DeQueue(&Q, &e);
		printf("%d  ", e);
	
		return 0;
}

总结

1.此周的学习内容比较多,但只是学到了上述知识的皮毛,之后笔者仍需对此处知识进行更加深层次的学习,如多刷有关单调栈的相关题目如接雨水等问题。
2.且在后续学习过程中随着知识广度的扩展还需明白优先队列的原理,还需要刷有关队列的题。

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值