Go基础包之"heap"
基本概念
堆分为大顶堆和小顶堆。大顶堆的堆顶元素最大,子节点元素小于父节点,但是子节点直接没有必然的大小关系。堆的结构是一颗完全二叉树
一般使用数组来存储完全二叉树,也就是用数组来存储堆结构。节点之间的数组索引关系如下图所示:
Go语言中的container/heap包提供了堆这个数据结构,我们不需要从头开始实现堆这个数据结构。
其核心接口为:
heap.Interface
type Interface interface {
sort.Interface
Push(x interface{}) // 将x添加至元素Len()的位置
Pop() interface{} // 移除并且返回元素Len()-1
}
实现该接口的任何类型都可以用作具有以下不变量的最小堆(在调用Init()后或数据为空或排序时建立)
注意,接口中的Pop()和Push()是由实现该接口的对象调用的。要实现符合堆数据结构特征的添加和删除内容,需要使用heap.Pop()和heap.Push()
sort.Interface
type Interface interface {
Len() int // Len是集合中的元素个数。
Less(i, j int) bool
Swap(i, j int) // Swap交换索引i和索引j的元素
}
任意实现了该接口的结构体,均可以被sort包下的各种方法进行排序。这些方法同通过整数索引来引用基础集合的元素。
实现了这五个方法的数据类型才能使用go标准库给我们提供的heap
初始化
Init()
函数建立此包中其他例程所需的堆不变量。
Init()
对于堆不变量是幂等的(可被多次调用),并且可以在堆不变量失效时调用。
Init()
的时间复杂度是O(n)
,n
为堆中的元素个数。
更多详细内容:
347. 前 K 个高频元素
这个题目其实不是很难,其关键点有两个:1)如何计算元素的频率;2)如何选取前K个高频元素
- 第一点比较简单,一个map数据结构就能够统计出元素出现的频率
- 第二点,一般的初始想法都是将map中的key值根据value进行排序,然后取前K个
题目需求是前K个高频元素,如果维护一个全排列的数据是需要耗费大量时间的,特别是在大规模数据下。因此,我们需要找一个数据结构来维护前K个key值数据结构,这里可以使用堆的特性。小顶堆:堆顶元素最小。维护一个大小为K的小顶堆,我们只需要比较新的元素和堆顶的大小关系,大于执行Push操作,小于则无操作。这样堆里的元素就是前K个高频元素。
package main
import "container/heap"
type HeapInts [][2]int
func (h *HeapInts) Len() int {
return len(*h)
}
func (h *HeapInts) Less(i, j int) bool {
return (*h)[i][1] < (*h)[j][1]
}
func (h *HeapInts) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}
func (h *HeapInts) Push(x interface{}) {
*h = append(*h, x.([2]int))
}
func (h *HeapInts) Pop() interface{} {
temp := (*h)[len(*h)-1]
*h = (*h)[:len(*h)-1]
return temp
}
func topKFrequent(nums []int, k int) []int {
map1 := make(map[int]int, 0)
headInt := &HeapInts{}
heap.Init(headInt)
res := make([]int, k)
for i := 0; i < len(nums); i++ {
map1[nums[i]]++
}
for key, v := range map1 {
heap.Push(headInt, [2]int{key, v})
if len(*headInt) > k {
heap.Pop(headInt)
}
}
for i := 0; i < k; i++ {
res[k-i-1] = heap.Pop(headInt).([2]int)[0]
}
return res
}
// 大顶堆和小顶堆:擅长从大规模数据中找前k个高频或者低频元素
// 堆是从堆顶移除元素,所以,求前k个高频元素使用小顶堆
239. 滑动窗口最大值
题目链接:239. 滑动窗口最大值
这个题目使用到的数据结构为单调队列:维护元素单调递减的队列(队头--->队尾)
题目的难点在于如何求一个区间里的最大值。最开始的想法是直接暴力搜索,但是时间复杂度为O(n*k)。
使用单调队列的优势在于,可以维护窗口里的元素在队列中,窗口每移动一次,队列也一进一出,然后给出每次移动之后窗口里的最大值是什么。
基于上述描述,我们是需要对队列里的元素进行排序的,所以我们需要考虑队列元素变化的两个时刻,应该如何操作队列里的元素。窗口进行移动时:
- 窗口弹出的元素,只需要判断是否与队头元素相同,相同就出队;不相同不需要操作
- 窗口新进的元素,大于队尾元素,则弹出队尾元素,直到队尾元素大于等于窗口新进的元素
图片参考:代码随想录
还有另外一种想法,使用上面提到的堆数据结构。使用大顶堆,窗口每次移动之后,都弹出堆顶元素。但是有一个问题,需要维护堆内元素仅为窗口内元素,而堆只能从堆顶弹出,所以使用起来较为不方便。这里不考虑使用。
func maxSlidingWindow(nums []int, k int) []int {
queue := make([]int, 0)
res := []int{}
for i := 0; i < k; i++ {
if len(queue) == 0 {
queue = append(queue, nums[i])
} else {
for len(queue) != 0 {
if nums[i] > queue[len(queue)-1] {
queue = queue[:len(queue)-1]
} else {
break
}
}
queue = append(queue, nums[i])
}
}
res = append(res, queue[0])
for i := 1; i < len(nums)-k+1; i++ {
if nums[i-1] == queue[0] {
queue = queue[1:]
}
// 新进窗口的元素,如果比单调队列中尾部的元素大,则弹出队列尾部元素,直到小于队尾元素
for len(queue) != 0 {
if nums[i+k-1] > queue[len(queue)-1] {
queue = queue[:len(queue)-1]
} else {
break
}
}
queue = append(queue, nums[i+k-1])
res = append(res, queue[0])
}
return res
}
栈和队列总结
通过括号匹配问题、字符串去重问题、逆波兰表达式问题掌握栈在做题当中的使用技巧。
通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列
主要需要了解在Go语言中,是如何实现常见的数据结构以及使用方法。在滑动窗口最大值问题中学会了使用单调队列,优先级队列的实现在这一块还没有掌握,其底层实现是利用堆的数据结构,后续需要对其进行实现,掌握其底层原理。
优先队列实现
import (
"container/heap"
"fmt"
"testing"
)
// Item 是我们在优先队列中管理的东西
type Item struct {
value string // The value of the item; arbitrary.
priority int // 队列中项目的优先级
// The index is needed by update and is maintained by the heap.Interface methods.
index int //在堆中的索引
}
// 优先级队列
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
// 我们希望 Pop 给我们最高而不是最低的优先级,所以我们使用比这里更大的优先级。
return pq[i].priority > pq[j].priority
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // 避免内存泄漏
item.index = -1 // 为了安全
*pq = old[0 : n-1]
return item
}
// update 修改队列中项目的优先级和值。
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
item.value = value
item.priority = priority
heap.Fix(pq, item.index)
}
// This example creates a PriorityQueue with some items, adds and manipulates an item,
// and then removes the items in priority order.
func Test_priorityQueue(t *testing.T) {
// Some items and their priorities.
items := map[string]int{
"banana": 3, "apple": 2, "pear": 4,
}
// Create a priority queue, put the items in it, and
// establish the priority queue (heap) invariants.
pq := make(PriorityQueue, len(items))
i := 0
for value, priority := range items {
pq[i] = &Item{
value: value,
priority: priority,
index: i,
}
i++
}
heap.Init(&pq)
// 插入一个新项目,然后修改其优先级.
item := &Item{
value: "orange",
priority: 1,
}
heap.Push(&pq, item)
pq.update(item, item.value, 5)
// Take the items out; they arrive in decreasing priority order.
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("%.2d:%s ", item.priority, item.value)
}
// Output:
// 05:orange 04:pear 03:banana 02:apple
}