Go語言實現(xiàn)數(shù)組去重的20種實現(xiàn)方式總結(jié)
數(shù)組去重是最常見的算法??此坪唵?,但不同實現(xiàn)方式的性能差異可能高達(dá)幾百倍。本文整理 Go 數(shù)組和切片去重的 20 種寫法,按 5 個策略分類,幫你理解每類的核心思路。AI時代,可以不手寫代碼了,但需要知道代碼背后的原理,這樣才能更好地指導(dǎo)AI編程。
為什么性能差異這么大?
最簡單的寫法,新建一個切片,把不在結(jié)果里的添加進(jìn)去。
func unique(arr []int) []int {
result := []int{}
for _, item := range arr {
// 如果當(dāng)前元素不在結(jié)果切片里,添加進(jìn)去
// slices.Contains 是 O(n) 線性掃描,整體則是 O(n2)
if !slices.Contains(result, item) {
result = append(result, item)
}
}
return result
}
問題在于每次 Contains 都要遍歷一遍 result,復(fù)雜度是 O(n²)。
優(yōu)化思路:換一種判重方式
- map / Set O(1) 查詢:
map[T]bool、map[T]struct{} - 排序 O(n log n):相同元素相鄰后掃一遍
- 泛型 + 標(biāo)準(zhǔn)庫:
slices.Compact一行搞定 - 位圖:
[]uint64實現(xiàn) BitSet,海量非負(fù)整數(shù)極致空間效率 - 遞歸:換種表達(dá)方式,本質(zhì)還是上面幾種
推薦方案
| 需求 | 代碼 | 性能 | 保序 |
|---|---|---|---|
| 通用工具 | unique[T comparable] 泛型函數(shù) | O(n) | ? |
| 標(biāo)準(zhǔn)庫一行 | slices.Sort(arr); slices.Compact(arr) | O(n log n) | 排序 |
| 最快無序 | map[T]struct{} 轉(zhuǎn)切片 | O(n) | ? |
| 海量整數(shù) | []uint64 位圖 | O(n) | ? |
第1類:基礎(chǔ)循環(huán)(方法1-6)
策略原理:不依賴 map 或泛型,純靠下標(biāo)、嵌套循環(huán)、slices.Index 這種"原始"手段完成去重。每一步判重都是 O(n),整體 O(n²)。
適用場景:教學(xué)、面試手撕、嵌入式或受限環(huán)境。生產(chǎn)代碼不建議使用。

// 方法1:雙循環(huán)索引比較——i 與左側(cè)每個 j 比對
func unique1(arr []int) []int {
result := make([]int, 0, len(arr))
for i := 0; i < len(arr); i++ {
for j := 0; j <= i; j++ {
if arr[i] == arr[j] {
// i == j 表示前面沒有相同值,是首次出現(xiàn)
if i == j {
result = append(result, arr[i])
}
break
}
}
}
return result
}
// 方法2:新建切片 + slices.Contains 檢查
func unique2(arr []int) []int {
result := make([]int, 0, len(arr))
for _, item := range arr {
// Contains 仍然是 O(n) 線性掃描
if !slices.Contains(result, item) {
result = append(result, item)
}
}
return result
}
// 方法3:從后往前原地刪除
// 倒序遍歷,與左側(cè)任意相同則刪除自身
func unique3(arr []int) []int {
l := len(arr)
for l > 0 {
l--
i := l
for i > 0 {
i--
if arr[l] == arr[i] {
// append 拼接相當(dāng)于 splice,刪除下標(biāo) l 處的元素
arr = append(arr[:l], arr[l+1:]...)
break
}
}
}
return arr
}
// 方法4:從前往后原地刪除(刪后面相同項)
func unique4(arr []int) []int {
l := len(arr)
for i := 0; i < l; i++ {
for j := i + 1; j < l; j++ {
if arr[i] == arr[j] {
arr = append(arr[:j], arr[j+1:]...)
j-- // 刪除后下標(biāo)回退
l-- // 長度同步減一
}
}
}
return arr[:l]
}
// 方法5:slices.Index 索引判等
// 首次位置等于當(dāng)前下標(biāo)即首次出現(xiàn)
func unique5(arr []int) []int {
result := make([]int, 0, len(arr))
for i, item := range arr {
if slices.Index(arr, item) == i {
result = append(result, item)
}
}
return result
}
// 方法6:從右往左跳過重復(fù)
// 倒序掃描,遇相同則把 i 整體左移跳過這一段重復(fù)區(qū)
func unique6(arr []int) []int {
n := len(arr)
tmp := make([]int, n)
x := n
for i := n - 1; i >= 0; i-- {
for j := i - 1; j >= 0; j-- {
if arr[i] == arr[j] {
i--
j = i
}
}
x--
tmp[x] = arr[i]
}
return tmp[x:]
}
第2類:map 與 Set(方法7-11)
策略原理:Go 沒有原生 Set 類型,但 map[T]struct{} 是社區(qū)公認(rèn)的 Set 慣用法——struct{} 不占內(nèi)存,只用鍵去重。把數(shù)據(jù)塞進(jìn) map,去重就自然完成。
map[T]bool:最直觀,bool 值 1 字節(jié)map[T]struct{}:節(jié)省內(nèi)存,工程首選- 自定義
Set結(jié)構(gòu)體:封裝 Add/Contains 接口,復(fù)用性更強 - 頻次 map:去重同時統(tǒng)計
代價是元素必須可比較(Go 中"可比較"指能用 == 比較,包含基本類型、指針、可比較的結(jié)構(gòu)體等)。
適用場景:日常項目首選。需要保序就維護一個 result 切片,寫入時同步推到結(jié)果尾。

