使用 CSS 來做素數的判定與篩選

前情提要

此文內容涉及:

  • 素數的判定與篩選
  • CSS 計數器及其作用域
  • 偽元素
  • 生成內容
  • 層疊樣式
  • flex

突發奇想

某天在查 nth-child 偽類的時候,突然想到,既然 nth-child 偽類可以選擇【處於某個數的所有倍數位置】上的元素,那是不是可以用來做素數判定呢?

如果我把所有數除自己以外的倍數位置上的元素都選中,剩下的不就是素數了嘛。

這麼有意思的事情,想到就立馬開始行動了,於是我寫出了第一版代碼:

<style>n li:first-child {n color: grey;n }n li:nth-child(2n + 4) {n color: grey;n }n li:nth-child(3n + 6) {n color: grey;n }n li:nth-child(4n + 8) {n color: grey;n }n li:nth-child(5n + 10) {n color: grey;n }n</style>n<ul>n <li>01</li>n <li>02</li>n <li>03</li>n <li>04</li>n <li>05</li>n <li>06</li>n <li>07</li>n <li>08</li>n <li>09</li>n <li>10</li>n</ul>n

上面這段代碼讓所有非素數位置上的元素的顏色變為灰色。

注意 nth-child 偽類的參數是 Xn + 2X,而不是 Xn + X,這是因為 n 是從 0 開始的。而我們是想要選中的是所有 X 的倍數,但不包含 X 自己。所以最小需要選中的的數即是 2X。另外我們只需要寫到 5n + 10 即可,因為 6 的倍數已經大於 10 了。

上面所有的選擇器的聲明塊都是一樣的,於是可以把所有的選擇器合成一個:

<style>n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(4n + 8),n li:nth-child(5n + 10) {n color: grey;n }n</style>n<ul>n <li>01</li>n <li>02</li>n <li>03</li>n <li>04</li>n <li>05</li>n <li>06</li>n <li>07</li>n <li>08</li>n <li>09</li>n <li>10</li>n</ul>n

這樣看起來就清爽多了。

讓素數項高亮,合數項低亮

不過問題來了:如果我們想要讓所有素數項全部高亮的話,該怎麼做呢?上面代碼中的選擇器並沒有選中素數項。

容易,我們讓所有的項都變成紅色,但讓非素數項變成灰色,因為選中非素數項的那些選擇器的優先順序更高,就實現了素數項的高亮。

<style>n /*優先順序為 0,0,0,1*/n li {n color: red;n }nn /*優先順序為 0,0,1,1*/n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(4n + 8),n li:nth-child(5n + 10) {n color: grey;n }n</style>n<ul>n <li>01</li>n <li>02</li>n <li>03</li>n <li>04</li>n <li>05</li>n <li>06</li>n <li>07</li>n <li>08</li>n <li>09</li>n <li>10</li>n</ul>n

然而問題又來了,如果我們想要用這種方式高亮出 100 以內的所有素數,第二條選擇器豈不是要寫 50 行?

減少選擇器的條數

通過觀察我們可以發現,li:nth-child(4n + 8) 這條選擇器是可以不寫的:它把除 4 以外所有 4 的倍數的項都選中,但實際上 li:nth-child(2n + 4) 這條選擇器已經把包括 4 在內的所有 4 的倍數項都選中了。同理,我們可以推導出,如果寫了 li:nth-child(3n + 6) 這條選擇器,就不必要寫 li:nth-child(6n + 12),li:nth-child(9n + 18) 等所有【n 前面的係數為 3 的倍數】的選擇器了。

事實上,如果把不必要的選擇器項全部都去掉,你會發現剩下的選擇器中,n 前面的係數全部都是素數。因為如果 n 前面的係數為合數,那麼這個合數的所有倍數一定會被其素數因子的所有倍數選中,即【一個合數的所有倍數是這個合數某個素數因子的所有倍數的子集】,這就使得所有 n 的係數為合數的選擇器不需要存在。這其實跟我們小學時學的使用篩選法來找出素數的方法是類似的。而這正是埃拉托斯特尼篩法的篩選過程。

