【Go】内置容器container

Go标准库的container包提供了3个包,分别是heaplistring,分别实现了堆,链表和环形链表。

heap

Go语言中堆的定义是一个接口。

type Interface interface {
  sort.Interface
  Push(x any) // add x as element Len()
  Pop() any   // remove and return element Len() - 1.
}

根据注释我们发现Push的实现是要求在末尾插入,Pop的实现要求删除末尾元素。sort.Interface是另一个接口,定义了一个可排序的容器。

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)
}

接口中Less方法的实现方式决定了排序方式,是增序还是降序。sort包提供了3种默认实现:IntSliceFloat64SliceStringSlice。它们默认都是增序的,如果需要降序排序,可以使用Reverse函数快速得到一个降序数组。

例子:

package main

import (
  "fmt"
  "sort"
)

func main() {
  s1 := sort.IntSlice([]int{3, 1, 5, 2, 4})
  sort.Sort(s1)
  fmt.Println(s1)

  s2 := sort.Reverse(s1)
  sort.Sort(s2)
  fmt.Println(s2)
}
// 输出
// [1 2 3 4 5]
// &{[5 4 3 2 1]}

因此我们可以借用InsSlice快速实现一个int堆。

package main

import (
  "container/heap"
  "fmt"
  "sort"
)

type IntHp struct { // 堆实现
  sort.IntSlice
}

func (hp *IntHp) Push(v any) {
  hp.IntSlice = append(hp.IntSlice, v.(int))
}

func (hp *IntHp) Pop() (v any) {
  v = hp.IntSlice[hp.Len()-1]
  hp.IntSlice = hp.IntSlice[:hp.Len()-1]
  return
}

func main() {
  var hp IntHp
  heap.Push(&hp, 5)
  fmt.Println(hp)
  heap.Push(&hp, 3)
  fmt.Println(hp)
  heap.Push(&hp, 1)
  fmt.Println(hp)
  heap.Push(&hp, 10)
  fmt.Println(hp)
  fmt.Println(heap.Pop(&hp))
  fmt.Println(hp)
  fmt.Println(heap.Pop(&hp))
  fmt.Println(hp)
}
/* 输出
{[5]}
{[3 5]}
{[1 5 3]}
{[1 5 3 10]}
1
{[3 5 10]}
3
{[5 10]}
*/

这样就实现了一个小顶堆。堆内的数据并不是严格排序的,只保证了最小的数在第一个,因此必须通过heap.Pushheap.Pop来操作堆,不能直接调用堆上的PushPop方法,这是一个比较容易出错的地方,特别是习惯了面对对象那一套的人。

如果要实现大顶堆,可以自己实现sort.Interface接口的Less方法,改成升序,这样就得到了一个大顶堆。

除了heap.Pop删除堆顶,也可以使用heap.Remove删除任意下标的元素。不管是哪种删除方式,其原理都是将要删除的元素交换到数组末尾,然后调用接口的Pop方法删除末尾元素,因为删除数组末尾元素可以避免数组元素的移动。对数组来说,无论是插入还是删除,尾插/删总是最高效的。

Tips:这里补充一个小知识点,如果要实现高效的删除数组中的某个元素算法,可以将目标元素交换到末尾,然后删除末尾元素。

heap.Remove(hp, 1)

如果将非空数组转换成堆,堆内数据无序,我们有两种方式可以修正堆。第一种方式是使用heap.Init,第二种方式是使用heap.Fix

func main() {
  hp := IntHp{
    []int{3, 2, 1},
  }
  fmt.Println(hp)
  heap.Init(&hp)
  fmt.Println(hp)
  hp.IntSlice[0], hp.IntSlice[1] = hp.IntSlice[1], hp.IntSlice[0]
  fmt.Println(hp)
  heap.Fix(&hp, 0)
  fmt.Println(hp)
}
/* 输出
{[3 2 1]}
{[1 2 3]}
{[2 1 3]}
{[1 2 3]}
*/

维护堆的函数有两个,updonwup将小元素"上浮",down让大元素"下沉"。

up的源码如下:

func up(h heap.Interface, j int) {
  for {                          //        i  i+1      j
    i := (j - 1) / 2             // |⬛⬛⬛|⬛|⬛⬛⬛|
    if i == j || !h.Less(j, i) { // i == j || [i] <= [j]
      break
    }
    h.Swap(i, j)
    j = i
  }
}

up在将元素上浮时,并不是挨个比较上浮,而是向当前位置往前一半的地方上浮,所以堆的底层数组并不是有序的。使用up维护堆的目的是不是让堆有序,而是让最小的元素始终是数组的第一个元素。

down源码如下:

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

down在将大元素下沉的时候也是按当前位置2倍的距离下沉,同样也不一定能让数组有序。参数i0表示要下沉的元素位置,n表示下沉的界限,超过该位置便不再下沉,注意这个位置是达不到的。down的返回值表示有没有下沉,因为如果当前元素没有被下沉,就说明当前元素比后面的元素小,那么就需要考虑将当前元素上浮,具体可以看heap.Remove的实现。

heap提供的API如下:

API说明
Init初始化堆
Fix修复堆
Push入堆
Pop删除堆顶
Remove删除指定下标的元素

list

标准库的list是一个双向环链表。

链表节点定义如下:

type Element struct {
  // Next and previous pointers in the doubly-linked list of elements.
  // To simplify the implementation, internally a list l is implemented
  // as a ring, such that &l.root is both the next element of the last
  // list element (l.Back()) and the previous element of the first list
  // element (l.Front()).
  next, prev *Element

  // The list to which this element belongs.
  list *List

  // The value stored with this element.
  Value any
}

链表的定义如下:

type List struct {
  root Element // sentinel list element, only &root, root.prev, and root.next are used
  len  int     // current list length excluding (this) sentinel element
}

List可以开箱即用,也可以通过list.New()返回一个初始化过的链表。

package main

import (
  "container/list"
  "fmt"
)

func main() {
  var l1 list.List
  l1.PushBack(1)
  fmt.Println(l1)

  l2 := list.New()
  l2.PushFront("a")
  fmt.Println(l2)
}

List的API都非常简单。

API说明
Front获取头节点
Back获取尾节点
PushFront头插入
PushFrontList头插入链表
PushBack尾插入
PushBackList尾插入链表
InsertAfter在节点后插入
InsterBefore在节点前插入
MoveAfter移动一个节点到另一个节点前
MoveBefore移动一个节点到另一个节点后
MoveToFront移动节点到链表头
MoveToBack移动节点到链表尾
Remove删除节点
Len获取链表长度

ring

Ring是内置的环形链表,定义如下:

type Ring struct {
  next, prev *Ring
  Value      any // for use by client; untouched by this library
}

使用Ring建议通过ring.New函数创建环形链表,并指定容量。Ring提供的API也很简单。

API说明
Do接受一个函数,遍历环形链表并应用该函数
Len返回环形链表长度
Next返回下一节点
Prev返回上一个节点
Move移动n步,负数向前移动,正数向后移动
Link将一个环形链表连接进来
UnLink从环形链表中分离出一个n节点的环形链表

注意哪些和移动相关的函数,它们会返回一个Ring节点,我们用一个变量接收它。

例子:

package main

import (
  "container/ring"
  "fmt"
)

func main() {
  show := func(x any) {
    fmt.Printf("%v ", x)
  }
  r := ring.New(2)
  r.Value = 1
  r.Next().Value = 2
  r.Do(show)
  fmt.Println()
  r = r.Move(-1)
  r.Do(show)
  fmt.Println()
}
/* 输出
1 2 
2 1 
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值