文中前半部分图片大部分来自:https://www.cnblogs.com/chengxiao/p/6129630.html
堆的基本概念:
堆是具有以下性质的完全二叉树:每个父结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个父结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
大小堆如下图:
我们将这种逻辑结构映射到数组中,如下图:
我们通过两组简单的公式描述一下上述两种堆的特性:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序基本原理和步骤:
1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆;3、重复步骤2,如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。最后,就得到一个有序的序列了(一般升序采用大顶堆,降序采用小顶堆),同时堆排序一种平均时间复杂度最坏、最好均为O(nlogn),它也是不稳定排序。
步骤一
构造初始堆将给定无序序列构造成一个大顶堆(升序采用大顶堆,降序采用小顶堆)。
2.从最后一个非叶子结点开始(arr.length/2-1=5/2-1=1,也就是6结点),从左至右,从下至上进行调整。
4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6,到这里,我们就将一个无需序列构造成了一个大顶堆。
步骤二
将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
大小顶堆代码实现如下:
void heap_adjust_big(int ch[], int parent, int lengh)
{
int temp = ch[parent];
for (int child = 2*parent+1; child<lengh; child = child*2+1)
{
if (child+1<lengh && ch[child]<ch[child+1])
{
child++;
}
if (ch[child]>temp)
{
ch[parent] = ch[child];
parent = child;
}
else
{
break;
}
}
ch[parent] = temp;
}
void heap_sort_big(int ch[], int lengh)
{
for (int i = lengh/2-1; i>=0; i--)
{
heap_adjust_big(ch, i, lengh);
}
for (int j = lengh-1; j>0; j--)
{
int temp = ch[j];
ch[j] = ch[0];
ch[0] = temp;
heap_adjust_big(ch, 0, j);
}
}
void heap_adjust_small(int ch[], int parent, int lengh)
{
int temp = ch[parent];
for (int child = 2*parent+1; child<lengh; child = child*2+1)
{
if (child+1<lengh && ch[child]>ch[child+1])
{
child++;
}
if (ch[child]<temp)
{
ch[parent] = ch[child];
parent = child;
}
else
{
break;
}
}
ch[parent] = temp;
}
void heap_sort_small(int ch[], int lengh)
{
for (int i = lengh/2-1; i>=0; i--)
{
heap_adjust_small(ch, i, lengh);
}
for (int j = lengh-1; j>0; j--)
{
int temp = ch[j];
ch[j] = ch[0];
ch[0] = temp;
heap_adjust_small(ch, 0, j);
}
}
经过上面的基础只是铺垫,下面我们来看一下基于最小堆原理实现的定时器
时间堆的概念:
由于定时器的触发是由于时间到了,因此只有时间最短的定时器会首先被触发,通过这个原理,我们可以采用最小堆,将按时间顺序排序,堆顶元素是时间最短的定时器,因此只要判断堆顶元素是否被触发即可。只有堆顶定时器的时间到了,才会到其他时间较晚的定时器的时间。
时间堆解决的问题:
首先我们应该都知道定时器的概念,实现起来也很简单,开个线程循环调用就行了,但那只是少量定时器的场景适用;如果业务中有几万个定时业务,那难道我们要开几万个线程吗?这几乎是不可能会有人这样干的,耗性能系统也撑不住,这时候就需要我们设计出这样一种数据结构(有序升序、插入删除速度极快),然后我们将过期时间(timeout)映射到这个有序的序列中,然后我们用一个线程轮询检测这个队列最小头部(时间最小也就是过期最快的元素),这时候我们就能以极小的代价和极高的效率管理这几万个定时任务,恰好最小堆就是这样一种数据结构;
最小堆定时器实现分析
实现最小堆定时器步骤:
1讲时间序列按照小顶堆排序,这样序列头就是最小的时间元素;
2每次检测队列头是否过期即可,不必全量序列检测;
3删除队列头后只需要将堆尾移到被删除的堆顶,然后重上到下调整为小顶堆即可;
4插入定时器只需要讲其先插入到堆尾部,然后将其循环和父节点比较上浮,最后构建成最小堆即可;
5直接删除任意定时器(定时业务异常需要删除),其实这个可以直接查找到这个节点,并且做个标记就行了,没必要真正删除,然后再去调整堆,等到这个标记的定时器过期在删除就好(等到这个节点上浮到堆顶);
话不多说上代码
#include <iostream>
#include <netinet/in.h>
#include <time.h>
class HeapTimer {
public:
time_t time_out;
void* p_data;
HeapTimer( int delay ) {
time_out = time( NULL ) + delay;
}
void ( *callback ) ( void* );
};
class TimerMgr{
private:
HeapTimer** array;
int capacity; // 容量
int used_size; // 当前元素个数
public:
TimerMgr( int cap );
TimerMgr( HeapTimer** init_array, int size, int cap );
~TimerMgr();
public:
void heap_adjust_small( int index );
void add_timer( HeapTimer* timer );
void del_timer( HeapTimer* timer );
void pop_timer();
void tick();
void resize();
};
TimerMgr::TimerMgr( int cap ) : capacity(cap), used_size(0) {
array = new HeapTimer*[ capacity ];
for( int i = 0; i < capacity; i++ ) {
array[i] = nullptr;
}
}
TimerMgr::TimerMgr( HeapTimer** init_array, int size, int cap ) : used_size(size), capacity(cap) {
if( capacity < size ) {
return;
}
array = new HeapTimer*[ capacity ];
for( int i = 0; i < size; i++ ) {
array[i] = init_array[i];
}
//构建小顶堆
for( int i = size/2 - 1; i >= 0 ; i-- ) {
heap_adjust_small( i );
}
}
TimerMgr::~TimerMgr() {
for( int i = 0; i < used_size; i++ ) {
if( !array[i] ) {
delete array[i];
}
}
delete[] array;
}
void TimerMgr::heap_adjust_small( int parent ) {
HeapTimer* tmp = array[parent];
for( int child = parent*2+1; child < used_size; child = child*2+1) {
if( child+1<used_size && array[child]->time_out > array[child+1]->time_out ){
child++;
}
if( tmp->time_out > array[child]->time_out ){
array[parent] = array[child];
parent = child;
} else {
break;
}
}
array[parent] = tmp;
}
//先放在数组末尾,在进行上滤使其满足最小堆
void TimerMgr::add_timer( HeapTimer* timer ) {
if( !timer ) {
return ;
}
if( used_size >= capacity ) {
resize();
}
int hole = used_size++;
// 由于新结点在最后,因此将其进行上滤,以符合最小堆
for( int parent = (hole-1)/2; hole > 0; parent = (hole-1)/2){
if( array[parent]->time_out > timer->time_out ) {
array[hole] = array[parent];
hole = parent;
} else {
break;
}
}
array[hole] = timer;
}
void TimerMgr::del_timer( HeapTimer* timer ) {
if( !timer ) {
return;
}
//tick到了自然删掉(节省删除的开销,数组膨胀很小)
timer->callback = nullptr;
}
void TimerMgr::pop_timer() {
if( !used_size ) {
return;
}
if( array[0] ) {
delete array[0];
array[0] = array[--used_size];
heap_adjust_small( 0 );
}
}
void TimerMgr::tick() {
HeapTimer* tmp = array[0];
time_t cur = time( NULL );
while( !used_size ) {
if( !tmp ) {
break ;
}
if( tmp->time_out > cur ) {
break;
}
if( array[0]->callback ) {
array[0]->callback( array[0]->p_data );
}
pop_timer();
tmp = array[0];
}
}
// 扩容 N=2N
void TimerMgr::resize() {
HeapTimer** tmp = new HeapTimer*[ capacity * 2 ];
for( int i = 0; i < 2 * capacity; i++ ) {
tmp[i] = nullptr;
}
capacity *= 2;
for( int i = 0; i < used_size; i++ ) {
tmp[i] = array[i];
}
delete[] array;
array = tmp;
}
#endif