# 第 4 节 最大堆的第 2 个重要操作:从一个最大堆中取出元素

这个操作叫 sift down。

  • 根据堆有序的性质,根结点是堆(数组)中最大的元素,即数组里下标为 1 的元素;
  • 从最大堆中取出一个元素,即取出根结点元素,同时还要保持最大堆的性质;
  • 根结点取出以后,1 号下标位置为空,于是我们将当前数组的最后一个元素放到 1 号下标的位置。这样做是 因为交换和移动的次数最少,这种想法也是十分自然的,并且保持了完全二叉树的性质;
  • 但是此时数组并不满足最大堆的性质,我们就要进行 siftDown 操作使这个数组保持最大堆的性质。

# siftDown 的具体操作步骤

从 1 号下标开始,如果存在右孩子,就把右孩子和左孩子比较,比出较大的那个,再和自己比较,如果比自己大,就交换位置,这样的过程直到「不小于两个孩子结点中的最大者」。

同理,我们可以写出 shitDown 的两个版本。

# 代码实现一:逐层交换下移

说明

  • 当我们从下标 1 开始存放最大堆的元素的时候,最大堆的最后一个元素是 data[count]
  • 在完全二叉树中,如何表示有孩子?其实有左孩子就够了。这里的循环条件是 2 * k <= count ,等于号不能漏掉,自己画一个完全二叉树就清楚了;
  • 在结点存在孩子结点的情况下,先判断是否存在右孩子。如果存在右孩子,就一定有左孩子,然后把右孩子和左孩子比较,比出最大的那个,再和自己比较,如果比自己大,就交换位置,这样的过程直到「自己比左右两个孩子都大」为止。

Java 代码:

/**
 * 取出最大堆中的根结点
 * 1、把最后一个元素和下标是 1 的元素进行交换
 * 2、从根结点开始逐层下移:下移的过程中将与左右孩子结点进行比较,把最大的那个跟自己交换
 *
 * @return 根结点的元素
 */
public int extractMax() {
    assert count > 0;
    int ret = data[1];
    swap(data, 1, count);
    count--;
    siftDown(1);
    return ret;
}

/**
* 只要有左右孩子,左右孩子只要比自己大,就交换
*
* @param h
*/
private void siftDown(int h) {
  	// 如果这个元素有左边的孩子
    while (2 * h <= count) { 
        int k = 2 * h;
      	// 如果有右边的孩子,并且还大于左边的孩子,就好像左边的孩子不存在一样
        if (k + 1 <= count && data[k + 1] > data[k]) { 
            k = k + 1;
        }
        if (data[h] < data[k]) {
            swap(data, h, k);
        }
        h *= 2;
    }
}

Python 代码:

def extract_max(self):
    if self.count == 0:
        raise Exception('堆里没有可以取出的元素')
    ret = self.data[1]
    self.data[1], self.data[self.count] = self.data[self.count], self.data[1]
    self.count -= 1
    self.__sift_down(1)
    return ret

def __sift_down(self, k):
    # 只要有左右孩子,左右孩子只要比自己大,就交换
    while 2 * k <= self.count:
        # 如果这个元素有左边的孩子
        j = 2 * k
        # 如果有右边的孩子,大于左边的孩子,就好像左边的孩子不存在一样
        if j + 1 <= self.count and self.data[j + 1] > self.data[j]:
            j = j + 1
        if self.data[k] >= self.data[j]:
            break
        self.data[k], self.data[j] = self.data[j], self.data[k]
        k = j

# 代码实现二:先暂存,再逐层复制

和上一节 shitUp 的优化的思路一样,借用插入排序优化的思路,先存一下这个可能会下移的元素,通过逐层赋值,实现与逐层交换下移等价的操作。

Java 代码:

private void shiftDown(int k) {
    int temp = data[k];
    // 只要它有孩子,注意,这里的等于号是十分关键的
    while (2 * k <= count) {
        int j = 2 * k;
        // 如果它有右边的孩子,并且右边的孩子大于左边的孩子
        if (j + 1 <= count && data[j + 1] > data[j]) {
            // 右边的孩子胜出,此时可以认为没有左孩子,
            j = j + 1;
        }
        // 如果当前的元素的值,比右边的孩子结点要大,则逐渐下落的过程到此结束
        if (temp >= data[j]) {
            break;
        }
        // 否则,交换位置,继续循环
        data[k] = data[j];
        k = j;
    }
    data[k] = temp;
}

Python 代码:

