虾编
首发于虾编

正则实现数组滤重

有很多种方法能实现数组滤重功能,有人统计过在 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正则迷你书》

编辑于 2019-01-11

文章被以下专栏收录