go判断元素是否在切片中(代码片段)

恋喵大鲤鱼 恋喵大鲤鱼     2023-01-16     687

关键词:


1.问题

如何判断元素是否在切片中,Golang 并没有提供直接的库函数来判断,最容易想到的实现便是通过遍历来判断。

2.遍历查询

以字符串切片为例,判断字符串切片中是否包含某个字符串。

// ContainsInSlice 判断字符串是否在 slice 中
func ContainsInSlice(items []string, item string) bool 
	for _, eachItem := range items 
		if eachItem == item 
			return true
		
	
	return false

这种实现时间复杂度是 O(n),n 为切片元素个数。

如果切片长度比较短(10以内)或者不是频繁调用,该性能是可以接受的。但是如果切片长度较长且频繁调用,那么这种方法的性能将无法接受,我们可以借助 map 优化一波。

3.map 查询

具体实现是先将 slice 转为 map,通过查询 map 快速查看元素是否存在于 slice。

// ConvertStrSlice2Map 将字符串 slice 转为 map[string]struct
func ConvertStrSlice2Map(sl []string) map[string]struct 
	set := make(map[string]struct, len(sl))
	for _, v := range sl 
		set[v] = struct
	
	return set


// ContainsInMap 判断字符串是否在 map 中
func ContainsInMap(m map[string]struct, s string) bool 
	_, ok := m[s]
	return ok

注意:使用空结构体 struct 作为 value 的类型,因为 struct 不占用任何内存空间。

fmt.Println(unsafe.Sizeof(bool(false))) // 1
fmt.Println(unsafe.Sizeof(struct))  // 0

虽然将 slice 转为 map 的时间复杂度为 O(n),但是只转换一次可以忽略。查询元素是否在 map 中的时间复杂度为 O(1)。

4.性能对比

我们可以看下在元素数量为 26 的情况下,取中位元素,做个基准测试(benchmark),对比下二者的查询性能。

func BenchmarkContainsInSlice(b *testing.B) 
	for i := 0; i < b.N; i++ 
		ContainsInSlice(sl, "m")
	


func BenchmarkContainsInMap(b *testing.B) 
	m := ConvertStrSlice2Map(sl)
	for i := 0; i < b.N; i++ 
		ContainsInMap(m, "m")
	

执行测试命令输出:

D:\\code\\gotest\\contain>go test -bench=.
goos: windows
goarch: amd64
pkg: main/contain
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkContainsInSlice-8      30564058                38.35 ns/op
BenchmarkContainsInMap-8        134556465                8.846 ns/op
PASS
ok      main/contain    3.479s

测试结果中,看到函数后面的 -8 个表示运行时对应的 GOMAXPROCS 的值。接着的一串很大的数字表示运行 for 循环的次数,也就是调用被测试代码的次数,最后的38.35 ns/op表示每次需要花费 38.35 纳秒。
以上是测试时间默认是 1 秒,也就是1秒的时间,如果想让测试运行的时间更长,可以通过 -lunchtime 指定,比如 5 秒。

性能对比:

可以预料到的是随着切片长度增长,性能差距会越来越大。

5.转换通用化

我们可以借助空接口 interface 来实现任意类型的切片转换为 map,方便调用方使用。

// ToMapSetE converts a slice or array to map[interface]struct with error
func ToMapSetE(i interface) (map[interface]struct, error) 
	// judge the validation of the input
	if i == nil 
		return nil, fmt.Errorf("unable to converts %#v of type %T to map[interface]struct", i, i)
	
	kind := reflect.TypeOf(i).Kind()
	if kind != reflect.Slice && kind != reflect.Array 
		return nil, fmt.Errorf("the input %#v of type %T isn't a slice or array", i, i)
	

	// execute the convert
	v := reflect.ValueOf(i)
	m := make(map[interface]struct, v.Len())
	for j := 0; j < v.Len(); j++ 
		m[v.Index(j).Interface()] = struct
	
	return m, nil


