正則實現陣列濾重

老姚發表於2019-01-04

有很多種方法能實現陣列濾重功能,有人統計過在 JS 裡至少就有 10 種方式。

本文關心的是:能否用正則來實現濾重這個功能呢?

誠然,就算能實現,估計也沒人會把它當成最佳實踐的。

所以這裡,我們只考慮可能性。

本文給出的答案:可以!而且不止一種方式。

下面我們從易到難一步步來看如何實現的。

1. 相鄰字元濾重問題

"abbccc" => "abc"

正則裡要匹配之前出現過的字元,需要使用反向引用:

function distinct(string) {
  return string.replace(/(.)\1+/g, '$1')
}
console.log(distinct("abbccc"))
// => "abc"
複製程式碼

其中 \1 是反向引用,指代第一個括號捕獲的資料,其中稱為 (.) 為捕獲分組。而 $1 也表示第一個括號捕獲的資料。具體過程請看下圖。

正則實現陣列濾重

其中藍色表示捕獲分組捕獲到的資料,粉色的表示反向引用指代的資料。進行替換操作後帶顏色的資料只保留了藍色資料。

2. 字串濾重

"abbacbc" => "abc"

方式一

一般的字串這麼辦呢?

最直接的思路是把問題轉化為已解決過的問題。

把字串拆分成陣列,然後位元組碼排序,轉化成相鄰字元濾重問題。

這種方式,用了陣列相關方法,正則的意味就沒那麼濃烈了。

方式二

使用迴圈,刪除重複出現的字元。

function distinct(string){
  while(/(.).*?\1/.test(string)) {
    string = string.replace(/(.)(.*?)\1/, '$1$2')
  }
  return string;
}
console.log(distinct("abbacbc"))
// => "abc"
複製程式碼

用正則 /(.).*?\1/ 來判斷字串裡是否還有重複字元,有的話,就替換一下。 替換的正則是 /(.)(.*?)\1/,其中使用了兩組括號,為引用 $1$2 提供了資料。具體過程示圖如下:

正則實現陣列濾重
其中藍色表示第一個捕獲分組捕獲的資料。黑色表示第二組捕獲分組捕獲的資訊,粉色表示引用第一個捕獲分組捕獲的資料。每一次替換,粉色資訊都被刪除了。

方式三

方式二里使用了迴圈,總覺得有點太笨。其實可以直接使用 replace。此時需要使用 (?=p)

function distinct(string) {
  return string.replace(/(.)(?=.*?\1)/g, '')
}
console.log(distinct("abbacbc"))
// => "abc"
複製程式碼

具體過程示圖如下:

正則實現陣列濾重

(?=.*?\1)表示匹配位置,即圖中綠色箭頭所示。如第一行中字元 a 後面的位置,改位置後面的字元匹配 .*?\1,其中 \1即圖中粉色的資料,對應於第一個分組捕獲的藍色資料。最後所有的藍色資料都被替換成 '' 了。

這種實現方式有一個問題,就是重複字元只保留最後出現的字元。如果在原來字串後面加個 "a" 變成 "abbacbca",最終結果卻是 "bca"

方式四

方式三的思路是看當前字元是否會在後面出現,如果出現就刪除。方式四的邏輯卻可以說反過來的:如果當前字元在前面出現過,那麼就刪除。此時需要用斷言 (?<=p),看當前位置前面是否匹配 p

正則不能想當然地寫成 /(?<=.*?\1)(.)/g,因為 \1 是“反向”引用,只能引用它之前的分組。所以這裡要把它放在目標字元後面:

function distinct(string) {
  return string.replace(/(.)(?<=\1.*?\1)/g, '')
}
console.log(distinct("abbacbc"))
// => "abc"
複製程式碼

具體過程如下:

正則實現陣列濾重
比如圖中第一行中第二個b後面的綠色箭頭表示 (?<=\1.*?\1)。第一個 \1 是粉色 b,第二個是藍色的那個。

3. 陣列濾重

有字串濾重後,陣列濾重就簡單了。上面四種方法都可以寫成陣列版本的。比如第四種方案如下:

function distinct(arr) {
  return arr.join('').replace(/(.)(?<=\1.*?\1)/g, '').split('')
}
console.log(distinct(['a','b','b','a','c','b','c']))
// => ['a', 'b', 'c']
複製程式碼

至此我們的解決方案還有一些問題:

  • 只能過濾陣列的每個元素是一個字元的情形
  • 過濾的結果會把元素轉化為字元。

支援多位字元相對容易解決,但是要保持型別的話,需要JSON兩個方法了。

最後給出方案四的最終版本:

function distinct(arr) {
  var string = JSON.stringify(arr)
  string = string.replace(/,([^,]+)(?<=\1.*?\1)(?=,|])/g, (m, $1) => $1 == '"' ? m : '')
  return JSON.parse(string)
}
console.log(distinct(["aa",1,"ab",true,1,true,"aa"]))
// => ["aa", 1, "ab", true]
複製程式碼

本文完。

另外,歡迎閱讀本人的《JS正則迷你書》

相關文章