最小堆
最小堆的定义
1、是一棵满二叉树
2、子树也是一棵满二叉树
3、左右节点的值都比当前节点的大
从定义可知,根节点是树中的最小值,
最小堆一般用链表存储,父子节点的关系,用纯数学的关系表示为:假设当前节点的在链表中的索引为n,左子节点为2n+1 ,右子节点为2n+2
操作
对最小堆操作后,依然要保证树结构为最小堆
1、增加
在增加一个节点时,我们直接从链表末尾添加数据,同时对最后一个节点进行调整,如果比父节点小就需要为父节点交换位置,这就是“上升”操作
2、删除
在删除一个节点时,我们先找到当前节点,然后将其与未节点进行交换位置,这时候就需要对交换上的末尾节点进行调整,比子节点大就要与2个子节点中key最小的节点交换位置,这叫“下沉”操作,比父节点小就要进行“上升”操作
时间复杂度
增查时间复杂度为O(
log
2
n
\log_2n
log2n)
删除的时间复杂度为O(n)
最小值的查找时间复杂度为O(1)
定时器
作用
很显然就是定时的去执行任务,比如:应用层左的心跳检测,游戏里面的技能冷却
数据结构
当有大量的任务需要放入定时器中时,就需要考虑效率问题,主要体现在增加和删除上,所有需要选取一个高效的数据结构,一般来说选取最小堆作为定时器的存储数据结构,原因如下:
首先分析以下数据结构的效率:
1、红黑树:增删时间复杂度为O(
log
2
n
\log_2n
log2n)
2、跳表:增删时间复杂度为O(
log
2
n
\log_2n
log2n) ;对于跳表最⼩节点为最左侧节点,时间复杂度为O(1);但是空间复杂度⽐较⾼,为 O(1.5n);
3、最小堆:增时间复杂度为O(
log
2
n
\log_2n
log2n) ;删除时间复杂度为O(n),对于最小节点的获取,时间复杂度为O(1);
选择最小堆做数据结构,是因为会在使用最小堆的同时会辅以一个map的数据结构来快速找到节点,以解决删除效率低下的问题,这样他就是效率最高的数据结构了。
实现
设计
1、数据成员应该包含 一个 链表 vector和一个map
2、接口应包含增加和删除操作
3、以时间作为数据结构的key,value是待实现的任务
在这里我使用了模板类,以便用通用性
template <class T>
class Node{
public:
int key;
int idx;
T t;
};
template <class T>
class MinHeap
{
public:
void addNode(int key,T v);
void removeNode(int key);
vector<Node<T> *> heap;
map<int,Node<T>*> maps;
...
}
增加节点的代码,先添加在上升调整,上升调整的目的是为了保证数据结构依然是最小堆
void addNode(int key,T v)
{
Node<T> *node = new Node<T>();
node->key = key;
node->t = v;
node->idx = heap.size();
heap.push_back(node);
shiftUp(heap.size()-1);
maps.insert(make_pair(key,node));
}
删除代码,通过map结构找到节点,然后从链表中删除,然后调整树结构(先下沉,在决定是否上升),同时将节点从map中移除
void removeNode(int key){
if(maps.find(key) == maps.end())
return;
Node<T> *node = maps.at(key);
int index = node->idx;
int lastone = heap.size()-1;
if(index != lastone){
swap(heap[index],heap[lastone]);
heap[index]->idx = index;
if(!shiftDown(index))//下沉不成功则上升
shiftUp(index);
}
heap.pop_back();
maps.erase(key);
}
值得一提的是:如果是插入相同的节点,对于定时器来说,可以考虑将节点的key 加一个很小很小的值,这样就不会存在一样的值了
结尾附上源码 以及qt的可视化界面
key为定时的时间戳,点击按钮添加节点时产生随机数自动填充最小堆,任务过期时会自动执行任务并删除节点
定时器可视化实现