一文搞懂Go语言中的切片排序

切片是Go语言中引入的用于在大多数场合替代数组的语法元素。切片是长度可变的同类型元素序列,它不支持存储不同类型的元素,当然如果你非用sl := []interface{}{"hello", 11,  3.14}来抬杠^_^,那就另当别论。

有序列的地方就有排序的需求。在各种排序算法都已经成熟的今天,我们完全可以针对特定元素类型的切片手写排序函数/方法,但多数情况下不推荐这么做,因为Go标准库内置了sort包可以很好地帮助我们实现原生类型元素切片以及自定义类型元素切片的排序任务。

1. sort包的排序原理

截至目前(Go 1.15版本),Go还不支持泛型。因此,为了支持任意元素类型的切片的排序,标准库sort包定义了一个Interface接口和一个接受该接口类型参数的Sort函数:

// $GOROOT/src/sort/sort.go
type Interface interface {
 Len() int
 Less(i, j int) bool
 Swap(i, j int)
}

func Sort(data Interface) {
        n := data.Len()
        quickSort(data, 0, n, maxDepth(n))
}

为了应用这个排序函数Sort,我们需要让被排序的切片类型实现sort.Interface接口,以整型切片为例:

// slice-sort-in-go/sort_int_slice.go
type IntSlice []int

func (p IntSlice) Len() int  { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

func main() {
 sl := IntSlice([]int{89, 14, 8, 9, 17, 56, 95, 3})
 fmt.Println(sl) // [89 14 8 9 17 56 95 3]
 sort.Sort(sl)
 fmt.Println(sl) // [3 8 9 14 17 56 89 95]
}

从sort.Sort函数的实现来看,它使用的是快速排序(quickSort)。我们知道快速排序是在所有数量级为(o(nlogn))的排序算法中其平均性能最好的算法,但在某些情况下其性能却并非最佳,Go sort包中的quickSort函数也没有严格拘泥于仅使用快排算法,而是以快速排序为主,并根据目标状况在特殊条件下选择了其他不同的排序算法,包括堆排序(heapSort)、插入排序(insertionSort)等。

sort.Sort函数不保证排序是稳定的,要想使用稳定排序,需要使用sort.Stable函数。

注:稳定排序:假定在待排序的序列中存在多个具有相同值的元素,若经过排序,这些元素的相对次序保持不变,即在原序列中,若r[i]=r[j]且r[i]在r[j]之前,在排序后的序列中,若r[i]仍在r[j]之前,则称这种排序算法是稳定的(stable);否则称为不稳定的。

2. sort包的“语法糖”排序函数

我们看到,直接使用sort.Sort函数对切片进行排序是比较繁琐的。如果仅仅排序一个原生的整型切片都这么繁琐(要实现三个方法),那么sort包是会被“诟病”惨了的。还好,对于以常见原生类型为元素的切片,sort包提供了类“语法糖”的简化函数,比如:sort.Ints、sort.Float64s和sort.Strings等。上述整型切片的排序代码可以直接改造成下面这个样子:

// slice-sort-in-go/sort_int_slice_with_sugar.go

func main() {
 sl := []int{89, 14, 8, 9, 17, 56, 95, 3}
 fmt.Println(sl) // [89 14 8 9 17 56 95 3]
 sort.Ints(sl)
 fmt.Println(sl) // [3 8 9 14 17 56 89 95]
}

原生类型有“语法糖”可用了,那么对于自定义类型作为元素的切片,是不是每次都得实现Interface接口的三个方法呢?Go团队也想到了这个问题! 所以在Go 1.8版本中加入了sort.Slice函数,我们只需传入一个比较函数实现即可:

// slice-sort-in-go/custom-type-slice-sort-in-go.go

type Lang struct {
 Name string
 Rank int
}

func main() {
 langs := []Lang{
  {"rust", 2},
  {"go", 1},
  {"swift", 3},
 }
 sort.Slice(langs, func(i, j int) bool { return langs[i].Rank < langs[j].Rank })
 fmt.Printf("%v\n", langs) // [{go 1} {rust 2} {swift 3}]
}

同理,如果要进行稳定排序,则用sort.SliceStable替换上面的sort.Slice。

3. 引入泛型后的切片排序

Russ Cox已经确认了Go泛型(type parameter)将在Go 1.18版本落地,我们来展望一下在2022年2月Go泛型落地后,切片排序该怎么做。好在现在有https://go2goplay.golang.org/可以用于试验go泛型技术草案中的语法。

在泛型加入后,我们可以按如下方式对原生类型切片进行排序:

// https://go2goplay.golang.org/p/lKG3saE-1ek

package main

import (
 "fmt"
 "sort"
)

type Ordered interface {
 type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string
}

type orderedSlice[T Ordered] []T

func (s orderedSlice[T]) Len() int           { return len(s) }
func (s orderedSlice[T]) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice[T]) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func OrderedSlice[T Ordered](s []T) {
 sort.Sort(orderedSlice[T](s))
}

func main() {
 s1 := []int32{3, 5, 2}
 fmt.Println(s1) // [3 5 2] 
 OrderedSlice(s1)
 fmt.Println(s1) // [2 3 5]

 s2 := []string{"jim", "amy", "tom"}
 fmt.Println(s2) // [jim amy tom]  
 OrderedSlice(s2)
 fmt.Println(s2) // [amy jim tom] 
}

