【知识分享】数据结构的应用——队列

背景

    对于数据结构,其实学过C语言的都不陌生,无外乎就队列、栈、二叉树等等这些。但其实对于初学者最困惑的不是数据结构是怎么样的,而是数据结构有什么用?我自己也是工作好几年后才体验到数据结构的快乐。所以本系列文章重点从应用场景切入,让大家理解数据结构的好处。

简介

    队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。队列有两种形态,顺序排列的线性队列,也有头尾闭环的环形队列,也称作循环队列。

  • 线性队列(循环队列)

    简单的理解,就像是排队做核酸,大家自觉排成一队或几队,这每一队就是一个队列,先来的人先做,后到的人后做。如下图,当前有数据Data1过来,则先进入队里,排在队头,再来个Data2时,则排在Data1后面,再来就Data3就排在Data2后面。
队列
    如果此时要让数据出队,则先让Data1出队,然后Data2和3往前移一个位置。
队列2
    如果此时再来个Data4,则把Data4填到队尾即可。
队列3

  • 环形队列(循环队列)

    跟顺序队列唯一的区别,就是它首尾相接,就是相当于一队人排成一个环状,环形队列也因此得名。
环形队列1
    当需要把Data1移出队时,可以获取出数据,然后修改队头的位置即可,不用去搬运其他成员的数据,甚至出队的数据都不需要删除,等着后面新数据将其覆盖即可。这就是循环队列的好处。
在这里插入图片描述
    但实际在内存中,不可能真的存在一个环形的缓存(起码现在没有)。在内存里,环形队列跟顺序队列完全一样,只是其队头跟队尾根据出队入队在不断地变化。

应用场景

    就如前面所说,队列有个特点是可以缓冲,那这里可以想到什么呢?对了,就是小学做的那个数学题,一边进水xx升/小时,另一边排水xx升/小时,中间的那个水箱就是个队列。。。。不举这种浪费水的例子,咱们换一个,其实最常用的地方,就是在有事件请求跟事件消耗中,并且请求跟消耗是异步进行的场景。

    举个实际例子,现在有一个触摸屏,用户点击下屏幕,显示屏会有对应的动作,点一次动一次,并且动作跟点击顺序有关系,先点的先动作。那按这样的设计思路,最简单的写法如下:

void Check(void)
{
	/* 检测的相关处理 */
}

void Operation(void)
{
	/* 触发的相关执行动作 */
}

int main(void)
{
	/* 检测触摸位置 */
	Check();

	/* 执行对应的动作 */
	Operation();
	
	return 0;
}

    但上面的实现有什么问题?如果这时候执行的动作很多,占用时间很多,比如整个屏刷新需要1s(夸张点的说法哈),那上面这种写法就会导致我按键只能1s按一次,按快了没反应。那这里我们先引入实时操作系统的概念——时间片,把需要执行的动作切分成很多个时间片,比如整屏刷新原本需要1s,那就把它分成100份10ms的时间片,触摸检测也一样,然后在整屏每刷新10ms的时候,就检测一次触摸情况。这样在用户的角度来看,基本就看不出来延时的时间,按键和屏的刷新变成几乎同步进行。

void Check(void)
{
	while(1)
	{
		/* 检测触摸事件--10ms检测一次 */
		os_delay(10);
	}
}

void Operation(void)
{
	while(1)
	{
		/* 刷屏--刷新一部分就延时让出一下,或者利用操作系统本身时间片特性让出也行。 */
		os_delay(1);
	}
}

int main(void)
{
	/* 创建触摸检测和刷屏两个任务--伪代码,重在理解 */
	task_create(Check);
	task_create(Operation);
	return 0;
}

    但这上面的实现还是有问题,虽然同一个屏,刷新的速度是一样的,但是不同的人,操作的速度是不一样的,老年人可能几秒钟才能操作一下,但单身20年的小伙可能1秒可以操作20下。因为屏刷新只能1s刷一下,那精神小伙操作的那20下,可能就只有最后一下可以显示出来。这时候小伙就不高兴了,为什么我一顿操作猛如虎,这屏就给这么点反应,要求点多少下刷新多少次。那么这时候我们的主角终于可以登场了,分析下这个场景,触摸检测到1s操作了20次,而屏只能1s刷新一次,典型的事件请求>事件消耗,并且这个事件请求是有间断性的,也就是小伙并不是一直按1秒操作20下的速度来操作的,只是这么操作了一次。那这里我们就引入队列,把小伙的操作都给存起来。

  • 顺序队列
/*******************队列的相关操作********************/
/* 预留的缓存大小 */
#define QUEUE_MAX			30

struct tagQueueCB
{
	uint8_t Buff[QUEUE_MAX];
	uint32_t Tail;
};

struct tagQueueCB QueueCB;

/* 入队操作 */
uint8_t Queue_Push(uint8_t data)
{
	uint8_t rtn = 0;
	if (QueueCB.Tail < QUEUE_MAX)
	{
		QueueCB.Buff[QueueCB.Tail++] = data;
		rtn = 1;
	}
	else
	{
		/* 队列满,返回失败 */
		rtn = 0;
	}
	return rtn;
}

