目录
(一) 定义
堆(Heap): 是计算机科学中一类特殊的非线性数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆总是满足下列性质:
- 堆一定是一棵完全树(按照元素循序排列成树的形状)
- 堆中任意一个结点的值总是不大于或不小于其父节点的值
最大堆: 堆中任意一个结点的值总是不大于其父节点的值; 最小堆: 堆中任意一个结点的值总是不小于其父节点的值
二叉堆: 是一棵完全二叉树
基于完全二叉树的特性: 将节点按顺序一层一层的码放出来(层序输出), 因此我们可以使用使用数组的方式表示一颗完全二叉树(两种方式).
(二) 自定义基于动态数组的最大堆
1.自定义最大堆的基础结构
public class MaxHeap<E extends Comparable<E>> {
/**
* 存储数据的动态数组
*/
private Array<E> data;
public MaxHeap(int capacity) {
data = new Array<>(capacity);
}
public MaxHeap() {
data = new Array<>();
}
/**
* 获取堆中的元素个数
*
* @return
*/
public int getSize() {
return data.getSize();
}
/**
* 返回堆中的元素是否为空
*
* @return
*/
public boolean isEmpty() {
return data.isEmpty();
}
/**
* 返回完全二叉树的数组表示中, 一个索引所表示的元素的父亲节点的索引
*
* @param index
* @return
*/
private int parent(int index) {
return (index - 1) / 2;
}
/**
* 返回完全二叉树的数组表示中, 一个索引所表示的元素的左孩子节点的索引
*
* @param index
* @return
*/
private int leftChild(int index) {
return index * 2 + 1;
}
/**
* 返回完全二叉树的数组表示中, 一个索引所表示的元素的右孩子节点的索引
*
* @param index
* @return
*/
private int rightChild(int index) {
return index * 2 + 2;
}
}
2.查看堆中最大的元素
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("MaxHeap is empty.");
}
return data.get(0);
}
3.堆的添加元素和Sift Up(上浮操作)
按层序添加元素65(即数组末尾添加元素), 为了满足最大堆的特性(堆中任意一个结点的值总是不大于其父节点的值), 进行上浮操作
/**
* 向堆中添加元素e
*
* @param e
*/
public void add(E e) {
// 层序添加元素 相当于 动态数组末尾追加元素
data.addLast(e);
// 为满足最大堆的性质: 堆中任意一个结点的值总是不大于其父节点的值. 对添加的元素进行上浮操作
siftUp(data.getSize() - 1);
}
/**
* 上浮操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
*
* @param index
*/
private void siftUp(int index) {
// while循环的条件: 索引大于0 且 当前索引父节点的值 小于 当前索引节点的值
while(index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
// 交换当前索引 与 当前索引父节点索引 的值
data.swap(index, parent(index));
index = parent(index);
}
}
4.堆的取出最大元素和Sift Down(下沉操作)
取出数组中0索引处的元素为最大元素, 将数组中最后一个元素的值作为新0索引处的值同时删除最后一个元素.为了满足最大堆的特性(堆中任意一个结点的值总是不大于其父节点的值), 进行下沉操作.
/**
* 取出堆中最大的元素
*
* @return
*/
public E extractMax() {
E res = findMax();
// 交换 0索引处节点 与 最后索引处节点 的值
data.swap(0, data.getSize() - 1);
// 删除数组最后一个元素
data.removeLast();
// 下沉操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
siftDown(0);
return res;
}
/**
* 下沉操作: 为满足最大堆的性质, 堆中任意一个结点的值总是不大于其父节点的值.
*
* @param index
*/
private void siftDown(int index) {
// while循环的条件: index索引的左孩子结点的索引 小于 数组的最大索引
while (leftChild(index) < data.getSize()) {
// index索引的左孩子结点的索引
int j = leftChild(index);
// 存在 index索引的右孩子结点的索引 且 index索引的右孩子结点的索引的值 大于 index索引的左孩子结点的索引的值
if (j + 1 < data.getSize() && data.get(j+1).compareTo(data.get(j)) > 0) {
// 右孩子节点索引
j++;
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if (data.get(index).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(index, j);
index = j;
}
}
测试
public static void main(String[] args) {
int n = 100000;
Random random = new Random();
MaxHeap<Integer> maxHeap = new MaxHeap<>();
for (int i = 0; i < n; i++) {
// 向最大堆中添加一百万个Integer类型数据
maxHeap.add(random.nextInt(Integer.MAX_VALUE));
}
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
// 从堆中依次提取最大元素装入数组中
arr[i] = maxHeap.extractMax();
}
for (int i = 1; i < n; i++) {
// 遍历数组, 比较相邻两个元素的大小, 假设索引较小的元素大于索引较大的元素, 则抛出异常证明自定义二叉堆实现错误
if (arr[i - 1] < arr[i]) {
throw new IllegalArgumentException("Error");
}
}
System.out.println("Test MaxHeap completed.");
}
5.堆的Heapify操作
将任意数组整理形成堆数据结构(两种方式).
- 遍历数组: 依次将元素插入到一个空堆中, 时间复杂度为 O(nlogn)
- Heapify(堆化):
- 首先将数组看作是一个二叉堆, 当然此时二叉堆并不满足堆的性质, 但此二叉堆中的每一个叶子节点都可以看作是一棵二叉堆
- 然后在二叉堆中, 找到所有非叶子节点, 依次从最后一个非叶子节点到0索引所在的叶子节点 执行siftDown()下沉操作, 以此保证二叉堆的性质. 时间复杂度为 O(n)
最后一个非叶子结点所在的索引 = 数组最后一个元素的父节点所在的索引
- 首先将数组看作是一个二叉堆, 当然此时二叉堆并不满足堆的性质, 但此二叉堆中的每一个叶子节点都可以看作是一棵二叉堆
public MaxHeap(E[] arr) {
data = new Array<>(arr);
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftDown(i);
}
}
(三) 时间复杂度分析
二叉堆是一颗完全二叉树
函数 | 时间复杂度 | 分析 |
---|---|---|
add(e) | O(h) => O(logn) | 二叉堆是一颗完全二叉树, 因此SiftUp上浮操作的时间复杂度为 O(h) => O(logn) |
extractMax() | O(h) => O(logn) | 完全二叉树永远不会退化成链表, 因此SiftDown下沉操作的时间复杂度为 O(h) => O(logn) |