数据结构和算法(十二)堆

166 阅读3分钟

定义

  1. 堆是完全二叉树
  2. 堆的每一个节点都大于等于(或者小于等于)子树每一个节点的值

大顶堆和小顶堆

大顶堆:每个节点的值都大于等于子树中每个节点值的堆

小顶堆:每个节点的值都小于等于子树中每个节点值的堆

堆.png

堆的实现

如图,由于堆是一个完全二叉树,因此堆可以通过数组来实现。

堆的实现.png

当堆通过数组来实现时,我们就可以通过索引来快速获取它的左右节点。比如当节点i的索引为k时,它的左子树的索引为2k,右子树的索引为2k+1,它的父节点的索引为k/2.

堆的插入

    // 用于存储堆中的元素
    private val heapArray = mutableListOf<Int>()

    // 插入元素方法
    fun insert(value: Int) {
        // 将元素添加到堆末尾
        heapArray.add(value)
        var index = heapArray.size - 1
        // 从下往上调整堆,以满足堆的性质
        while (index > 0) {
            // 计算当前节点的父节点索引
            val parentIndex = (index - 1) / 2
            // 如果是大顶堆且当前节点小于等于父节点,或者是小顶堆且当前节点大于等于父节点,则停止调整
            if ((isMaxHeap && heapArray[index] <= heapArray[parentIndex]) ||
                (!isMaxHeap && heapArray[index] >= heapArray[parentIndex])
            ) {
                break
            }
            // 交换当前节点和父节点的值
            val temp = heapArray[index]
            heapArray[index] = heapArray[parentIndex]
            heapArray[parentIndex] = temp
            // 更新当前节点索引为父节点索引,继续向上调整
            index = parentIndex
        }
    }

堆的插入代码如上所示。可以看到,我们首先会将新元素添加到堆的末尾。然后从新元素所在的位置开始,向上与父节点比较,如果不满足堆的性质(大顶堆中当前节点小于父节点或小顶堆中当前节点大于父节点),则交换当前节点和父节点的值,并继续向上调整,直到满足堆的性质或者到达根节点。

堆的删除

    // 用于存储堆中的元素
    private val heapArray = mutableListOf<Int>()

    // 删除堆顶元素方法
    fun delete(): Int? {
        // 如果堆为空,返回 null
        if (heapArray.isEmpty()) {
            return null
        }
        // 保存堆顶元素的值
        val rootValue = heapArray[0]
        // 将堆尾元素移到堆顶
        heapArray[0] = heapArray.last()
        // 删除堆尾元素
        heapArray.removeAt(heapArray.size - 1)
        var index = 0
        // 从堆顶开始往下调整堆,以满足堆的性质
        while (true) {
            // 计算左子节点索引
            val leftChildIndex = 2 * index + 1
            // 计算右子节点索引
            val rightChildIndex = 2 * index + 2
            var targetIndex = index
            // 如果左子节点存在
            if (leftChildIndex < heapArray.size) {
                // 根据堆的类型(大顶堆或小顶堆)确定目标索引
                targetIndex = if ((isMaxHeap && heapArray[leftChildIndex] > heapArray[targetIndex]) ||
                    (!isMaxHeap && heapArray[leftChildIndex] < heapArray[targetIndex])
                ) {
                    leftChildIndex
                } else {
                    targetIndex
                }
            }
            // 如果右子节点存在
            if (rightChildIndex < heapArray.size) {
                // 根据堆的类型(大顶堆或小顶堆)确定目标索引
                targetIndex = if ((isMaxHeap && heapArray[rightChildIndex] > heapArray[targetIndex]) ||
                    (!isMaxHeap && heapArray[rightChildIndex] < heapArray[targetIndex])
                ) {
                    rightChildIndex
                } else {
                    targetIndex
                }
            }
            // 如果目标索引没有变化,说明堆已满足性质,停止调整
            if (targetIndex == index) {
                break
            }
            // 交换当前节点和目标节点的值
            val temp = heapArray[index]
            heapArray[index] = heapArray[targetIndex]
            heapArray[targetIndex] = temp
            // 更新当前节点索引为目标节点索引,继续向下调整
            index = targetIndex
        }
        // 返回删除的堆顶元素值
        return rootValue
    }

堆的删除代码如上所示。可以看到堆删除元素的流程为:

  1. 如果堆为空,返回null
  2. 保存堆顶元素的值。
  3. 将堆的最后一个元素移到堆顶位置。
  4. 删除堆的最后一个元素。
  5. 从堆顶开始向下调整堆,首先计算左右子节点的索引。
  6. 根据堆的类型(大顶堆或小顶堆)确定当前节点与其左右子节点中的最小(或最大)值所在的索引作为目标索引。
  7. 如果目标索引与当前节点索引不同,则交换当前节点和目标节点的值,并继续向下调整,直到满足堆的性质或者到达叶子节点。
  8. 最后返回删除的堆顶元素值。