开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
二叉堆-最大堆
介绍
最大堆(max heap),又称大根堆(大顶堆)是指根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,属于二叉堆的两种形式之一,与之类似的还有最小堆(min heap)。此文下面的二叉堆都指最大堆。
定义
- 完全二叉树
- 堆顶元素一定是整个堆中最大的值
- 每一个节点的值既大于或等于左子树的值,又大于或等于右子树的值。
二叉堆示例
往堆中新增元素
在往堆新增元素之前,我们先想想二叉树的存储方式,二叉树的存储方式一个一个节点(node)进行存储堆,每个节点都有两个指针,分别指向其左孩子和右孩子。而最大堆因为是完全二叉树,在内存是可以连续的存储的,并且最大堆一般的应用上都会定义二叉堆最大的存储容量,因此通常来说都是选择数组的结构进行存储。
节点下标的计算方式
从0开始
- 左孩子index = 2 * n + 1
- 右孩子index = 2 * n + 2
- 父亲节点index = (n - 1) / 2
从1开始
- 左孩子index = 2 * n
- 右孩子index = 2 * n + 1
- 父亲节点index = n / 2
我们这里选用从零开始的计算方法进行后续的新增,删除等操作,新增元素的核心思路:,因为是完全二叉树,因此按照我们的计算规则,数组里面是连续存储的,所以新增的元素都直接存放在数组最后一位不为空的位置。但是最大堆的有一条要求是必须保证堆顶元素是堆中最大的值,因此我们新增元素后,这个元素可能比堆顶元素大,因此我们需要对我们的堆进行调整,通常采用上浮操作,即不停的拿当前元素跟他的父亲节点的值进行比较,如果比父亲节点大就跟父亲节点交换位置,直到小于其父亲节点或者到达堆顶就结束,新增的元素也就处在了其正确的位置上。
上浮操作
代码实现
二叉堆的基本属性
// 使用范型E来存储元素,因为需要堆元素上浮,下沉操作需要比较大小,因此存储的需要实现Comparable,
// 让E可以进行比较
public class MaxHeap<E extends Comparable<E>> {
// 用于存储元素
private ArrayList<E> data;
public MaxHeap(int capacity) {
data = new ArrayList<>(capacity);
}
public MaxHeap() {
data = new ArrayList<>();
}
public int size() {
return data.size();
}
public boolean isEmpty() {
return data.isEmpty();
}
}
首先根据计算公式写出求左孩子,右孩子,父亲节点的代码
// 父亲节点index
public int parent(int index) {
if (index == 0) throw new IllegalArgumentException("index-0 doesn't have parent");
return (index - 1) / 2;
}
// 左孩子index
public int leftChild(int index) {
return index * 2 + 1;
}
// 右孩子index
public int rightChild(int index) {
return index * 2 + 2;
}
根据上浮逻辑写出新增元素的代码
/**
* 新增元素
* 思路: 添加至数组最后一个 然后对数据进行上浮操作, 如果父亲节点小于该节点,则与父亲节点交换位置,继续向上进行上浮操作
* 直到达到堆顶或者小于其父亲节点,上浮操作结束
*
* @param e
*/
public void addElement(E e) {
// 将元素添加到数组末尾
data.add(e);
// 如果最大堆的size大于1 就需要进行上浮操作
if (data.size() > 1) {
siftUp(data.size() - 1, e);
}
}
/**
* 上浮操作
* @param j
* @param e
*/
public void siftUp(int j, E e) {
// j = 0表示已经到达堆顶了,如果该元素的值大于父亲节点的值,就进行交换,知道小于父亲节点或者到达堆顶结束
while (j > 0 && e.compareTo(data.get(parent(j))) > 0) {
swap(j, parent(j));
j = parent(j);
}
}
/**
* 两元素交换
* @param i
* @param j
*/
public void swap(int i, int j) {
E e = data.get(i);
data.set(i, data.get(j));
data.set(j, e);
}
弹出堆顶元素
弹出堆顶元素,也就是删除顶堆元素,堆顶元素就是数组中下标为0的,弹出堆顶元素之后,堆顶元素就空了,就不满足二叉树的性质了,因此为了维持最大堆,删除堆顶元素之后将数组末尾的元素移动到堆顶,但是末尾的元素并不一定就是剩下的元素中最大的值了,就不满足最大堆的性质了,为了满足最大堆的性质,就需要将堆顶元素进行下沉操作,下沉操作就是用该元素与其左右孩子对比,与左右孩子中最大的值进行交换,直到该元素的左右孩子都小于该元素为止,下沉操作就结束了,堆顶元素就是整个堆中最大的值了。
弹出元素实例图
弹出堆顶元素的代码
/**
* 弹出堆顶元素
* 实现方式: 先拿到堆顶元素-> 用队尾的元素覆盖堆顶的元素-> 删除队尾元素 -> 对堆顶元素进行下沉操作
* 下沉操作: 用堆顶元素跟其孩子节点最大对进行比较,如果堆顶元素小于孩子节点中最大的,将其进行交换位置,
* 再继续重复上述比较操作,直到该元素没有孩子节点或者孩子节点都小于该元素的值位置,下沉操作结束
* @return
*/
public E popFront() {
E front = getFront();
data.set(0, data.get(data.size() - 1));
data.remove(data.size() - 1);
siftDown(0);
return front;
}
public E getFront() {
return data.get(0);
}
/**
* 下沉操作
*
* @param j
*/
public void siftDown(int j) {
// 如果j节点存在左孩子,说明该堆不止一个元素需要进行比较
while (leftChild(j) < data.size()) {
// j的左孩子index
int childrenIndex = leftChild(j);
// 如果j的右孩子存在并且右孩子比左孩子大就跟右孩子对比(也就是左右孩子都存在,就跟最大的比较就行了)
if (rightChild(j) < data.size() && data.get(leftChild(j)).compareTo(data.get(rightChild(j))) < 0) {
childrenIndex = rightChild(j);
}
// 到此时 childrenIndex 是j的左右孩子中值叫大的那一个
// 如果两个孩子最大的都小于j那就结束下沉
if (data.get(j).compareTo(data.get(childrenIndex)) > 0) break;
// 否则交换j跟childrenIndex的值
E e = data.get(j);
data.set(j, data.get(childrenIndex));
data.set(childrenIndex, e);
j = childrenIndex; // j = 等于childrenIndex进行下一次下沉操作
}
}
查看堆顶元素
介个就很简单啦,直接get(0)就是堆顶元素啦!
/**
* 查看堆顶元素
* @return
*/
public E getFront() {
return data.get(0);
}
总结
二叉堆其实很好理解,也很好实现,最小堆跟最大堆就是相反而已,可以尝试实现下,顶堆元素永远是整个堆中最小的值。很多算法题都是可以用这个数据结构解决,比如查找100万个数中最大的100个等等,
结束语
江湖可能因为少了谁而失色,却不会因为少了谁后就不再是江湖。