从青铜到王者:LeetCode-Go中的堆应用实战指南

从青铜到王者:LeetCode-Go中的堆应用实战指南

【免费下载链接】LeetCode-Go 该内容是使用Go语言编写的LeetCode题目的完整解决方案集合,实现了100%的测试覆盖率,并且运行时间优于所有题目100%的提交结果。 【免费下载链接】LeetCode-Go 项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

你是否还在为TopK问题绞尽脑汁?是否在滑动窗口中位数计算中迷失方向?本文将带你深入理解Go语言中堆(Heap)的强大应用,通过优先队列(Priority Queue)轻松解决TopK与中位数问题,让你的算法效率提升10倍!读完本文,你将掌握堆的核心原理、实战技巧以及LeetCode-Go项目中的最佳实践。

堆与优先队列:数据结构中的效率王者

堆(Heap)是一种特殊的完全二叉树,它具有以下特性:

  • 大顶堆:每个节点的值都大于或等于其子节点的值
  • 小顶堆:每个节点的值都小于或等于其子节点的值

优先队列(Priority Queue)是一种基于堆实现的数据结构,它能够保证每次取出的元素都是队列中优先级最高的。在Go语言中,我们可以通过container/heap包来实现自定义堆。

LeetCode-Go项目中大量使用了堆结构来解决各种复杂问题,例如0630.课程表III0692.前K个高频单词等。

TopK问题完美解决方案

TopK问题是面试中的常客,它要求在一组数据中找出前K个最大或最小的元素。使用堆来解决TopK问题,时间复杂度可以达到O(n log K),远优于O(n log n)的排序方法。

实战案例:前K个高频单词

0692.前K个高频单词问题中,我们需要找出出现频率最高的K个单词。如果频率相同,则按字母顺序排列。

解决方案步骤:

  1. 使用哈希表统计每个单词的出现频率
  2. 使用小顶堆来维护前K个高频单词
  3. 当堆的大小超过K时,弹出频率最低的元素
  4. 最后将堆中的元素逆序输出

核心代码实现:

type wordCount struct {
    word string
    cnt  int
}

type PQ []*wordCount

func (pq PQ) Len() int      { return len(pq) }
func (pq PQ) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq PQ) Less(i, j int) bool {
    if pq[i].cnt == pq[j].cnt {
        return pq[i].word > pq[j].word // 频率相同时按字母逆序排列
    }
    return pq[i].cnt < pq[j].cnt // 小顶堆
}

func topKFrequent(words []string, k int) []string {
    m := map[string]int{}
    for _, word := range words {
        m[word]++
    }
    pq := &PQ{}
    heap.Init(pq)
    for w, c := range m {
        heap.Push(pq, &wordCount{w, c})
        if pq.Len() > k {
            heap.Pop(pq) // 保持堆的大小为k
        }
    }
    res := make([]string, k)
    for i := k - 1; i >= 0; i-- {
        res[i] = heap.Pop(pq).(*wordCount).word
    }
    return res
}

滑动窗口中位数:双堆协作方案

计算滑动窗口的中位数是一个更具挑战性的问题。我们可以使用两个堆来解决:

  • 大顶堆存储窗口中较小的一半元素
  • 小顶堆存储窗口中较大的一半元素

通过维护两个堆的平衡,我们可以在O(1)时间内获取中位数,插入和删除操作的时间复杂度为O(log n)。

实战案例:滑动窗口中位数

0480.滑动窗口中位数问题中,我们需要计算一个滑动窗口中的中位数。

解决方案核心思想:

  1. 使用两个堆:大顶堆(maxH)和小顶堆(minH)
  2. 大顶堆存储窗口中较小的一半元素,小顶堆存储较大的一半元素
  3. 保持小顶堆的大小等于或比大顶堆大1
  4. 中位数为小顶堆的堆顶元素(当窗口大小为奇数时)或两个堆顶元素的平均值(当窗口大小为偶数时)

