最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

TypeScript數(shù)組去重的20種實(shí)現(xiàn)方式

 更新時(shí)間:2026年05月09日 08:28:10   作者:刀法如飛  
文章主要介紹了使用TypeScript實(shí)現(xiàn)數(shù)組去重的20種方法,從基礎(chǔ)循環(huán)、內(nèi)置數(shù)組方法、集合容器、排序后去重和遞歸與特殊等五個(gè)策略進(jìn)行分類,并詳細(xì)講解了每種方法的實(shí)現(xiàn)原理、適用場(chǎng)景以及性能特點(diǎn),此外,還提供了選擇不同去重方法的建議和實(shí)際項(xiàng)目中的應(yīng)用示例

數(shù)組去重是最常見(jiàn)的編程算法,非常簡(jiǎn)單,但也可以有很多的實(shí)現(xiàn)方案。TypeScript 在 JavaScript 的基礎(chǔ)上加了靜態(tài)類型,讓通用工具函數(shù)可以用泛型寫(xiě)一次、對(duì)所有可比較類型可用。本文整理 TS 數(shù)組去重的 20 種寫(xiě)法,按 5 個(gè)策略分類。AI時(shí)代,可以不手寫(xiě)代碼了,但需要知道代碼背后的原理,這樣才能更好地指導(dǎo)AI編程。

為什么性能差異這么大?

最簡(jiǎn)單的寫(xiě)法,新建一個(gè)數(shù)組,把不在結(jié)果里的添加進(jìn)去。

function unique<T>(arr: T[]): T[] {
  const result: T[] = []
  for (const item of arr) {
    // includes 是 O(n) 線性掃描,整體則是 O(n2)
    if (!result.includes(item)) {
      result.push(item)
    }
  }
  return result
}

問(wèn)題在于每次 includes 都要全量掃一遍 result,復(fù)雜度是 O(n²)。

優(yōu)化思路:換一種判重方式

  • Set / Map O(1) 查詢:new Set(arr)
  • 排序 O(n log n):相同元素相鄰后掃一遍
  • filter + 閉包:在函數(shù)式管道里攜帶"已見(jiàn)"狀態(tài)
  • JSON 序列化:處理對(duì)象、嵌套數(shù)組等不可哈希元素
  • 遞歸:換種表達(dá)方式,本質(zhì)仍是上面的思路

TS 相比 JS 的優(yōu)勢(shì)

  • 泛型 <T>:寫(xiě)一次、對(duì)所有類型類型安全可用
  • 類型約束T extends string | number 限定基本類型,避免對(duì)象誤用 Object 字面量
  • 編譯期校驗(yàn):傳入錯(cuò)誤類型立即報(bào)錯(cuò),不會(huì)在運(yùn)行時(shí)才崩

推薦方案

需求代碼性能保序
一行最簡(jiǎn)[...new Set<T>(arr)]O(n)?
函數(shù)式 + Setarr.filter(x => !seen.has(x) && seen.add(x))O(n)?
按字段去重[...new Map(arr.map(x => [x.id, x])).values()]O(n)?
對(duì)象數(shù)組JSON.stringify 作為 Set 的鍵O(n×m)?

第1類:基礎(chǔ)循環(huán)(方法1-6)

策略原理:不用任何內(nèi)置數(shù)組方法,純靠下標(biāo)、嵌套循環(huán)、indexOf 這種"原始"手段完成去重。每一步判重都是 O(n),整體 O(n²)。

適用場(chǎng)景:教學(xué)、面試手撕。生產(chǎn)代碼不建議使用。

// 方法1:雙循環(huán)索引比較——i 與左側(cè)每個(gè) j 比對(duì)
static unique1<T>(arr: T[]): T[] {
  const result: T[] = []
  for (let i = 0, l = arr.length; i < l; i++) {
    for (let j = 0; j <= i; j++) {
      if (arr[i] === arr[j]) {
        // i === j 表示前面沒(méi)有相同值,是首次出現(xiàn)
        if (i === j) result.push(arr[i])
        break
      }
    }
  }
  return result
}

