在看libevent时看到它用小根堆来管理时间,忽然猛地发现堆的概念有点忘了(上一次看数据结构是两年半之前的一个暑假),所以此处复习一下。
堆是利用完全二叉树来维护的一种数据结构,存取操作时间复杂度在O(1)-O(log n)之间,完全二叉树就是除了最后一层外,其他所有层都是满节点,且最后一层如果缺少节点,缺的部分应该在最右边。
堆分为两种类型,大根堆与小根堆,以大根堆为例,每个节点都小于它的父节点。假设现在随手写一串数字 1 , 2 , 10 , -1, 0 ,8,3
先建立成完全二叉树
要把它初始化为一个大顶堆的话,只需要从最后一个非叶子节点开始,依次做下沉动作。对了,操作可分为上浮跟下沉,就是向上或向下调整到对应的位置。最后一个叶子节点索引号可用总个数/2 得到。因为只要所有的非叶子节点位置正确,那整个堆就是正确的。此处就是10这个节点,下沉动作就是向叶子节点找一个大一点的进行比较,如果大于自己,则进行对调,对调后重复比较动作。所以10就是跟8,3比较,没有比10 大的,不做任何改变。同样2,也是。 到1这个节点,首先由于2<10,并且1<10所以,1和10对调
接着,由于3<8,且1<8,所以1要跟8对调。最终结果就是下面这张图
可以看到,10大于2和8, 2大于-1和0 , 8大于1和3 满足大根堆的性质。
上浮动作类似,就是一直跟父节点比较,如果自己大于父节点,那就对调位置,对调后继续跟新的父节点比较,重复这个比较动作即可。
很多书上讲堆排序,其实就是做一个大根堆,然后每次取出根节点,然后把最后一个节点放到第一个,再做下沉动作。这样保证取出一个节点后仍然为一个大根堆,接着继续取,继续调整...最终节点个数为1,结束。
下面给出一个代码,其中通过>>运算符可以插入一个节点,<<运算符可以每次取根元素并调整,也就是说cout这个对象后会得到一个有序序列。
#include<iostream>
using namespace std;
class Big_Heap
{
public:
void shift_down(int k) //大根堆
{
for (int i = 2 * k; i <= size; i *= 2) //i*2 下一次循环时就会指向当前的左孩子节点
{
if (i < size && queue[i] < queue[i + 1]) //如果右孩子较大则让i指向右孩子
i++;
if (queue[k] < queue[i])
{
std::swap(queue[i], queue[k]);
k = i;
}
else
break; // 调整结束
}
}
void shift_up(int k)
{
for (int i = k/2; i >= 1; i/=2)
{
if (queue[i] < queue[k])
{
std::swap(queue[i], queue[k]);
k = i;
}
}
}
friend ostream & operator << (ostream &, Big_Heap &);
Big_Heap& operator >> (int t)
{
queue[++size] = t;
shift_up(size);
for (int i = 1; i <= size; i++)
cout << queue[i] << ' ';
cout << endl;
return *this;
}
void pop() //弹出操作
{
int temp = queue[1];
queue[1] = queue[size];
queue[size] = temp;
size--;
shift_down(1);
}
int top() { return queue[1]; }
private:
int size = 0;
int queue[1024];
};
ostream & operator << (ostream & out, Big_Heap & obj)
{
const int num = obj.size;//下面不断在pop,会改变size的值,所以要先保存下
for (int i = 1; i <= num; i++)
{
out << obj.top() << " "; //输出堆顶元素
obj.pop(); //弹出堆顶元素
}
return out;
}
int main()
{
Big_Heap obj;
obj >> 1 >> 2 >> 10 >> -1 >> 0 >> 8 >> 3;
cout << obj;
system("pause");
}
这是大根堆的实现方式,小根堆就只需要改几个符号即可,不再重复贴出代码