首頁 > 後端開發 > Golang > 主體

golang有沒有in

青灯夜游
發布: 2023-01-18 15:01:16
原創
1526 人瀏覽過

golang沒有in。 golang中即沒有提供類似Python操作符in,也沒有像其他語言那樣提供這樣的標準函式庫函數,如PHP中in_array。原因:1、in功能實作非常簡單,沒有必要;2、在不同場景下,我們還需要根據實際情況分析用哪種方式實現,而不是固定的方式。

golang有沒有in

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

之前在知乎看到一個問題:為什麼 Golang 沒有像 Python 中 in 一樣的功能?於是,搜了下這個問題,發現還是有不少人有這樣的疑問。

今天來談談這個話題。

in 是一個很常用的功能,有些語言中可能也稱為 contains,雖然不同語言的表示不同,但基本上都是有的。不過可惜的是,Go 卻沒有,它即沒有提供類似 Python 運算子 in,也沒有像其他語言那樣提供這樣的標準函式庫函數,如 PHP 中 in_array。

Go 的哲學是追求少即是多。我想或許 Go 團隊覺得這是一個實現起來不足為奇的功能。

為何說微不足道?如果要自己實現,又該如何做呢?

我所想到的有三種實作方式,一是遍歷,二是 sort 的二分查找,三是 map 的 key 索引。

遍歷

遍歷應該是我們最容易想到的最簡單的實作方式。

範例如下:

func InIntSlice(haystack []int, needle int) bool {
    for _, e := range haystack {
        if e == needle {
            return true
        }
    }

    return false
}
登入後複製

上面示範如何在一個[]int 類型變數中找出指定int 是否存在的例子,是不是非常簡單,由此我們也可以感受到我為什麼說它實現起來微不足道。

這個例子有個缺陷,它只支援單一型別。如果要支援像解釋語言一樣的通用 in 功能,則需藉助反射實作。

程式碼如下:

func In(haystack interface{}, needle interface{}) (bool, error) {
    sVal := reflect.ValueOf(haystack)
    kind := sVal.Kind()
    if kind == reflect.Slice || kind == reflect.Array {
        for i := 0; i < sVal.Len(); i++ {
            if sVal.Index(i).Interface() == needle {
                return true, nil
            }
        }

        return false, nil
    }

    return false, ErrUnSupportHaystack
}
登入後複製

為了更通用,In 函數的輸入參數 haystack 和 needle 都是 interface{} 類型。

簡單說說輸入參數都是interface{} 的好處吧,主要有兩點,如下:

  • 其一,haystack 是interface{} 類型,使in 支援的類型不只於slice,還包含array。我們看到,函數內部透過反射對 haystack 進行了類型檢查,支援 slice(切片)與 array(陣列)。如果是其他類型則會提示錯誤,增加新的類型支持,如 map,其實也很簡單。但不推薦這種方式,因為透過 _, ok := m[k] 的語法即可達到 in 的效果。

  • 其二,haystack 是 interface{},則 []interface{} 也符合要求,且 needle 是 interface{}。如此一來,我們就可以實現類似解釋型語言一樣的效果了。

怎麼理解?直接範例說明,如下:

gotin.In([]interface{}{1, "two", 3}, "two")
登入後複製

haystack 是 []interface{}{1, "two", 3},而且 needle 是 interface{},此時的值是 "two"。如此看起來,是不是實現了解釋型語言中,元素可以是任意類型,不必完全相同效果。如此一來,我們就可以肆意妄為的使用了。

但有一點要說明,In 函數的實作中有這樣一段程式碼:

if sVal.Index(i).Interface() == needle {
    ...
}
登入後複製

Go 中並非任何型別都可以使用== 比較的,如果元素中含有slice 或map,則可能會報錯誤。

二分查找

以遍歷確認元素是否存在有個缺點,那就是,如果數組或切片中包含了大量數據,例如1000000 條數據,即一百萬,最壞的情況是,我們要遍歷1000000 次才能確認,時間複雜度On。