// 方法2:新建數(shù)組 + includes 檢查
static unique2<T>(arr: T[]): T[] {
  const result: T[] = []
  for (const item of arr) {
    // includes 是 O(n) 線性掃描,每個(gè)元素都要掃描一次,整體是 O(n2)
    if (!result.includes(item)) {
      result.push(item)
    }
  }
  return result
}

// 方法3:從后往前原地 splice
static unique3<T>(arr: T[]): T[] {
  let l = arr.length
  while (l-- > 0) {
    // 從后往前遍歷,避免刪除后索引變化導(dǎo)致跳過(guò)元素
    // 每個(gè)元素都要掃描一次,整體是 O(n2)
    for (let i = 0; i < l; i++) {
      if (arr[l] === arr[i]) {
        arr.splice(l, 1)
        break
      }
    }
  }
  return arr
}

// 方法4:從前往后原地 splice(刪后面相同項(xiàng))
static unique4<T>(arr: T[]): T[] {
  let l = arr.length
  for (let i = 0; i < l; i++) {
    // 從前往后遍歷,每個(gè)元素都要掃描一次,整體是 O(n2)
    for (let j = i + 1; j < l; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1)
        j--; l--
      }
    }
  }
  return arr
}

// 方法5:forEach + indexOf
// indexOf 返回首次出現(xiàn)下標(biāo),等于當(dāng)前下標(biāo)即首次
static unique5<T>(arr: T[]): T[] {
  const result: T[] = []
  // forEach 是 O(n) 線性掃描,每個(gè)元素都要掃描一次
  arr.forEach((item, i) => {
    if (arr.indexOf(item) === i) result.push(item)
  })
  return result
}

// 方法6:雙重 while 倒序 splice
static unique6<T>(arr: T[]): T[] {
  let l = arr.length
  while (l-- > 0) {
    let i = l
    // 從后往前遍歷,每個(gè)元素都要掃描一次,整體是 O(n2)
    while (i-- > 0) {
      if (arr[l] === arr[i]) {
        arr.splice(l, 1)
        break
      }
    }
  }
  return arr
}

所有泛型方法的 T 不需要額外約束——=== 比較對(duì)所有 TS 類型都有效(雖然引用類型只比指針)。

第2類:內(nèi)置數(shù)組方法(方法7-11)

策略原理:JavaScript 數(shù)組自帶 filter、reduce、forEach 等高階方法,可以把"判重 + 收集"寫(xiě)成函數(shù)式風(fēng)格。注意 indexOf / includes 仍是 O(n),需要用 Set<T> 閉包才能壓到 O(n)。

適用場(chǎng)景:現(xiàn)代 TS 工程的常態(tài)寫(xiě)法??勺x性高,鏈?zhǔn)浇M合方便。

// 方法7:filter + indexOf 一行經(jīng)典
// indexOf 返回首次出現(xiàn)下標(biāo),等于當(dāng)前下標(biāo)即首次出現(xiàn),用 filter 過(guò)濾出首次出現(xiàn)的元素
static unique7<T>(arr: T[]): T[] {
  return arr.filter((item, i) => arr.indexOf(item) === i)
}

// 方法8:filter + Set 閉包——推薦寫(xiě)法
// Set.add 返回 Set 自身(truthy),結(jié)合短路 && 實(shí)現(xiàn)首次見(jiàn)到才返回 true
static unique8<T>(arr: T[]): T[] {
  const seen = new Set<T>()
  return arr.filter(item => !seen.has(item) && !!seen.add(item))
}

// 方法9:reduce 累加(用數(shù)組)
// 函數(shù)式風(fēng)格,但 includes 仍是 O(n2)
// 注意 reduce 的泛型參數(shù) T[],初始值為 [] as T[]
static unique9<T>(arr: T[]): T[] {
  // 用 reduce 累加數(shù)組,每次判斷是否已存在,不存在則添加
  return arr.reduce<T[]>((acc, item) => {
    if (!acc.includes(item)) acc.push(item)
    return acc
  }, [])
}

