跳转至

堆的定义

「堆 heap」是一种满足特定条件的完全二叉树,主要可分为两种类型,如下图所示。

「大顶堆 max heap」:任意节点的值 \(\ge\) 其子节点的值。 「小顶堆 min heap」:任意节点的值 \(\le\) 其子节点的值。 img

堆的实现

1.堆的储存和表示

“二叉树”章节讲过,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,因此我们将采用数组来存储堆。

当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。节点指针通过索引映射公式来实现。

如图 8-2 所示,给定索引 \(i\) ,其左子节点索引为 \(2i+1\) ,右子节点索引为\(2i+2\) ,父节点索引为 \(\frac{i-1}{2}\)(向下整除)。当索引越界时,表示空节点或节点不存在。 img

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

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

/* 获取父节点索引 */
int parent(MaxHeap *maxHeap, int i) {
    return (i - 1) / 2;
}

2.访问堆顶元素

/* 访问堆顶元素 */
int peek(MaxHeap *maxHeap) {
    return maxHeap->data[0];
}

3.元素入堆

给定元素 val ,我们首先将其添加到堆底。添加之后,由于 `val`` 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为「堆化 heapify」。

考虑从入堆节点开始,从底至顶执行堆化 。如下图所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。

/* 元素入堆 */
void push(MaxHeap *maxHeap, int val) {
    // 默认情况下,不应该添加这么多节点
    if (maxHeap->size == MAX_SIZE) {
        printf("heap is full!");
        return;
    }
    // 添加节点
    maxHeap->data[maxHeap->size] = val;
    maxHeap->size++;

    // 从底至顶堆化
    siftUp(maxHeap, maxHeap->size - 1);
}

/* 从节点 i 开始,从底至顶堆化 */
void siftUp(MaxHeap *maxHeap, int i) {
    while (true) {
        // 获取节点 i 的父节点
        int p = parent(maxHeap, i);
        // 当“越过根节点”或“节点无须修复”时,结束堆化
        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {
            break;
        }
        // 交换两节点
        swap(maxHeap, i, p);
        // 循环向上堆化
        i = p;
    }
}

时间复杂度\(O(logn)\)

堆顶出堆

堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化进行修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。

  • 交换堆顶元素与堆底元素(交换根节点与最右叶节点)。
  • 交换完成后,将堆底从列表中删除(注意,由于已经交换,因此实际上删除的是原来的堆顶元素)。
  • 从根节点开始,从顶至底执行堆化。 “从顶至底堆化”的操作方向与“从底至顶堆化”相反,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。

/* 元素出堆 */
int pop(MaxHeap *maxHeap) {
    // 判空处理
    if (isEmpty(maxHeap)) {
        printf("heap is empty!");
        return INT_MAX;
    }
    // 交换根节点与最右叶节点(交换首元素与尾元素)
    swap(maxHeap, 0, size(maxHeap) - 1);
    // 删除节点
    int val = maxHeap->data[maxHeap->size - 1];
    maxHeap->size--;
    // 从顶至底堆化
    siftDown(maxHeap, 0);

    // 返回堆顶元素
    return val;
}

/* 从节点 i 开始,从顶至底堆化 */
void siftDown(MaxHeap *maxHeap, int i) {
    while (true) {
        // 判断节点 i, l, r 中值最大的节点,记为 max
        int l = left(maxHeap, i);
        int r = right(maxHeap, i);
        int max = i;
        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {
            max = l;
        }
        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {
            max = r;
        }
        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
        if (max == i) {
            break;
        }
        // 交换两节点
        swap(maxHeap, i, max);
        // 循环向下堆化
        i = max;
    }
}
时间复杂度\(O(logn)\)