一、待解决问题
对于一个数组,如果用滑动窗口的方式求解数组某一段的最值,在不考虑时间复杂度的情况下,用循环就能轻松实现,而如果考虑时间复杂度,"队列"这种先进先出的数据结构,就十分实用于这种情况。
与静态的滑动窗口相类似,当动态地获取数据时,我们也可以用循环队列来模拟一个滑动的串口。
本次针对的问题是:当传感器每0.1s读取数据时,实时计算得到过去5s的最值之差。
二、方法对比
由于博主比较久没有使用数据结构了,因此在开始时先向计算机专业的同学寻求帮助,得到了“使用最大堆”的回复,但经过对比发现,最大堆作为一种树形结构,虽然很好地模拟了队列的模式,并同时能时刻得到堆中的最大值,在输入新元素和输出最大值时,能以较小的时间复杂度实现堆中元素的快速重新排列,从而得到最大值,但缺点是无法在最大堆中直接得到最小值。
因此,我重新选择了使用“循环队列”的方式,在占用不太多的空间资源下,实现滑动串口的模拟,并动态求最值。
图1 滑动窗口
三、思路和程序
1.程序设计流程图如下
2.程序代码
#define MaxQSize 8 //7+1,多出的一位元素作为队列满的检验
typedef enum {ERROR = 0, SUCCESS = !ERROR} ErrorStatus;
//初始化循环队列
ErrorStatus Init_SeQueue(SeQueue* Q)
{
uint8_t i;
Q->base = (uint16_t*)malloc(MaxQSize*sizeof(uint16_t));
if(! Q->base) return(0); //分配空间失败
for(i = 0;i < MaxQSize;i++)
{
Q->base[i] = 0; //将所有元素清0,避免出现其它数值的情况
}
Q->front = Q->rear = 0;
return SUCCESS;
}
//求队列已存元素的个数
uint8_t SeQueue_Length(SeQueue* Q)
{
return ((MaxQSize + Q->rear - Q->front) % MaxQSize);
}
//往队尾插入元素
ErrorStatus In_SeQueue(SeQueue* Q,uint16_t e)
{
//若队列未满,则在队尾插入新元素
//否则,返回ERROR
if((Q->rear + 1)%MaxQSize == Q->front) //队列是否已满
return ERROR;
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1)%MaxQSize; //实现队列循环,防止队尾序号溢出
return SUCCESS;
}
//输出队首元素
ErrorStatus Out_SeQueue(SeQueue* Q,uint16_t *e,uint16_t new_dat)
{
//若队列不空,则输出队首元素
//否则,返回ERROR
if(Q->front == Q->rear) return ERROR;
*e = Q->base[Q->front]; //记录队首元素
Q->base[Q->front] = new_dat; //为了方便重新求最值,可直接将队首元素赋即将插入的队尾元素队尾元素
Q->front = (Q->front + 1)%MaxQSize; //实现队列循环,防止队头序号溢出
return SUCCESS;
}
//滑动窗口求峰峰值,在这里就可以引用上面写好的队列基本操作来进行运算啦
void Window_PP(SeQueue *Q,uint16_t rear_dat)
{
//step1:窗口建立
//step2:窗口动态移动
static uint16_t max = 0; //这里将最大值和最小值的摆幅限制在0~4096之间
static uint16_t min = 4096;
uint16_t *front_dat = (uint16_t*)malloc(sizeof(uint16_t));
uint8_t i;
//队列是否满
if(SeQueue_Length(Q) == MaxQSize - 1) //队列满
{
Out_SeQueue(Q,front_dat,rear_dat); //输出队首元素
//是否重新找出队列中的最大值
if(*front_dat == max) //输出的队首元素为最大值
{
max = 0;
for(i = 0;i < MaxQSize;i++)
{
if(Q->base[i] > max)
max = Q->base[i];
}
}
//是否重新找出队列中的最小值
else if(*front_dat == min)
{
min = 4096;
for(i = 0;i < MaxQSize;i++)
{
if(Q->base[i] < min)
min = Q->base[i];
}
}
In_SeQueue(Q,rear_dat); //插入新的队尾元素
if(rear_dat > max)
max = rear_dat; //更新极值
if(rear_dat < min)
min = rear_dat;
PP_Value = max - min; //更新峰峰值
}
else
{
In_SeQueue(Q,rear_dat);
if(rear_dat > max)
max = rear_dat;
if(rear_dat < min)
min = rear_dat;
}
MAX = max;
MIN = min;
}
四、总结
队列的特点使其十分适用于滑动窗口的模拟,来一个,走一个,First In First Out
在实现队列的初始化、插入、输出、求长度的基本操作后,就能够利用它们编写一个求极值差(峰峰值)了,具体的队列长度、最大值和最小值的范围可以自己diy。
最后要注意的是,在求最大值和最小值的过程中,当队列输出的首元素是最大值或最小值时,需要重新找到队列中的最大值或最小值,但队列的输出并没有真正的清除掉那个元素A,而只是不将元素纳入队列的运算了,等待新的元素去替换掉A。
注意:rear所指为队尾元素的下一位。
因此,为了能正确在全队列中重新找到最大值或最小值,可以把即将输入的元素B先替换掉A,这样就能从队列的地址初(数组下标为0)->地址尾(数组下标为 队列长度-1)来找最值,而不需要确定从哪个位置(数组下标a)到哪个元素(数组下标b)找最值,简化寻找最值这个过程。