// 方法10:reduce + Set 閉包——O(n) 函數(shù)式
static unique10<T>(arr: T[]): T[] {
  const seen = new Set<T>()
  // 用 reduce 累加數(shù)組,每次判斷是否已存在,不存在則添加
  return arr.reduce<T[]>((acc, item) => {
    if (!seen.has(item)) {
      seen.add(item)
      acc.push(item)
    }
    return acc
  }, [])
}

// 方法11:Object + typeof 鍵
// 用 typeof + value 拼成字符串作為對(duì)象鍵,避免 1 與 '1' 沖突
// 類型約束 T extends string | number | boolean 限制為基本類型
static unique11<T extends string | number | boolean>(arr: T[]): T[] {
  const obj: Record<string, true> = {}
  // 用 filter 過(guò)濾出首次出現(xiàn)的元素,用 typeof + value 拼成字符串作為對(duì)象鍵
  return arr.filter(item => {
    const key = typeof item + String(item)
    return Object.prototype.hasOwnProperty.call(obj, key)
      ? false
      : (obj[key] = true)
  })
}

TS 加分項(xiàng)T extends string | number | boolean 限定調(diào)用方只能傳基本類型數(shù)組,對(duì)象數(shù)組在編譯期就會(huì)報(bào)錯(cuò)——避免運(yùn)行時(shí)陷阱。

第3類:集合容器(方法12-14)

策略原理:ES6 引入的 SetMap 用 SameValueZero 算法判等,鍵唯一且 O(1),是 JS/TS 里最自然的去重工具。Object 字面量雖然也能當(dāng)哈希用,但有"鍵自動(dòng)字符串化""數(shù)字鍵被引擎重排"等坑。

適用場(chǎng)景:日常項(xiàng)目首選 Set;需要保留 value 選 Map;只在小數(shù)據(jù)或特殊兼容場(chǎng)景才用 Object。

// 方法12:new Set 轉(zhuǎn)數(shù)組——一行經(jīng)典
// Set<T> 用 SameValueZero 比較,NaN 也能正確去重
static unique12<T>(arr: T[]): T[] {
  return [...new Set(arr)]
}

// 方法13:Map<T, T> + keys
// 適合"按鍵去重,值攜帶其他信息"的場(chǎng)景
static unique13<T>(arr: T[]): T[] {
  const map = new Map<T, T>()
  // 用 Map<T, T> + keys 轉(zhuǎn)數(shù)組,保持插入順序
  arr.forEach(item => map.set(item, item))
  return [...map.keys()]
}

// 方法14:Object 字面量哈?!猅 extends string | number 防誤用
// 注意:1 與 '1' 會(huì)被合并;數(shù)字鍵會(huì)被引擎按升序重排
static unique14<T extends string | number>(arr: T[]): T[] {
  const obj = {} as Record<string, T>
  // 用 Object 字面量哈希,鍵自動(dòng)字符串化,數(shù)字鍵會(huì)被引擎按升序重排
  for (const item of arr) obj[String(item)] = item
  return Object.values(obj)
}

TS 類型提醒Map<K, V> 的兩個(gè)泛型參數(shù)讓你顯式聲明鍵值類型,比 JS 的 new Map() 更安全。如果按業(yè)務(wù)字段去重,可以寫(xiě) new Map<number, User>() 表明鍵是 id(number),值是 User。

第4類:排序后去重(方法15-17)

策略原理:先 sort 讓相同元素相鄰,再掃一遍刪除相鄰相同項(xiàng)。復(fù)雜度由排序決定,O(n log n)。優(yōu)點(diǎn)是不需要額外的哈希結(jié)構(gòu),"相鄰判等"是最便宜的判重方式;缺點(diǎn)是會(huì)破壞原順序。

適用場(chǎng)景:輸出本就需要排序、不在意原順序。

// 方法15:sort + splice 升序去重(僅 number[])
// JS sort 不傳比較函數(shù)會(huì)按字符串排序,必須傳 (a, b) => a - b
static unique15(arr: number[]): number[] {
  arr.sort((a, b) => a - b)
  let l = arr.length
  // 先排序,從后往前遍歷,相鄰元素相同則刪除當(dāng)前元素
  while (l-- > 1) {
    if (arr[l] === arr[l - 1]) arr.splice(l, 1)
  }
  return arr
}