关键实现代码:

// 用两个堆记录窗口内的值
// 大顶堆里面的元素都比小顶堆里面的元素小
// 如果 k 是偶数,那么两个堆都有 k/2 个元素,中间值就是两个堆顶的元素
// 如果 k 是奇数,那么小顶堆比大顶堆多一个元素,中间值就是小顶堆的堆顶元素
func medianSlidingWindow1(nums []int, k int) []float64 {
    ans := []float64{}
    minH := MinHeapR{}
    maxH := MaxHeapR{}
    
    for i := range nums {
        // 根据当前元素大小插入到对应的堆中
        if minH.Len() == 0 || nums[i] >= minH.Top() {
            minH.Push(nums[i])
        } else {
            maxH.Push(nums[i])
        }
        
        // 移除窗口外的元素
        if i >= k {
            if nums[i-k] >= minH.Top() {
                minH.Remove(nums[i-k])
            } else {
                maxH.Remove(nums[i-k])
            }
        }
        
        // 平衡两个堆的大小
        if minH.Len() > maxH.Len()+1 {
            maxH.Push(minH.Pop())
        } else if minH.Len() < maxH.Len() {
            minH.Push(maxH.Pop())
        }
        
        // 计算中位数
        if minH.Len()+maxH.Len() == k {
            if k%2 == 0 {
                ans = append(ans, float64(minH.Top()+maxH.Top())/2.0)
            } else {
                ans = append(ans, float64(minH.Top()))
            }
        }
    }
    return ans
}

堆的更多应用场景

堆不仅可以解决TopK和中位数问题,还有许多其他应用场景:

1. 合并K个排序链表

在合并K个排序链表时,可以使用小顶堆来每次选择最小的节点,时间复杂度为O(N log K),其中N是总节点数,K是链表数。

2. 任务调度问题

0630.课程表III问题中,我们需要根据课程的截止时间来安排学习计划,使用大顶堆来选择耗时最长的课程进行替换,以最大化可以完成的课程数量。

// 使用大顶堆来存储已选择的课程时长
func scheduleCourse(courses [][]int) int {
    sort.Slice(courses, func(i, j int) bool {
        return courses[i][1] < courses[j][1]
    })
    
    maxHeap := &IntHeap{}
    heap.Init(maxHeap)
    time := 0
    
    for _, c := range courses {
        if time+c[0] <= c[1] {
            time += c[0]
            heap.Push(maxHeap, c[0])
        } else if maxHeap.Len() > 0 && c[0] < (*maxHeap)[0] {
            // 如果当前课程时长小于堆中最长课程,进行替换
            time -= heap.Pop(maxHeap).(int) - c[0]
            heap.Push(maxHeap, c[0])
        }
    }
    
    return maxHeap.Len()
}

3. 数据流中的中位数

与滑动窗口中位数类似,我们可以使用两个堆来实时计算数据流中的中位数,实现O(1)时间复杂度的中位数获取和O(log n)时间复杂度的插入操作。

总结与提升

堆作为一种高效的数据结构,在解决TopK、中位数、任务调度等问题时表现出色。通过本文的学习,你已经掌握了堆的核心原理和实战应用,特别是在LeetCode-Go项目中的最佳实践。

为了进一步提升你的堆应用能力,建议深入研究以下问题:

记住,熟练掌握堆的应用,将为你的算法能力带来质的飞跃,让你在面试和实际工作中应对各种复杂问题时游刃有余!

如果你觉得本文对你有帮助,请点赞、收藏并关注LeetCode-Go项目,我们将持续推出更多优质的算法解析文章。下期预告:"深度剖析动态规划在LeetCode中的应用",敬请期待!

【免费下载链接】LeetCode-Go 该内容是使用Go语言编写的LeetCode题目的完整解决方案集合,实现了100%的测试覆盖率,并且运行时间优于所有题目100%的提交结果。 【免费下载链接】LeetCode-Go 项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值