事實上,為了篩選的更迅速,對於一個素數 X 的倍數,我們是可以從 Xn + X * X 開始篩選掉數字的。因為如果你已經把小於 X 的所有素數的倍數都篩選掉了,那麼小於 X * X 的數中,所有的合數也已經全部被篩掉了。因為小於 X * X 的任意合數一定能夠找到至少一個小於 X 的素數約數。

而基於上面這條規則,我們想要篩選出 M 以內所有的素數,只需要【把所有小於等於根號 M 的素數的所有倍數】篩選掉即可。

所以想要篩選出 100 以內的所有素數,我們只需要寫出下面這一條複合選擇器即可:

<style>n li {n color: red;n }n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(4n + 8),n li:nth-child(5n + 10),n li:nth-child(7n + 14) {n color: grey;n }n</style>n

因為小於等於根號 100 的最大素數為 7,所以我們的選擇器只需要寫到 li:(7n + 14) 就足夠了。

代碼量複雜度(我杜撰的詞)

其實上面繞了個大圈子,只是用另一種形式證明了素數篩選的原理而已。

最終,我們可以使用根號 M 以內素數的個數條選擇器的代碼量來篩選出小於 M 的所有素數。

那問題又來了,小於某個數的素數個數大概有多少個呢?其實前人早就已經研究過這個問題:

n 以內素數的個數大約是 n/ln(n)。n 越大,小於 n 的素數個數越接近這個公式的值。參見維基百科素數計數函數

所以我們大概可以以 O(sqrt(n)/ln(sqrt(n))) 這麼多的 CSS 代碼篩選出 n 以內的所有素數,1000 以內的素數,我們只需要一個 12 行代碼的選擇器即可選出來:

