使用 CSS 检测和计算素数
如果您不想阅读所有这些,最后的演示在这里: https ,如果您愿意,请查看源代码。
本文可能涉及以下内容:
- 如何决定和选择素数
- CSS 计数器及其范围
- 伪元素
- 生成的内容
- 层叠样式
- 弹性盒布局
灵感
有一天,当我阅读 nth-child
伪类,我想到是否 nth-child
伪类可用于确定素数。 条件是它可以用来 选择位于任意数的每个倍数中的特定元素 。
如果我选择多个位置中除它们自身之外的所有元素,则其余元素将位于主要位置。
多么有趣的事情! 我一想到就写下来。 这是第一个版本:
<style> li:first-child { color: grey; } li:nth-child(2n + 4) { color: grey; } li:nth-child(3n + 6) { color: grey; } li:nth-child(4n + 8) { color: grey; } li:nth-child(5n + 10) { color: grey; } </style> <ul> <li>01</li> <li>02</li> <li>03</li> <li>04</li> <li>05</li> <li>06</li> <li>07</li> <li>08</li> <li>09</li> <li>10</li> </ul>
上面的代码将所有不在主要位置的元素变成灰色。
注意参数 nth-child
伪类是 Xn + 2X
代替 Xn + X
, 因为 n
开始于 0
. 我们需要选择的就是所有 X
的倍数,除了它自己。 所以最小选择的数字是 2X
. 我们只需要写到 5n + 10
,因为 6 的倍数大于 10。
上面所有选择器的声明块都是一样的,因此我们可以将所有的选择器组合成一个组合选择器。
<style> li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(4n + 8), li:nth-child(5n + 10) { color: grey; } </style> <ul> <li>01</li> <li>02</li> <li>03</li> <li>04</li> <li>05</li> <li>06</li> <li>07</li> <li>08</li> <li>09</li> <li>10</li> </ul>
现在看起来好多了。
突出质数,淡化非质数
问题是如果我们想突出显示所有素数怎么办。 上面代码中的选择器没有选择素数。
我们可以做到,很简单。 将所有项目设为红色,将非素数项目设为灰色。 对非素数项的选择器处理具有更高的优先级,从而使素数突出显示。
<style> /*优先级为 0,0,0,1*/ li { color: red; } /*优先级为 0,0,1,1*/ li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(4n + 8), li:nth-child(5n + 10) { color: grey; } </style> <ul> <li>01</li> <li>02</li> <li>03</li> <li>04</li> <li>05</li> <li>06</li> <li>07</li> <li>08</li> <li>09</li> <li>10</li> </ul>
但是还有一个问题,如果我们想用这种方法突出显示所有小于 100 的素数,第二个组合选择器会是 50 行,太多了。
减少选择器的数量
通过观察我们可以发现: li:nth-child(4n + 8)
selector 不用写:它选择了除 4 以外的所有 4 的倍数,但实际上 li:nth-child(2n + 4)
selector 已经选择了包括 4 在内的所有 4 的多项。类似地,我们可以推断如果写 li:nth-child(3n + 6)
选择器,不必写 li:nth-child(6n + 12)
, li:nth-child(9n + 18)
等等。 n 之后的 3 的倍数的 都不需要写。
实际上,如果删除所有不必要的选择器,您会发现 n 之前的系数在其余选择器中都是素数。 如果 n 之前的系数是非质数,则这个非质数的所有倍数都将被其质因数的倍数所选择。 即一个合数的所有倍数都是一个素因数的倍数的子集,这使得n之前的所有合数系数都不需要存在。 这类似于我们在小学学习的寻找素数的筛选方法。 这就是筛选过程 sieve of Eratosthenes
方法。
但是,为了更快地过滤,我们可以从 Xn + X * X
过滤掉一个因素的数字 X
的倍数。 因为如果你过滤掉了所有小于的素数倍数 X
, 所有小于的合数 X * X
已被筛选出来。 由于任何合数小于 X * X
必须能够找到至少一个素数的除数小于 X
.
而根据上述规则,如果我们要过滤掉M内的所有素数,我们只需要过滤掉所有小于等于M平方根的素数的倍数即可。
因此,如果我们想过滤掉 100 以内的所有素数,下面的组合选择器就足够了:
<style> li { color: red; } li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(4n + 8), li:nth-child(5n + 10), li:nth-child(7n + 14) { color: grey; } </style>
小于或等于 100 的平方根的最大素数是 7,所以我们的选择器写到 li:(7n + 14)
.
代码量复杂度(我发明了这个词)
事实上,绕了一大圈之后,素数筛选的原理以另一种形式得到了证明。
作为结论,我们只需要 其中的素数个数 Sqrt(M)
选择器筛选所有小于 M 的数字。
那么,还有一个问题,有多少个质数小于某个数? 其实我们的前人已经研究过这个问题:
内的素数个数 n
是关于 n/ln(n)
. 数字越大 n
,素数的个数更接近这个公式的值。 的更多信息,请参见此处 Prime Counting Functions 。
所以我们大概可以使用 O(sqrt(n)/ln(sqrt(n))
过滤掉所有素数的 CSS 代码(更具体地说,选择器) n
. 对于小于 1000 的素数,我们只需要 12 个组合选择器的选择器:
<style> li { color: red; } li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(5n + 10), li:nth-child(7n + 14), li:nth-child(11n + 22), li:nth-child(13n + 26), li:nth-child(17n + 34), li:nth-child(19n + 38), li:nth-child(23n + 46), li:nth-child(29n + 58), li:nth-child(31n + 62) { color: #ddd; } </style> <ul> <li>01</li> <li>02</li> <li>03</li> <li>04</li> <li>05</li> ... <li>996</li> <li>997</li> <li>998</li> <li>999</li> <li>1000</li> </ul>
在上面的代码中,伪类选择器参数没有写入 Xn + X * X
, 因为使用 2X
将使我们的代码量更少。 由于平方比它的双倍占据更多的数字,例如,4 乘以 2 是 8,但 4 的平方是 16,比 8 长。
自动计数
问题又出现了,上面的代码,我们还是要在里面放一个数字 li
标签。 这些标签可以用 JS 生成。 但对于一个强迫症极客来说,这让我们感到不舒服。 更重要的是,我提到我们将使用 CSS 来决定和选择素数。
您可能认为我们可以替换 ul
标记为 ol
标签。 在这种情况下, li
标签的列表标记将自动为数字。 确实有道理,但是目前CSS很难控制列表项编号的样式。 比如我想调整它的位置,就没有办法了。
此外,即使我们使用 ul
标记而不是 ol
标签 li
标签的列表标记也可以设置为数字。 即,设置 list-style-type
的属性 li
元素到 decimal
或者 decimal-leading-zero
是答案。
那么,有没有办法用 CSS 生成这些数字呢?
我们可以使用 CSS 计数器 和生成的内容来插入这些数字。
<style> li { /*遍历 DOM 的过程中,每遇到 li 就让 nature-count 计数器变量的值加一*/ /*Every time we encounter li, we make nature-count++*/ counter-increment: nature-count; } li::before { /*在 li 的 before 伪元素中插入计数器变量 nature-count 当前的值*/ /*insert the counter value as generated content by pseudo element*/ content: counter(nature-count); } </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> </ul>
渲染结果如下所示:
关于 CSS 计数器,可以参考 这里的 MDN 文档
既然它可以数数,那我想知道它是否可以数出素数和非素数的个数。
我们可以很容易地数出非质数的个数,因为前面的 li:nth-child
选择器选择了那些非主要项目,我们只需将计数器增加 1
当我们遇到他们时:
<style> li { counter-increment: nature-count } li::before { content: counter(nature-count); } li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(5n + 10), li:nth-child(7n + 14) { color: grey; counter-increment: nonprime-count; } </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul>
但是,渲染结果与我们预期的不一样:
分析原因,我们会发现,是因为非素数选择器的 counter-increment 属性把 li 选择器对应的这个属性覆盖了,CSS 在发生属性覆盖时,是不会将两个相同属性值联合起来的,而是会选择最终生效的那一个,此处对于素数位置上的 li 元素,显然是 counter-increment: nonprime-count; 这一句会生效。所以导致了当解析器遇到合数位置上的 li 元素时,只给 nonprime-count 计数器加了一,知道了原因,就很好解决了,我们让遇到这个元素时同时给自然数计数器和非素数计数器都加一:counter-increment: nature-count nonprime-count;
我们发现,因为 counter-increment
非主选择器的属性覆盖对应的属性 li
选择器,也就是当我们遇到一个 li
标记在非主要位置,只有 nonprime-counter
会增加,但不会 nature-counter
和 nonprime-counter
两者都增加。 当属性被覆盖时,CSS 不会组合相同的两个属性值 ,它会选择它的选择器具有更高优先级的一个。 在这里,对于 li
非素数位置上的元素,显然是 counter-increment: nonprime-count;
这将优先。 所以当解析器遇到 li
位置上的元素,它只加一到 nonprime-count
柜台。 知道原因后很容易解决这个问题。 我们可以加一到 nature-count
计数器和 nonprime-count
如果非主要位置则计数器 li
遇到: counter-increment: nature-count nonprime-count;
而且,我们得到了正确的结果:
显示统计结果
我们可以在 ul 的后面加一个标签,以便统计数据显示在里面。
<style> li { counter-increment: nature-count } li::before { content: counter(nature-count); } li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(5n + 10), li:nth-child(7n + 14) { color: grey; counter-increment: nature-count nonprime-count; } p::before { content: '前' counter(nature-count) '个自然数中,有' counter(nonprime-count) '个合数' '' '' ; } </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <p></p>
但结果又出乎我们的意料:
这两个 CSS 计数器变量显然存在值,并将其插入到 li
刚才的伪元素。 为什么会变成0?
CSS 计数器的范围
要理解这一点,我们需要了解 CSS 计数器范围的概念:计数器 的范围仅在最外层元素的父元素内,可以对其产生影响 。
在上面的例子中,两个计数器的计数元素是 li
,所以这两个计数器只在父元素内有效 li
,即在 ul
. 要解决这个问题也很简单,我们只需要让最外面的元素影响计数器。 我们可以在遇到 body
元素,通过这种方式,该计数器在整个页面中可用:
<style> body { counter-reset: nature-count nonprime-count; } li { counter-increment: nature-count } li::before { content: counter(nature-count); } li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(5n + 10), li:nth-child(7n + 14) { color: grey; counter-increment: nature-count nonprime-count; } p::before { content: '前' counter(nature-count) '个自然数中,有' counter(nonprime-count) '个合数' '' '' ; } </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <p></p>
这就是我们想要的结果
现在我们已经数完了自然数的个数和合数的个数,但是如何知道素数的个数呢?
我们都知道CSS不能做减法。 此外,这两个值存在于 CSS 计数器中, calc
函数只能实现硬编码字面量值的计算,结果不能直接显示值。
所以我们必须要找到一种让素数计数器递增的方法。这就意味着,我们必须使用选择器选出素数项才可以!
好像有点无能为力了,nth-child 选出非素数好办,但是选出素数,肯定没有能够实现这件事的选择器了。
黑暗中总有一线光明
我们还有 not
伪类选择器! 因为我们可以使用 nth-child
选择所有合数的伪类,这些选择器可以充当 not
伪类选择器的参数。 在这种情况下,可以选择所有素数。
li:not(:first-child):not(:nth-child(2n + 4)):not(:nth-child(3n + 6)) { color: red; counter-increment: prime-count nature-count; }
伪类选择器可以组合在一起,所以我们可以组合一些 not
伪类选择器,并使合数的选择器为 not
的参数。 通过这种方法,可以达到只选择素数的目的。 然后我们在选择器中添加一个计数器,以达到计算素数的目的。
只有最后一个问题,即统计结果总是显示在底部。 如果数据相对较小,则效果很好。 但是如果数据很大,效果就不太好了,因为如果你想看到统计结果,你必须滚动到页面底部。
假设我们把p标签移到ul前面,统计数据会显示0,因为每个计数器变量的值还是0。这就是为什么在DOM结构中p标签必须出现在ul后面的原因。
我们当然可以使用绝对定位将p标签移动到顶部,但是控制起来并不容易。
如果可以让计数器有值,并且没有绝对定位,除了让p标签的内容出现在ul前面,那就太好了。
还有一个方法,就是我们可以使用flex布局的 order
属性。 它可以在不改变 DOM 结构的情况下改变文档中显示的元素的顺序。 因为counter的计数只与DOM结构有关,不会影响统计结果的正确性。
最终代码如下:
<style> body { /*用body元素的counter-reset属性重置三个计数器以使它们的作用域在整个body内*/ /*make the counters global by reset them by body tag*/ counter-reset: nature-count prime-count nonprime-count; display: flex; flex-direction: column; } li { list-style-type: none; display: inline-block; } /*在before伪元素中插入计数器的值以实现数值递增*/ /*insert counter value into li tag*/ li::before { content: counter(nature-count) ','; } li:last-child::before { content: counter(nature-count);/*最后一个元素不需要逗号分隔*/ /*the last element do not neet a comma after it*/ } /*合数项选择器*/ /*non-prime selector*/ li:first-child, li:nth-child(2n + 4), li:nth-child(3n + 6), li:nth-child(5n + 10), li:nth-child(7n + 14) { /*递增自然数与合数计数器*/ /*increase nature and non-prime counter*/ counter-increment: nature-count nonprime-count; color: #ddd;/*合数变灰*/ /*如果想只显示素数项,可以把合数全部隐藏起来*/ /*display为none并不影响计数器的计数*/ /*display: none;*/ } /*素数项选择器*/ /*prime selectors*/ li:not(:first-child):not(:nth-child(2n + 4)):not(:nth-child(3n + 6)):not(:nth-child(5n + 10)) { /*递增自然数与素数计数器*/ counter-increment: nature-count prime-count; color: red;/*素数变红*/ } p { order: -1;/*让p元素显示在ul的前面*/ } p::before { /*通过p标签的before伪元素插入统计结果*/ content: '前 ' counter(nature-count) ' 个自然数中,有 ' counter(nonprime-count) ' 个合数,' counter(prime-count) ' 个素数' ; } </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <p></p>
您可以添加任意数量的 li
随时标记 ul
显示更广泛的素数和统计结果,而无需在其他任何地方更改代码。
渲染结果显示如下,题目是1000个数字的渲染效果:
Complete demo is here: CSS Prime
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
@joezimjs Wow, very clever idea!!! I have take a while understanding it~this is exactly how cascading works, thanks
The
:not
selector part is completely unnecessary. Just put it in the normalli
styles and theli:nth-child...
stuff will override it when it's not a prime number.Of course you could automate this bit with Sass:
I suppose you could just use an
<ol>
ordered list to get the numbers.I wonder if you could use selectors + css counter to only output prime numbers without hidding stuff?
Yeah I tried highlighting primes about 2 years ago and came up with a similar thing! https://codepen.io/matthewfelgate/pen/VYazKa?editors=1100