func main() 
	var sl = []string"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
	m, _ := ToMapSetE(sl)
	if _, ok := m["m"]; ok 
		fmt.Println("in")
	
	if _, ok := m["mm"]; !ok 
		fmt.Println("not in")
	

运行输出:

in
not in

上面的转换函数ToMapSetE()已经放到开源 Go 工具库go-huge-util,可直接通过 go mod 方式 import 使用。

import (
	huge "github.com/dablelv/go-huge-util"
)

// 使用 go-huge-util
m, _ := huge.ToMapSetE(sl)

6.借助开源库 golang-set

上面其实是利用 map 实现了一个 set(元素不重复集合),然后再判断某个 set 中是否存在某个元素。Golang 标准库并没有 set,但是我们可以用 map 来间接实现,就像上面那样子。

如果想使用 set 的完整功能,如初始化、Add、Del、Clear、Contains等操作,推荐使用 github 上成熟的开源包 golang-set。描述中说 Docker 用的也是它。包中提供了两种 set 实现,线程安全的 set 和非线程安全的 set。

golang-set 提供了五个生成 set 的函数:

// NewSet creates and returns a reference to an empty set.  Operations
// on the resulting set are thread-safe.
func NewSet(s ...interface) Set 

// NewSetWith creates and returns a new set with the given elements.
// Operations on the resulting set are thread-safe.
func NewSetWith(elts ...interface) Set 

// NewSetFromSlice creates and returns a reference to a set from an
// existing slice.  Operations on the resulting set are thread-safe.
func NewSetFromSlice(s []interface) Set 

// NewThreadUnsafeSet creates and returns a reference to an empty set.
// Operations on the resulting set are not thread-safe.
func NewThreadUnsafeSet() Set 

// NewThreadUnsafeSetFromSlice creates and returns a reference to a
// set from an existing slice.  Operations on the resulting set are
// not thread-safe.
func NewThreadUnsafeSetFromSlice(s []interface) Set 

下面借助golang-set来判断切片中是否存在某个元素。

package main

import (
	"fmt"

	mapset "github.com/deckarep/golang-set"
)

func main() 
	var sl = []interface"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r",
		"s", "t",
		"u", "v", "w", "x", "y", "z"
	s := mapset.NewSetFromSlice(sl)
	fmt.Println(s.Contains("m"))	// true
	fmt.Println(s.Contains("mm"))	// false

7.小结

本文从问题“判断元素是否在切片中”开始讨论,给出相关的实现方式。这个问题可以引申抽象为“如何将 slice 转为元素不重复的 set”,并给出自己的通用转换函数 go-huge-util ToMapSetE()

当然,网上已经有很多成熟优秀的代码库直接使用,比如 golang-set,感兴趣的同学可以深入了解其用法和实现。


参考文献

知乎.Go 小知识之 Go 中如何使用 set
Golang 基准测试(Benchmark)

go语言基础切片,map(代码片段)

...片扩容动态扩容切片扩切片切片copy删除切片map简单使用判断键是否在map中遍历map删除键值对按照某个固定顺序遍历map元素为map类型的切片元素为map类型的切片循环初始化值为切片的mapmap计算字符串每个单词出现的次数切片(Slice)... 查看详情

go语言容器(container)(代码片段)

...的声明比较两个数组是否相等遍历数组—访问每一个数组元素Go语言多维数组简述Go语言切片详解从数组或切片生成新的切片1)从指定范围中生成切片2)表示原有的切片3)重置切片,清空拥有的元素直接声明新的切片使用make()函... 查看详情

go语言排序与搜索切片

...该函数是一个闭包,如果该有序切片是升序排列,那么在判断时使用有序切片的元素>=目标元素。该函数返回一个int值,表示与目标元素相同的切片元素的索引。在切片中查找出某个与目标字符串相同的元素索引 查看详情

go语言字典和结构体(代码片段)

....map中的key需要我们自己指定只要是可以做==、!=判断的数据类型都可以作为key(数值类型、 查看详情

go语言容器(container)(代码片段)

...的声明比较两个数组是否相等遍历数组—访问每一个数组元素Go语言多维数组简述Go语言切片详解从数组或切片生成新的切片1)从指定范围中生成切片2)表示原有的切片3)重置切片,清空拥有的元素直接声明新的切片使用make()函... 查看详情

