Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

最小堆 以及 优先级队列的 Golang 实现 #10

Open
JemmyH opened this issue Jul 6, 2021 · 3 comments
Open

最小堆 以及 优先级队列的 Golang 实现 #10

JemmyH opened this issue Jul 6, 2021 · 3 comments
Assignees
Labels
documentation Improvements or additions to documentation done Implementation by Go

Comments

@JemmyH
Copy link
Owner

JemmyH commented Jul 6, 2021

,是计算机科学中的一种特别的完全二叉树。若父节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作 根节点(root node),根节点本身没有 父节点(parent node)。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

优先级队列 是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现

@JemmyH
Copy link
Owner Author

JemmyH commented Jul 6, 2021

Golang实现一:根据原理简单实现:

package minheap

import (
	"container/heap"
	"fmt"
	"math"

	"github.com/pkg/errors"
)

/*
* @CreateTime: 2021/7/6 21:57
* @Author: hujiaming
* @Description: Golang实现最小堆
 */

var ErrMinHeapEmpty = errors.New("minHeap is empty")

const HeapHeadTag int64 = math.MinInt64

type MinHeap struct {
	elements []int64
}

// NewMinHeap 创建一个最小堆实例
func NewMinHeap() *MinHeap {
	return &MinHeap{elements: []int64{HeapHeadTag}}
}

/*
Add 将一个元素添加到最小堆中,并且添加后要使其满足最小堆的特性
首先将该元素插入到数组最后,然后对这最后一个元素进行 “上浮” 操作:
该元素与父元素进行大小比较,如果小于父元素,则和父元素交换位置,如此循环,直到 到达堆顶 或 子元素小于父元素。
*/
func (mh *MinHeap) Add(v int64) {
	// 1. 先将元素插在数组最后面
	mh.elements = append(mh.elements, v)
	// 2. 将最后一个元素上浮,使其符合最小堆的性质。其实是为 v 找位置
	i := len(mh.elements) - 1
	for ; mh.elements[i/2] > v; i /= 2 {
		mh.elements[i] = mh.elements[i/2]
	}
	mh.elements[i] = v
}

/*
PopMin 弹出堆中最小的元素
对最小堆而言,移除元素,只能移除堆顶(最小值)的元素。
首先,移除堆顶元素,然后将最后一个元素放在堆顶,之后对这第一个元素进行 “下沉” 操作:
将此元素与两个子节点元素比较,如果当前结点大于两个子节点,则与较小的子节点交换位置,如此循环,直到 到达叶子结点 或 小于较小子节点。
*/
func (mh *MinHeap) PopMin() (int64, error) {
	if mh.IsEmpty() {
		return 0, ErrMinHeapEmpty
	}
	res := mh.elements[1]
	last := mh.elements[len(mh.elements)-1]
	// idx 表示最后一个元素应该在的位置
	var idx int
	for idx = 1; idx*2 < len(mh.elements); {
		// 找出子节点中较小的元素的 index
		minChildIdx := idx * 2
		if minChildIdx < len(mh.elements)-1 && mh.elements[minChildIdx+1] < mh.elements[minChildIdx] {
			minChildIdx++
		}
		// 当前结点 大于 较小子节点,和这个较小子节点交换位置,继续循环
		if last > mh.elements[minChildIdx] {
			mh.elements[idx] = mh.elements[minChildIdx]
			idx = minChildIdx
			continue
		}
		break
	}
	mh.elements[idx] = last
	mh.elements = mh.elements[:len(mh.elements)-1]

	return res, nil
}

// PeekHead 只返回堆顶元素(最小值),不进行下沉操作
func (mh *MinHeap) PeekHead() (int64, error) {
	if mh.IsEmpty() {
		return 0, ErrMinHeapEmpty
	}
	return mh.elements[1], nil
}

// IsEmpty 最小堆是否是空的
func (mh *MinHeap) IsEmpty() bool {
	if len(mh.elements) == 0 || (len(mh.elements) == 1 && mh.elements[0] == HeapHeadTag) {
		return true
	}
	return false
}

// Length 返回最小堆中的元素个数
func (mh *MinHeap) Length() int {
	return len(mh.elements) - 1
}

// Print 打印代表最小堆的数组
func (mh *MinHeap) Print() {
	fmt.Println(mh.elements[1:])
}

Test 如下:

func TestMinHeap(t *testing.T) {
	mh := NewMinHeap()
	mh.Add(4)
	mh.Add(2)
	mh.Add(7)
	mh.Add(9)
	mh.Add(1)
	mh.Add(5)
	mh.Add(10)
	mh.Add(3)
	mh.Add(2)
	mh.Print()
	for !mh.IsEmpty() {
		fmt.Println(mh.PopMin())
	}
	assert.Equal(t, mh.Length(), 0)
}

// 输出
/*

[1 2 5 2 4 7 10 9 3]
1 <nil>
2 <nil>
2 <nil>
3 <nil>
4 <nil>
5 <nil>
7 <nil>
9 <nil>
10 <nil>

*/

