1.1 原理
首先我们先回忆一下小堆这种数据结构:
在小堆中,树的每个节点的值都小于或等于其子节点的值。这意味着堆的最小元素总是位于树的根部,即堆顶。 当我们取出该堆的最小元素时,步骤是:
- 交换堆顶和末尾元素:将堆顶元素(最小元素)与堆的最后一个元素交换位置。此时,堆顶元素不再是最小元素
- 调整堆:从堆顶开始,将交换后的堆顶元素与它的子节点比较,如果它大于其子节点中的任何一个,则与较小的子节点交换位置,并继续这一过程直到堆顶元素小于其所有子节点或到达叶节点。
- 移除最小元素:由于最小元素已经与堆的最后一个元素交换,现在可以直接从数组中移除它
这和我们说的任务定时器啥关系呢?现在,我们堆的每一个元素就不再是一个数了,而是我们的 TimerTask
,我们将所有任务构造为一个小堆,之后任务检测步骤如下:
- 检查堆顶元素:检查堆顶的任务(时间最小的任务)执行时间是否到了
-
- 条件不满足: 直接休眠指定时间再检查,时间最小的都不行,其他的更不行了
- 条件满足:取下堆顶元素执行任务,调整堆的结构使其为小堆
- 重复 1 - 2 的步骤
这样的话我们就不需要每次都遍历整个数组,具体的优缺后面说。
1.2 实现
在这里我们存储人物的容器就换成 priority_queue
了,因为需要实现一个小堆。其次任务结构体,我们需要在原来的基础上加上一个比较函数,因为涉及到自定义结构体排序比较的操作:
struct TimerTask
{
TimerTask(Timer time, TaskFunc callback)
: _execute_time(time), _call_back(callback)
{}
// 比较函数但是底层我们写为 > , 因为我们需要得到一个小堆
bool operator<(const TimerTask& other) const
{
return _execute_time > other._execute_time;
}
Timer _execute_time; // 定时器
TaskFunc _call_back; // 回调函数
};
之后稍有一些不一样的地方就是启动函数:
void Loop()
{
while (true)
{
auto now = std::chrono::steady_clock::now();
while (! _tasks.empty() && _tasks.top()._execute_time <= now)
{
auto task = _tasks.top();
_tasks.pop();
task._call_back();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
实现起来不难,主要是思想很重要。所以说熟悉数据结构是很重要一件事!
1.3 优缺点
网上都说小堆的性能是好于我们第一种遍历方案的,但是我们要思考高效在哪里呢?我们算一笔账,在 n 个任务中 m 个任务就绪了, 前者执行定时任务的时间复杂度是 O(n)
,因为是遍历嘛;后者的话涉及到堆的调整操作 O(logn)
,一共取 m,那就是 O(mlogn)
。
当我们的数据量也就是 n 较小的情况下,请问谁更好一点呢?我想,是前者吧。(化学上有一句话是,抛开计量谈毒性,就是耍流氓!咋们计算机也差不多,抛开数据量谈高效,也是耍流氓!)
优点:
- 高效的任务管理:当数据量较大时,性能更加的不错。
- 底部开销:不和 vector 一样,不需要频繁的扩容操作
缺点:
- 不适合小任务集:当任务数量很少时,使用小堆可能反而不如简单的线性结构(如数组或链表)高效,因为堆的维护开销可能超过直接遍历的成本。
所以我们还是要带着辩证的角度看待问题!