昨天看到一篇博文,文中談到一道 Java 面試題:
給定一字符串,若該字符串中間包含 "*",則刪除該 "*";若該字符串首字符或尾字符為 "*",則保留該 "*"。
舉幾個例子(箭頭左邊為輸入,箭頭右邊為輸出):
* --> *
** --> **
**** --> **
*ab**de** --> *abde*
我覺得應該用正則表達式來處理,但想不出正則表達式該怎麼寫。
該博文的回復中有人給出下面的答案:
str.replaceAll("(^\\*)|(\\*$)|\\*", "$1$2");
上機驗證一下,答案是對的,但不懂為什麼正則表達式要這麼寫。到 stackoverflow 上發帖問了一下,才大概明白是怎麼回事兒。當時問的時候, 對這個問題想得不清楚,所以問的問題也是糊裡糊塗。
下面是我的理解,不對之處請多拍磚:
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, 匹配字符串結尾處的 * \\* : 匹配任意位置的 *
$1 :backreference 第一個 capturing group $2 :backreference 第二個 capturing group
這個參數中 "$1" 和 "$2" 的內容被用來替換前一個參數中匹配的字符串。
這裡有一點要注意:在正則表達式中,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 請看這裡。
該網站可以測試正則表達式,並給出詳細的解釋。它還給出匹配所需的步數,你可以用這個步數來比較表達式的效率。從這個網站上看,第二種方法效率最高。
正則表達式30分鐘入門教程 (強烈推薦)