Go数据结构之HeapMap实现指定Key删除堆
作者:Goland猫
堆(Heap)
堆(Heap),又称为优先队列(Priority Queue)。尽管名为优先队列,但堆并不是队列。在队列中,我们可以进行的操作是向队列中添加元素和按照元素进入队列的顺序取出元素。而在堆中,我们不是按照元素进入队列的先后顺序,而是按照元素的优先级取出元素。
问题背景
在Linux内核中,调度器根据各个进程的优先级来进行程序的执行调度。在操作系统运行时,通常会有很多个不同的进程,各自优先级也不相同。调度器的作用是让优先级高的进程得到优先执行,而优先级较低的则需要等待。堆是一种适用于实现这种调度器的数据结构。需要提一下,现在Linux内核的调度器使用的是基于红黑树的CFS(Completely Fair Scheduler)。
二叉堆的概念
我们常用的二叉堆是一颗任意节点的优先级不小于其子节点的完全二叉树。
完全二叉树的定义如下:
- 若设二叉树的高度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点,这就是完全二叉树。
比如下图就是一颗完全二叉树:
10 / \ 15 30 / \ / \ 40 50 100 40
现在假设保存的数值越小的节点的优先级越高,那么上图就是一个堆。我们将任意节点不大于其子节点的堆叫做最小堆或小根堆,将任意节点不小于其子节点的堆叫做最大堆或大根堆。因此,上图就是一个小根堆。
优先级队列的实现
通过使用Go语言中的container/heap
包,我们可以轻松地实现一个优先级队列。这个队列可以用于解决许多问题,如任务调度、事件处理等。通过设置每个项的优先级,我们可以确保在处理队列时按照指定的顺序进行操作。
Item
通过定义Item结构体来表示优先级队列中的项。每个项具有值(value)和优先级(priority)。index表示项在优先级队列中的索引。
// Item represents an item in the priority queue. type Item struct { value int // 项的值。 priority int // 项的优先级。 index int // 项在队列中的索引。 }
PriorityQueue
PriorityQueue是一个切片类型,实现了heap.Interface接口。它提供了用于操作优先级队列的方法,如插入、删除和修改。
Len方法返回优先级队列的长度。
Less方法比较两个项的优先级。
Swap方法交换两个项在优先级队列中的位置。
Push方法向优先级队列中添加一个项。
Pop方法移除并返回优先级队列中的最小项。
Update方法用于修改项的优先级并更新其在优先级队列中的位置。
// PriorityQueue 实现了 heap.Interface 接口。 type PriorityQueue []*Item // Len 返回优先级队列的长度。 func (pq PriorityQueue) Len() int { return len(pq) } // Less 比较优先级队列中的两个项。 func (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority < pq[j].priority } // Swap 交换优先级队列中的两个项。 func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].index = i pq[j].index = j } // Push 向优先级队列中添加一个项。 func (pq *PriorityQueue) Push(x interface{}) { item := x.(*Item) item.index = len(*pq) *pq = append(*pq, item) } // Pop 移除并返回优先级队列中的最小项。 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, priority int) { item.value = value item.priority = priority heap.Fix(pq, item.index) }
改进
但是我们经常有一种场景,需要堆的快速求最值的性质,又需要能够支持快速的访问元素,特别是删除元素。 如果我们要查找堆中的某个元素,需要遍历一遍。非常麻烦。
比如延迟任务的场景,我们可以使用堆对任务的到期时间戳进行排序,从而实现到期任务自动执行,但是它没办法支持删除一个延迟任务的需求。
HeapMap
一种能够快速随机访问元素的数据结构是哈希表。使用哈希表实现的map可以在O(1)的时间复杂度下进行随机访问。
另外,堆结构可以在O(log(n))
的时间复杂度下删除元素,前提是知道要删除的元素的下标。因此,我们可以将这两个数据结构结合起来使用。使用哈希表记录堆中每个元素的下标,同时使用堆来获取最值元素。
// PriorityQueue facilitates queue of Items, providing Push, Pop, and // PopByKey convenience methods. The ordering (priority) is an int64 value // with the smallest value is the highest priority. PriorityQueue maintains both // an internal slice for the queue as well as a map of the same items with their // keys as the index. This enables users to find specific items by key. The map // must be kept in sync with the data slice. // See https://golang.org/pkg/container/heap/#example__priorityQueue type PriorityQueue struct { // data is the internal structure that holds the queue, and is operated on by // heap functions data queue // dataMap represents all the items in the queue, with unique indexes, used // for finding specific items. dataMap is kept in sync with the data slice dataMap map[string]*Item // lock is a read/write mutex, and used to facilitate read/write locks on the // data and dataMap fields lock sync.RWMutex } // queue is the internal data structure used to satisfy heap.Interface. This // prevents users from calling Pop and Push heap methods directly type queue []*Item // Item is something managed in the priority queue type Item struct { // Key is a unique string used to identify items in the internal data map Key string // Value is an unspecified type that implementations can use to store // information Value interface{} // Priority determines ordering in the queue, with the lowest value being the // highest priority Priority int64 // index is an internal value used by the heap package, and should not be // modified by any consumer of the priority queue index int }
在PriorityQueue
中定义一个dataMap
dataMap是一个用于存储队列中的项的映射表,它的好处是可以根据项的键快速地查找到对应的项。 在PriorityQueue中,有一个数据切片data,用于存储队列中的项,并且用一个索引值index来表示项在切片中的位置。
dataMap则以项的键作为索引,将项的指针映射到该键上。
使用dataMap的好处是可以快速地根据键找到对应的项,而不需要遍历整个切片。这对于需要频繁查找和修改项的场景非常重要,可以提高代码的效率。
如果没有dataMap,想要根据键找到对应的项则需要遍历整个切片进行查找,时间复杂度将为O(n)。而使用dataMap可以将查找的时间复杂度降低到O(1),提高代码的性能。
另外,需要注意的是dataMap必须与data切片保持同步,即在对切片进行修改时,需要同时更新dataMap,保持两者的一致性。否则,在使用dataMap时会出现不一致的情况,导致错误的结果。因此,在使用PriorityQueue时,需要确保维护dataMap和data切片的一致性。
push
在Push时需要保证Key值唯一
func (pq *PriorityQueue) Push(i *Item) error { if i == nil || i.Key == "" { return errors.New("error adding item: Item Key is required") } pq.lock.Lock() defer pq.lock.Unlock() if _, ok := pq.dataMap[i.Key]; ok { return ErrDuplicateItem } // Copy the item value(s) so that modifications to the source item does not // affect the item on the queue clone, err := copystructure.Copy(i) if err != nil { return err } pq.dataMap[i.Key] = clone.(*Item) heap.Push(&pq.data, clone) return nil }
pop
PopByKey方法可以根据Key查找并移除对应的元素
// PopByKey searches the queue for an item with the given key and removes it // from the queue if found. Returns nil if not found. This method must fix the // queue after removing any key. func (pq *PriorityQueue) PopByKey(key string) (*Item, error) { pq.lock.Lock() defer pq.lock.Unlock() item, ok := pq.dataMap[key] if !ok { return nil, nil } // Remove the item the heap and delete it from the dataMap itemRaw := heap.Remove(&pq.data, item.index) delete(pq.dataMap, key) if itemRaw != nil { if i, ok := itemRaw.(*Item); ok { return i, nil } } return nil, nil }
测试用例
func main() { // 测试Push方法 pq := &PriorityQueue{ data: queue{}, dataMap: make(map[string]*Item), } item := &Item{ Key: "key1", Value: "value1", Priority: 1, index: 0, } err := pq.Push(item) if err != nil { fmt.Println("Push error:", err) } else { fmt.Println("Push success") } // 测试Pop方法 poppedItem, err := pq.Pop() if err != nil { fmt.Println("Pop error:", err) } else { fmt.Println("Popped item key:", poppedItem.Key) } // 测试PopByKey方法 item2 := &Item{ Key: "key2", Value: "value2", Priority: 2, index: 1, } pq.Push(item2) poppedItemByKey, err := pq.PopByKey("key2") if err != nil { fmt.Println("PopByKey error:", err) } else if poppedItemByKey == nil { fmt.Println("Item not found") } else { fmt.Println("Popped item by key:", poppedItemByKey.Key) } // 测试Len方法 item3 := &Item{ Key: "key3", Value: "value3", Priority: 3, index: 2, } pq.Push(item3) length := pq.Len() fmt.Println("Queue length:", length) }
以上就是Go数据结构之HeapMap实现指定Key删除堆的详细内容,更多关于Go HeapMap指定Key删除堆的资料请关注脚本之家其它相关文章!