// 方法16:sort + filter 相鄰判重
static unique16(arr: number[]): number[] {
  arr.sort((a, b) => a - b)
  // 先排序,從后往前遍歷,相鄰元素相同則刪除當(dāng)前元素
  return arr.filter((item, i) => i === 0 || item !== arr[i - 1])
}

// 方法17:經(jīng)典雙指針(LeetCode 26)
// 排序后原地雙指針,O(1) 額外空間
static unique17(arr: number[]): number[] {
  if (arr.length === 0) return arr
  arr.sort((a, b) => a - b)
  let slow = 0
  // 先排序,從后往前遍歷,相鄰元素相同則刪除當(dāng)前元素
  for (let fast = 1; fast < arr.length; fast++) {
    if (arr[fast] !== arr[slow]) {
      arr[++slow] = arr[fast]
    }
  }
  return arr.slice(0, slow + 1)
}

泛化排序的難點(diǎn):要讓排序方法也支持任意 T,得讓調(diào)用方傳 compareFn: (a: T, b: T) => number——參考 Array.prototype.sort 的設(shè)計(jì)。這里為簡(jiǎn)明起見(jiàn)限定為 number[]。

第5類:遞歸與特殊(方法18-20)

策略原理:遞歸用自調(diào)用替代循環(huán),是函數(shù)式思維的體現(xiàn),主要用于教學(xué)。JSON.stringify 把對(duì)象映射為字符串,是處理"不可哈希元素"(對(duì)象數(shù)組、嵌套數(shù)組)的常見(jiàn)招數(shù)。

適用場(chǎng)景:遞歸——教學(xué);JSON——對(duì)象數(shù)組按整體結(jié)構(gòu)去重。

// 方法18:遞歸原地刪除
static unique18<T>(arr: T[], length: number): T[] {
  if (length <= 1) return arr
  const last = length - 1
  // 從后往前遍歷,檢查末尾元素是否在前面出現(xiàn)
  for (let i = last - 1; i >= 0; i--) {
    if (arr[last] === arr[i]) {
      arr.splice(last, 1)
      break
    }
  }
  return UniqueArray.unique18(arr, length - 1)
}

// 方法19:遞歸拼接返回(不修改原數(shù)組)
static unique19<T>(arr: T[], length: number): T[] {
  if (length <= 1) return arr.slice(0, length)
  const last = length - 1
  const lastItem = arr[last]
  let isRepeat = false
  // 從后往前遍歷,檢查末尾元素是否在前面出現(xiàn)
  for (let i = last - 1; i >= 0; i--) {
    if (lastItem === arr[i]) {
      isRepeat = true
      break
    }
  }
  const head = UniqueArray.unique19(arr, length - 1)
  return isRepeat ? head : head.concat(lastItem)
}

// 方法20:JSON 字符串判重——處理對(duì)象數(shù)組
// 把對(duì)象序列化成字符串作為 Set 的鍵,能去重 {id:1} 這類結(jié)構(gòu)
static unique20<T>(arr: T[]): T[] {
  const seen = new Set<string>()
  const result: T[] = []
  // 遍歷數(shù)組,把每個(gè)對(duì)象序列化成字符串作為 Set 的鍵
  for (const item of arr) {
    const key = JSON.stringify(item)
    if (!seen.has(key)) {
      seen.add(key)
      result.push(item)
    }
  }
  return result
}

// 用法示例:
// UniqueArray.unique20<{id: number}>([{id: 1}, {id: 2}, {id: 1}])
// => [{id: 1}, {id: 2}]

JSON 的兩個(gè)限制:① 字段順序不同的對(duì)象會(huì)被認(rèn)為不同({a:1,b:2}{b:2,a:1});② undefined、函數(shù)、循環(huán)引用會(huì)丟失或拋錯(cuò)。

選擇指南

類別時(shí)間復(fù)雜度是否保序主要場(chǎng)景
基礎(chǔ)循環(huán)O(n²)教學(xué)、面試手撕
內(nèi)置數(shù)組方法O(n) ~ O(n²)函數(shù)式風(fēng)格
集合容器O(n)看具體類日常項(xiàng)目首選
排序后去重O(n log n)順便要排序
遞歸 / JSON視實(shí)現(xiàn)看實(shí)現(xiàn)教學(xué) / 對(duì)象數(shù)組