有什麼辦法可以降低遍歷次數?

自然而然想到的方法是二分查找,它的時間複雜度 log2(n) 。但這個演算法有前提,需要依賴有序序列。

於是,第一個要我們解決的問題是讓序列有序,Go 的標準函式庫已經提供了這個功能,在 sort 套件下。

範例程式碼如下:

fmt.Println(sort.SortInts([]int{4, 2, 5, 1, 6}))
登入後複製

對於[]int,我們使用的函數是SortInts,如果是其他型別切片,sort 也提供了相關的函數,例如[]string 可透過SortStrings排序。

完成排序就可以進行二分查找,幸運的是,這個功能 Go 也提供了,[]int 型別對應函數是 SearchInts。

簡單介紹下這個函數,先看定義:

func SearchInts(a []int, x int) int
登入後複製

輸入參數容易理解,從切片 a 搜尋 x。重點要說下回傳值,這對於我們後面確認元素是否存在至關重要。傳回值的意義,傳回尋找元素在切片中的位置,如果元素不存在,則傳回,在保持切片有序情況下,插入該元素應該在什麼位置。

例如,序列如下:

1 2 6 8 9 11
登入後複製

假設,x 為6,查找之後將發現它的位置在索引2 處;x 如果是7,發現不存在該元素,如果插入序列,將會放在6 和8 之間,索引位置是3,因而傳回值為3。

程式碼測試下:

fmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 6)) // 2
fmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 7)) // 3
登入後複製

如果判断元素是否在序列中,只要判断返回位置上的值是否和查找的值相同即可。

但还有另外一种情况,如果插入元素位于序列最后,例如元素值为 12,插入位置即为序列的长度 6。如果直接查找 6 位置上的元素就可能发生越界的情况。那怎么办呢?其实判断返回是否小于切片长度即可,小于则说明元素不在切片序列中。

完整的实现代码如下:

func SortInIntSlice(haystack []int, needle int) bool {
    sort.Ints(haystack)
    
    index := sort.SearchInts(haystack, needle)
    return index < len(haystack) && haystack[index] == needle
}
登入後複製

但这还有个问题,对于无序的场景,如果每次查询都要经过一次排序并不划算。最好能实现一次排序,稍微修改下代码。

func InIntSliceSortedFunc(haystack []int) func(int) bool {
    sort.Ints(haystack)
    
    return func(needle int) bool {
        index := sort.SearchInts(haystack, needle)
        return index < len(haystack) && haystack[index] == needle
    }
}
登入後複製

上面的实现,我们通过调用 InIntSliceSortedFunc 对 haystack 切片排序,并返回一个可多次使用的函数。

使用案例如下:

in := gotin.InIntSliceSortedFunc(haystack)

for i := 0; i<maxNeedle; i++ {
    if in(i) {
        fmt.Printf("%d is in %v", i, haystack)
    }
}
登入後複製

二分查找的方式有什么不足呢?

我想到的重要一点,要实现二分查找,元素必须是可排序的,如 int,string,float 类型。而对于结构体、切片、数组、映射等类型,使用起来就不是那么方便,当然,如果要用,也是可以的,不过需要我们进行一些适当扩展,按指定标准排序,比如结构的某个成员。

到此,二分查找的 in 实现就介绍完毕了。

map key

本节介绍 map key 方式。它的算法复杂度是 O1,无论数据量多大,查询性能始终不变。它主要依赖的是 Go 中的 map 数据类型,通过 hash map 直接检查 key 是否存在,算法大家应该都比较熟悉,通过 key 可直接映射到索引位置。

我们常会用到这个方法。

_, ok := m[k]
if ok {
    fmt.Println("Found")
}
登入後複製

那么它和 in 如何结合呢?一个案例就说明白了这个问题。

假设,我们有一个 []int 类型变量,如下:

s := []int{1, 2, 3}
登入後複製

为了使用 map 的能力检查某个元素是否存在,可以将 s 转化 map[int]struct{}。