// 方法7:map[int]bool 顯式判重
func unique7(arr []int) []int {
seen := make(map[int]bool, len(arr))
result := make([]int, 0, len(arr))
for _, item := range arr {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// 方法8:map[int]struct{} 空值集合——Set 慣用法
func unique8(arr []int) []int {
seen := make(map[int]struct{}, len(arr))
result := make([]int, 0, len(arr))
for _, item := range arr {
if _, ok := seen[item]; !ok {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
// 方法9:自定義 Set 結(jié)構(gòu)體
type IntSet struct {
data map[int]struct{}
}
func newIntSet() *IntSet { return &IntSet{data: map[int]struct{}{}} }
func (s *IntSet) Add(v int) { s.data[v] = struct{}{} }
func (s *IntSet) Contains(v int) bool { _, ok := s.data[v]; return ok }
// 方法10:直接 map 轉(zhuǎn)切片——寫法最短,但順序隨機
func unique10(arr []int) []int {
m := make(map[int]struct{}, len(arr))
for _, item := range arr {
m[item] = struct{}{}
}
result := make([]int, 0, len(m))
for k := range m {
// Go 的 map 遍歷順序是隨機的(Go 團隊故意設(shè)計的)
result = append(result, k)
}
return result
}
// 方法11:頻次統(tǒng)計 map——去重 + 業(yè)務(wù)統(tǒng)計
func unique11(arr []int) []int {
count := make(map[int]int, len(arr))
result := make([]int, 0, len(arr))
for _, item := range arr {
if count[item] == 0 {
result = append(result, item)
}
count[item]++ // 累加頻次
}
return result
}
第3類:排序后去重(方法12-14)
策略原理:先 sort.Ints 讓相同元素相鄰,再掃一遍刪除相鄰相同項。復(fù)雜度由排序決定,O(n log n)。優(yōu)點是不需要額外哈希結(jié)構(gòu),"相鄰判等"是最便宜的判重方式;缺點是會破壞原順序,且要求元素可比較(基本類型直接 OK,自定義類型需要 sort.Slice 配合)。
適用場景:輸出本就需要排序、不在意原順序、內(nèi)存敏感。

// 方法12:排序后從后往前刪
func unique12(arr []int) []int {
sort.Ints(arr)
for l := len(arr) - 1; l > 0; l-- {
if arr[l] == arr[l-1] {
arr = append(arr[:l], arr[l+1:]...)
}
}
return arr
}
// 方法13:排序后從前往后刪
func unique13(arr []int) []int {
sort.Ints(arr)
l := len(arr) - 1
for i := 0; i < l; i++ {
if arr[i] == arr[i+1] {
arr = append(arr[:i], arr[i+1:]...)
i--
l--
}
}
return arr
}
// 方法14:經(jīng)典雙指針(LeetCode 26 題解法)
// 原地排序后,在原切片上原地去重,O(1) 額外空間
func unique14(arr []int) []int {
if len(arr) == 0 {
return arr
}
sort.Ints(arr)
slow := 0
for fast := 1; fast < len(arr); fast++ {
// 快指針發(fā)現(xiàn)新值,slow 前進(jìn)一步并寫入
if arr[fast] != arr[slow] {
slow++
arr[slow] = arr[fast]
}
}
return arr[:slow+1]
}
第4類:泛型與函數(shù)式(方法15-17)
策略原理:Go 1.18 引入泛型,讓"通用工具函數(shù)"成為可能;Go 1.21 標(biāo)準(zhǔn)庫新增 slices.Compact,把"已排序切片相鄰去重"做成一行。函數(shù)式方面,閉包 + 高階函數(shù)也能模擬其他語言里的 filter。
適用場景:現(xiàn)代 Go 工程的常態(tài)寫法??勺x性高、復(fù)用性好,特別適合編寫通用的工具庫。

// 方法15:泛型去重(Go 1.18+)
// comparable 約束保證 == 比較有效
func unique15[T comparable](arr []T) []T {
seen := make(map[T]struct{}, len(arr))
result := make([]T, 0, len(arr))
for _, item := range arr {
if _, ok := seen[item]; !ok {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
// 方法16:標(biāo)準(zhǔn)庫 slices.Compact(Go 1.21+)
// Compact 刪除相鄰重復(fù)元素,要求已排序
func unique16(arr []int) []int {
slices.Sort(arr)
return slices.Compact(arr)
}
// 方法17:高階 filter + 閉包謂詞
func filter[T any](arr []T, pred func(T) bool) []T {
result := make([]T, 0, len(arr))
for _, item := range arr {
if pred(item) {
result = append(result, item)
}
}
return result
}
// 方法17:高階函數(shù)式法, 用閉包封裝"已存在"狀態(tài),模擬函數(shù)式 filter
func unique17(arr []int) []int {
seen := make(map[int]struct{}, len(arr))
// 閉包捕獲 seen,謂詞帶副作用:首次見到才返回 true
return filter(arr, func(x int) bool {
if _, ok := seen[x]; ok {
return false
}
seen[x] = struct{}{}
return true
})
}
第5類:遞歸與位圖(方法18-20)
策略原理:遞歸用自調(diào)用替代循環(huán),是函數(shù)式思維的體現(xiàn),主要用于教學(xué);位圖用 []uint64 自己實現(xiàn) BitSet——每一位標(biāo)記一個非負(fù)整數(shù)是否出現(xiàn)過,對整數(shù)集合有極致的空間效率(10 億個 int 只要 128MB),是大數(shù)據(jù)去重的常見選型。
適用場景:遞歸——教學(xué)、題型熟悉;BitSet——大規(guī)模非負(fù)整數(shù)(如用戶 ID、訂單號)的去重統(tǒng)計。

// 方法18:遞歸原地刪除
func unique18(arr []int, length int) []int {
if length < 1 {
return arr
}
last := length - 1
for i := last - 1; i >= 0; i-- {
if arr[last] == arr[i] {
arr = append(arr[:last], arr[last+1:]...)
break
}
}
return unique18(arr, length-1)
}
// 方法19:遞歸拼接返回(不修改原切片,純函數(shù)式)
func unique19(arr []int, length int) []int {
if length < 1 {
return []int{}
}
last := length - 1
isRepeat := false
for i := last - 1; i >= 0; i-- {
if arr[last] == arr[i] {
isRepeat = true
break
}
}
head := unique19(arr, length-1)
if !isRepeat {
head = append(head, arr[last])
}
return head
}
// 方法20:BitSet 位圖(僅適用于非負(fù)整數(shù))
// 用 []uint64 自己實現(xiàn)位圖,每個 int 占一位
func unique20(arr []int) []int {
maxVal := 0
for _, v := range arr {
if v < 0 {
panic("BitSet 不支持負(fù)數(shù),需要先偏移")
}
if v > maxVal {
maxVal = v
}
}
bits := make([]uint64, maxVal/64+1)
result := make([]int, 0, len(arr))
for _, v := range arr {
// 第 v 位為 0 表示首次出現(xiàn)
if bits[v/64]&(1<<(v%64)) == 0 {
bits[v/64] |= 1 << (v % 64)
result = append(result, v)
}
}
return result
}
選擇指南

| 類別 | 時間復(fù)雜度 | 是否保序 | 主要場景 |
|---|---|---|---|
| 基礎(chǔ)循環(huán) | O(n²) | 是 | 教學(xué)、面試手撕 |
| map / Set | O(n) | 看實現(xiàn) | 日常項目首選 |
| 排序后去重 | O(n log n) | 否(變排序) | 順便要排序 |
| 泛型與函數(shù)式 | O(n) | 是 | 現(xiàn)代 Go 通用工具 |
| 遞歸 / 位圖 | 視實現(xiàn) | 看實現(xiàn) | 教學(xué) / 海量整數(shù) |
實際項目里怎么選
絕大多數(shù)情況一個泛型函數(shù)就夠:
// 保序、O(n)、對所有 comparable 類型有效
func unique[T comparable](arr []T) []T {
seen := make(map[T]struct{}, len(arr))
result := make([]T, 0, len(arr))
for _, item := range arr {
if _, ok := seen[item]; !ok {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
不在意順序:
m := make(map[int]struct{})
for _, v := range data {
m[v] = struct{}{}
}
result := make([]int, 0, len(m))
for k := range m {
result = append(result, k)
}
需要排序:
slices.Sort(data) data = slices.Compact(data) // Go 1.21+
海量非負(fù)整數(shù):
// 用 []uint64 自己實現(xiàn)位圖,10 億規(guī)模也只要 ~128MB
bits := make([]uint64, maxVal/64+1)
for _, v := range data {
bits[v/64] |= 1 << (v % 64)
}
帶業(yè)務(wù)邏輯的去重
實際工作里經(jīng)常遇到這樣的情況:遇到重復(fù)時不能簡單丟棄,要按某個規(guī)則做處理。比如:
- 按
ID去重,但要保留分?jǐn)?shù)最高的那條記錄 - 去重的同時累加重復(fù)次數(shù)
- 數(shù)值在某個區(qū)間內(nèi)才參與去重
這類需求 map 直接搞不定,需要把"判重"和"處理"兩步拆開來寫。Go 里通常用泛型函數(shù) + 合并函數(shù):
// UniqueBy 帶業(yè)務(wù)規(guī)則的去重。
//
// keyFn 從元素提取去重鍵。
// onDup 遇到重復(fù)時如何合并 (舊值, 新值) -> 新代表值。
func UniqueBy[T any, K comparable](
data []T,
keyFn func(T) K,
onDup func(old, new T) T,
) []T {
chosen := make(map[K]T)
order := make([]K, 0)
for _, item := range data {
k := keyFn(item)
if _, ok := chosen[k]; !ok {
chosen[k] = item
order = append(order, k)
} else if onDup != nil {
chosen[k] = onDup(chosen[k], item)
}
}
result := make([]T, 0, len(order))
for _, k := range order {
result = append(result, chosen[k])
}
return result
}
例 1:按 ID 去重,保留分?jǐn)?shù)最高的:
type Student struct {
ID int
Name string
Score int
}
students := []Student{
{ID: 1, Name: "張三", Score: 90},
{ID: 1, Name: "張三", Score: 95}, // 同 id,分?jǐn)?shù)更高
{ID: 2, Name: "李四", Score: 85},
}
result := UniqueBy(
students,
func(s Student) int { return s.ID },
func(old, new Student) Student {
if new.Score > old.Score {
return new
}
return old
},
)
// result: [{1 張三 95} {2 李四 85}]
例 2:去重同時統(tǒng)計頻次:
counts := make(map[string]int)
order := []string{}
for _, item := range data {
if _, ok := counts[item]; !ok {
order = append(order, item)
}
counts[item]++
}
// order 是保序的去重結(jié)果,counts 是頻次統(tǒng)計
例 3:區(qū)間過濾——只對 [0, 100] 區(qū)間內(nèi)的值去重,區(qū)間外原樣保留:
seen := make(map[int]struct{})
result := []int{}
for _, x := range data {
if x >= 0 && x <= 100 {
if _, dup := seen[x]; dup {
continue
}
seen[x] = struct{}{}
}
result = append(result, x)
}
這三個例子是同一種思路:把判重與業(yè)務(wù)規(guī)則分開。判重用 map 保證 O(n),規(guī)則部分留給回調(diào)或顯式分支處理。
自定義對象去重:comparable 約束
Go 的"可比較"概念比 Java 的 equals 更嚴(yán)格——不是所有類型都能直接進(jìn) map 當(dāng)鍵:
| 類型 | 是否可比較 | 備注 |
|---|---|---|
| 基本類型(int, string, bool 等) | ? | 直接可用 |
| 指針 | ? | 比較指針地址 |
數(shù)組 [N]T | ? | 元素可比較即可 |
| 字段全部可比較的 struct | ? | 逐字段比較 |
| 接口類型 | ? | 但運行時 panic 風(fēng)險 |
| slice / map / func | ? | 不可比較 |
| 含 slice 字段的 struct | ? | 不可比較 |
如果元素是含 slice 的 struct,需要自己提取一個可比較的"鍵":
type User struct {
ID int64
Name string
Tags []string // 不可比較!
}
// 用 ID 作為去重鍵
seen := make(map[int64]struct{})
result := []User{}
for _, u := range users {
if _, ok := seen[u.ID]; !ok {
seen[u.ID] = struct{}{}
result = append(result, u)
}
}
注意:Go 沒有 equals/hashCode 重寫機制。要按業(yè)務(wù)字段去重,就要么自己定義 keyFn,要么把"業(yè)務(wù)等價"壓進(jìn)一個可比較的 struct 字段。
總結(jié)
工程應(yīng)用選擇:
- 默認(rèn)用泛型
unique[T comparable](arr []T) []T:保序、一行、O(n) - 標(biāo)準(zhǔn)庫一步到位
slices.Sort+slices.Compact:原地、O(n log n)、順便排序 - 不要順序就直接
map[T]struct{}轉(zhuǎn)切片 - 海量非負(fù)整數(shù)用
[]uint64自實現(xiàn)位圖 - 含 slice 字段的 struct 先提取可比較鍵再去重
- 業(yè)務(wù)規(guī)則干預(yù)用 keyFn + 合并函數(shù)
核心思路:
- 同一個問題可以從多個角度切入
- 選對數(shù)據(jù)結(jié)構(gòu)往往比寫更聰明的代碼更重要
- O(n²) 與 O(n) 在數(shù)據(jù)變大時是幾百倍的實際差距
- 不要過度優(yōu)化——能用
slices.Compact就別繞彎 - 遇到新問題先寫最直觀的版本,再按瓶頸逐步優(yōu)化
到此這篇關(guān)于Go語言實現(xiàn)數(shù)組去重的20種實現(xiàn)方式總結(jié)的文章就介紹到這了,更多相關(guān)Go數(shù)組去重內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VsCode下開發(fā)Go語言的環(huán)境配置超詳細(xì)圖文詳解
vscode是一款跨平臺、輕量級、插件多的開源IDE,在vscode不僅可以配置C/C++、Python、R、Ruby等語言的環(huán)境,還可以配置Go語言的環(huán)境,下面這篇文章主要給大家介紹了關(guān)于VsCode下開發(fā)Go語言的環(huán)境配置,需要的朋友可以參考下2024-03-03
golang gin ShouldBind的介紹和使用示例詳解
在 Go 語言的 Gin 框架中,ShouldBind 是用于將請求中的數(shù)據(jù)綁定到結(jié)構(gòu)體的一個方法,它簡化了從請求中提取參數(shù)的過程,支持多種數(shù)據(jù)格式,下面給大家分享golang gin ShouldBind的介紹和使用示例,感興趣的朋友一起看看吧2024-10-10
Go語言實現(xiàn)一個簡單的并發(fā)聊天室的項目實戰(zhàn)
本文主要介紹了Go語言實現(xiàn)一個簡單的并發(fā)聊天室的項目實戰(zhàn),文中根據(jù)實例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03

