正則表達式匹配素數的原理講解

爲什麼要寫這麼一篇文章呢?是因爲自己最近在研究和學習正則表達式,然後在RegexGolf上練習技能的時候遇到了這麼一道題目,覺得很有趣。我當時雖然也解決了這個問題,但是正則表達式寫的有點長,而且也只算是一種取巧的解決方案。因爲如果測試用例再多一點可能我寫的這個正則表達式就不能夠滿足需求了。

後來在覆盤這道題目的解決方案的時候,查閱了很多相關的資料。發現了更簡潔,更準確的答案。當我看到答案的那一瞬間,我忽然發現自己當初距離這個答案其實也不遠,如果自己當時再好好研究一下,有可能就想出了更簡潔的答案了。當然,也許最終還是沒有想出來,那裏有那麼多的如果呢😂。

說了這麼多,讓我們來看一下這道題目吧。如下圖所示:

RegexGolf Prime

我們需要匹配左邊的這些字符串,同時排除右邊的這些字符串。不知道大家之前有沒有做過類似的題目,現在大家就可以測試一下自己的正則的水平。個人感覺如果這道題目可以做出來的話,那麼你的正則水平至少是中等偏上的水平啦。沒有做出來也沒有關係,畢竟在平時的開發過程中我們很難會遇到這種需求,不過也可以學習一下。加深一下自己對正則匹配過程的理解。

我當時看了題目的提示,知道是匹配素數,但是對於如何匹配一個素數我卻是不知道的。於是我就取了個巧,在測試用例不是很多的情況下,可以進行窮舉呀。於是我就寫下了下面的答案:

^(?!(?:(x{2}){2,}|(x{3}){2,}|(x{5}){2,})$)

雖然有點長,但是好歹也算是通過了測試用例。

對正則表達式學習的比較深入的一些同學,看到了上面的正則應該很快就知道是什麼意思了。我可以先簡單的給大家講解一下。上面的正則表達式匹配的過程是這樣的:

  • ^:匹配一行的開始。
  • (?!...)否定的順序環視,匹配一個位置,後面緊跟着的是需要排除的條件。
  • (?:)非捕獲型括號,在這裏用來限制多選結構的作用範圍,當然在這裏也可以使用捕獲型的括號。
  • (x{2}){2,}:首先{2,}限定了前面的表達式(x{2})出現的次數只能是兩次或者兩次以上。(x{2})表示需要匹配兩個x
  • (x{3}){2,}(x{5}){2,}:這兩個部分跟上面的正則片段是類似的意思。
  • $:匹配一行的結尾。

所以上面的正則表達式表示的意思就是匹配一個字符串的開始,然後緊接着字符串中x的個數不能是2個x的2倍或者以上,不能是3個x的2倍或者以上,不能是5個x的2倍或者以上;直到字符串的結尾

接下來,我們針對這個問題深入的研究一下。首先我們需要知道什麼是素數,素數就是除了1和它本身之外不能夠被其他整數整除的數,不包括1。那麼我們怎麼判斷一個數字是不是素數呢?我們假設這個數字是N,然後我們用N去除以2,看一下是否能夠整除,不能夠整除的話,我們再用N去除以3,如果還不可以就除以4,一直除到N/2(N/2如果不能夠整除,需要取整)。如果都不可以的話,我們就可以判定N是一個素數

如果我們可以把上面這個過程使用正則表達式表示出來,那麼我們就能夠匹配一個素數了。但是想要一下子解決這個問題可能有點難,我們需要按照剛纔的思路一步一步來構造我們的正則表達式。首先我們可以轉換一下思路,直接匹配一個素數有點難度,我們可以先匹配一個合數,然後排除掉匹配的這個合數就可以了。

首先我們需要匹配能夠被2整除的數,按照上面題目的要求的話,我們需要匹配一個字符串,這個字符串中x出現的次數需要是偶數次。對於這個問題,我們很容易想到這樣一個表達式/(xx)+/;如果需要匹配能夠被3整除的數的話,按照上面題目的要求,我們很容易想到正則表達式/(xxx)+/,於是到這裏你可能會覺得我好像知道了匹配能夠被大於等於2整除的數的答案,那就是/(xx+)+/。你可以自己試一下這個正則表達式,結果並不是你所想的那樣。