m := map[interface{}]struct{}{
    1: struct{}{},
    2: struct{}{},
    3: struct{}{},
    4: struct{}{},
}
登入後複製

如果检查某个元素是否存在,只需要通过如下写法即可确定:

k := 4
if _, ok := m[k]; ok {
    fmt.Printf("%d is found\n", k)
}
登入後複製

是不是非常简单?

补充一点,关于这里为什么使用 struct{},可以阅读我之前写的一篇关于 Go 中如何使用 set 的文章。

按照这个思路,实现函数如下:

func MapKeyInIntSlice(haystack []int, needle int) bool {
    set := make(map[int]struct{})
    
    for _ , e := range haystack {
        set[e] = struct{}{}
    }
    
    _, ok := set[needle]
    return ok
}
登入後複製

实现起来不难,但和二分查找有着同样的问题,开始要做数据处理,将 slice 转化为 map。如果是每次数据相同,稍微修改下它的实现。

func InIntSliceMapKeyFunc(haystack []int) func(int) bool {
    set := make(map[int]struct{})

    for _ , e := range haystack {
        set[e] = struct{}{}
    }

    return func(needle int) bool {
        _, ok := set[needle]
        return ok
    }
}
登入後複製

对于相同的数据,它会返回一个可多次使用的 in 函数,一个使用案例如下:

in := gotin.InIntSliceMapKeyFunc(haystack)

for i := 0; i<maxNeedle; i++ {
    if in(i) {
        fmt.Printf("%d is in %v", i, haystack)
    }
}
登入後複製

对比前两种算法,这种方式的处理效率最高,非常适合于大数据的处理。接下来的性能测试,我们将会看到效果。

性能

介绍完所有方式,我们来实际对比下每种算法的性能。测试源码位于 gotin_test.go 文件中。

基准测试主要是从数据量大小考察不同算法的性能,本文中选择了三个量级的测试样本数据,分别是 10、1000、1000000。

为便于测试,首先定义了一个用于生成 haystack 和 needle 样本数据的函数。

代码如下:

func randomHaystackAndNeedle(size int) ([]int, int){
    haystack := make([]int, size)

    for i := 0; i<size ; i++{
        haystack[i] = rand.Int()
    }

    return haystack, rand.Int()
}
登入後複製

输入参数是 size,通过 http://rand.Int() 随机生成切片大小为 size 的 haystack 和 1 个 needle。在基准测试用例中,引入这个随机函数生成数据即可。

举个例子,如下:

func BenchmarkIn_10(b *testing.B) {
    haystack, needle := randomHaystackAndNeedle(10)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = gotin.In(haystack, needle)
    }
}
登入後複製

首先,通过 randomHaystackAndNeedle 随机生成了一个含有 10 个元素的切片。因为生成样本数据的时间不应该计入到基准测试中,我们使用 b.ResetTimer() 重置了时间。

其次,压测函数是按照 Test+函数名+样本数据量 规则编写,如案例中 BenchmarkIn_10,表示测试 In 函数,样本数据量为 10。如果我们要用 1000 数据量测试 InIntSlice,压测函数名为 BenchmarkInIntSlice_1000。

测试开始吧!简单说下我的笔记本配置,Mac Pro 15 版,16G 内存,512 SSD,4 核 8 线程的 CPU。

测试所有函数在数据量在 10 的情况下的表现。

$ go test -run=none -bench=10$ -benchmem
登入後複製

匹配所有以 10 结尾的压测函数。

测试结果:

goos: darwin
goarch: amd64
pkg: github.com/poloxue/gotin
BenchmarkIn_10-8                         3000000               501 ns/op             112 B/op         11 allocs/op
BenchmarkInIntSlice_10-8                200000000                7.47 ns/op            0 B/op          0 allocs/op
BenchmarkInIntSliceSortedFunc_10-8      100000000               22.3 ns/op             0 B/op          0 allocs/op
BenchmarkSortInIntSlice_10-8            10000000               162 ns/op              32 B/op          1 allocs/op
BenchmarkInIntSliceMapKeyFunc_10-8      100000000               17.7 ns/op             0 B/op          0 allocs/op
BenchmarkMapKeyInIntSlice_10-8           3000000               513 ns/op             163 B/op          1 allocs/op
PASS
ok      github.com/poloxue/gotin        13.162s
登入後複製