實(shí)際項(xiàng)目里怎么選

絕大多數(shù)情況一行就夠:

// 保序、O(n)、寫(xiě)法最短,工程首選
const result = [...new Set<T>(arr)]

// 或函數(shù)式風(fēng)格,O(n)
const seen = new Set<T>()
const result = arr.filter(x => !seen.has(x) && !!seen.add(x))

按業(yè)務(wù)字段去重(最常用):

interface User {
  id: number
  name: string
}

// 按 id 去重
const result = [...new Map(users.map(u => [u.id, u])).values()]

對(duì)象數(shù)組按整體結(jié)構(gòu)去重:

const seen = new Set<string>()
const result = arr.filter(x => {
  const key = JSON.stringify(x)
  return !seen.has(key) && !!seen.add(key)
})

需要排序:

const result = [...new Set(arr)].sort((a, b) => a - b)

帶業(yè)務(wù)邏輯的去重

實(shí)際工作里經(jīng)常遇到這樣的情況:遇到重復(fù)時(shí)不能簡(jiǎn)單丟棄,要按某個(gè)規(guī)則做處理。比如:

  • id 去重,但要保留分?jǐn)?shù)最高的那條記錄
  • 去重的同時(shí)累加重復(fù)次數(shù)
  • 數(shù)值在某個(gè)區(qū)間內(nèi)才參與去重

這類需求 Set 直接搞不定,需要把"判重"和"處理"兩步拆開(kāi)來(lái)寫(xiě)。TS 里通常用泛型 Map<K, V> + 合并函數(shù):

/**
 * 帶業(yè)務(wù)規(guī)則的去重。
 *
 * @param data 原數(shù)據(jù)
 * @param keyFn 從元素提取去重鍵
 * @param onDup 遇到重復(fù)時(shí)如何合并 (舊值, 新值) -> 新代表值
 */
function uniqueBy<T, K>(
  data: T[],
  keyFn: (item: T) => K,
  onDup?: (oldVal: T, newVal: T) => T,
): T[] {
  // Map 保證遍歷順序與首次出現(xiàn)順序一致
  const chosen = new Map<K, T>()
  for (const item of data) {
    const key = keyFn(item)
    if (!chosen.has(key)) {
      chosen.set(key, item)
    } else if (onDup) {
      chosen.set(key, onDup(chosen.get(key)!, item))
    }
  }
  return [...chosen.values()]
}

例 1:按 id 去重,保留分?jǐn)?shù)最高的:

interface Student {
  id: number
  name: string
  score: number
}

const students: Student[] = [
  { id: 1, name: '張三', score: 90 },
  { id: 1, name: '張三', score: 95 },   // 同 id,分?jǐn)?shù)更高
  { id: 2, name: '李四', score: 85 },
]

const result = uniqueBy(
  students,
  s => s.id,
  (oldS, newS) => newS.score > oldS.score ? newS : oldS,
)
// [{id:1, score:95, ...}, {id:2, score:85, ...}]

例 2:去重同時(shí)統(tǒng)計(jì)頻次:

const counts = new Map<string, number>()
for (const item of data) {
  counts.set(item, (counts.get(item) ?? 0) + 1)
}
// counts.keys() 是保序的去重結(jié)果

例 3:區(qū)間過(guò)濾——只對(duì) [0, 100] 區(qū)間內(nèi)的值去重,區(qū)間外原樣保留:

const seen = new Set<number>()
const result: number[] = []
for (const x of data) {
  if (x >= 0 && x <= 100) {
    if (seen.has(x)) continue
    seen.add(x)
  }
  result.push(x)
}

這三個(gè)例子是同一種思路:把判重與業(yè)務(wù)規(guī)則分開(kāi)。判重用 Set/Map 保證 O(n),規(guī)則部分留給回調(diào)或顯式分支處理。

對(duì)象數(shù)組去重的幾種 TS 寫(xiě)法

