本文根据现实合理想象,如有与当时现实相符,请以现实为准。
1
这是禅城区某小学唯一的00后班主任,姓梁,他也是这所小学唯一的男班主任。
三年级的某次综合测验完成了,梁老师看到班里的35个可爱的学生,然后绞尽脑汁,在想一个问题:如何让这些学生能够实现综合实力强的帮助综合实力弱的,从而实现总体的提升呢?
梁老师想到35个学生的排列方式时,想到他曾经学过的二叉树问题。又想到班里的男孩子总是对着能力强的“叫爸爸”,这不正是二叉树的概念吗?
这天早上,梁老师在拥挤的广佛线上,终于想到了能够让学生们互助和共进的方法。
梁老师这个二叉树分座位的方法,是这样的。
梁老师把全班的同学分为1-35编号(不是学号)。规定,先把最中间的的18号学生作为根,然后让18/2=9号学生作为18号学生的左孩子,然后让9/2=5(“/”表示整除,我们默认向上取整)号作为9号的左孩子,一直到1号(走到最左边)。
然后,最左边的各个节点对应的右孩子默认为根和上一级的根的算术平均(如5号的右孩子是5号和9号的平均数7号,当算数平均不是整数时向下取整(注意:同一层节点向下取整和向上取整需要对称,下同)。
接着,把剩下的节点按照对称的模式填满整棵二叉树,这就是一棵平衡二叉树了。
梁老师规定,整棵二叉树的根节点(也就是18号节点)由综合实力最好的男同学担任。综合实力次之的一个男同学,一个女同学分别放在9号和27号(就是左子树和右子树的根)。然后根据级别,一直排下去,形成班级综合实力梯级管理方案,也就是1梯层1人,2梯层2人,3梯层4人,4梯层8人,5梯层16人,6梯层4人的组织管理。
班会课开始了。
梁老师走进了班级的课室,然后他和同学们说:“今天我要让你们自己交好朋友,好不好?”“好!”教室里齐声呼应。
梁老师班会课上,表彰了在综合测验中成绩最优秀的几位同学,给了他们礼物以示奖励。然后,梁老师把那棵二叉树展示在PPT上,梁老师对同学们说:“同学们,这里是即将要实施的梯层管理的方案,你们可以看到,这是一棵倒着的树(像不像你们的学习笔记?),树上有很多层,每一层都对应着你们的不同角色。这些数字代表你们的班级位置,除了那些最好和最差的同学之外,其他的同学可以自由选择你自己的位置(意思就是1、2、6梯层由老师确定,3、4、5梯层由学生自己选)。
但是要注意,梯层高(也就是离树根近)的同学,要有管理梯层低的同学的义务;梯层低的同学要向梯层高的同学学习。这样的话,每个人都能成为班级的小主人,好不好?”
那些学生都沸腾了起来。
梁老师继续说:“我会给你们抽号码的机会,1号、9号、17号、18号、19号、27号和35号都是已经确定同学的,其他号码你们可以随机抽取。大家排好队,到讲台前面随机抽一张纸,纸上的号码是几,就对应树上的位置,也就是说,你就和哪些人一组了。”
学生们欢呼雀跃,很快,28个3、4、5梯层的同学究竟是谁确定了。接下来,就是调位的时候了。
根据二叉树和梁老师的安排,课室被分为7排,老师确定的7位同学预订7排座位的一部分,然后按照下图的座位表就坐,数字代表几梯层的同学/对应二叉树是几号。(注意,座位是没有同桌的,只有直接前往后的7排)
同学们刚开始很兴奋,不过在一段时间的磨合之后,他们开始学会了互相合作,并懂得了责任感。
梁老师这样调位的目的,主要是为了让同学们以大组套小组的方式,成为班级的小主人。这样可以让优秀的同学和相对较差的同学坐在一起,实现互帮互助,共同进步。
平衡二叉树美观整齐,可以更直观地看一些东西,给人整齐划一的美感。而且平衡二叉树更适用于没有外在约束条件(如学生的性别、性格等)时的排序,如果一群新生编座位的时候,就可以利用汉语拼音的排序来编二叉树,进而像这样确定一开始的座位。
2
这是2029年一个春光明媚,木棉盛开的日子。一群青年正在长者中心做志愿者,他们志愿者中的其中一个任务,就是开着电单车在街道的特定区域为行动不便的老人派送午餐。
如何使总行程最短?(总行程=路程*装载的物品质量)
有6名志愿者负责6个社区的配餐任务,他们分别有5、7、9、11、13、15份长者午餐,需要派送往6个不同的社区。因长者午餐与社区的对应数量是一定的,以下讨论社区如何分布时总行程最短。
这种最优化的二叉树是赫夫曼树,也就是一种所有双亲节点都有两个孩子的二叉树。这样能实现尽可能短的总行程。
首先,我们把这些数字都罗列出来,按照大小升序排列。
然后,我们找出这些数字里面最小的两个,把他们连起来,把他们的和作为他们的双亲。
接着,重复上述操作,把每一次没有双亲的节点放在一起,比较出最小的两个,再连起来,直到只剩下一个没有双亲的节点为止。
最终经过整理之后,就是一棵赫夫曼树。
我们可以看到,这时候的总行程为(5+7+9+11)*3+(13+15)*2=152,也就是说,当需求大的社区离长者中心更近的时候,总行程更短。
两天之后,志愿者们正在给长者中心准备即将到来的周末活动的礼品,需要购买199个6种颜色的礼物,礼物按无规律顺序分发。
有人认为,如果是单纯的“红黄黄绿紫蓝绿红绿黄红橙……”的顺序分发礼物,是很乱的,为什么不能以0和1来标记礼物的颜色呢?
这也需要用到赫夫曼树,6种颜色编号需要3位二进制,而且是每个颜色编号都要用3位。而赫夫曼树可以显著缩短编码。
而缩短编码也属于上面更短的行程的一种解决方式,如当红、黄、蓝、绿、橙、紫色礼物的数量分别为39,58,25,18,43,16个的时候,如果用一般的编码,需要3*199=597位,如果使用赫夫曼树,只需要2*(58+39+43)+3*25+4*(16+18)=491位(缩减了约18%)。
虽然0和1的编码对人类语言来说还是不好记,但到了计算机领域,0和1的前缀码更加容易被执行。
赫夫曼树的结构数量很少,而且赫夫曼树的形态很优美。
通过枚举可以知道,2个叶子节点的赫夫曼树有1种,3个为2种,4个为5种,n个为C(2*(n-1),n-1)/n!种。(即卡特兰数)
当我们规定左子树必须小于等于右子树的时候,我们可以得到,通常情况下,根据每个叶子节点所有叶子节点的值的和的1/2^n,可以大致判定它在哪一层。而当叶子节点大于所有叶子节点和的1/2时,则这个节点必在第1层。
而赫夫曼树最终有多少层的问题,在通常情况下是log2(n)+1(n为叶子节点个数)。当叶子节点间方差较大的时候,就需要用到更多的层数,但不会大于n层。
在前缀码的压缩效率上,当叶子节点数量近似于2^n+1的时候,或者叶子节点的值的方差很大的时候,压缩效率会更高,尤其是字符数量更多的时候,这个数值会趋向于[log2(n)]/[log2(n)+1](n为字符种数)。例如包含9种10000个字符的字符串,正常编码需要4*10000=40000位,但当9种字符分别有9992,1,1,1,1,1,1,1,1个时,只需要9992+2+3+4+5+6+7+8+9=10036位,压缩了75%。
总之,赫夫曼树把树的路径降低到最小的方法,使得对于形似树结构的问题,可以用更加压缩的方式解决。
志愿者们通过对赫夫曼树的应用,锻炼了他们实现整体最优的能力。一棵二叉树让他们的脉络更加清晰。
3
一个小男孩正在做四则混合运算的作业题,他在计算到4*(2+3)-9/3的时候,想着如何更细化地解决问题。
他回去问老师,老师说:“先算2+3,然后乘4,之后再减去9-3就得到了。”。然而他还是没有找到答案,直到他按计算器的时候发现计算如此迅速,他才想到更深入的事情。
小男孩想到,计算器并不懂运算规则,可不可以用盒子储存计算的数,然后一个个计算呢?
现实中我们计算四则运算的时候也是这样的。我们需要用到两个被称为栈的盒子,它的特点是像只有一个出口的停车场一样,只能从一端进出,最后进去的最先出来,最先进去的最后出来。
我们设置这两个栈,规定都只能从上面进出(下同)
所以在计算4*(2+3)-9/3的时候,需要设计两个栈,一个存储数字,一个存储运算符。
出于辨别表达式首尾,需要先把‘#’压入运算符栈。然后判断下一个运算符,如果是数字,压入数字栈,如果是运算符(指‘+’、‘-’、‘*’、‘/’、‘%’、‘(’、‘)’、‘#’,‘#’用于判断开始和结束),压入运算符栈。所以之后把4压入数字栈,‘*’压入运算符栈,‘(’压入运算符栈,2压入数字栈,‘+’压入运算符栈。当3压入数字栈之后,由于‘+’优先级小于‘(’且大于下一个压入运算符栈的‘)’(见附表),,产生局部极大优先级,这时先计算2+3,把结果5压入数字栈并把括号消掉。之后依次类推,直到最后把‘#’压入运算符栈,把两个‘#’消掉,清空运算符栈,并把合并到最后的数字栈弹出。
上图显示的就是这个计算的过程。计算的顺序是2+3,4*5,9/3,20-3得到17的结果,但是运算符的优先级是人定义的,计算机不知道4*(2+3)-9/3和4*2+3-9/3有什么区别。所以,需要更少歧义的计算方式。
我们不妨用树的思想去思考。假如把一个表达式按照二叉树的形式排布,我们就看到一棵以运算符为根,以数字为叶子的二叉树。这样的表示可以更直观地看到四则运算的运算顺序。
我们可以思考一下:这样的二叉树有多少种遍历它的所有元素的方法?
常用有3种,分别是左根右、根左右、左右根,它们分别是二叉树的中序、先序、后序遍历,这3种遍历方式产生了中缀、前缀、后缀表达式。我们正常看到的四则运算表达式是中序遍历的结果,表示为中缀表达式(4*(2+3)-9/3),而先序遍历会产生前缀表达式(-*4+23/93),后序遍历会产生后缀表达式(423+*93/-)。这样的表达式不需要括号,在一位数下没有歧义,能让计算机知道计算的确实是4*(2+3)-9/3而不是4*2+3-9/3。
这种表达方式同样需要用到栈的结构,只是它的进栈顺序并不是上面讲到的那种按表达式顺序入栈的方法,而是根据二叉树的根-左子树-右子树的方式入栈。三种遍历方式之间的差别仅在于是在即将入栈的时候就输出,还是左子树全部入栈后和根结点一起输出,或是在右子树遍历完成后出栈并输出。
当然也可以用一分为二的方法,也就是在根结点下,每次分别对左子树和右子树执行同样的操作,只需要更改先输出、遍历完左子树输出、或遍历完左右子树输出即可实现先序、中序和后序的输出。这也是栈的调用,我们称为递归。
小男孩想到这里顿然开悟了,原来四则运算就是把数字和运算符想象成车,放在两个只有一个出口的停车场。当对应的车进去的时候刚好出现局部极大优先级,就把运算符和数字栈中的对应的车子开出来,对应地运算。一直这样做,直到所有的车都开出来。还可以想象成以运算符为根,数字为叶子的二叉树,用左子树-根-右子树的方式计算出来。
不过需要补充的是,以字符型存储栈或二叉树时,只能做一位数的运算,需要完成多位数甚至是浮点数的计算的话,需要把数字栈定义为整型或浮点型。同样地,由于不能分清楚是几位数,所以两位数以上的前缀和后缀表达都是有歧义的,像95/(29-24)+(1+5)*6的后缀表达式952924-/15+6*+,是不能在计算机上运行的。
二叉树遍历本质是一个栈的过程,因为它的思想用到了栈的递归思想,也就是逆序输出。
附表:运算符比较
4
2031年的春天,思聪和大家正在讨论着新研发的物流用自动驾驶飞行器在粤港澳大湾区的发展。大家讨论的分歧在于应该如何开通第一阶段的航线。
出于飞行器的续航里程和应用于飞行器的大功率充电设备的限制,开通的航线数量不能太多,而且每条航线的距离不能超过飞行器电量从100%到40%可以飞行的里程。
思聪想到,这是最小生成树的思想,也就是覆盖所有航点的同时,开通最少的航线、且每条航线的里程尽可能短,反映在总航线的里程最短。
在经过讨论之后,大家决定先挑选10个最有价值的航点先开通航线,这10个航点的对应关系如下图所示,以紫洞为1号航点,按红色航路逆时针方向依次编号2-10号航点。
主要的意见分成两组,一组是以飞行器所在基地为原点,不断向最近的航点开辟新航线。另一组则是和同行联手,从各个公司的基地出发,各自组建航线网络,最后再连成一体。他们的区别正是克鲁斯卡尔算法和普里姆算法的区别。
其中一组的方法认为,在目前还没有和其他飞行器运营企业统一标准之前,应该做好自己在紫洞村的基地,通过一站一站的接续连接去其他地方的航线。
他们的想法是:以1号航点为起点,默认没有通达的航点的距离都是2147483647。然后先寻找和1号航点距离最近的航点(2号),开通该点航线,然后从2号航点出发,继续寻找最近的航点加上去,一直循环下去,直到所有的航点都被连接。
这样的方法被称为普里姆算法,它的特点是每一步寻找最近的点,然后把边加上去,再从这个点出发继续寻找。所以需要寻找n*(n-1)/2次(n为点的个数)。源代码如下:
这样的好处是一步步开通航线,先从自己的基地开始,逐步逐步走出去。但缺点是初期无法获得多个基地,紫洞村的基地还是10个航点中最西边的,在飞行器续航里程仍然受限的情况下,难以保障维护需要。
另一组则认为,只有与其他飞行器运营企业团结起来,才能实现在粤港澳大湾区内物流用飞行器的行业标准统一。所以他们认为,先以当时大湾区三大物流飞行器基地为基础,各自先开辟航线,然后再连接在一起形成航线网络。
它的方式就是先开通所有航线中最短的紫洞-三龙湾,灵山-石岐,塘厦-龙岗(里程分别为24、25、27),然后根据距离最短的原则开通其他航线,直到所有的10个航点都被连接上,这时一共选择了9条最短的航线。
这种方式称为克鲁斯卡尔算法。它的主要计算难点在于需要对所有的边进行排序,比较费时间。源代码如下:
这样的好处是每个基地各自开通航线直到统一为一张网的过程,同时也为各个运营企业的磨合做准备,有利于更好地服务于大湾区航空物流网。缺点是初期各自自行组网的情况下大湾区内的航空物流网络发挥不出对拥堵的公路和稀疏的货运专线铁路的分流作用。
思聪支持支持克鲁斯卡尔算法的一组,他认为在普里姆算法的一组中,不但需要繁琐的计算(如附表),而且在没有统一标准的情况下,飞行器在基地以外的保障不得到统一,影响整体运营。
这是交通网中最东、最西、最北的航点所在位置,互相距离遥远,只有一个基地难以保障故障维修等运营问题
而克鲁斯卡尔算法一组中,只开通最短的9条路径作为最小生成树使得飞行器的运营成本最低的同时,多个公司联合组网相当于有多个基地,统一标准下不同公司的飞行器可以到不同的基地进行同样的保障工作,是一个一举多得的举措。
最后,大家一致同意了克鲁斯卡尔算法的那种思想。这样可以保证公司之间的磨合,使得交通网更成熟。
全世界的物质之间都是先形成局域小网络,再形成大网络的,交通也是
附表:
普里姆算法计算表格
5
蓝蓝的数学辅导书上,有给出一个格子图,问有多少条最短路的问题。思聪看到这个问题,联想起众多的数学知识。
下面是蓝蓝辅导书上的街区图。规定,目的地在哪个方向(正北朝上)就只能向哪个方向行走,例如目的地在出发地东北方向时只能向北或向东,且规定所有道路正交,每个路口间距离默认为1里程。
在这个街区图上,可以数出来,从同济大桥枢纽站到锦华路福宁路(以下都会用这种路名路名的方式命名路口以便定位)有2种走法,到建新路福宁路有3种走法,所以到三鸣台小镇(锦屏路福宁路)有4种走法。
这样一直数下去,就可以得到同济大桥枢纽站到锦屏山公园(凤鸣路锦屏路)有10种走法,同济大桥枢纽站到聚贤学校有924种走法。于是得到结论:在这样的情况下,从一点到另一点有C(n+m,n)种走法,可以理解为n次向一个方向,m次向另一个正交的方向行走,一共需要n+m次行走。这个公式就能推出在这个街区里任意两点的路径数量。
此方法源代码如下:
上面的是很简单的,但是如果增加一条分隔线,只准行走在分隔线的一侧,就不同了。
我们在聚贤学校和同济大桥枢纽站之间画一条对角线,如下图所示。
因制图原因,对角线并不是直线
在此处规定,从同济大桥枢纽站出发初始只能向北走,而且不能越过浅蓝色的边界。这时候就像是一群有2元纸币和1元纸币的孩子玩1元的游戏,在没有零钱的情况下,有2元纸币的孩子必须等前面有1元纸币的孩子付费之后才能给2元的纸币并找回1元的纸币。
这需要用到卡特兰数,也就是《一棵更优化的二叉树》(点击可跳转)留下的问题。
在不能越过起点向目标方向平行于对角线的线的情况下,从起点到终点的走法和之前是一样的,唯一的区别是在遇到对角线的时候可能只有一种走法。所以对角线上的两点之间有K(n)=C(2n,n)-C(2n,n-1)种走法(n表示跨过的对角线格数,K(n)表示卡特兰数),如果超出对角线,则按照走到每一点的路径种类数等于走到之前的2点的路径种类数之和进行递推。
通过这样就可以得到K(0)=K(1)=1,K(2)=2,K(3)=5,K(4)=14……这就是卡特兰数的具体数值,于是可以得到从同济大桥枢纽站到聚贤学校(不越过浅蓝色的边界)有132种走法。源代码中实现,需修改gezirecu函数为如下:
这是最后一种,也是最现实的一种街区走法,也就是有些路是走不通的,被山水或建筑拦住。这种的计算方法也是一样的,只需要通过设置通过无法通行的路段的路径数量为0,源代码需修改如下:
思聪想起他曾经做过的奥数题目,发现一切如此相通。可以看到,在简单的最短路径数量问题上添加不同的限制条件,就可以得到不同的限制方式,充分体现着动态规划的思想。
当限定不能越过对角线的时候,就是二叉树的种类的体现。因为在有n个结点的二叉树上,可以在其所有形态的树对应的左子树或右子树上任意不造成对称的位置新增一个结点,有K(n+1)种方式。就像到对角线经过的第n个点一样,每走一步都有向x方向或向y方向的可能,也就是K(n)种方式。
而限定和不限定不能越过对角线的差别,也是二叉树考虑对称与否的差别,因为从对角线的两侧到达对角线上的点的路径是等效的。这就是街区路径与二叉树之间的联系。
蓝蓝把每一步的数字标在格子的交点,利用上面的方式,在抽象的街区上,写下优美的数字。
6
在本号描述的世界里面,有许多的儿童,他们组成了本号情节背景的重要的一部分。本号的号主常常走在大湾区的街头巷尾,看到入学的儿童名册,看到儿童们在少年宫的表演,发现浩、宇、涵、晓、雨等等太多了(含同音字)。经常在一个片区里面出现一堆“子浩”、“雨涵”等等,这对于大量数据的录入很不理想。
一棵树从他的脑海中长出,它如同鸡蛋花或是凤凰木,数据沿着鹰爪似的树枝走下去,它的数据总是按着有序的方式插入,这就是二叉排序树。
在第1部中曾经留下一个问题,是否可以把儿童们按照他们的姓名的汉语拼音排序分布在一棵二叉树中?答案是可行的。下面将体现如何把这么多的儿童的数据加载在一棵二叉排序树中。
在开始的时候,第一个结点直接成为根结点。然后第二个结点开始,每次和前面的第一个结点比较,如果比它小就会插入到左子树,比它大就会插入右子树,当结点与之前的结点有重复时,新结点插入在其左子树的最右边。
在计算机中操作时,结点的比较方式为其对应编码方式的值的大小,英文为ASCII码,中文根据编译器的不同,可以是部首或汉语拼音排序。这样就能得到一个有序的二叉树。
但问题在于,当插入的顺序近似于有序的时候,二叉树就会退化为单支树,无法发挥二叉排序树把搜索次数尽量压缩到log2(n)的作用。同样的,在删除的时候,虽然在删除的时候,叶子结点直接删除、度为1的结点用其左或右子树补上,度为2的结点用左子树的最右结点顶上,但当只删除一侧的结点时,还是会出现不平衡或退化为单支树的情况,也无法发挥二叉树的优势。
所以为了防止出现这样的问题,就涉及到旋转的问题。平衡二叉树也是从这里开始的。
平衡二叉树就是任何一个结点的左右子树的高度差的绝对值不大于1的二叉树。这样的二叉树可以大幅减小平均搜索长度,使得平均搜索长度与log2(n)成正比(平均搜索长度准确值为((n+1)/n)*log2(n+1)-1),同时使得二叉树形态更加美观。为了实现平衡二叉树,就需要涉及4种旋转方式。分别是LL、LR、RR、RL型旋转方式。
其中LL型的调整方式是把根结点和它的左子树向右旋转,把根的左孩子的右子树变成右孩子的左子树,根的左孩子变成新的根结点,原来的根结点变成新的根的右孩子。RR型的调整方式和LL型相反,但类似。LR型的调整方式是先把根结点的左子树左旋转,然后把根结点右旋转,RL型的调整方式与LR型相反。它们分别对应左子树的左子树高(高指出现不平衡)、右子树的右子树高、左子树的右子树高、右子树的左子树高的情形。
通过在插入和删除的时候及时对二叉树进行适当的旋转,就可以保证任何时候任何一个结点的左右子树的高度差的绝对值不大于1,实现平衡二叉树的目的。
在这样的自平衡算法下,二叉排序树的效率得到更大化,于是就能把这些儿童们的信息以更高的效率存储在二叉树当中,体现在最多n次搜索和最多(log((1+√5)/2)(√5*(n+1))-2)次搜索的区别,大大节省了时间,也能让线性的空间得到扩展。
例如100个数据在最坏的情况下,只需要搜索9次,这相比于顺序表中最大的100次搜索节省很多。而且把儿童们的信息存储在二叉排序树插入新结点或删除结点时,可以不断平衡优化,使得搜索次数总是趋向于一定值。当儿童们的数据变得很大的时候,平衡二叉树的优势会更加显著。
他很喜欢平衡二叉树的美观和高效性,所以在存储查找一堆的子轩、雨涵、子浩(含同音字)的时候,平衡二叉树就会显得特别有序。因为即使有很多同名的儿童,也可以根据姓名之外的其他字段(例如ID号码等)再次进行判断插入不同的位置,使得每个儿童在二叉树中都是唯一的。
于是儿童们就可以根据这棵平衡二叉树找到自己的朋友了,无论是分层还是按照左右子树分区,这样就解决了《在二叉树中成为主人》中的问题。儿童们因此能在二叉树中找到自己的位置,建立起同龄人的树状协作网络。
他希望这样美观的二叉树,可以让儿童们利用他们的数学等知识,努力探究这棵树中的真谛,真正成为二叉树的主人。
他的思绪如生长之树,写下了计算机实现儿童的数据存储的源代码,见本段附录。
本号所写的数据结构,看似是高深的问题,实际上,就是计算机与数学在现实世界的应用。这些文章的字里行间,是对世界思维网络的思考,期待后辈能够从小建设起兼具深度和广度的思维网络,让思维成为改变世界的动力。
附:儿童平衡二叉树源代码:
7
大湾区的仲夏,是属于龙舟的。
思聪已经两年没有看过龙舟了,即使这样,龙舟精神仍然在他的世界观中形成重要的一部分。齐心协力,共创幸福,风雨同舟,甘苦与共,不仅是龙舟竞渡活的精神,更是共克时艰的精神支柱。当困难的日子过去,龙舟竞渡将更有意义。
思聪的思绪流淌着龙舟竞渡的画面,然后推演着龙舟赛中排出所有名次的方式,想到了两种不同的龙舟赛方式。此处默认龙舟有13队,它们划过整个社区的平均时间分别为212,188,175,204,223,193,221,197,235,184,216,230,199(单位为秒),体现为好与差。
为体现排序方法的过程,龙舟比赛单次参与的龙舟队数量应尽可能少。
其中一种是堆排序,把龙舟赛中所有的龙舟队排列在一棵完全二叉树上,然后进行调整。此时对龙舟队中对应不是叶子结点的与其对应的孩子结点的龙舟队之间进行比赛,如果某龙舟队的结果比其两个孩子中较差(对应龙舟队)的好,则交换位置。如果交换后仍然出现双亲比孩子好,继续交换。然后每轮重复,直到最终龙舟队中的根结点对应为最差的龙舟队,此时会形成每个结点的都比自己的叶子大的大顶堆(如下图)。
为什么要让最差的调整到最上面?因为虽然已经调整成为了大顶堆,但它排序仍然不是有序排列。所以需要交换在根结点的龙舟队和二叉树内最后一个结点的龙舟队的位置,交换后最后一个结点固定,同时把其他结点调整为大顶堆(每一轮都会筛选出最差的龙舟队,也就是最后的结点,但实际只需要对双亲比孩子好的进行比赛,使得最差的一直在上面),源代码如下:
比赛结果如下图所示,绿色为已决出排名。
这样的情况下,需要经过2n次比较(即两两之间的比赛,k队比赛(k>2)以上视为C(k,2)次比赛,下同),然后进行n-1轮循环,每轮比赛log2(n)次。因此需要不大于nlog2(n)+2n次比较实现决出所有名次且每次龙舟赛中参赛队伍都尽可能少。
另一种是归并排序。通过把龙舟队分成2队2队1组(按初始顺序,有剩余的单独比赛)各自进行比赛。当所有龙舟队的数据出来之后,通过把数据中相邻的2组合并为新的1组(不足的自成1组),一直到所有都被合并,最终即得出有序结果。
这种方法的特点在于通过对2队2队1组比赛数据的比较,即可实现排序。其源代码及结果如下(结果第1、2行为输入,其他为结果):
其实现方式在于在比赛结果出来之后,每次把2个小组的数据进行合并,不够合并后的组长的自成一组合并,每次选组内对应的更小的结果合并到新的结果当中。一直到全部被合并,即可得到有序的第几名。
这样需要的比较次数为nlog2(n),并需要在2个数据域之间倒来倒去轮流合并,所以每轮都要比较n次用于确定下一个合并的,总共需要合并log2(n)轮。
这样只需要比赛1次,然后就只分析数据。其不足之处在于不能精确地决出每一名,对于足球等项目来说不适用。不过其最大的好处是由于只需要不断合并而无需交换,所以它不会破坏原来数据的先后关系。
形成大顶堆的过程,就是把差的浮上去并不断交换调整的过程,通过不断在完全二叉树中起起落落(指层次)实现每次决出最后一名。不过这样比赛的次数更多,需要一边比赛一边调整完全二叉树的序列。
归并排序是对比赛的整理。仅2队2队比赛减少了每次龙舟赛的参赛队伍和比赛次数。虽然不能实现每次筛选第几名,但这种方法更有利于在龙舟队数量巨大,无法保证河涌净宽和龙舟净长能容纳足够多龙舟一起竞赛的情况下,尽量减少比赛。
尽量减少龙舟比赛场数的思想,是智慧的体现。这两种排序都用到了类似二叉树的思想,堆排序直接使用完全二叉树,归并排序则使用类似二叉树的循环方法,这些方式使得比较次数降低到nlog2(n)级别,减少了比较的次数。
当芳村按下了暂停键,思聪更加体会到齐心协力的龙舟精神,可以聚集思维的力量,解决问题,让幸福生活继续下去。期待未来,龙舟的精神能够进一步升华,为龙舟竞渡带来新的内涵,继续在每年仲夏,成为鼓舞大湾区发展的力量。
本文于2021年6月6日第1次增补