表现最好的并非 SortedFunc 和 MapKeyFunc,而是最简单的针对单类型的遍历查询,平均耗时 7.47ns/op,当然,另外两种方式表现也不错,分别是 22.3ns/op 和 17.7ns/op。

表现最差的是 In、SortIn(每次重复排序) 和 MapKeyIn(每次重复创建 map)两种方式,平均耗时分别为 501ns/op 和 513ns/op。

测试所有函数在数据量在 1000 的情况下的表现。

$ go test -run=none -bench=1000$ -benchmem
登入後複製

测试结果:

goos: darwin
goarch: amd64
pkg: github.com/poloxue/gotin
BenchmarkIn_1000-8                         30000             45074 ns/op            8032 B/op       1001 allocs/op
BenchmarkInIntSlice_1000-8               5000000               313 ns/op               0 B/op          0 allocs/op
BenchmarkInIntSliceSortedFunc_1000-8    30000000                44.0 ns/op             0 B/op          0 allocs/op
BenchmarkSortInIntSlice_1000-8             20000             65401 ns/op              32 B/op          1 allocs/op
BenchmarkInIntSliceMapKeyFunc_1000-8    100000000               17.6 ns/op             0 B/op          0 allocs/op
BenchmarkMapKeyInIntSlice_1000-8           20000             82761 ns/op           47798 B/op         65 allocs/op
PASS
ok      github.com/poloxue/gotin        11.312s
登入後複製

表现前三依然是 InIntSlice、InIntSliceSortedFunc 和 InIntSliceMapKeyFunc,但这次顺序发生了变化,MapKeyFunc 表现最好,17.6 ns/op,与数据量 10 的时候相比基本无变化。再次验证了前文的说法。

同样的,数据量 1000000 的时候。

$ go test -run=none -bench=1000000$ -benchmem
登入後複製

测试结果如下:

goos: darwin
goarch: amd64
pkg: github.com/poloxue/gotin
BenchmarkIn_1000000-8                                 30          46099678 ns/op         8000098 B/op    1000001 allocs/op
BenchmarkInIntSlice_1000000-8                       3000            424623 ns/op               0 B/op          0 allocs/op
BenchmarkInIntSliceSortedFunc_1000000-8         20000000                72.8 ns/op             0 B/op          0 allocs/op
BenchmarkSortInIntSlice_1000000-8                     10         138873420 ns/op              32 B/op          1 allocs/op
BenchmarkInIntSliceMapKeyFunc_1000000-8         100000000               16.5 ns/op             0 B/op          0 allocs/op
BenchmarkMapKeyInIntSlice_1000000-8                   10         156215889 ns/op        49824225 B/op      38313 allocs/op
PASS
ok      github.com/poloxue/gotin        15.178s
登入後複製

MapKeyFunc 依然表现最好,每次操作用时 17.2 ns,Sort 次之,而 InIntSlice 呈现线性增加的趋势。一般情况下,如果不是对性能要特殊要求,数据量特别大的场景,针对单类型的遍历已经有非常好的性能了。

从测试结果可以看出,反射实现的通用 In 函数每次执行需要进行大量的内存分配,方便的同时,也是以牺牲性能为代价的。

总结

本文通过一个问题引出主题,为什么 Go 中没有类似 Python 的 In 方法。我认为,一方面是实现非常简单,没有必要。除此以外,另一方面,在不同场景下,我们还需要根据实际情况分析用哪种方式实现,而不是一种固定的方式。

接着,我们介绍了 In 实现的三种方式,并分析了各自的优劣。通过性能分析测试,我们能得出大致的结论,什么方式适合什么场景,但总体还是不能说足够细致,有兴趣的朋友可以继续研究下。

【相关推荐:Go视频教程编程教学

以上是golang有沒有in的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!