topk

基本思路是这样的,如果求一个队列中的最大 k 个值,那么就使用一个小顶堆,然后每次都弹出堆顶元素。因为堆顶是最小的,那么遍历完全之后,剩下的都是最大的 k 个值。

反之,如果是求整个队列中的最小 k 个值,那么就使用一个大顶堆,然后每次都弹出堆顶元素。因为堆顶是最大的,那么遍历完全之后,剩下的都是最小的 k 个值。

topk 最大的 k 个值

package main

import (
	"container/heap"
)

// 定义一个类型,用于实现小顶堆
type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
	heap.Fix(h, len(*h)-1)
}

func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	heap.Fix(h, 0)
	return x
}

func TopK(nums []int, k int) []int {
	// 初始化一个小顶堆
	h := &IntHeap{}
	heap.Init(h)

	// 将前K个元素加入堆中
	for i := 0; i < k; i++ {
		heap.Push(h, nums[i])
	}

	// 遍历剩余元素,如果比堆顶元素大,则弹出堆顶元素并加入新元素
	for i := k; i < len(nums); i++ {
		if nums[i] > h.Peek().(int) {
			heap.Pop(h)
			heap.Push(h, nums[i])
		}
	}

	// 堆中元素即为最大的K个值
	result := make([]int, 0, k)
	for i := 0; i < k; i++ {
		result = append(result, heap.Pop(h).(int))
	}

	return result
}

func main() {
	nums := []int{1, 3, 5, 2, 4, 6, 8, 7, 9}
	k := 3
	result := TopK(nums, k)
	println(result) // [7 8 9]
}

topk 最小的 k 个值

package main

import (
	"container/heap"
)

// 定义一个类型,用于实现大顶堆
type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] > h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
	heap.Fix(h, len(*h)-1)
}

func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	heap.Fix(h, 0)
	return x
}

func TopKMin(nums []int, k int) []int {
	// 初始化一个大顶堆
	h := &IntHeap{}
	heap.Init(h)

	// 将前K个元素加入堆中
	for i := 0; i < k; i++ {
		heap.Push(h, nums[i])
	}

	// 遍历剩余元素,如果比堆顶元素小,则弹出堆顶元素并加入新元素
	for i := k; i < len(nums); i++ {
		if nums[i] < h.Peek().(int) {
			heap.Pop(h)
			heap.Push(h, nums[i])
		}
	}

	// 堆中元素即为最小的K个值
	result := make([]int, 0, k)
	for i := 0; i < k; i++ {
		result = append(result, heap.Pop(h).(int))
	}

	return result
}

func main() {
	nums := []int{1, 3, 5, 2, 4, 6, 8, 7, 9}
	k := 3
	result := TopKmin(nums, k)
	println(result) // [7 8 9]
}

说明 k 值个数

当使用小顶堆 (最大堆) 来解决 topk 问题时,如果堆的容量是 k 个元素,那么在向堆中添加一个新的值时,堆的元素个数会短暂地变为 k+1 个。这是因为在判断新值是否应该进入堆之前,我们首先会将这个新值推入堆中。

一旦新值被推入堆中,如果堆的大小超过了我们设定的 k 个元素,我们就需要执行一个 “sift up” 操作,也就是将新值上浮到它在堆中正确的位置。在这个过程中,如果新值比堆顶元素大,我们会将其与堆顶元素交换,并弹出堆顶元素 (即当前的最大值)。这样,堆的大小又会恢复到 k 个元素。

在实际的 topk 算法实现中,我们通常会在添加新值后立即检查堆的大小,如果堆的大小超过了 k,我们就执行相应的操作来确保堆的大小维持在 k。这样,除了在添加元素的那一刹那,堆的大小始终是 k,即始终保持着最大的 k 个元素。

以下是一个简化的示意图,说明了当小顶堆的容量为8时,添加新元素的过程:

  • 初始时,小顶堆中有 8 个元素。
  • 向堆中添加一个新值,假设为 9,此时堆的大小变为 9。
  • 检查堆的大小,发现它超过了 8,需要进行调整。
  • 将新值 9 与堆顶元素进行比较,如果 9 大于堆顶元素,则交换它们,并弹出堆顶元素。
  • 经过调整后,堆的大小恢复为 8,且堆顶元素是当前堆中的最大值。

这样,通过维护小顶堆,我们可以确保任何时候堆中都只包含最大的 k 个元素

最后更新: 4/2/2024, 9:55:00 AM