数据结构与算法:最大堆
程序员文章站
2024-02-13 23:05:10
...
堆
堆是用数组实现的二叉树,堆根据**“堆属性”来排序**,“堆属性”决定了树中节点的位置。
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的**“堆属性”**,并且这个属性对堆中的每一个节点都成立。
根据这一属性,那么最大堆总是将其中的最大值存放在树的根节点。而对于最小堆,根节点中的元素总是树中的最小值。堆属性非常的有用,因为堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素。
注意:堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于索引 0 的位置,但是最小的元素则未必是最后一个元素。唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。
堆和普通树的区别
堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。来看一下两者的主要差别:
- 节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。
- 内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额外内存。堆仅仅使用一个数组,且不使用指针。
- 平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足堆属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。
- 搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。
二叉堆
- 二叉堆是一颗完全二叉树
-
用数组存储二叉堆
最大堆
- 最大堆中某个节点的值总是不大于其父节点的值且是一颗完全二叉树
- 用数组存储,父节点与左右子节点的索引满足:
parent[i] = (i - 1) / 2
leftChild[i] = 2 * i + 1
rightChild[i] = 2 * i + 2
注:本文实现代码中使用到了数据结构与算法:动态数组中的动态数组
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data; // 动态数组
public MaxHeap(int capacity) {
data = new Array<E>(capacity);
}
public MaxHeap() {
data = new Array<E>();
}
/**
* 返回完全二叉树的数组表示中,元素的父亲节点索引
* @param index
* @return
*/
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent.");
}
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;
}
}
向堆中添加元素和SiftUp
- 添加一个元素就相当于是在层序遍历的最后一层的最右边添加一个元素,如果没有位置了,就新加一层;
- 图中52为新添加的元素,从数组的角度看就是在索引为10的位置添加一个元素,现在虽然满足完全二叉树,但不满足父节点均大于孩子节点,因此,从新加入的节点开始,依次和其父节点作比较,若比父节点大则交换位置
- 交换52和16的位置,此时以52为根的二叉树满足堆的性质,52继续和其父节点比较
- 继续交换52和41的位置,此时以52为根的二叉树满足堆的性质,现在52<62,没有破坏堆的性质
- Sift Up结束
/**
* 上浮,用于添加元素
* @param k 需要上浮的元素对应的索引
*/
private void siftUp(int k) {
while(k>0 && data.get(parent(k)).compareTo(data.get(k)) < 0) { // 索引未越界且父节点的值小于该节点的值
data.swap(k, parent(k));
k = parent(k);
}
}
/**
* 向堆中添加元素
* O(logn)
* @param e
*/
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
从堆中取出元素和siftDown
- 只取出堆顶的元素,即数组索引0处的元素
- 将堆顶元素取出后,将左右子树融合成新的堆
- 直接融合比较复杂,此处使用一个小技巧,将原堆中的最后一个元素放到堆顶去,此时仍然满足完全二叉树,但堆顶的元素打破了堆的性质,需要将堆顶的元素进行下沉
- 每次和它的左右子节点进行比较,并与较大的那一个交换位置,若仍然不满足堆的性质就继续下沉
- 将16和41交换位置,此时没有违反堆的性质,Sift Down结束
/**
* 查看堆中的最大元素
* @return
*/
public E findMax() {
if(data.getSize() == 0) {
throw new IllegalArgumentException("Cannot findMax when heap is empty.");
}
return data.get(0);
}
/**
* 取出堆中的最大元素
* O(logn)
* @return
*/
public E extractMax() {
E ret = findMax();
data.swap(0, data.getSize()-1); // 将数组末尾元素放置堆顶
data.removeLast(); // 删除末尾元素
siftDown(0);
return ret;
}
/**
* 下沉,用于取出最大元素
* @param k 需要下沉元素的索引
*/
private void siftDown(int k) {
while(leftChild(k) < data.getSize()) {
int j = leftChild(k);
// 存在右孩子节点且右孩子节点值大于左孩子节点值
if (j+1 < data.getSize() &&
data.get(j+1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(k, j);
k = j;
}
}
完整的代码
上一篇: 游戏AI:机器人反击!