Go sort包使用与源码剖析

本文深入剖析Go语言的sort包,详细介绍了sort包的文件结构、各文件的使用方法及注意事项,包括排序算法的选择与应用、搜索算法的使用,以及Go语言通过嵌套实现继承的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

包方面

  • sort包里包括哪些文件

  • sort.go如何使用,有什么需要注意的地方

  • example_*_test.go格式的文件是做什么用的

  • slice.go如何使用,有什么需要注意的地方

  • search.go如何使用,有什么需要注意的地方

  • genzfunc.go是什么,如何使用

   算法方面

  • 涉及到哪些算法

  • 算法的比较

  • 算法稳定性的重要性

 Go语言方面

  • Go通过嵌套实现继承

  • Go interface

 

有句话很有趣:Stay hungry, stay foolish. 个人根据对这句话的理解 以一个有强烈求知欲的小白的角度,用提问解答的方式组织全文。以此发现自己知识的不足并学习新的知识。

解答

sort包里包括哪些文件

如下所示

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
├── example_interface_test.go├── example_keys_test.go├── example_multi_test.go├── example_search_test.go├── example_test.go├── example_wrapper_test.go├── export_test.go├── genzfunc.go├── search.go├── search_test.go├── slice.go├── sort.go├── sort_test.go└── zfuncversion.go

 

sort.go如何使用,有什么需要注意的地方

在 sort.go文件中,排序算法有: 插入排序(insertionSort)、堆排序(heapSort),快速排序(quickSort)、希尔排序(ShellSort)、归并排序(SymMerge)。 这些函数都是以小写字母开头,意味着他们对外是不可见的(letter case set visibility)。其中,归并排序用于 Stable函数,其余算法用于 Sort函数。

