什么是堆?
在数据结构中, 堆是一个具有特殊顺序的数组, 不过能近似的看成一个完全二叉树.
假设有一个数组, 长度为6. 该数组下标从1开始.
将数组转换成完全二叉树的形式
通过上图看出下面非常关键的性质:
- 左孩子序号 = 父节点序号 * 2
- 右孩子序号 = 父节点序号 * 2 + 1
- 父节点序号 = 孩子节点序号 / 2
以上三个性质是非常重要的性质. 后续的编程也会利用到提及的性质.
为什么下标从1开始?
下标从1开始是为了好理解. 在实际编码的时候, 下标是从0开始. 则对应的节点关系如下:
- 左孩子序号 = 父节点序号 * 2 + 1, 也等于 ( 父节点序号 + 1 ) * 2 - 1
- 右孩子序号 = 父节点序号 * 2 + 2, 也等于 ( 父节点序号 + 1 ) * 2
- 父节点序号 = ( 孩子节点序号 - 1 ) / 2
最小堆/最大堆
这里以最小堆举例. 最小堆的性质: 任意一个节点, 都必须小于等于他的子节点. 根据这个性质. 可以举个例子
建立最小堆
假设现在有一个整数数组[2 ,4 ,1 ,9 ,10 ,-1], 现在对该数组建立最小堆. 先用完全二叉树描述建立过程, 然后再用数组描述过程.
- 判断序号为3的节点,(思考为什么从序号为3的节点开始?), 判断需要交换. 则生成第二个图.
- 判断序号为2的节点, 发现不需要数据交换.
- 判断序号为1的节点, 需要发生数据交换
- 判断序号为3的节点, 因为序号为3的节点含有子节点, 所以需要判断. 并且判断为需要数据交换.
下面用数组描述具体过程:
为什么要从序号为3的节点进行循环?
因为序号为3的节点是最后一个非叶子节点.
假设有N个节点. 则有 ⌊logN⌋ 层. 最后一层可能的序号则在 2 ⌊ l o g 2 N ⌋ − 1 2^{⌊log_2N⌋-1} 2⌊log2N⌋−1 ~ 2 ⌊ l o g 2 N ⌋ 2^{⌊log_2N⌋} 2⌊log2N⌋. 所以从要从 2 ⌊ l o g 2 N ⌋ − 1 − 1 2^{⌊log_2N⌋-1}-1 2⌊log2N⌋−1−1 序号的节点遍历, 对 2 ⌊ l o g 2 N ⌋ − 1 − 1 2^{⌊log_2N⌋-1}-1 2⌊log2N⌋−1−1 简化等于 N / 2 − 1 N/2 - 1 N/2−1. 所以要从 N / 2 − 1 N/2 -1 N/2−1 ~ 1.
删除堆中一个节点
删除节点需要把要删除的节点与最后一个元素交换, 然后递归的调用维持堆的性质.
例如如下例子, 用完全二叉树过程进行解释.
灰色节点: 已删除节点
绿色节点: 进行逻辑计算节点
箭头: 表示数据交换
堆排序代码
public static void heapSort(int[] data) {
for (int i = data.length / 2 - 1; i >= 0; i--) {
adjust(data, i, data.length);
}
for (int j = data.length - 1; j > 0; j--) {
//交换元素
int temp = data[0];
data[0] = data[j];
data[j] = temp;
adjust(data, 0, j);
}
}
public static void adjust(int[] data, int i, int length) {
int temp = data[i];
// 调整完成之后,也会调整它的子节点
// i * 2 + 1 是左节点.
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
// 判断指针是否向右节点移动,关系着下一次迭代
if (k + 1 < length && data[k] < data[k + 1]) {
k = k + 1;
}
if (data[k] > temp) {
data[i] = data[k];
i = k;
} else {
break;
}
}
data[i] = temp;
}
优先队列
优先队列是堆排序的一个应用. 优先队列的删除和增加一个节点都在 O ( l o g 2 N ) O(log_2N) O(log2N), 具体操作的时间主要是花费在查找删除的元素和维护堆的性质.
优先队列一般操作都是从数组开头删除元素(取)元素, 所以查询元素时间为 O ( 1 ) O(1) O(1),但是需要维护堆的性质, 维护堆的性质需要花费 O ( l o g 2 N ) O(log_2N) O(log2N).
各位观众, 该文章已经临近尾声, 弱弱的求赞.
总结
- 最小堆和最大堆的核心概念
- 堆排序
- 优先队列的核心概念