TS 比 JS 的優(yōu)勢(shì)在于——可以為每種去重寫(xiě)法顯式標(biāo)注鍵類型,編譯器會(huì)幫你檢查:

寫(xiě)法 1:按字段去重(最常見(jiàn))

interface User {
  id: number
  name: string
}

// new Map<id類型, 值類型>
const result: User[] = [
  ...new Map<number, User>(users.map(u => [u.id, u])).values()
]

寫(xiě)法 2:按多字段組合

const result: User[] = [
  ...new Map<string, User>(
    users.map(u => [`${u.id}|${u.name}`, u])
  ).values()
]

寫(xiě)法 3:按整體結(jié)構(gòu)(用 JSON)

const seen = new Set<string>()
const result = arr.filter(x => {
  const key = JSON.stringify(x)
  return !seen.has(key) && !!seen.add(key)
})

寫(xiě)法 4:寫(xiě)一個(gè)通用的 uniqueBy(推薦)

function uniqueBy<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
  const seen = new Set<K>()
  return arr.filter(item => {
    const key = keyFn(item)
    return !seen.has(key) && !!seen.add(key)
  })
}

// 使用
const unique = uniqueBy(users, u => u.id)

TS 類型小貼士

Set 的泛型參數(shù):永遠(yuǎn)顯式標(biāo)注 Set<T>,避免推斷成 Set<unknown>

const seen = new Set<number>()      // ? 類型明確
const seen = new Set()              // ? Set<unknown>

reduce 的初始值:泛型推斷有時(shí)會(huì)推成原數(shù)組類型,需要顯式標(biāo)注:

// ? 類型錯(cuò)誤:推斷 acc 為 number 而非 number[]
arr.reduce((acc, x) => [...acc, x], [])

// ? 用 reduce<T[]> 或 [] as T[]
arr.reduce<number[]>((acc, x) => [...acc, x], [])

Set.add 返回類型Set<T>.add 返回 Set<T>(truthy),用 && 短路時(shí)需要 !! 轉(zhuǎn)布爾:

// JS 里直接 && 即可,TS 嚴(yán)格模式下需 !!
return !seen.has(item) && !!seen.add(item)

總結(jié)

工程應(yīng)用選擇:

  • 默認(rèn)用 [...new Set<T>(arr)]:保序、一行、O(n)、類型安全
  • 函數(shù)式用 arr.filter(x => !seen.has(x) && !!seen.add(x))
  • 按字段去重用通用泛型 uniqueBy<T, K>(arr, keyFn)
  • 對(duì)象整體去重用 JSON.stringify 作為鍵
  • 順便排序用 [...new Set(arr)].sort((a, b) => a - b)
  • 業(yè)務(wù)規(guī)則干預(yù)用 Map<K, V> + 合并函數(shù)

核心思路:

  1. 同一個(gè)問(wèn)題可以從多個(gè)角度切入
  2. 選對(duì)數(shù)據(jù)結(jié)構(gòu)往往比寫(xiě)更聰明的代碼更重要
  3. O(n²) 與 O(n) 在數(shù)據(jù)變大時(shí)是幾百倍的實(shí)際差距
  4. 不要過(guò)度優(yōu)化——能用 new Set 就別繞彎
  5. TS 的泛型讓通用工具函數(shù)寫(xiě)一次、對(duì)所有類型可用,比 JS 更值得封裝

以上就是TypeScript數(shù)組去重的20種實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于TypeScript數(shù)組去重方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

乐业县| 昌吉市| 富裕县| 兴国县| 无极县| 乳山市| 乌拉特前旗| 广州市| 鹤岗市| 昭觉县| 桂阳县| 淮安市| 巩留县| 扶沟县| 公主岭市| 漳浦县| 石楼县| 普兰县| 桐柏县| 崇文区| 柳河县| 阳高县| 洪湖市| 禄丰县| 禹城市| 邹城市| 循化| 临沂市| 阿尔山市| 赫章县| 扎囊县| 岳普湖县| 乌兰县| 临桂县| 酉阳| 泽普县| 会宁县| 张掖市| 丰镇市| 博爱县| 花莲县|