zoukankan      html  css  js  c++  java
  • Java String.replaceAll() 与后向引用(backreference)

    问题

    昨天看到一篇博文,文中谈到一道 Java 面试题:

    给定一字符串,若该字符串中间包含 "*",则删除该 "*";若该字符串首字符或尾字符为 "*",则保留该 "*"。

    举几个例子(箭头左边为输入,箭头右边为输出):

    * --> *

    ** --> **

    **** --> **

    *ab**de** --> *abde*

    我觉得应该用正则表达式来处理,但想不出正则表达式该怎么写。

    第一种解答

    该博文的回复中有人给出下面的答案

    str.replaceAll("(^\*)|(\*$)|\*", "$1$2");

    上机验证一下,答案是对的,但不懂为什么正则表达式要这么写。到 stackoverflow 上发帖问了一下,才大概明白是怎么回事儿。当时问的时候, 对这个问题想得不清楚,所以问的问题也是糊里糊涂。

    下面是我的理解,不对之处请多拍砖:

    replaceAll() 是 Java String 类的一个方法:

    public String replaceAll(String regex, String replacement)
    Replaces each substring of this string that matches the given regular expression with the given replacement.
    (特别要注意的是,这个方法的第一个参数是一个正则表达式。我过去在第一个参数上栽过跟头。不过,这回我栽在第二个参数上。)

    "(^\*)|(\*$)|\*" 解释: 

    (^\*) :capturing group 1, 匹配字符串开始处的 *
    (\*$) :capturing group 2, 匹配字符串结尾处的 *
    \* : 匹配任意位置的 *
    • 因为 "*" 在正则表达式中是特殊字符,所以需要使用转义字符 ""。但在Java中 "" 也是特殊字符,所以需要再一次使用 "",这样就造成 "*" 前面有两个 ""。
    • 圆括号 "()" 把括号内的内容作为一个 capturing group,为后面的 backreference 做准备。关于 capturing group 请看这里
    • "|" 表明左右的表达式是 "或" 的关系。
    • "\*" 单独使用的话可以匹配字符串中任意位置的 "*"。但在上述的表达式中,开始和结尾处的 "*" 优先被 "(^\*)" 或 "(\*$)" 匹配了。
    因此上面的表达式可以匹配字符串开始处的 "*",或者匹配字符串结尾处的 "*",或者匹配字符串任意位置的 "*"。也就是说,字符串中所有的 "*" 都匹配上了。 

    "$1$2" 解释:

    $1 :backreference 第一个 capturing group
    $2 :backreference 第二个 capturing group

    这个参数中 "$1" 和 "$2" 的内容被用来替换前一个参数中匹配的字符串。

    以字符串 "*ab**de**" 为例: 

    1. 第一个 "*" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为 "*","$2" 的内容为空,所以第一个 "*"  被它自己替换。

    2. 接下来 "a" 和 "b" 都不匹配,略过,继续往后走。

    3. 第二个 "*" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为空,"$2" 的内容为空,所以这个 "*" 被替换为空。

    4. 第三个 "*" 跟第二个 "*" 一样,也被替换为空。

    5. 接下来的 "d" 和 "e" 不匹配,继续往后走。

    6. 第四个 "*" 匹配,跟第二个、第三个 "*" 一样,被替换为空。

    7. 最后一个 "*" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为空,"$2" 的内容为 "*",所以最后一个 "*" 被它自己替换。

    8. 最后的结果是:"*abde*" 

    这里有一点要注意:在正则表达式中,backreference 是用 "反斜杠 + 数字" 来表示的,比如:1, 2 。但是,当 backreference 出现在替换字符串中时,Java 的 backreference 使用 "美元符号 + 数字" 来表示,比如:$1, $2 。据说这是跟 Perl 学的。不嫌累的话看看这个帖子吧。

    第二种解答

    另外一种使用正则表达式的方法是:如果 "*" 不在头,也不在尾,则替换为空。这种想法很自然,但实现起来却不容易。
    String repl = str.replaceAll("(?<!^)\*+(?!$)", "");

    正则表达式解释:

    (?<!^)   # 如果前一个位置不是行首
    \*+     # 匹配一个或多个 *
    (?!$)    # 如果下一个位置不是行尾

    "?<!" 表示 Negative Lookahead,"?!" 表示 Negative Lookbehind 。详细说明请参考这里这里

    第三种解答

    String repl = str.replaceAll("(^\*)|(\*$)|\*+", "$1$2");

    这个跟上面的第二种解答都是由同一个人回复的,但这个解答有点问题:如果结尾处有两个或两个以上的 "*" 时,这些 "*" 都被替换为空。

    例如,若输入为 "*ab**de**",则输出为"*abde",最后的那个 "*" 不见了。

    这是因为缺省情况下,正则匹配处于 Greediness(贪婪) 匹配模式,会匹配尽量多的字符。"*+" 可以匹配一个或多个 "*" 。在倒数第二个 "*" 的时候,匹配一个 "*" 或两个 "*" 都可以。但它比较贪婪,所以把最后两个 "*" 都匹配上了,然后被 "$1$2" 替换为空。

    把正则匹配改为 Laziness(偷懒)匹配可以解决这个问题。在表达式后面加一个 "?" 就变成 Laziness 匹配了:"*+?" 。

    String repl = str.replaceAll("(^\*)|(\*$)|\*+?", "$1$2");

    关于 Greediness 和 Laziness 请看这里

    正则表达式效率

    该网站可以测试正则表达式,并给出详细的解释。它还给出匹配所需的步数,你可以用这个步数来比较表达式的效率。从这个网站上看,第二种方法效率最高。

    参考链接

  • 相关阅读:
    [NOI2014]动物园 题解(预览)
    CF1200E 题解
    KMP算法略解
    [EER2]谔运算 口胡
    CF504E Misha and LCP on Tree 题解
    长链剖分 解 k级祖先问题
    双哈希模板
    Luogu P5333 [JSOI2019]神经网络
    UOJ449 【集训队作业2018】喂鸽子
    LOJ6503 「雅礼集训 2018 Day4」Magic
  • 原文地址:https://www.cnblogs.com/xiaomiganfan/p/5332555.html
Copyright © 2011-2022 走看看