/* 出队操作 */
uint8_t Queue_Poll(uint8_t *data)
{
	uint8_t rtn = 0;
	if (QueueCB.Tail)
	{
		*data = QueueCB.Buff[0];
		QueueCB.Tail--;
	
		for (uint32_t i = 0; i < QueueCB.Tail; i++)
		{
			QueueCB.Buff[i] = QueueCB.Buff[i + 1];
		}
		rtn = 1;
	}
	else
	{
		/* 队列空时返回失败 */
		rtn = 0;
	}
	return rtn;
}
/***************************************************/
void Check(void)
{
	uint8_t data = 0;
	while(1)
	{
		/* 检测事件 */
		
		/* 有触摸事件,入一次队 */
		if (0 == Queue_Push(data))
		{
			/* 异常处理 */
		}

		/* 检测触摸事件--10ms检测一次 */
		os_delay(10);
	}
}

void Operation(void)
{
	uint8_t data = 0;
	while(1)
	{
		/* 检测队列,非空则出队,并刷整屏 */
		if (0 == Queue_Poll(&data))
		{
			/* 异常处理 */
		}

		/* 执行刷屏 */
		
		/* 刷屏--刷新一部分就延时让出一下,或者利用操作系统本身时间片特性让出也行。 */
		os_delay(1);
	}
}

int main(void)
{
	/* 创建触摸检测和刷屏两个任务--伪代码,重在理解 */
	task_create(Check);
	task_create(Operation);
	return 0;
}

    这样就解决了小伙的手速问题。当然如果过两年小伙手速又提升了,那这30的缓存可能也扛不住。这上面实现的就是最普通的顺序队列,使用数组存储数据,并提供出队、入队的操作。然而细心的小伙伴可能已经发现了,这出队操作效率也太低了吧,把队头数据移出后,后续所有数据都得往前移,而且现在是多线程,如果在入队的过程中,执行了出队,数据就会错乱(重入)。那么这里我们就提供第二种队列的实现方式——循环队列。

  • 循环队列
/*******************队列的相关操作********************/
/* 预留的缓存大小 */
#define QUEUE_MAX			30

struct tagQueueCB
{
	uint8_t Buff[QUEUE_MAX];
	uint32_t Head;				/* 队头位置 */
	uint32_t Tail;				/* 队尾位置 */
	uint32_t Num;				/* 队列元素个数 */
};

struct tagQueueCB QueueCB;

/* 入队操作 */
uint8_t Queue_Push(uint8_t data)
{
	uint8_t rtn = 0;
	if (QueueCB.Num < QUEUE_MAX)
	{
		QueueCB.Buff[QueueCB.Tail] = data;
		QueueCB.Tail = (QueueCB.Tail + 1) % QUEUE_MAX;
		
		/* 数量记得放最后修改,不然还是会有重入问题 */
		QueueCB.Num++;
		rtn = 1;
	}
	else
	{
		/* 队列满,返回失败 */
		rtn = 0;
	}
	return rtn;
}

/* 出队操作 */
uint8_t Queue_Poll(uint8_t *data)
{
	uint8_t rtn = 0;
	if (QueueCB.Num)
	{
		*data = QueueCB.Buff[QueueCB.Head];
		QueueCB.Head = (QueueCB.Head + 1) % QUEUE_MAX;
		
		/* 数量记得放最后修改,不然还是会有重入问题 */
		QueueCB.Num--;
		rtn = 1;
	}
	else
	{
		/* 队列空时返回失败 */
		rtn = 0;
	}
	return rtn;
}
/***************************************************/
void Check(void)
{
	uint8_t data = 0;
	while(1)
	{
		/* 检测事件 */
		
		/* 有触摸事件,入一次队 */
		if (0 == Queue_Push(data))
		{
			/* 异常处理 */
		}
		
		/* 检测触摸事件--10ms检测一次 */
		os_delay(10);
	}
}

void Operation(void)
{
	uint8_t data = 0;
	while(1)
	{
		/* 检测队列,非空则出队,并刷整屏 */
		if (0 == Queue_Poll(&data))
		{
			/* 异常处理 */
		}
		/* 执行刷屏 */
		
		/* 刷屏--刷新一部分就延时让出一下,或者利用操作系统本身时间片特性让出也行。 */
		os_delay(1);
	}
}

int main(void)
{
	/* 创建触摸检测和刷屏两个任务--伪代码,重在理解 */
	task_create(Check);
	task_create(Operation);
	return 0;
}

    除了上面这种应用,还有像通信协议,也会用到队列,一般叫通信缓存。另外还有像实时操作系统里的邮箱,其实就是一个事件队列。

    还有一种应用,是用在日志存储,存储的结构其实就是一个循环队列,但跟通信缓存有一点不同的是,通信缓存一般是不允许入队入满的,如果队满了,一般的做法也是把要入队的信息舍弃掉。但日志存储则是需要把最开始的数据覆盖掉,以此来保持记录最近的记录。

总结

  1. 常见队列有两种,顺序队列和循环队列。
  2. 一般用在需要数据缓存的地方,比如通信驱动与应用处理的衔接,日志存储服务与应用的衔接等等。
  3. 对于队列的操作,要多注意重入的问题,毕竟这种数据结构就是在多方共用内存的情况下使用的。

相关知识

栈、链表、树、图、散列表

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识噬元兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值