原来如此-最大堆

330 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

二叉堆-最大堆

介绍

最大堆(max heap),又称大根堆(大顶堆)是指根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,属于二叉堆的两种形式之一,与之类似的还有最小堆(min heap)。此文下面的二叉堆都指最大堆。

定义

  • 完全二叉树
  • 堆顶元素一定是整个堆中最大的值
  • 每一个节点的值既大于或等于左子树的值,又大于或等于右子树的值。

二叉堆示例

image.png

往堆中新增元素

在往堆新增元素之前,我们先想想二叉树的存储方式,二叉树的存储方式一个一个节点(node)进行存储堆,每个节点都有两个指针,分别指向其左孩子和右孩子。而最大堆因为是完全二叉树,在内存是可以连续的存储的,并且最大堆一般的应用上都会定义二叉堆最大的存储容量,因此通常来说都是选择数组的结构进行存储。

image.png

节点下标的计算方式

从0开始

  • 左孩子index = 2 * n + 1
  • 右孩子index = 2 * n + 2
  • 父亲节点index = (n - 1) / 2

从1开始

  • 左孩子index = 2 * n
  • 右孩子index = 2 * n + 1
  • 父亲节点index = n / 2

我们这里选用从零开始的计算方法进行后续的新增,删除等操作,新增元素的核心思路:,因为是完全二叉树,因此按照我们的计算规则,数组里面是连续存储的,所以新增的元素都直接存放在数组最后一位不为空的位置。但是最大堆的有一条要求是必须保证堆顶元素是堆中最大的值,因此我们新增元素后,这个元素可能比堆顶元素大,因此我们需要对我们的堆进行调整,通常采用上浮操作,即不停的拿当前元素跟他的父亲节点的值进行比较,如果比父亲节点大就跟父亲节点交换位置,直到小于其父亲节点或者到达堆顶就结束,新增的元素也就处在了其正确的位置上。

上浮操作

image.png

代码实现

二叉堆的基本属性

// 使用范型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的,弹出堆顶元素之后,堆顶元素就空了,就不满足二叉树的性质了,因此为了维持最大堆,删除堆顶元素之后将数组末尾的元素移动到堆顶,但是末尾的元素并不一定就是剩下的元素中最大的值了,就不满足最大堆的性质了,为了满足最大堆的性质,就需要将堆顶元素进行下沉操作,下沉操作就是用该元素与其左右孩子对比,与左右孩子中最大的值进行交换,直到该元素的左右孩子都小于该元素为止,下沉操作就结束了,堆顶元素就是整个堆中最大的值了。

弹出元素实例图

image.png

弹出堆顶元素的代码


/**
 * 弹出堆顶元素
 * 实现方式: 先拿到堆顶元素-> 用队尾的元素覆盖堆顶的元素-> 删除队尾元素 -> 对堆顶元素进行下沉操作
 * 下沉操作: 用堆顶元素跟其孩子节点最大对进行比较,如果堆顶元素小于孩子节点中最大的,将其进行交换位置,
 *           再继续重复上述比较操作,直到该元素没有孩子节点或者孩子节点都小于该元素的值位置,下沉操作结束
 * @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个等等,

结束语

江湖可能因为少了谁而失色,却不会因为少了谁后就不再是江湖。