堆是一种满足特定条件的完全二叉树,可以分为两种:

  • 最小堆:任意节点的值 \le 其子节点的值。

  • 最大堆:任意节点的值 \ge 其子节点的值。

一、堆的实现

1.1 堆存储与表示

完全二叉树非常适合用数组表示给定索引 i,左子节点的索引为 2i+1,右子节点的索引为 2i+2,父节点的索引为 (i-1)/2,索引越界表示节点不存在。注:索引从 0 开始,到 n-1 结束。

// 数组实现堆
List<Integer> maxHeap = new ArrayList<>();

// 获取左子节点的索引
public int left(int i) {
    return 2 * i + 1;
}

// 获取右子节点的索引
public int right(int i) {
    return 2 * i + 2;
}

// 获取父节点的索引
public int parent(int i) {
    return (i - 1) / 2;
}

1.2 访问堆顶元素

根节点就是堆顶元素,最大/小堆的堆顶元素是最大/小的元素。

// 访问堆顶元素
public int peek(){
    return maxHeap.getFirst();
}

1.3 元素入堆

给定元素 x,首先将其插入堆尾,然后自底向上维护堆的性质。

// 元素如堆
public void push(int x) {
    // 在堆尾插入元素
    maxHeap.add(x);
    // 自底向上堆化
    siftUp(maxHeap.size() - 1);
}

// 从节点 i 开始,自底向上堆化
public void siftUp(int i) {
    // 获取父节点
    int p = parent(i);
    // 当“越过根节点”或“节点无须修复”时,结束堆化
    if (p < 0 || maxHeap.get(p) >= maxHeap.get(i)) {
        return;
    }
    swap(i, p); // 交换两个节点
    siftUp(p);  // 递归向上堆化
}

// 交换两个元素
public void swap(int i, int j) {
    int tmp = maxHeap.get(i);
    maxHeap.set(i, maxHeap.get(j));
    maxHeap.set(j, tmp);
}

1.4 元素出堆

堆顶元素是二叉树的根节点,如果直接删除首元素,二叉树中所有节点的索引发生变化,会为维护堆的性质带来麻烦。因此,堆顶元素出堆可以通过下面的步骤实现:

  • 交换堆顶元素与堆底元素

  • 删除堆底元素

  • 从根节点开始,自顶向下堆化

// 判断堆是否为空
public boolean isEmpty(){
    return maxHeap.isEmpty();
}

// 堆顶元素出堆
public int pop(){
    // 判空处理
    if (isEmpty()){
        throw new IndexOutOfBoundsException();
    }
    // 交换堆顶元素和堆底元素
    swap(0, maxHeap.size() - 1);
    // 删除堆底元素
    int val = maxHeap.removeLast();
    // 自顶向下堆化
    siftDown(0);
    return val;
}

public void siftDown(int i){
    int l = left(i);
    int r = right(i);
    int mx = i; // 判断节点 i, l, r 中值最大的节点,记为 mx
    if (l < maxHeap.size() && maxHeap.get(l) > maxHeap.get(mx)){
        mx = l;
    }
    if (r < maxHeap.size() && maxHeap.get(r) > maxHeap.get(mx)){
        mx = r;
    }
    // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
    if (mx == i){
        return;
    }
    swap(i, mx);    // 交换两个节点
    siftDown(mx);   // 继续向下堆化
}

1.5 建堆操作

一种高效的建堆操作:

  • 将列表中所有元素原封不动的添加到堆中;

  • 倒序遍历数组,堆每个非叶子节点执行「自顶向下堆化」。

每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆。由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化。

// 原地堆化
public void heapify(List<Integer> nums) {
    maxHeap = new ArrayList<>(nums);
    // 堆化除叶节点以外的其他所有节点
    for (int i = parent(maxHeap.size() - 1); i >= 0; i--) {
        siftDown(i);
    }
}

二、堆排序

我们可以基于「建堆操作」和「元素出堆操作」实现堆排序:

  • 将输入数组建立最小/大堆。

  • 不断执行出堆操作,用辅助数组记录出堆的元素,即可得到排序后的数组。

这种方式虽然可以实现堆排序,但是需要辅助数组,比较浪费空间,不够优雅。