@JemmyH
Copy link
Owner Author

JemmyH commented Jul 6, 2021

Golang 实现二:实现标准库 heap.Interface 接口

先看下标准库中的 Interface,位置在 container/heap/heap.go

// The Interface type describes the requirements
// for a type using the routines in this package.
// Any type that implements it may be used as a
// min-heap with the following invariants (established after
// Init has been called or if the data is empty or sorted):
//
//	!h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
//
// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface interface {
	sort.Interface
	Push(x interface{}) // add x as element Len()
	Pop() interface{}   // remove and return element Len() - 1.
}

// An implementation of Interface can be sorted by the routines in this package.
// The methods refer to elements of the underlying collection by integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int

	// Less reports whether the element with index i
	// must sort before the element with index j.
	//
	// If both Less(i, j) and Less(j, i) are false,
	// then the elements at index i and j are considered equal.
	// Sort may place equal elements in any order in the final result,
	// while Stable preserves the original input order of equal elements.
	//
	// Less must describe a transitive ordering:
	//  - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
	//  - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
	//
	// Note that floating-point comparison (the < operator on float32 or float64 values)
	// is not a transitive ordering when not-a-number (NaN) values are involved.
	// See Float64Slice.Less for a correct implementation for floating-point values.
	Less(i, j int) bool

	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

我们以此为基础,实现一个 优先级队列:

package priorityqueen


type Item struct {
	value    int64 // 实际值
	priority int64 // 优先级
	index    int   // 当前 item 在数组中的 index
}

// PriorityQueen 表示优先级队列
type PriorityQueen []*Item

func (mh2 PriorityQueen) Len() int {
	return len(mh2)
}

func (mh2 PriorityQueen) Less(i, j int) bool {
	return mh2[i].priority < mh2[j].priority
}

func (mh2 PriorityQueen) Swap(i, j int) {
	mh2[i], mh2[j] = mh2[j], mh2[i]
	mh2[i].index = i
	mh2[j].index = j
}

// Push 将 x 添加到数组最后
func (mh2 *PriorityQueen) Push(x interface{}) {
	l := len(*mh2)
	c := cap(*mh2)
	if l+1 > c {
		cmh2 := make([]*Item, l, c/2)
		copy(*mh2, cmh2)
		*mh2 = cmh2
	}
	*mh2 = (*mh2)[:l+1]
	item := (x).(*Item)
	item.index = l
	(*mh2)[l] = item
}

// Pop 返回数组最后一个元素
func (mh2 *PriorityQueen) Pop() interface{} {
	l := len(*mh2)
	c := cap(*mh2)
	if l < c/2 && c > 25 {
		cmh2 := make([]*Item, l, c/2)
		copy(cmh2, *mh2)
		*mh2 = cmh2
	}
	item := (*mh2)[l-1]
	item.index = -1 // for safety
	*mh2 = (*mh2)[:l-1]
	return item
}

// PopHead 弹出堆顶元素
func (mh2 *PriorityQueen) PopHead() *Item {
	if mh2.Len() == 0 {
		return nil
	}
	item := (*mh2)[0]

	heap.Remove(mh2, 0)

	return item
}

// PopWithPriority 弹出优先级小于 maxP 的堆顶元素,如果没有,返回 nil 和 当前堆顶和maxP的距离
func (mh2 *PriorityQueen) PopWithPriority(maxP int64) (*Item, int64) {
	if mh2.Len() == 0 {
		return nil, 0
	}
	item := (*mh2)[0]
	if item.priority > maxP {
		return nil, item.priority - maxP
	}

	heap.Remove(mh2, 0)

	return item, 0
}

// PeekHead 显示堆顶元素
func (mh2 *PriorityQueen) PeekHead() *Item {
	if mh2.Len() == 0 {
		return nil
	}
	heap.Init(mh2)
	item := (*mh2)[0]
	return item
}

测试一下:

func TestPriorityQueen(t *testing.T) {
	items := make([]*Item, 0)
	rand.Seed(time.Now().UnixNano())

	for i := 0; i < 10; i++ {
		v := rand.Int63n(100)
		items = append(items, &Item{
			value:    v,
			priority: v,
			index:    i,
		})
	}
	q := PriorityQueen(items)
	heap.Init(&q)

	fmt.Println(q.PeekHead())

	maxP := int64(50)
	for _, i := range q {
		if i.priority < maxP {
			fmt.Println(fmt.Sprintf("p: %d, v: %d", i.priority, i.value))
		}
	}

	fmt.Println("====")
	for i := 0; i < 10; i++ {
		item, _ := q.PopWithPriority(maxP)
		if item != nil {
			fmt.Println(item)
		}
	}

	fmt.Println("====")
	for {
		item := q.PopHead()
		if item == nil {
			break
		}
		fmt.Println(item)
	}
}