def __sink(self, k):
    # 下沉
    temp = self.data[k]
    # 只要它有孩子,注意,这里的等于号是十分关键的
    while 2 * k <= self.count:
        j = 2 * k
        # 如果它有右边的孩子,并且右边的孩子大于左边的孩子
        if j + 1 <= self.count and self.data[j + 1] > self.data[j]:
            # 右边的孩子胜出,此时可以认为没有左孩子
            j += 1
        # 如果当前的元素的值,比右边的孩子节点要大,则逐渐下落的过程到此结束
        if temp >= self.data[j]:
            break
        # 否则,交换位置,继续循环
        self.data[k] = self.data[j]
        k = j
    self.data[k] = temp

# 完整代码

Python 代码:

# 通过 LeetCode 第 215 题、第 295 题测试
class MaxHeap:
    def __init__(self, capacity):
        # 我们这个版本的实现中,0 号索引是不存数据的,这一点一定要注意
        # 因为数组从索引 1 开始存放数值
        # 所以开辟 capacity + 1 这么多大小的空间
        self.data = [None for _ in range(capacity + 1)]
        # 当前堆中存储的元素的个数
        self.count = 0
        # 堆中能够存储的元素的最大数量(为简化问题,不考虑动态扩展)
        self.capacity = capacity

    def size(self):
        """
        返回最大堆中的元素的个数
        :return:
        """
        return self.count

    def is_empty(self):
        """
        返回最大堆中的元素是否为空
        :return:
        """
        return self.count == 0

    def insert(self, item):
        if self.count + 1 > self.capacity:
            raise Exception('堆的容量不够了')
        self.count += 1
        self.data[self.count] = item
        # 考虑将它上移
        self.__swim(self.count)

    def __shift_up(self, k):
        # 有索引就要考虑索引越界的情况,已经在索引 1 的位置,就没有必要上移了
        while k > 1 and self.data[k // 2] < self.data[k]:
            self.data[k // 2], self.data[k] = self.data[k], self.data[k // 2]
            k //= 2

    def __swim(self, k):
        # 上浮,与父结点进行比较
        temp = self.data[k]
        # 有索引就要考虑索引越界的情况,已经在索引 1 的位置,就没有必要上移了
        while k > 1 and self.data[k // 2] < temp:
            self.data[k] = self.data[k // 2]
            k //= 2
        self.data[k] = temp

    def extract_max(self):
        if self.count == 0:
            raise Exception('堆里没有可以取出的元素')
        ret = self.data[1]
        self.data[1], self.data[self.count] = self.data[self.count], self.data[1]
        self.count -= 1
        self.__sink(1)
        return ret

    def __shift_down(self, k):
        # 只要有左右孩子,左右孩子只要比自己大,就交换
        while 2 * k <= self.count:
            # 如果这个元素有左边的孩子
            j = 2 * k
            # 如果有右边的孩子,大于左边的孩子,就好像左边的孩子不存在一样
            if j + 1 <= self.count and self.data[j + 1] > self.data[j]:
                j = j + 1
            if self.data[k] >= self.data[j]:
                break
            self.data[k], self.data[j] = self.data[j], self.data[k]
            k = j

    def __sink(self, k):
        # 下沉
        temp = self.data[k]
        # 只要它有孩子,注意,这里的等于号是十分关键的
        while 2 * k <= self.count:
            j = 2 * k
            # 如果它有右边的孩子,并且右边的孩子大于左边的孩子
            if j + 1 <= self.count and self.data[j + 1] > self.data[j]:
                # 右边的孩子胜出,此时可以认为没有左孩子
                j += 1
            # 如果当前的元素的值,比右边的孩子节点要大,则逐渐下落的过程到此结束
            if temp >= self.data[j]:
                break
            # 否则,交换位置,继续循环
            self.data[k] = self.data[j]
            k = j
        self.data[k] = temp


if __name__ == '__main__':
    max_heap = MaxHeap(6)
    max_heap.insert(3)
    print(max_heap.data[1])
    max_heap.insert(5)
    print(max_heap.data[1])
    max_heap.insert(1)
    print(max_heap.data[1])
    max_heap.insert(8)
    print(max_heap.data[1])
    max_heap.insert(7)
    print(max_heap.data[1])
    max_heap.insert(12)

    while not max_heap.is_empty():
        print('取出', max_heap.extract_max())

到这里,我们就可以通过「最大堆」实现排序功能了,「最小堆」可以类似地实现。

到此为止,我们已经实现了最大堆的「入队」和「出队」两个基本操作,我们完全通过直接输出元素来检验一下,自己写出的最大堆是否符合最大堆的性质。因为每一次从最大堆取出的总是数组中最大的元素,所以可以将最大堆用于排序。


作者:liweiwei1419 链接:https://suanfa8.com/priority-queue/sift-down 来源:算法吧 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Last Updated: 11/19/2024, 1:33:17 AM