爲什麼呢?因爲正則表達式/(xx+)+/的意思是,(xx+)可以出現至少一次,但是(xx+)可能是2個x,也可能是3個x或者4個x上面的正則表達式不能夠滿足在同一次的匹配過程中(xx+)都表示相同個數的x。那麼我們應該如何解決這個問題呢?這個時候我們就要思考,在正則表達式中什麼可以表示已經匹配了的內容呢?沒錯,就是捕獲與反向引用。如果我們需要在匹配的過程中繼續匹配相同的次數的話,我們需要使用反向引用表示前面已經匹配的結果。那麼到這裏,這個正則表達式就呼之欲出了。這個正則表達式就是:

/(xx+)\1/

我來解釋一下上面的這個正則表達式,首先(xx+)表示x的個數>=2,\1表示反向引用,引用的值就是(xx+)已經匹配的值。當(xx+)後面緊跟着\1時,(xx+)在實際中到底能匹配多少個x呢?這由要匹配的字符中出現多少個x決定的。我們一起來看一下上面的正則表達式能匹配的x的個數。

regex101.com 圖二

regex101.com 圖三

上面的圖二中,藍色背景表示整個正則表達式匹配的內容。在圖三中,青色部分表明了(xx+)匹配的內容。\1匹配的內容和(xx+)匹配的內容是一樣的。這就是捕獲((xx+))與反向引用(\1)。

上面的正則表達式表示了字符串的長度能夠被二等分,我們還要更進一步;我們需要字符串的長度還可以被三等分,四等分甚至更多。那我們應該怎麼做呢?答案似乎已經很明顯了,我們只需要在\1後面添加一個+就可以了。

/(xx+)\1+/

這樣,上面的正則表達式表示的意思就是,首先匹配2個或2個以上的x,然後記住匹配的結果。接下來需要將這個匹配結果重複至少1次以上。在這裏需要注意的是+是貪婪匹配,它會把它作用的那個匹配匹配儘可能多的次數。

所以到目前爲止我們已經可以成功的匹配字符串的長度是合數的這些字符串了,但是還不滿足題目的需求。我們要做的是需要排除上面匹配到的這些結果。遇到這種情況,我們首先想到的應該就是順序環視了,當然我們在這裏需要使用否定的順序環視(?!...),它表示當前位置的後面不能夠出現匹配...的情況。

所以這個正則表達式的完整結果就是:

/^(?!(xx+)\1+$)/

其中^$也是必須的,對於這裏的^的意思是,匹配字符串的開頭,然後接下來整個(?!(xx+)\1+$)匹配的也是一個位置,它表示在這個位置的後面字符串中x的長度不能夠是一個合數,直到字符串的結尾

到這裏爲止,關於上面題目的講解就算完成啦。但是我們還沒有解決如何匹配一個真正的素數而不是一個字符串的問題。當然有了上面上面的經驗,我想這對大家來說應該也不是一個難題了。我們可以把這個素數N,轉換成一個字符串,這個字符串中的所有字符都是相同的,並且長度爲N。那麼我們馬上就可以寫出一個這樣的函數。在這裏我們使用JavaScript語言來表示這樣一個函數。如下所示:

const isPrime = (n) => /^(?!(11+)\1+$)/.test(new Array(n).fill(1).join(''));
console.log(isPrime(2)); // true
console.log(isPrime(3)); // true
console.log(isPrime(4)); // false
console.log(isPrime(5)); // true

在這裏給大家推薦幾個學習和練習正則表達式的網站:

關於正則表達式匹配一個素數的原理到這裏就結束啦,如果大家有什麼疑問和建議都可以在這裏提出來。歡迎大家關注我的公衆號「關山不難越」,我們一起學習更多有用的正則知識,一起進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章