Sort是基于interface实现的,新建数据类型只要实现 sort.Interface中的三种方法,就能使用Sort方法。下面看下接口中的方法的功能。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type Interface interface {   // Len返回序列中的元素数量   Len() int   // 若i < j,则Less返回true   Less(i, j int) bool   // Swap 交换下标为i和j的元素   Swap(i, j int)}

 

在sort.go中支持了[]Int切片( IntSlice)、[]Float64切片( Float64Slice)和[]string切片(StringSlice)这三种类型。以 IntSlice为例,可使用的方法主要有:

  • Sort()            对序列进行排序

  • Reverse()      结合Sort对序列进行逆序排序

  • IsSorted()     判断序列是否有序

  • Stable()        对序列进行排序,同时保证相同元素排序后和原始顺序相同(即使用稳定的算法)。注意使用Stable需实现 sort.Interface中的所有方法

下面来看一个具体的例子:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
package mainimport "fmt"import "sort"func main() {    strs := []string{"c", "a", "b"} // 未排序    sort.Strings(strs)    fmt.Println("Strings:", strs)    ints := []int{7, 2, 4}          //未排序    sort.Ints(ints)    fmt.Println("Ints:   ", ints)    s := sort.IntsAreSorted(ints)    fmt.Println("Sorted: ", s)    //Output: Strings: [a b c]    //Ints:    [2 4 7]    //Sorted:  true}

 

example_*_test.go格式的文件是做什么用的

在sort包中,有很多 example_*_test.go格式的文件,这些文件中的以 Example开头的函数讲解了Sort包各种方法的使用方法。这是官方提供的使用案例,强烈建议读者看看这几份代码。下面简单说明一下这些代码的用途。

  •  
  •  
  •  
  •  
  •  
  •  
├── example_interface_test.go //基础用法,对一个[]struct进行排序├── example_keys_test.go      //这个例子蛮有趣的,对struct中的元素可进行可编程化的排序(即通过struct中的不同元素进行排序)├── example_multi_test.go     //这个例子蛮有趣的,演示了用struct中不同的元素进行排序的方法├── example_search_test.go    //升序和降序的序列如何使用Search()├── example_test.go           //sort.go和slice的使用方法,列举了上述过的三种数据类型的使用方法,Reverse()和 Slice()的使用方法├── example_wrapper_test.go   //通过srtuct嵌套[]struct达到利用struct中不同元素进行排序的目的

 

Go中的map是未经排序的k-v对,如果需要一个排序后的map,可以开一个key/value类型的序列,对序列进行排序,再利用序列遍历map。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
m := map[string]int{"Alice": 2, "Cecil": 1, "Bob": 3}keys := make([]string, 0, len(m))for k := range m {    keys = append(keys, k)}sort.Strings(keys)for _, k := range keys {    fmt.Println(k, m[k])}// Output:// Alice 2// Bob 3// Cecil 1

 

注意

Float64Slice

在Float64Slice的Less方法中,为避免依赖 math.IsNaN,在这里写了一个功能一样的IsNaN函数

  •  
  •  
  •  
  •  
// isNaN is a copy of math.IsNaN to avoid a dependency on the math package.func isNaN(f float64) bool {  return f != f}

 

Reverse

Reverse的实现比较有趣,来看下源码

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type reverse struct {  // 在reverse结构体中内嵌Interface接口,使Reverse能使用Interface接口实现的方法  Interface}//Less()把Interface接口的Less()的参数翻转,从而达到反转的目的func (r reverse) Less(i, j int) bool {  return r.Interface.Less(j, i)}// Reverse返回data的反转序列func Reverse(data Interface) Interface {  return &reverse{data}}

 

slice.go如何使用,有什么需要注意的地方

思考一下可以想到,在 sort.Interface这个接口中, Len()和 Swap()方法一般是不需要改动的,只有 Less()方法需要指出具体的元素比较项。若每写一个新类型就需要实现三种方法比较麻烦, slice.go解决了这个问题,它里面的方法只需提供less函数即可。

  •  
  •  
  •  
  •  
  •  
type Interface interface {   Len() int   Less(i, j int) bool   Swap(i, j int)}

 

你可能要问了,不提供 Len()和 Swap()并没有实现 sort.Interface接口啊。带着疑问,来看下Slice()的源码。

  •  
  •  
  •  
  •  
  •  
  •  
func Slice(slice interface{}, less func(i, j int) bool) {  rv := reflect.ValueOf(slice)  swap := reflect.Swapper(slice) //reflect.Swapper根据slice类型返回具体的swap func。  length := rv.Len()  quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))}

 

可以看到,Slice通过反射获得 Len()和 Swap()。函数中的 lessSwap结构如下:

  •  
  •  
  •  
  •  
  •  
// lessSwap有Less和Swap方法,用于自动生成且优化后的的sort.go的变种zfuncversion.gotype lessSwap struct {  Less func(i, j int) bool  Swap func(i, j int)}

 

注意

slice.go里面的方法提供的interface类型必须是切片类型,否则会panic。

search.go如何使用,有什么需要注意的地方

Go中的 Search函数是用二分查找算法实现的,比较简单。 example_search_test.go的的使用方法如下。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func ExampleSearch() {  a := []int{1, 3, 6, 10, 15, 21, 28, 36, 45, 55}  x := 6  i := sort.Search(len(a), func(i int) bool { return a[i] >= x })  if i < len(a) && a[i] == x {    fmt.Printf("found %d at index %d in %v\n", x, i, a)  } else {    fmt.Printf("%d not found in %v\n", x, a)  }  // Output:  // found 6 at index 2 in [1 3 6 10 15 21 28 36 45 55]}

 

search.go中为上述三种数据类型([]Int切片( IntSlice)、[]Float64切片( Float64Slice)和[]string切片(StringSlice))分别提供了函数。

  •  
  •  
  •  
func SearchInts(a []int, x int) int {  return Search(len(a), func(i int) bool { return a[i] >= x })}

 

我们可以借鉴下源码中求中点的方式:

  •  
h := int(uint(i+j) >> 1) // avoid overflow when computing h

 

注意

Search()函数不能单独使用,需要在其下方配合判断条件组合使用。search.go函数中传入序列需要是排序过的,否则会出现奇怪的现象(因为 Search函数是通过序列下标进行搜索的)。由于是通过序列下标进行搜索的,在搜索序列中不存在的元素时会出现下面的现象(笔者之前是写Python的,不太喜欢这种设计: Ifthereisnosuch index,Searchreturns n)。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() {  a := []int{1, 2, 3, 4, 5, 6}  fmt.Println(sort.SearchInts(a, 78))  fmt.Println(sort.SearchInts(a, -1))  // Output 6  // 0}// Search()不能单独使用,正确的写法应该是这样。func ExampleSearch() {  a := []int{1, 3, 6, 10, 15, 21, 28, 36, 45, 55}  x := 6  i := sort.Search(len(a), func(i int) bool { return a[i] >= x })  if i < len(a) && a[i] == x { // 需进行判断,看是否找到了元素    fmt.Printf("found %d at index %d in %v\n", x, i, a)  } else {    fmt.Printf("%d not found in %v\n", x, a)  }  // Output:  // found 6 at index 2 in [1 3 6 10 15 21 28 36 45 55]}

 

genzfunc.go是什么,如何使用

genzfunc.go通过运行go generate命令生成zfuncversion.go。简单来说,就是生成代码的。它主要是给开发者在写Go包的时候用的。在生成的zfuncversion.go中,原sort.go中的若干内部函数被改写,以insertionSort为例,以下是生成的代码被改动的情况。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// sort.go中的insertSortfunc insertionSort(data Interface, a, b int) {  for i := a + 1; i < b; i++ {    for j := i; j > a && data.Less(j, j-1); j-- {      data.Swap(j, j-1)    }  }}//zfuncversion.go中的insertionSort_func,可以看到,其中函数名加了_func后缀,data类型由Interface变为lessSwapfunc insertionSort_func(data lessSwap, a, b int) {  for i := a + 1; i < b; i++ {    for j := i; j > a && data.Less(j, j-1); j-- {      data.Swap(j, j-1)    }  }}

 

sort库涉及到哪些算法

排序算法用到插入排序(insertionSort)、堆排序(heapSort)、快速排序(quickSort)、希尔排序(ShellSort)和归并排序(SymMerge);搜索算法用到二分查找算法。

在 Sort()函数中,选择算法的依据如图所示。由于Sort()函数不能保证稳定性,Go用归并排序提供了一个稳定的排序函数 Stable()

排序算法的比较

快排、堆排序和归并排序

算法时间复杂度稳定性原地排序
快排平均O(nlogn) 最好O(nlogn) 最坏O(n*n)不稳定
堆排序平均O(nlogn) 最好O(nlogn) 最坏O(nlogn)不稳定
归并排序平均O(nlogn) 最好O(nlogn) 最坏O(nlogn)稳定

注意: 这里列举的都是基本的排序算法。对于排序算法而言,算法是否稳定需进行具体分析,不可一概而论。

在sort源码中,在切片数量大于12时,用到快排和堆排序这两种排序算法,当 maxDepth为0时,会从快排转换为使用堆排序。作者根据这篇论文写算法的。Engineering a Sort Functionfollowing Bentley and McIlroy SP&E November 1993。 其中 maxDepth的计算函数如下:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
func maxDepth(n int) int {  var depth int  for i := n; i > 0; i >>= 1 {    depth++  }  return depth * 2 //return 2*向上取整(lg(n+1))}

 

sort源码中主要使用快排进行排序的。也许读者有疑问,归并排序的时间复杂度稳定,同时也是一种稳定的排序的算法,为何不使用这种排序算法呢。原因是归并排序不是原地排序算法,他需要借助额外空间进行归并,空间复杂度较高,为O(n)。而对于堆排序,要使用首先需要建堆然后排序。源码中的堆排序首先对数组中 (hi-1)/2个节点依次堆化,再依次pop堆顶元素,完成排序。相较于快排,堆排序需要建堆这个过程,这个过程会打乱原来的数据顺序,可能会将数据的有序度降低,即经过建堆之后,数据反而变得更无序了。

稳定性的用途

首先需明确稳定性的定义:稳定性指待排序的序列中,值相等的元素排序后和原始序列顺序相同。在大学教学过程中,课上老师用来举例的例子一般是一个int序列,在这个情境中难以看出稳定性的实际作用。但实际开发过程中,当对含多个有效元素的[]struct进行排序时,稳定性的作用就发挥出来了:先用struct中某个元素进行排序,再对struct中的另一个元素进行排序,第一个元素排序的结果可以作为第二个元素排序的输入。

举个例子,现在需要对学生的成绩数据进行排序。希望按照总分从大到小排序,总分相同的学生,按照英语成绩从大到小排序。有了稳定的排序算法,可以先按照英语成绩从大到小排序,再按照总分进行排序。

Go语言方面

Go通过嵌套实现继承

在sort包中很多地方都通过struct和interface的嵌套去实现继承。从而继承内部嵌套结构的方法和属性。建议读者多看看嵌套的相关代码。举例:

  • Reverse()的实现

  • sort/example_wrapper_test.go的实现

品略图书馆 http://www.pinlue.com/ http://m.pinlue.com/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值