由于 Golang 的标准库中包含现成的heap包,所以网上大部分文章都是在写如何使用这个heap包,不过堆排作为大厂的一个常见面试考点,是不会满足于仅让你用heap包去实现的,至少要做到能够手搓一个简易版堆排。
以下是来自力扣官网的友情提醒:
「堆排」在很多大公司的面试中都很常见,不了解的同学建议参考《算法导论》或者大家的数据结构教材,一定要学会这个知识点哦!^_^

一个简易版的堆排,主要包含四个方法(方法名称随意):

  • 插入-Push:用于向堆中插入数据
  • 弹出-Shift:用于取出堆顶数据
  • 上浮-Up:一般不对外,仅在堆内部使用,主要用于在插入数据后重构堆
  • 下沉-Down:同上,一般仅在内部使用,主要用于在取出堆顶数据后重构堆

由于go标准包集成了小根堆,所以此处实现一个大根堆,如果想要小根堆,只需将上浮下沉中的判断条件取反即可;
废话不多说,直接上代码:

// 大根堆结构:用一个队列模拟二叉树结构
type MaxHeap struct {
    Que []int
}
// 插入数据:插到切片末尾,执行上浮操作
func (p *MaxHeap) Push(v int) {
    p.Que = append(p.Que, v)
    p.HeapUp(len(p.Que) - 1)
}
// 上浮逻辑:较简单,就是不断与parent节点值对比,大于parent,就交换双方的值
func (p *MaxHeap) HeapUp(idx int) {
    parentId := (idx - 1) / 2 //获取parent节点下标:由于0下标是堆顶,所以是 (idx - 1) / 2
    if parentId < 0 || p.Que[idx] <= p.Que[parentId] {
        return
    }
    p.Que[idx], p.Que[parentId] = p.Que[parentId], p.Que[idx]
    p.HeapUp(parentId)
}
// 取出堆顶数据:0下标即为堆顶;为了维持堆结构的正确性,取出后需与最后一个叶子节点交换数据并执行下沉操作
func (p *MaxHeap) Shift() int {
    n := len(p.Que)
    // 0下标代表堆顶:即堆中的最大或最小值
    v := p.Que[0]
    // 注意这里:需要将队列的最后一个下标与0下标调换值,再对0下标执行下沉操作;
    // 由于是模拟二叉树,所以队列最后一个下标,即代表最后一个叶子节点,之所以与这个节点调换,是因为:
    // 堆排是一棵【完全二叉树】,最后一个叶子节点被移除后,不影响完全二叉树结构,正好用来替换被移除的头节点;
    p.Que[0], p.Que[n-1] = p.Que[n-1], p.Que[0]
    p.Que = p.Que[:n-1]
    p.HeapDown(0)
    return v
}
// 下沉操作:不断与自己的左右孩子节点对比,并与其中的较大值作调换
func (p *MaxHeap) HeapDown(idx int) {
    n := len(p.Que)
    // 获取左右孩子下标:堆排中的队列下标其实就是模拟二叉树的层序遍历顺序;
    // Root节点是0下标,依次类推,左右孩子坐标不难推导出来;
    L, R := idx*2+1, idx*2+2
    if n == 0 || L >= n {
        return // 越界判断
    }
    peakId := idx // 峰值下标,即子堆的堆顶
    // 分别与左右孩子对比,选择其中的较大值作为子堆的堆顶
    if p.Que[peakId] < p.Que[L] {
        peakId = L
    }
    if R < n && p.Que[peakId] < p.Que[R] {
        peakId = R
    }
    if peakId != idx {
        // 如果左右孩子的值大于自身,则调换值,继续下沉
        p.Que[idx], p.Que[peakId] = p.Que[peakId], p.Que[idx]
        p.HeapDown(peakId)
    }
}

堆排实践:
这里选取力扣【215. 数组中的第K个最大元素】作为测试题目:

func findKthLargest(nums []int, k int) int {
    mHeap := MaxHeap{}
    for _, v := range nums {
        mHeap.Push(v) // 建立堆
    }
    for i := 0; i < k-1; i++ {
        mHeap.Shift() // 执行 k-1 次堆顶取出操作,则此时的堆顶即为答案
    }
    return mHeap.Que[0]
}
func main() {
    arr := []int{5, 2, 4, 1, 3, 6, 0}
    fmt.Println(findKthLargest(arr, 4))
}
// 输出:3

时间复杂度:O(nlogn),建堆的时间代价是 O(n),删除的总代价是 O(klogn),因为 k<n,故渐进时间复杂为 O(n+klogn)=O(nlogn)。
空间复杂度:O(logn),即递归使用栈空间的空间代价。


顺带吐槽一句:力扣的部分题解,纯粹是为了炫技写的,可读性真的不敢恭维。


后厂村村长
7 声望2 粉丝

Hello, Debug World