引子

前段时间,因为要评估图片的缓存,所以需要同事统计一套网页应用中图片的文件大小。统计结果大意为:

页面大概会引用 20 张图片,每张图片的大小,最大值为90K,最小值为10K,取平均,(10+90)/2=50K。

问题

在统计学中,有几个概念,最常见的是均值。假设随机取100张图片,那么均值的大小,应该是这100张图片的总大小,再除以100,得到的值。

另外还有中位数,这需要将100张图片,按文件大小,从小到大排序。由于总数100是偶数,所以需要取排序后第50张和第51张的大小,求和,再除以2,得到中位数。

最后,还有一个众数的概念,是指出现次数最多的值。对于图片大小的统计,我们先需要对文件大小划分几个区间格子,例如以 10K 为一个单位,分别统计 0K~10K、10K~20K、…、80K~90K 区间的图片数量,以数量最多的区间,作为最终的结果。

上述三个概念都可以作为普通意义上的均值,即可以用来表示一张图片平均多大。有了这个值之后,只需要知道图片的总数量,就可以大致评估图片的总大小。实际上,在这个目标下,仍然是均值是最好的度量。

而例子中的50K,却不是上述三个概念中的任何一个。

例子中的10K和90K应该可以看作是样本的极小值和极大值。50K即为极小值和极大值的均值。似乎并没有见到这种计算结果对应的概念名称,暂定为均极值。我甚至怀疑是因为均极值并不能对估计样本有帮助,所以被人们忽略掉。

后来我们实际找了一组图片来统计,得到的均值为30K。那么均极值相对于这个真正的均值,偏差为

(50-30)/30*100% = 67%

已经错得离谱了。

为什么会犯这种错?

我怀疑人脑可能跟电脑类似,也有存储单元和计算单元。对于前述四个概念,我们可以从计算过程来对比存储的消耗和计算的消耗,也可以看作是一个人为了得到上述四个概念所需要付出的心智的努力。

假设

需要对问题的前提做一些简化的假设。

  • 图片的总数为100。这一个假设纯粹是为了描述方便。
  • 工具可以快速查看到每一张图片的大小,单位为K。但工具并无求和功能。

均值

如果要计算均值,需要求所有图片的大小总和。作为人类,是不太可能一次性将所有100个图片大小,全装进大脑里,然后得出总和。我们从小学接受的训练,最熟练的还是两个数相加。作为成人,心算两个多位数相加,是没有太大压力的。心算三个多位数相加,就有些考验人了。所以,这里自然而然的算法,是先将第1个数和第2个数相加,得到一个临时的和;再与第3个数相加,得到一个新的和;然后一直重复下去,直到所有数字都累加完,得到总的和,再除以图片的总数量,得到最后的均值。

这共需要进行99次加法,1次除法,并需要一直记住1个数,表示临时的和。

从算法的角度看,实际上有一个优化的版本:先将相邻的数相加。即第1个数和第2个数相加,第3个数和第4个数相加,一直到第99个数和第100个数相加,这样完成第一轮,共50次相加,得到50个临时的和。接下来

  • 第二轮:重复相同的过程 ,需要进行25次相加,得到25个和。
  • 第三轮:由于25不是偶数,所以只能进行12次相加,得到12个和,以及一个未参与相加的数,共13个数。
  • 第四轮:进行6次相加,得到6个和,以及一个未参加相加的数,共7个数。
  • 第五轮:进行3次相加,得到3个和,以及一个未参加相加的数,共4个数。
  • 第六轮:进行2次相加,得到2个和。
  • 第七轮:进行1次相加,得到最后的和,再除以图片的总数量,得到最后的均值。

这样相加的次数为分别为50、25、6、3、2、1,共87次。虽然相对于原始的累加算法,减少了12次加法,但付出的代价是需要记住更多的、临时的和。不借助笔和纸,人是很难记住第一轮产生的50个临时的和。所以这个方法并不实用。

我们需要增加一个假设:

  • 计算的过程不得使用笔和纸,即整个过程是一场心算。

