1. Heap usage
In the standard library container of the go language, three data types are implemented: heap, list, ring, list, and list have been written in the previous article. What we want to write is the source code analysis of heap (heap).
First, learn how to use heap. The first step is of course importing the package. The code is as follows:
package main import ( "container/heap" "fmt" )
The data structure used by this heap is the smallest binary tree, that is, the root node is smaller than all values of the left subtree and the right subtree. The source code only implements an interface, and its definition is as follows:
type Interface interface { Push(x interface{}) // add x as element Len() Pop() interface{} // remove and return element Len() - 1. }
From this interface, we can see that it inherits the interface, so what is the definition? The source code is as follows:
type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }
In other words, if we want to use the heap provided to us by the go standard library, we must implement the methods defined by these interfaces by ourselves. The methods we need to implement are as follows:
- Len() int
- Less(i, j int) bool
- Swap(i, j int)
- Push(x interface{})
- Pop() interface{}
Only by implementing these five methods can we use the heap provided to us by the go standard library. The following simple example is to define an IntHeap type and implement the above five methods.
type IntHeap []int // Define a type func (h IntHeap) Len() int { return len(h) } //Bind len method, return lengthfunc (h IntHeap) Less(i, j int) bool { // Bind less method return h[i] < h[j] // If h[i]<h[j] generates a small root heap, if h[i]>h[j] generates a large root heap} func (h IntHeap) Swap(i, j int) { // Bind swap method and swap two element positions h[i], h[j] = h[j], h[i] } func (h *IntHeap) Pop() interface{} { // Bind the pop method, take out an element from the end and return old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func (h *IntHeap) Push(x interface{}) { // Bind push method and insert new element *h = append(*h, x.(int)) }
After implementing these five methods for IntHeap, we can use heap. Here are the specific usage methods:
func main() { h := &IntHeap{2, 1, 5, 6, 4, 3, 7, 9, 8, 0} // Create slice (h) // Initialize heap (*h) ((h)) // Call pop (h, 6) // Call push (*h) for len(*h) > 0 { ("%d ", (h)) } }
Output result:
[0 1 3 6 2 5 7 9 8 4]
0
[1 2 3 6 4 5 7 9 8 6]
1 2 3 4 5 6 6 7 8 9
The above is the use of heap.
2. Methods provided by heap
There are not many methods provided by heap, as follows:
h := &IntHeap{3, 8, 6} // Create raw data of type IntHeapfunc Init(h Interface) // Initialize the heap and generate a small root heap (or large root heap)func Push(h Interface, x interface{}) // Insert content into the pilefunc Pop(h Interface) interface{} // pop content from the top of the pilefunc Remove(h Interface, i int) interface{} // Delete data from the specified location and return the deleted datafunc Fix(h Interface, i int) // fromiAfter the location data has changed,Rebalancing the heap,This method is used by the priority queue
3. Heap source code analysis
The internal implementation of heap is to use the minimum (maximum) heap, index sorting starts from the root node, and then the left subtree and right subtree. The down and up implemented internally represent the minimum (maximum) heap down and the minimum (maximum) heap up respectively for an element in the heap.
When an element is inserted into the heap, the element is inserted into the last node of the rightmost subtree, and then call up to ensure the minimum (maximum) heap.
When you want to push an element from the heap, first exchange the last node of the right subtree, then pop up the last node, and then call down on the root to ensure the minimum (maximum) heap.
OK, start analyzing the source code:
First, before using the heap, it must be called its Init method, initialized the heap, and generated a small (large root) heap. The source code of the Init method is as follows:
// A heap must be initialized before any of the heap operations // can be used. Init is idempotent with respect to the heap invariants // and may be called whenever the heap invariants may have been invalidated. // Its complexity is O(n) where n = (). // func Init(h Interface) { // heapify n := () // Get the length of the data for i := n/2 - 1; i >= 0; i-- { // Starting from half of the length and until the 0th data, the down method is called in each position. The function implemented by the down method is to ensure that the heap is formed from the down position. down(h, i, n) } }
Next, let’s look at the source code of down:
func down(h Interface, i0, n int) bool { i := i0 // The intermediate variable is stored for the first time. The node position that needs to be formed in the downward direction is to ensure the node position that needs to be formed in the heap. for { // A dead loop j1 := 2*i + 1 // The left child of the i node if j1 >= n || j1 < 0 { // j1 < 0 after int overflow // Ensure that the left child does not cross the line break } j := j1 // left child // The intermediate variable j is assigned as the left child child first, and then j will be assigned as the position of the smallest (large) child among the left child child if j2 := j1 + 1; j2 < n && !(j1, j2) { j = j2 // = 2*i + 2 // right child } // After this, j is assigned as the position of the smallest (large) child of the two children (minimum or maximum is determined by the definition in Less) if !(j, i) { break } // If j is greater than (less than) i, the loop is terminated (i, j) // Otherwise, exchange the values of i and j positions i = j // Let i=j, continue the loop, and ensure that the sub-number at position j is the heap structure } return i > i0 }
This is the core code for building the heap. In fact, down cannot completely ensure that each node from a certain node can maintain the characteristics of the heap. It can only ensure that if the value of a certain node does not meet the properties of the heap, the value is exchanged with its children until the value is placed in a suitable position to ensure that the value and its two childs meet the properties of the heap.
However, if you call down through an Init loop, it will ensure that all nodes maintain the heap characteristics after initialization, because the loop startsi := n/2 - 1
The value position of the node will be taken to the largest node with a child node, and the node has only two children at most, and its child node is a leaf node. If each node from this node can ensure the down characteristics, the entire list will conform to the nature of the heap.
Similarly, if there is down, there is up. Up guarantees that if a node does not guarantee the nature of the heap upward, it will be exchanged with the parent node until the node is placed in a specific position to ensure the nature of the heap. The code is as follows:
func up(h Interface, j int) { for { // A dead loop i := (j - 1) / 2 // parent // parent of j node if i == j || !(j, i) { // If the boundary is exceeded, or the heap condition is met, the loop ends break } (i, j) // Otherwise, exchange the node with the parent node j = i // Continue to check the parent node until the root node } }
The above two methods are the most core methods, and all exposed methods are nothing more than encapsulation of these two methods. Let's take a look at the source codes of these methods:
func Push(h Interface, x interface{}) { (x) // Put the newly inserted node to the end up(h, ()-1) // Ensure that the newly inserted node can ensure the heap structure on the Internet} func Pop(h Interface) interface{} { n := () - 1 // Exchange the last node with the first node, then re-ensure the heap structure from the root node, and finally throw out the last node data and return it (0, n) down(h, 0, n) return () } func Remove(h Interface, i int) interface{} { n := () - 1 poponlyremoveSpecial circumstances,removeYesiThe node of the location is exchanged with the last node,After that, guaranteediThe nodes are both down and up to ensure the heap structure,Finally throw out the data of the last node and return it if n != i { (i, n) down(h, i, n) up(h, i) } return () } func Fix(h Interface, i int) { if !down(h, i, ()) { // After the value of the i node changes, it is necessary to ensure the rebalancing of the heap. First call down to ensure the heap structure below the node. If there is position exchange, it is necessary to ensure the heap structure upwards of the node, otherwise there is no need to ensure the heap structure upwards, a small optimization up(h, i) } }
The above is all the source codes of heap in go, so I won’t post the full version of the source code. The above understanding is all based on personal understanding. If there is any inappropriateness, I hope to criticize and correct it.
4. Use heap to implement priority queues
Since heap is used, then use heap to implement a priority queue. This function is a very good function.
The source code is as follows:
package main import ( "container/heap" "fmt" ) type Item struct { value string // The data in the priority queue can be of any type, using string here priority int // Priority of nodes in priority queue index int // index is the location of the node in the heap} // Priority queues need to implement heap's interfacetype PriorityQueue []*Item // Bind Len methodfunc (pq PriorityQueue) Len() int { return len(pq) } //Bind the Less method, the less than sign is used here, and the small root heap is generatedfunc (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority < pq[j].priority } // Bind swap methodfunc (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].index, pq[j].index = i, j } // Bind the put method and set index to -1 to identify that the data has already had a priority queuefunc (pq *PriorityQueue) Pop() interface{} { old := *pq n := len(old) item := old[n-1] *pq = old[0 : n-1] = -1 return item } // Bind push methodfunc (pq *PriorityQueue) Push(x interface{}) { n := len(*pq) item := x.(*Item) = n *pq = append(*pq, item) } // Update the position of the item with priority and value in the priority queuefunc (pq *PriorityQueue) update(item *Item, value string, priority int) { = value = priority (pq, ) } func main() { // Create nodes and design their priorities items := map[string]int{"Ermao": 5, "Zhang San": 3, "Dog egg": 9} i := 0 pq := make(PriorityQueue, len(items)) // Create a priority queue and initialize it for k, v := range items { // Put nodes in priority queue pq[i] = &Item{ value: k, priority: v, index: i} i++ } (&pq) // Initialize the heap item := &Item{ // Create an item value: "Li Si", priority: 1, } (&pq, item) // Enter the priority queue (item, , 6) // Update item priority for len(pq) > 0 { item := (&pq).(*Item) ("%.2d:%s index:%.2d\n", , , ) } }
Output result:
03: Zhang San index:-01
05: Ermao index:-01
06: Li Si index:-01
09:Doodan index:-01
This is the end of this article about Go using heap to implement priority queues. For more related Go language heap content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!