<style>n li {n color: red;n }n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(5n + 10),n li:nth-child(7n + 14),n li:nth-child(11n + 22),n li:nth-child(13n + 26),n li:nth-child(17n + 34),n li:nth-child(19n + 38),n li:nth-child(23n + 46),n li:nth-child(29n + 58),n li:nth-child(31n + 62) {n color: #ddd;n }n</style>n<ul>n <li>01</li>n <li>02</li>n <li>03</li>n <li>04</li>n <li>05</li>n ...n <li>996</li>n <li>997</li>n <li>998</li>n <li>999</li>n <li>1000</li>n</ul>n

上面的代碼中,偽類選擇器的參數我之所以沒有用 Xn + X * X,是因為使用 2X 反而會使我們的代碼量更少,因為一個數的平方往往會比一個數的二倍佔用更多的位數。

自動計數

問題又來了,上面的代碼中,我們還是得把數字寫進 li 標籤里,雖然這些標籤可以用 JS 生成,但是對於強迫症來說,總還是有點不爽,而且我們說了,是用 CSS 來做素數判定和篩選。

你可能會想到,把 ul 標籤改成 ol 標籤就行了嘛,這樣的話 li 標籤會自動編號。確實可行,但是目前的 CSS 是很難控制項目編號的樣式和位置的。比如我不想要數字項目編號後面那個點,就沒有辦法了。

另外,其實不改變 ul 標籤為 ol 標籤,我們也可以實現讓 li 標籤自動編號,即設置 li 元素的 list-style-type 屬性為 decimal 或者 decimal-leading-zero 強行改變項目標號的類型。

有沒有辦法用 CSS 來生成這些數字呢?

Sure。當然是有的。

我們可以使用 CSS 的計數器以及生成元素來插入這些數字。

<style>n li {n /*遍歷 DOM 的過程中,每遇到 li 就讓 nature-count 計數器變數的值加一*/n counter-increment: nature-count;n }n li::before {n /*在 li 的 before 偽元素中插入計數器變數 nature-count 當前的值*/n content: counter(nature-count);n }n</style>n<ul>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n</ul>n

渲染結果如下:

關於 CSS 計數器,可以參閱 MDN 文檔:使用CSS計數器 - Web開發者指南 | MDN

既然能夠計數了,那我就在想,能不能順便再統計出素數的個數呢?

我們可以很容易的統計出非素數的個數,因為前面的 li:nth-child 選擇器就選中了那些非素數的項,只要在這些項目上讓計數器加一就可以了:

<style>n li {n counter-increment: nature-countn }n li::before {n content: counter(nature-count);n }n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(5n + 10),n li:nth-child(7n + 14) {n color: grey;n counter-increment: nonprime-count;n }n</style>n<ul>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n</ul>n

然而渲染結果跟我們預想的好像不太一樣:

分析原因,我們會發現,是因為非素數選擇器的 counter-increment 屬性把 li 選擇器對應的這個屬性覆蓋了,CSS 在發生屬性覆蓋時,是不會將兩個相同屬性值聯合起來的,而是會選擇最終生效的那一個,此處對於素數位置上的 li 元素,顯然是 counter-increment: nonprime-count; 這一句會生效。所以導致了當解析器遇到合數位置上的 li 元素時,只給 nonprime-count 計數器加了一,知道了原因,就很好解決了,我們讓遇到這個元素時同時給自然數計數器和非素數計數器都加一:counter-increment: nature-count nonprime-count;

顯示統計結果

這樣一來,我們可以在 ul 的後面再增加一個標籤,把統計信息顯示在裡面

<style>n li {n counter-increment: nature-countn }n li::before {n content: counter(nature-count);n }n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(5n + 10),n li:nth-child(7n + 14) {n color: grey;n counter-increment: nature-count nonprime-count;n }n p::before {n content: counter(nature-count) 個自然數中,有 counter(nonprime-count) 個合數 ;n }n</style>n<ul>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n</ul>n<p></p>n

然而結果再一次出乎我們的意料:

兩個 CSS 計數器變數明明是有值的,剛剛還在往 li 的偽元素里生成呢,怎麼現在成 0 了呢?

CSS 計數器的作用域

要理解這個行為,需要了解 CSS 計數器的作用域的概念:一個計數器的作用域只在對其產生影響的最外層元素的父元素的內部有效。

上面的例子中,對兩個計數器計數的元素都是 li,所以這兩個計數器只會在這些 li 的父元素內部生效:即 ul 的內部。

想要解決這個問題也很容易,我們只需要讓更外層的元素對計數器產生影響即可,我們可以在遇到 body 元素的時候讓計數器歸零,這樣這個計數器就在整個頁面里可用了:

<style>n body {n counter-reset: nature-count nonprime-count;n }n li {n counter-increment: nature-countn }n li::before {n content: counter(nature-count);n }n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(5n + 10),n li:nth-child(7n + 14) {n color: grey;n counter-increment: nature-count nonprime-count;n }n p::before {n content: counter(nature-count) 個自然數中,有 counter(nonprime-count) 個合數 ;n }n</style>n<ul>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n</ul>n<p></p>n

OK,這下得到我們想要的結果了:

現在我們已經統計出了自然數的個數,合數的個數了,那如何得到素數的個數呢?

要知道,CSS 里是不能做減法的,況且我們這兩個值是存在 CSS 的計數器里,calc 函數也只能對寫死的數值執行計算,且計算結果也是沒辦法直接顯示成數值的。

所以我們必須要找到一種讓素數計數器遞增的方法。這就意味著,我們必須使用選擇器選出素數項才可以!

好像有點無能為力了,nth-child 選出非素數好辦,但是選出素數,肯定沒有能夠實現這件事的選擇器了。

然而

車到山前必有路,柳暗花明又一村

我們還有 not 偽類選擇器!既然我們可以用 nth-child 偽類來選出所有的合數,那麼把這些選擇器做為 not 偽類的參數,不就選出了所有的素數項嗎?!

li:not(:first-child):not(:nth-child(2n + 4)):not(:nth-child(3n + 6)) {n color: red;n counter-increment: prime-count nature-count;n}n

偽類選擇器都是可以結合的,我們把多個 not 偽類選擇器結合起來,並把所有的合數項選擇器做為 not 的參數,就達到了只選擇素數項的目的。再給這個選擇器加上一個計數器,就達到了統計素數個數的目的了。

只剩最後一個問題了,那就是統計數據是顯示在下方的,當數據比較少時這也沒什麼,但數據較多時,想要看到統計數據就得拉到頁面下方。

如果我們直接把 p 標籤移到 ul 的前面,那裡面的統計數據就全部顯示成 0 了,因為此時各個計數器變數的值都還是 0。所以在 DOM 結構上,p 標籤必須出現在 ul 的後面。

我們當然可以用絕對定位把 p 標籤移到上方去,但總還是不那麼好控制。

如果能夠即讓計數器有值,又不用絕對定位,還能讓 p 標籤的內容顯示在 ul 的前面就好了。

辦法還是有的,那就是用 flex 布局模式下的 order 屬性,它可以在不改變 DOM 結構的情況下,改變元素顯示的順序。因為計數器的計數只跟 DOM 結構有關,所以並不會影響統計結果的正確性。

最終代碼如下:

<style>n body {n /*用body元素的counter-reset屬性重置三個計數器以使它們的作用域在整個body內*/n counter-reset: nature-count prime-count nonprime-count;n display: flex;n flex-direction: column;n }nn li {n list-style-type: none;n display: inline-block;n }nn /*在before偽元素中插入計數器的值以實現數值遞增*/n li::before {n content: counter(nature-count) ,;n }n li:last-child::before {n content: counter(nature-count);/*最後一個元素不需要逗號分隔*/n }nn /*合數項選擇器*/n li:first-child,n li:nth-child(2n + 4),n li:nth-child(3n + 6),n li:nth-child(5n + 10),n li:nth-child(7n + 14) {n /*遞增自然數與合數計數器*/n counter-increment: nature-count nonprime-count;n color: #ddd;/*合數變灰*/n /*如果想只顯示素數項,可以把合數全部隱藏起來*/n /*display為none並不影響計數器的計數*/n /*display: none;*/n }n /*素數項選擇器*/n li:not(:first-child):not(:nth-child(2n + 4)):not(:nth-child(3n + 6)):not(:nth-child(5n + 10)) {n /*遞增自然數與素數計數器*/n counter-increment: nature-count prime-count;n color: red;/*素數變紅*/n }nn p {n order: -1;/*讓p元素顯示在ul的前面*/n }n p::before {n /*通過p標籤的before偽元素插入統計結果*/n content: counter(nature-count) 個自然數中,有 counter(nonprime-count) 個合數, counter(prime-count) 個素數 ;n }n</style>n<ul>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n <li></li>n</ul>n<p></p>n

可以隨時給 ul 增加 li 標籤以顯示更大範圍內的素數及統計結果。而不用更改其它任何地方的代碼。

渲染結果如下,題圖中是 1000 個數時的渲染效果:

完整的 demo 在這裡:CSS Prime

點擊這裡可以查看我的其它技術博文:謝然的專欄文章 - 知乎

廣告時間

我叫謝然,網名充電大喵,高二開始接觸編程,2011 年畢業於華中師範大學計算機科學與技術專業,曾先後就職於全國百強中學、阿里巴巴、小米。2016 年 9 月創建了自己的前端培訓品牌 「大喵教育前端培訓」,首期前端培訓課程已過半。第二期班將於 2017 年 3 月 14 日(π)開課,地點杭州下沙。

我的培訓班與眾不同,著重培養學生的自學能力,英語能力(部分課程使用英文授課),及計算機基礎(包括演算法與數據結構及計算機網路),讓非科班的人也能有機會進入 IT 行業,我為學員設計了與眾不同的且有難度的實踐項目:

  • 初步實現 lodash
  • 初步實現 JSON Parser
  • 初步實現 jQuery
  • 初步實現 Sizzle
  • ……

如果你想參加,可以直接加我的 QQ 詳細溝通:285696737。如果你有朋友想要學習前端,也希望你能推薦給我,成功推薦入學將有現金獎勵。如果可以,也請各位朋友幫忙分享此文,分享後可以截屏找我要紅包哦,非常感謝。

詳情點擊:大喵教育前端培訓

推薦閱讀:

You Don't Know CSS(二)
CSS 設計理念
移動端實現內滾動的4種方案
外邊距摺疊-磨人的小妖精
Markdown入門指南

TAG:前端开发 | CSS | 素数 |