go语言学习笔记—基础—高级数据类型—数据容器—切片:访问切片元素和子切片(代码片段)

访问切片切片元素使用”切片变量[]”的方式进行访问,在方括号中提供切片的索引即可访问元素,索引范围从O开始,且不超过切片的最大容量。//初始化一个三元素切片a:=make([]int,3)//为切片的元素赋值a[O]=1a[1]&... 查看详情

go语言之go语言引用类型(代码片段)

...数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。定义切片申明一个未指定大小的数组来定义切片:varidentifier[]type切片不需要说明长度。或使用make()函数来创建切片:varslice1[]type=make... 查看详情

go语言系列之数组和切片(代码片段)

...干个相同类型的变量的集合。数组中每个变量称为数组的元素,每个元素都有一个数字编号——数组下标,该下标从0开始,用于区别各个元素。数组中可容纳的元素个数称为数组的长度。1.1.声明Go语言中数组的声明方式:vararr_n... 查看详情

go基础2(代码片段)

...符关系运算符逻辑运算符位运算符赋值运算符流程控制if判断if判断的特殊写法for循环死循环forrange循环练习break和continueswitchcasegoto(跳转到指定的标签)(Array)数组数组定义数组的初始化数组的遍历多维数组多维数组的遍... 查看详情

golang在go中附加切片(代码片段)

查看详情

go语言基础之切片(代码片段)

...对应的类型是切片slice,其代表变长的序列,序列中每个元素都是相同的类型。1.1切片的内部实现  如图1所示,可以看出切片的底层还是数组。slice从底层来说,其实就是一个数据结构(struct结构体),切片有3个字段的数据结... 查看详情

go数组字符串与切片(代码片段)

...类型,在底层原始数据有着相同的内存结构。虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。数组数组是... 查看详情

go语言容器—数组切片(代码片段)

...切片一、Go语言数组1.1数组的概念语法说明var数组变量名[元素数量]Type元素数量:必须在编译器就能够确定Type:可以是任意类型,类型为数组本身时,可以实现多维数组funcmain() vararr[3][3]int arr[0][0] 查看详情

golangbasic_leaming2语言容器(代码片段)

...遍历数组参考文献Go语言切片(Slice)初始化_删除元素_遍历什么是切片声明切片使用make()函数构造切片使用append()函数为切片添加元素从数组或切片生成新的切片从指定范围中生成切片重置切片复制切片元素到另一个切片... 查看详情

go入门学习理解区分数组和切片(代码片段)

...是一组同类型数据的集合,通过从0开始的下标索引访问元素值。在Go语言中,数组是值类型,这就意味着当你将一个数组赋值给另一个数组的时候,实际上是将这个数组拷贝了一份。  数组的声明语法为:var数组变量名[元素... 查看详情

golangbasic_leaming2语言容器(代码片段)

...遍历数组参考文献Go语言切片(Slice)初始化_删除元素_遍历什么是切片声明切片使用make()函数构造切片使用append()函数为切片添加元素从数组或切片生成新的切片从指定范围中生成切片重置切片复制切片元素到另一个切片... 查看详情

go删除切片元素的另一种姿势(代码片段)

...一个特殊场景:现有切片A,B,切片B中的部分元素是切片A的子集,求A删除B中子集后的部分先上常规思路代码:fork,v:=rangeA for_,m:=rangeB ifv==m switchk case1: A=A[1:] caselen(A): A=A[:len(A)-1] de... 查看详情

go删除切片元素的另一种姿势(代码片段)

...一个特殊场景:现有切片A,B,切片B中的部分元素是切片A的子集,求A删除B中子集后的部分先上常规思路代码:fork,v:=rangeA for_,m:=rangeB ifv==m switchk case1: A=A[1:] caselen(A): A=A[:len(A)-1] de... 查看详情