中位数

中位数的前提是要对所有图片按大小进行排序,这是一个非常痛苦的过程。普通的排序一般时间复杂度为O(n^2),对本例,通俗地说,需要进行约100*100次比较,实际比较的次数应该是(100x99)/2,共4950次。如果采用优化的算法,可能会降到100xlog2(100),大概在600到700之间。但这可能还不是最重要的,步骤多只是增加时间,只要有足够的耐心,总是可以解决。

对于中位数,最要命的是空间的消耗,也就是需要一直记住的数。不管是普通的排序,还是优化的排序,都要求记住这一百个所有的数。这对人类,几乎是不可行的。

众数

众数的心算相对中位数的心算,压力相对小一些。我们假设以10K为间距单位,假设图片的大小不超过100K,那么我们只用统计10个分段的图片数量。

判断一张图片是否在对应的分段中,只用比较大小。相对加法操作,比较大小的所需要消耗的心智要少很多。

更进一步地,我们自然而然会用上十进制,也会按十等份进行统计,所以实际的步骤是先大致感觉一下分布,可能主要的步骤是找到最大值,确定图片大小的最大值在100K左右,所以会决定按每个等份10K来统计。实际统计的过程中,只用看图片大小的十位上数值是多少,并不需要严格地进行两次比较。例如,某一张图片大小是37K,那么就根据十位上的3立即决定了30K~40K的分段对应的数量需要加一,并不需要将37与所有分段的两端进行比较。或许人脑自动地避免了原始的、耗时的、对各分段范围的检查:

    for i, segment in enumerate(segment_list):
        if segment.begin <= size and size < segment.end:
            count[i] += 1

而是采用了类似下面的优化后的算法:

    count[size//10] += 1

假设人脑真的如此,那么需要对100张图片进行类似的操作,需要用到10个临时的变量。其中除法的运算,未必是存在的。因为工具会直接给出图片的大小,图片的大小是37K,已经直接显示了37这个数值,人脑会直接用看到3,而不必用37去除以10,再取整。或许我们应该假设,有些步骤对人脑来说,时间复杂度是O(0)的。

均极值

这个概念在正统的教科书中未必存在。只是我们错误地用了这种方法,所以取了个名字来方便讨论。先找到极小值和极大值,再相加除以2。

要找到两个极值,至少需要记住两个临时的值。然后遍历100张图片,不断比较大小,并在遇到更小或更大的值时,及时替换,共要进行200次比较。

实际比较两个数值,人脑应该也会自动优化,跟电脑的逻辑似乎不同。电脑里,一般在芯片层,都直接提供了比较的指令。人脑的逻辑看起来似乎很复杂,但实际效率很高。我们会自动认为两位数比三位数要小。如果位数相同,则会依次从高位开始比较。为什么采取这种方式,应该还是为了偷懒。因为对人脑来说,输入并不是直接的数,而是查看工具所显示的结果,这种结果对人脑来说,首先是视觉的图像。我们应该是先识别出表示图片大小的数字,可能是一位数、两位数或三位数。如果把这种多位数,先转换成一个数值,再进行比较,需要的运算与记忆会更多更大。哪怕只是把看到的37,读成“三十七”,都需要额外增加一个“十”,这也是一种负担。

对比

我们可以对上述四种值的计算,做一个对比。

计算方式 需要记忆的数值 除法次数 加法次数 加一次数 比较次数
均值 1 1 99 0 0
中位数 100 0 0 0 4950
众数 10 0 0 100 0
均极值 2 0 0 0 200

对人脑来说,可能存在以下三点假设

  1. 相对“加法”,“比较”更轻松的事情。
  2. 计算的次数,只会导致消耗的时间变长,但在一个较大的范围内,都是可接受的,虽然多次进行,会考验一个人耐心。
  3. 记忆的数量,是有上限的。要记住100个会变化的数,是不现实的。但这个上限是多少,不确定。

以上三点假设,导致了我们会被懒惰诱惑,在四种计算方式中选择错误的方式,与实际需要均值,存在较大误差。