Go语言虽然包含丰富的标准库,但很多有用的函数还是要自己实现。今天我们要学习的是如何快速的删除切片中的重复元素。假设我们需要写一个函数实现用户ID切片元素去重。

方法1: Go安全方法

该方法在保持原切片不变的情况下很有用。创建一个新的切片只保存不同的元素。我们使用map的键来判别重复元素,map的值为空结构体不占内存。

func RemoveDuplicates(userIDs []int64) []int64 {
    // 只需要检查key,因此value使用空结构体,不占内存
    processed := make(map[int64]struct{})

    uniqUserIDs := make([]int64, 0)
    for _, uid := range userIDs {
        // 如果用户ID已经处理过,就跳过
        if _, ok := processed[uid]; ok {
            continue
        }

        // 将唯一的用户ID加到切片中
        uniqUserIDs = append(uniqUserIDs, uid)

        // 将用户ID标记为已存在
        processed[uid] = struct{}{}
    }

    return uniqUserIDs
}

方法2: 在原切片中移动元素

该方法能满足您的性能要求,并且可以修改原切片。首先对切片排序,然后通过两个迭代器:元素唯一迭代器和常规迭代器。通过一次遍历切片,返回一个包含不同元素的子切片。

import (
    "sort"
)

func RemoveDuplicatesInPlace(userIDs []int64) []int64 {
    // 如果有0或1个元素,则返回切片本身。
    if len(userIDs) < 2 {
        return userIDs
    }

    //  使切片升序排序
    sort.SliceStable(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })

    uniqPointer := 0

    for i := 1; i < len(userIDs); i++ {
        // 比较当前元素和唯一指针指向的元素
        //  如果它们不相同,则将项写入唯一指针的右侧。
        if userIDs[uniqPointer] != userIDs[i] {
            uniqPointer++
            userIDs[uniqPointer] = userIDs[i]
        }
    }

    return userIDs[:uniqPointer+1]
}

基准测试

让我们来测试这两种方法,看看它们在性能上有何不同。我们通过helper函数生成一个随机切片:

func generateSlice() []int64 {
    s := make([]int64, 0, l)

    for i := 0; i < 1_000_000; i++ {
        s = append(s, rand.Int63())
    }

    return s
}

基准测试代码:

func BenchmarkRemoveDuplicates(b *testing.B) {
    s := generateSlice()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        RemoveDuplicates(s)
    }
}

func BenchmarkRemoveDuplicatesInPlace(b *testing.B) {
    s := generateSlice()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        RemoveDuplicatesInPlace(s)
    }
}

1000个随机元素的测试结果:

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkRemoveDuplicates
BenchmarkRemoveDuplicates-8                14552         82441 ns/op       64115 B/op         76 allocs/op
BenchmarkRemoveDuplicatesInPlace
BenchmarkRemoveDuplicatesInPlace-8        238521          4852 ns/op          56 B/op          2 allocs/op

1000k个随机元素结果:

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkRemoveDuplicates
BenchmarkRemoveDuplicates-8                    6     169626486 ns/op    95007038 B/op      38356 allocs/op
BenchmarkRemoveDuplicatesInPlace
BenchmarkRemoveDuplicatesInPlace-8            82      13069547 ns/op          56 B/op          2 allocs/op

很明显第二种方法比第一种快了大概15倍,没有内存分配。第一次方法使用append函数会触发内存分配,元素拷贝影响效率。

总结

现在我们知道如何在Go切片中以不同的方式删除重复元素。根据您的项目和目标,您可以使用适合您需求的最佳方法。