由于我是一名业余开发者,仍在探索 JavaScript 的深度,并且之前从未编写或发布过包,所以我当然没有资格向任何人教授任何东西。不过,我可以做的是分享我的写作经验,虽然本质上仍然是一个不完美的包,但我在构思过程中得到了很多乐趣。
这一切都始于一个问题,我很惊讶地发现这个问题在网上并没有得到太多解决,而且我发现可以用一种意想不到的方式巧妙地解决这个问题:那就是使用递归。
喜欢足球(或一般体育运动,或任何需要排名的竞争环境)的人肯定熟悉联赛表的概念:一定数量的球队相互竞争在一系列比赛中,之后每队根据胜利、平局或失败获得一定数量的积分; 联赛表按照积分降序收集所有球队,从而明确哪支球队获得冠军。
就编程而言,这种情况不会造成任何问题:检查哪支球队赢得了一场比赛很简单(在足球中,就像查看两支球队中哪一支进球更多一样简单),并计算总数所有比赛的得分都可以归结为简单的相加。排序步骤也不复杂,因为 JavaScript 甚至自带了默认的数组排序方法。
人们会想象,当两支或更多球队在积分上持平时,事情会变得不那么琐碎,在这种情况下,需要决胜局。再回到足球,常见的决胜局通常包括净胜球数(进球数减去失球数)、进球数、胜场数等。这再次表明,问题并不是那么难以解决:如果出现积分相同的情况,只需切换到根据不同的标准对相关球队进行排序即可。只要选择池是有限的并且是硬编码的,即使允许用户选择应用哪个决胜局,或者以什么顺序,实现起来也不困难。
这就是大多数在线解决方案解决问题的程度。然而,事情比人们最初想象的要微妙一些,也更深入一些。
事实是,任何类型的标准(净胜球、进球数等等,甚至是积分本身)都可以通过两种不同的方式进行检查:在全球意义上(所谓的总体检查),这是一种标准的检查类型,人们会查看完整表,因为它出现在所有匹配的比赛中所有球队都参加过比赛,并据此对球队进行比较;或者在有限的意义上(所谓的面对面检查),如果两支或多支球队积分相同,则仅在两队之间进行的比赛中检查任何后续的决胜局。有问题的团队。
一个例子可以清楚地说明这种差异。以 2016 年欧洲杯 E 组为例,最终的决赛桌是这样的
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Italy | 2 | 0 | 1 | 3 | 1 | 2 | 6 |
2 | Belgium | 2 | 0 | 1 | 4 | 2 | 2 | 6 |
3 | Republic of Ireland | 1 | 1 | 1 | 2 | 4 | -2 | 4 |
4 | Sweden | 0 | 1 | 2 | 1 | 3 | -2 | 1 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
然而,
欧洲杯是一项比赛,采用面对面的方式对积分相同的球队进行排序。这意味着,一旦我们认识到意大利和比利时需要当他们的平局被打破时,我们立即计算一个新仅由平局相关球队组成的子表。由于他们在第一场比赛中以 0-2 的比分结束了意大利队的单场比赛,所以这个分表看起来像这样(足球为获胜分配 3 分,为平局分配 1 分,为失败分配 1 分)
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Italy | 1 | 0 | 0 | 2 | 0 | 2 | 3 |
2 | Belgium | 0 | 0 | 1 | 0 | 2 | -2 | 0 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
这意味着,由于正面交锋的分数(三比零),意大利立即排在比利时之上。
不同的比赛通常会采用不同的风格(欧洲杯使用我们刚才看到的面对面风格,而例如 FIFA 世界杯则以整体检查而闻名),尽管它们都会切换到另一个风格应该是第一轮标准在打破给定平局方面没有决定性。
这意味着,潜在的正面交锋迟早会发生(要么在欧洲杯这样的比赛中立即发生,要么作为 FIFA 世界杯上潜在的第二次决胜局)。因此,我的结论是,我们查看的表格在排序过程中可能会发生变化,,因为头对头风格(无论是否立即出现)取决于此事实。
但这并不是我唯一的收获,如以下示例所示。
比利时再次成为 2024 年欧洲杯著名 E 组的主角,每支球队均积 4 分小组出线
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Romania | 1 | 1 | 1 | 4 | 3 | 1 | 4 |
2 | Belgium | 1 | 1 | 1 | 2 | 1 | 1 | 4 |
3 | Slovakia | 1 | 1 | 1 | 3 | 3 | 0 | 4 |
4 | Ukraine | 1 | 1 | 1 | 2 | 4 | -2 | 4 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
由于所有队伍积分相同,所以对战结果的子表无论如何仍然是完整的表。斯洛伐克和乌克兰按净胜球数排名垫底,罗马尼亚则因进球数较多而排在比利时之前(罗马尼亚 4 球,比利时 2 球)。事实上,这就是决赛桌的样子。
但有人可能会觉得这很奇怪:比利时和罗马尼亚之间只有一场比赛,比利时在第二个比赛日以 2-0 获胜。那么为什么比利时的排名没有超过罗马尼亚呢?这没关系吗?为什么在斯洛伐克和乌克兰与其他国家分开后,没有像我们之前那样重新计算子表?
事情的真相是,根据欧洲足联的规定(§20.01-d.),每一步都不会重新计算面对面的结果,但只有在决胜局的完整名单用完后。由于在积分和净胜球平局后,仍然需要检查进球数,我们会查看并这就是罗马尼亚最终获胜的原因。如果那个也打成平局,只有在那个时候我们才会真正开始考虑基于仍然打平的球队(比利时和罗马尼亚)之间进行的单场比赛的分表,事实上,比利时会因为他们的胜利而名列前茅。
所以这是第二个要点:排序过程中涉及到一个深度的概念,取决于你对它的了解程度必须做出不同的决定——例如是否继续进行子表重新计算。在这种情况下,您暂时不会继续,因为标准列表仍在继续。
这些是导致我决定排序函数形式的要点。
通过我的包实现的类的 .stands() 方法访问的排序算法依赖于递归函数
const sortAndDivideTable = (table, iteration, criteria) => { // ... }
在任何给定的步骤表中,保存当前要排序的团队数据的表,迭代会跟踪迭代次数和其他相关信息(例如,如果这是头对头类型或整体类型)检查),而 criteria 是一个数组,表示要应用的标准的有序列表(例如积分、净胜球、进球数)。
递归从一个表格开始,该表格是根据所有球队参加的所有比赛计算得出的,并且仍然可能是无序的。请注意,算法的第一次迭代始终是整体类型(因此在递归开始时进行初始化),因为根据定义第一次检查始终是在所有中获得的点数matches; 再次根据定义,条件数组中的第一个元素始终是“点”。
这个起始表也被安全地隐藏起来:在任何给定的步骤中,它的条目都不会修改,但它的行会一点一点地排序。为了便于参考,我们可以将其称为‘原来的排名’。
考虑到这一点,可以通过 2013-14 赛季欧洲冠军联赛 D 组提供的示例来理解该算法,最终结果如下
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Bayern Munich | 5 | 0 | 1 | 17 | 5 | 12 | 15 |
2 | Manchester City | 5 | 0 | 1 | 18 | 10 | 8 | 15 |
3 | Viktoria Plzeň | 1 | 0 | 5 | 6 | 17 | -11 | 3 |
4 | CSKA Moscow | 1 | 0 | 5 | 8 | 17 | -9 | 3 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
正如我们所指出的,在算法的第一步,排序标准是“点”。 sortAndDivideTable 的第一个工作是根据它来分割表格,我们使用标准的 .reduce() JavaScript 数组方法来执行此操作。
在这种情况下,我们会生成两支球队的两个子表,因为有两组并列球队(拜仁慕尼黑/曼城和维多利亚比尔森/莫斯科中央陆军,分别获得 15 分和 3 分)。然后对原始排名进行部分排序:其中一些球队肯定会高于其他球队(例如曼城肯定高于比尔森维多利亚),但积分相同的球队仍悬而未决。
当第一个递归步骤接近结束时,我们决定下一步去哪里:我们知道欧洲冠军联赛采用了面对面的风格,因此我们将其写为下一次迭代的类型;由于切碎的组的长度大于一(每个组中有两个组),这意味着我们在排序方面还有一些工作要做,我们将它们中的每一个作为新表反馈到 sortAndDivideTable 中。
让我们回顾一下第一组平局球队的历史。我们已经进入了一场面对面的比赛标准,所以首先我们计算他们比赛的分表:我们看到拜仁慕尼黑在主场输了(拜仁慕尼黑 2-3 曼城),但随后在客场获胜以更大的优势(曼城1-3拜仁慕尼黑),从而产生分表
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Bayern Munich | 1 | 0 | 1 | 5 | 4 | 1 | 3 |
2 | Manchester City | 1 | 0 | 1 | 4 | 5 | -1 | 3 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
现在,它们被绑在(头对头)点上:因此分组步骤使该表保持不变,排序步骤无关,并且我们到达递归迭代的末尾而没有完成太多工作;因此,我们进入下一个标准(净胜球),保持头对头类型,然后将表反馈到函数中。
由于我们正处于一对一的运行过程中,我们跳过了表重新计算步骤(如第二个要点部分所述,这是我们仅在一对一过程中执行的操作开始, 或当 结束 后 所有 应用条件后做出决定);但这一次的标准是净胜球,因此分组步骤成功地将表格分成两个子表格(每个子表格的长度为一个),因为一支球队的净胜球为 1,而另一支球队的净胜球为 -1 。这使得排序步骤可以再次查看原来的排名,现在可以肯定地说拜仁慕尼黑排名高于曼城。
由于分组步骤中生成的切碎集的长度恰好为一(意味着没有剩余的联系),我们通过不再调用该函数来退出该分支上的递归。
另一方面,在第二盘的历史上,我们看到比尔森维多利亚同样在主场获胜(比尔森维多利亚 2-1 莫斯科中央陆军),但随后在客场以同样的优势输掉了比赛(莫斯科中央陆军 3 场) -2 Viktoria Plzeň),从而产生
Position | Team | Won | Drawn | Lost | GF | GA | GD | Points |
---|---|---|---|---|---|---|---|---|
1 | Viktoria Plzeň | 1 | 0 | 4 | 4 | 4 | 0 | 3 |
2 | CSKA Moscow | 1 | 0 | 1 | 4 | 4 | 0 | 3 |
GF = 进球数(进球数),GA = 失球数(失球数),GD = 净胜球数。
两支球队在头对头净胜球和头对头进球数上并列,因此与对手不同,他们需要额外的标准(因此需要额外的递归步骤)进行排序。就欧洲冠军联赛而言,下一个标准是客场进球数,其中比尔森维多利亚队为 2 粒,莫斯科中央陆军队为 1 粒,在他们的交锋记录中。
再次,递归在这一步结束,原来的排名现已完全排序。
这个例子展示了递归方法的一些积极方面:两个分支不需要互相“交谈”——事实上,其中一个分支甚至需要一个额外的决胜局来进行排序,但这与递归方法无关。其他。任何关于我们已经达到的递归深度的问题,比如是否需要进行头对头的重新申请(如上面同名部分所解释的),都可以由每个分支独立地进行排序。
此外,由于任何两个分支的交集始终为空(因为它们是通过 .reduce() 在分组步骤中创建的,它总是将数组拆分为不相交的等价类),因此每个分支都可以独立对自己的排序球队在原来的积分榜上不踩对方的脚。这意味着,一支由极其平局的球队组成的分支甚至可以用完整个面对面的标准,从而恢复到总体检查,希望打破平局,而不会影响任何其他球队的比较——因为没有分支曾经影响过任何其他人。
还要注意递归如何结束:每当两个团队根据当前标准排序时,.reduce() 将产生长度严格小于当前表长度的子数组,这样递归的下一步将更接近达到长度为 1 的数组(此时不再调用该函数);如果平局一直持续到最后,最终标准始终是随机抽签或按字母顺序排序,这必然会以任何一种方式产生结果(团队标识符在输入时必须是唯一的)。
关于决胜局风格,以及该软件包的功能以及用户在定义锦标赛规则时有权访问的所有自定义选项,还有很多要说的。
我将来可能会写更多相关内容,特别是有关 .ties() 方法的内容,该方法提供了对哪些团队进行排序的文本描述,包括最终用户可能想要打印的方式和步骤为了清晰起见。
但与此同时,如果您想的话,请随时检查 GitHub 上的存储库...
作为橄榄球(英式足球)的忠实粉丝,我可以自信地说,没有什么比将球队放入表格更容易的了 - 和没有什么更难的了。它简单,因为逻辑显然足够简单:跟踪结果,计算点,按点排序;如果有任何平局,则应用平局决胜局。撇开一些细微差别不谈,故事就到此结束了。但是,正如他们所说,细节决定成败。
客场进球规则是一项著名的规则,早在 2021 年就曾成为新闻,当时欧足联废除了这一规则;但它真的真的从决胜局名单中消失了吗?或者再说一遍,在 2022-23 年欧洲联赛 F 组中,所有球队均以 8 分结束小组赛:这些平局是如何解决的?谁能预测如果两支球队在积分、净胜球和进球数上相等,欧洲杯上会发生什么……
...当然,这也是我学习的机会。如果您发现任何问题或需要改进的地方,请不要害羞并在评论中告诉我!
以上是我的第一个 JavaScript 包(以递归取胜)的详细内容。更多信息请关注PHP中文网其他相关文章!