上面的Ordered接口类型、orderedSlice[T]切片类型以及OrderdSlice函数都可能会内置到sort包中,我们直接使用sort.OrderSlice函数即可对原生类型元素切片进行排序。

而对于自定义类型,如果我们将其加入到Ordered接口的类型列表(type list)中,像下面这样:

type Lang struct {
 Name string
 Rank int
}


type Ordered interface {
 type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string
}

那么,当我们像下面代码这样对元素类型为Lang的切片langs进行排序时,我们会遇到编译错误:

func main() {
 langs := []Lang{
  {"rust", 2},
  {"go", 1},
  {"swift", 3},
 }

 OrderedSlice(langs)
 fmt.Println(langs)
}

$prog.go2:20:55: cannot compare s[i] < s[j] (operator < not defined for T)

由于Lang类型不支持<比较,因此我们无法将Lang类型放入Ordered的类型列表中。而根本原因在于Go语言不支持运算符重载,这样我们永远无法让自定义类型支持<>,我们只能另辟蹊径,采用sort.Slice的思路:额外提供一个比较函数!

// https://go2goplay.golang.org/p/7K94ZJuaoDc

package main

import (
 "fmt"
 "sort"
)

type Lang struct {
 Name string
 Rank int
}

type sliceFn[T any] struct {
 s   []T
 cmp func(T, T) bool
}

func (s sliceFn[T]) Len() int           { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

func SliceFn[T any](s []T, cmp func(T, T) bool) {
 sort.Sort(sliceFn[T]{s, cmp})
}

func main() {
 langs := []Lang{
  {"rust", 2},
  {"go", 1},
  {"swift", 3},
 }

 SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank < p2.Rank })
 fmt.Println(langs) // [{go 1} {rust 2} {swift 3}]
}

有人说,SliceFn和非泛型版本的sort.Slice在使用时复杂度似乎也没啥差别啊。形式上的确如此,但内涵上还是有差别的

使用泛型方案, 由于少了到interface{}的装箱和拆箱操作,理论上SliceFn的性能要好于sort.Slice函数。根据Go语言之父Robert Griesemer对Go泛型的讲解

SliceFn(langs,...)

等价于下面过程:

sliceFnForLang := SliceFn(Lang) // 编译阶段,sliceFnForLang的函数原型为func(s []Lang, func(Lang, Lang) bool);
sliceFnForLang(langs) // 运行阶段,和普通函数调用无异,但没有了到interface{}类型装箱和拆箱的损耗。

注:本文涉及的源码可以在这里https://github.com/bigwhite/experiments/tree/master/slice-sort-in-go 下载到。

延伸阅读

  • Go语言的前世今生 - https://www.imooc.com/read/87/article/2320 

  • Go语言的设计哲学之一:简单 - https://www.imooc.com/read/87/article/2321

  • Go语言的设计哲学之二:组合 - https://www.imooc.com/read/87/article/2322

  • Go语言的设计哲学之三:并发 - https://www.imooc.com/read/87/article/2340


Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx

  • 微信公众号:iamtonybai

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

### 视觉语言模型的基本概念 视觉语言模型(Vision-Language Models, VLMs)是一种跨模态的人工智能技术,旨在连接视觉数据(如图像、视频)与自然语言之间的关系。这些模型可以理解并生成关于视觉内容的语言描述,或者反过来根据语言指令分析和解释视觉信息[^1]。 具体来说,在图像描述任务中,VLM 需要识别图像中的对象、场景及其相互作用,并将其转化为连贯的自然语言叙述;而在图像问答任务中,则需结合输入的文字问题解析图像内容,提供精确的回答。这表明 VLM 不仅具备强大的感知能力,还拥有一定的推理能力和表达能力。 ### 工作原理 视觉语言模型的核心工作流程通常涉及以下几个方面: #### 跨模态特征提取 为了实现对多种类型的数据的理解,VLM 使用预训练方法来获取高质量的跨模态表示向量。例如,基于 Transformer 的架构被广泛应用于这一过程,因为它能有效捕捉序列间的依赖关系,无论是来自文本还是像素级的空间分布信息[^2]。 ```python import torch from transformers import CLIPProcessor, CLIPModel model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") image = ... # PIL image object text = ["a photo of a cat", "a photo of a dog"] inputs = processor(text=text, images=image, return_tensors="pt", padding=True) outputs = model(**inputs) logits_per_image = outputs.logits_per_image # this is the image-text similarity score probs = logits_per_image.softmax(dim=1) # we can take the softmax to get probability distribution print(probs) ``` 此代码片段展示了如何利用 Hugging Face 提供的 `CLIP` 模型计算图片与若干候选词句之间的相似度得分,进而推断最匹配的结果。 #### 对齐机制 另一个关键技术是对齐机制的设计——即怎样让两个异质空间内的元素建立起映射关联。一些先进方案采用对比学习框架构建损失函数,促使正样本对拉近彼此距离的同时推开负样例组。 ### 应用领域 由于其卓越性能表现,VLM 正迅速渗透到多个实际应用场景之中: - **电子商务**: 自动生成商品详情页文案; - **社交媒体平台**: 辅助审核违规内容或增强用户体验交互界面设计; - **医疗健康行业**: 协助医生诊断疾病影像资料同时记录诊疗意见等复杂操作步骤说明文档撰写服务等等[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值