// 输出
/*

&{5 5 0}
p: 5, v: 5
p: 11, v: 11
p: 6, v: 6
p: 33, v: 33
====
&{5 5 -1}
&{6 6 -1}
&{11 11 -1}
&{33 33 -1}
&{50 50 -1}
====
&{52 52 -1}
&{73 73 -1}
&{85 85 -1}
&{97 97 -1}
&{99 99 -1}

*/

@JemmyH
Copy link
Owner Author

JemmyH commented Jul 6, 2021

Golang 标准库 heap.Interface 源码解析

整个包的实现非常简洁,加上注释以及空行,整个文件才只有120 行:

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package heap provides heap operations for any type that implements
// heap.Interface. A heap is a tree with the property that each node is the
// minimum-valued node in its subtree.
//
// The minimum element in the tree is the root, at index 0.
//
// A heap is a common way to implement a priority queue. To build a priority
// queue, implement the Heap interface with the (negative) priority as the
// ordering for the Less method, so Push adds items while Pop removes the
// highest-priority item from the queue. The Examples include such an
// implementation; the file example_pq_test.go has the complete source.
//
package heap

import "sort"

// The Interface type describes the requirements
// for a type using the routines in this package.
// Any type that implements it may be used as a
// min-heap with the following invariants (established after
// Init has been called or if the data is empty or sorted):
//
//	!h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
//
// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface interface {
	sort.Interface
	Push(x interface{}) // add x as element Len()
	Pop() interface{}   // remove and return element Len() - 1.
}

// Init establishes the heap invariants required by the other routines in this package.
// Init is idempotent with respect to the heap invariants
// and may be called whenever the heap invariants may have been invalidated.
// The complexity is O(n) where n = h.Len().
func Init(h Interface) {
	// heapify
	n := h.Len()
        // (n/2 - 1) 处的结点是最后一棵子树(没有孩子结点)的根节点
	for i := n/2 - 1; i >= 0; i-- {
		down(h, i, n)
	}
}

// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func Push(h Interface, x interface{}) {
	h.Push(x)
	up(h, h.Len()-1)
}

// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func Pop(h Interface) interface{} {
	n := h.Len() - 1
	h.Swap(0, n)
	down(h, 0, n)
	return h.Pop()
}

// Remove removes and returns the element at index i from the heap.
// The complexity is O(log n) where n = h.Len().
func Remove(h Interface, i int) interface{} {
	n := h.Len() - 1
	if n != i {
		h.Swap(i, n)
		if !down(h, i, n) {
			up(h, i)
		}
	}
	return h.Pop()
}

// Fix re-establishes the heap ordering after the element at index i has changed its value.
// Changing the value of the element at index i and then calling Fix is equivalent to,
// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
// The complexity is O(log n) where n = h.Len().
func Fix(h Interface, i int) {
	if !down(h, i, h.Len()) {
		up(h, i)
	}
}

func up(h Interface, j int) {
	for {
		i := (j - 1) / 2 // parent
		if i == j || !h.Less(j, i) {
			break
		}
		h.Swap(i, j)
		j = i
	}
}

func down(h Interface, i0, n int) bool {
	i := i0
	for {
		j1 := 2*i + 1
		if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
			break
		}
		j := j1 // left child
		if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
			j = j2 // = 2*i + 2  // right child
		}
		if !h.Less(j, i) {
			break
		}
		h.Swap(i, j)
		i = j
	}
	return i > i0
}

我们关注其中几个核心实现:

  • down(h Interface, idx, heapLen int) 下沉操作:
    首先,移除堆顶元素,然后将最后一个元素放在堆顶,之后对这第一个元素进行 “下沉” 操作:
    将此元素与两个子节点元素比较,如果当前结点大于两个子节点,则与较小的子节点交换位置,如此循环,直到 到达叶子结点 或 小于较小子节点。
    为什么元素 i 比它的两个子节点都小,就可以跳出循环,不再继续下去呢?这是由于,在 Init 函数中,**第一个开始 down 的元素是第 n/2 - 1 个,可以保证总是从最后一棵子树开始 down **,因此可以保证 Init->down 时,如果元素 i 比它的两个子节点都小,那么该元素对应的子树,就是最小堆。
    image

  • up(h Interface, curIdx int) 上浮操作:
    主要用在 Push 中,当我们向最小堆插入一个元素时,现将其插入到数组最后,之后进行上浮操作,此时的 curIdx 就是数组最后一个元素的 index,即 h.Len() - 1。当前元素与其父元素进行比较,如果当前元素小于父元素,则与父元素交换位置,如此往复,直到堆顶或者当前元素大于父元素。

@JemmyH JemmyH self-assigned this Jul 6, 2021
@JemmyH JemmyH added documentation Improvements or additions to documentation done Implementation by Go and removed done labels Jul 6, 2021
Repository owner locked and limited conversation to collaborators Jul 6, 2021
@JemmyH JemmyH added the done label Jul 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
documentation Improvements or additions to documentation done Implementation by Go
Projects
None yet
Development

No branches or pull requests

1 participant