堆通常是一个可以被看做一棵树的数组对象。
堆总是满足下列性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树
堆虽然可以看成一颗二叉树,但其本质还是数组,它的应用也与二叉树不同,堆常用于排序,因此物理结构上使用连续的内存空间存放数据要比指针方便得多。
堆作为一种数据结构,可以这样定义:
typedef int HPDataType;
typedef struct Heap{
HPDataType* _a;
int _size;
int _capacity;
}Heap;
HPDataType代表数据的类型,size代表堆中数据的大小,capacity代表堆的容量。
根据堆的性质,对堆中某个节点而言,如果它有孩子,那么它的孩子一定满足一种比较关系。例如我这里建的是大根堆,那么每个节点的值都要求不小于它的子节点。
由于我们得到的初始数据是无序的,要建堆就得先对数据进行处理,下面以大根堆为例介绍两种方法:
1. 向上调整
基本思想: 对于大根堆中的任意一个元素而言, 它的父节点按层依次递增. 如图, 对于节点 3 而言, 它的父节点 8 > 3 , 而节点 8 的父节点 9 > 8…要注意的是某节点的父节点的兄弟节点不一定比大于该节点, 如节点 8 的父节点的兄弟节点 6.
因此在对某节点进行向上调整时只需要将该节点和其父节点进行比较即可, 如果大于父节点就交换, 继续向上调整直到不需要交换或调整到了根节点结束.
向上调整的代码参考:
void ShiftUp(HPDataType *arr, int child) {
int parent = (child - 1) / 2;
while (parent >= 0) {
if (arr[parent] < arr[child]) {
//Swap是一个传指针的交换函数
Swap(arr + parent, arr + child);
child = parent;
parent = (child - 1) / 2;
}
else {
break; //无需继续
}
}
}
建堆步骤: 从堆顶开始, 依次对每个元素进行向上调整, 这样就能保证每一次操作的节点前的元素构成一个堆. 直到所有元素都调整完毕.
2. 向下调整
向下调整的思路与向上调整相反, 向下调整是保证每个节点后面的元素构成一个堆, 将该节点和它的左右孩子进行比较, 并不断地下移直到不需要调整或没有孩子节点为止.
向下调整代码参考:
void ShiftDown(HPDataType* arr, int n, int root) {
assert(arr);
int parent = root;
int child = 2 * parent + 1;
while (child < n) { //有孩子, 调整
if (child + 1 < n && arr[child] < arr[child + 1]) {
//比较左右孩子的大小
++child;
}
if (arr[child] > arr[parent]) {
Swap(arr + child, arr + parent);
parent = child;
child = 2 * parent + 1;
}
else {
break; //无需调整
}
}
}
建堆步骤: 从数组的最后一个元素开始, 依次对每个元素进行向下调整, 直到所有元素都调整完毕.
两种方法效率比较: 虽然向下调整每次要比较两次, 但实际上两种方法的效率是差不多的, 因为叶子节点不需要向下调整.
弹出堆顶元素的操作pop
有了向下调整算法, pop算法就不需要想太多了, 直接将堆顶元素和最后一个元素交换, 再对堆顶进行一次向下调整, 并将堆的size-1.
堆排序
不难发现, 在pop操作中, 我们每次都会得到堆顶元素, 即大根(小根)堆中最大(小)的元素, 进行size次pop操作, 就可以得到一个有序排序的数组了.