堆的定义
1.堆是一种
基于完全二叉树的数据结构。
2.完全二叉树
(1) 每个节点最多有两个子节点(二叉);
(2) 除了最底层,其他每一层都必须填满,最底层也需要从左到右依次填入数据;
3.当一棵完全二叉树满足下列条件时即称为堆:每个父节点都大于等于(或者小于等于)它的两个子节点。大于等于的情况称为大根堆,小于等于的情况称为小根堆(没有二叉排序树的要求,比如左儿子小于右儿子)。
本文以小根堆为例,大根堆可以很容易类比。
堆的存储
堆的存储通过线性表(如数组、vector等)来实现,由于其满足完全二叉树的性质。
则有
第i个节点(i从0开始算)的:
1.父节点: (i-1)/2 // 为负数时则说明父节点不存在
2.左右子节点: (i*2+1) 和 (i*2+2)
vector<int> heap;
插入堆
给出一个数组存储的堆,如果加入了新元素,必须想办法保持堆的特性:
1.完全二叉树
2.父节点小于等于其子节点
加入新元素后,只需要不断与其父节点进行比较,根据大小关系进行调整。
即分为两步:
void fix_up() {
int pos = heap.size() - 1;
int val = heap[pos], parent = (pos - 1) >> 1;
while(parent >= 0 && heap[parent] > heap[pos]) { //向上调整,直至父节点小于自己
swap(heap[pos], heap[parent]);
pos = parent;
parent = (pos - 1) >> 1;
}
}
void insert(int val) {
heap.push_back(val);
fix_up();
}
注意,这里每次从末尾插入元素的方式其实保证了最终为完全二叉树的条件。
从堆中删除
堆结构仅支持从堆顶进行POP操作, 每次都能够POP出最小的元素.
POP以后堆结构即遭到破坏(缺失了首元素), 此时可以通过下列步骤恢复:
1.将最后一个元素放到堆顶;
2.将堆顶元素向下调整;
void fix_down() {
int pos = 0, nextPos = 0, left = pos * 2 + 1, right = pos * 2 + 2;
if(heap.size() == 0) {
return;
}
while(left < heap.size()) {
if(right < heap.size()) {
nextPos = (heap[left] < heap[right]) ? left : right; //左右儿子中选择较小的
} else {
nextPos = left;
}
if(heap[pos] > heap[nextPos]) { //如果比左右儿子小,那么向下调整
swap(heap[pos], heap[nextPos]);
pos = nextPos;
left = pos * 2 + 1;
right = pos * 2 + 2;
} else {
break;
}
}
}
int pop() {
int res = heap[0];
heap[0] = heap[heap.size() - 1]; //最后一个元素放入堆顶
heap.pop_back();
fix_down(); //将新插入堆顶的元素向下调整
return res;
}
数组堆化
这一部分要解决给出一个数组, 用这个数组构建堆的问题。之前我们解决了堆的插入, 删除等操作的问题,现在再解决堆化的问题就比较容易了。
有以下两种思路:
1.把数组里的数一个一个取出来, 插入堆中;
2.对数组里的每一个非叶子节点的数进行向下调整的操作;
以上两种思路均可以通过上述实现的调整函数进行实现。
注:思路2下,最后一个非叶子节点的位置为n/2-1,所以从n/2-1往回遍历即可。
堆排序
由于堆的顶部总是最小的数,只需要每次将顶部的数取出,然后再将堆调整为最小堆即可。
取出一个数,最多需要调整logn次,有n个数需要取出,所以堆排序的时间复杂度为O(nlogn)。