举轻若重,于无声处听惊雷,微软大师级人物展示平平无奇的伟大算法

2023-05-16

近日微软神级人物Raymond Chen最近个人博客上,发布了一篇关于《如何计算平均值》的博。这个话题虽然看似平淡无奇,却意外在引爆,并带来无数讨论

看完这篇博客之后,也让我感叹于国外技术讨论氛围的浓烈,遥想笔者读大学时在技术讨论时多是储如i+=(++i)+(i++)之类的孔乙己式的问题,而最近我们关注的热点要不是删库跑路坐牢的程序员,要不是员工离职倾向分析系统;而反观国外大神的博客,要不就是这种切入点非常简单,但是最终能够升华至编程之道层面的举轻若重的文章,要不就是秀出那些智商碾压的神仙代码,从这个角度上看我们国内的IT技术氛围还有极大的提升空间。

有关求平均数算法的最初版本

有关如何求平均数这个问题,Raymond Chen并没有从一开始就炫技,而是循序渐进先放了一段最普通的实现,如下:

unsigned average(unsigned a, unsigned b)
{
    return (a + b) / 2;
}

 

相信绝大多数程序员都能一眼看出这种方法中可能隐藏的错误,那就是无法处理值溢出的问题,在Raymond的原文当中“if unsigned integers are 32 bits wide, then it says that average(0x80000000U, 0x80000000U) is zero.”也就是说一旦(a+b)已经溢出,也就是大于unsign类型所能表示的最大整改,那么其计算结果将是average(0x80000000U, 0x80000000U)=0

不过笔者在这里需要指出0x80000000U是X86平台特有的一个溢出表示方法,即indefinite integer value(不确定数值),不过同样是溢出ARM等RISC架构处理则非常清晰和简单,在上溢出或下溢出时,保留整型能表示的最大值或最小值,对照比较如下:

CPU

溢出值转为long

变量保留值说明

x86

范围0x8000000000000000

indefinite integer value

x86

范围0x8000000000000000

indefinite integer value

ARM

范围0x7FFFFFFFFFFFFFFF

变量赋值最大的正数

ARM

范围0x8000000000000000

变量赋值最大的正数

因此这段代码在ARM平台上运行时,如果出现溢出情况也并不会返回0,而会是该类型表示最大整数的一半,当然这个最大整数根据处理器的字长不同可能会有所变化。

   return (a + b) / 2

低调的改进版本

接下来Raymond又给出了几种考虑溢出处理,同时又兼顾空间复杂度的方案

  1. 变形法:

也就是将(a+b)/2变形,首先找到a和b当中较大的值,设为high,较小的值设为low,然后把(a+b)/2变成high-(high-low)/2或者low+(high-low)/2,如下:

unsigned average(unsigned low, unsigned high)
{
    return low + (high - low) / 2;
}

这种方法所需要的运算量是先进行一次比较以确定两个输入的大小,然后还需要再做两次加法(在计算机运算中加法和减法其实是基本等效的)和一次除法,最终得到答案。

  1. 除法前置方案:

也就是先对两个输入进行除2操作,即把(a+b)/2转换为a/2+b/2,当然这种方法需要考虑个位丢失的问题,比如说1/2在整形运算当中的结果会是0,因此1/2+1/2的结果是0而不是1,此时需要把两个输入的个位提取出来进行修正,具体如下:

unsigned average(unsigned a, unsigned b)
{
    return (a / 2) + (b / 2) + (a & b & 1);
}

这个算法当中的计算量是两次除法,两次加法和一次运算操作。

3.SWAR法

SWAR法也非常的巧妙,它的本质思路就是把求平均值变成位运算,位操作其实就是二进制的操作,如果我们按位考虑输入值与输出结果的对应关系,那么会有以下的需求要点

1.输入都是0,输出结果是0

2.输入都是1,输出是1

3.输入是一个0一个1,那么输出结果就是1/2

而满足以上条件的位运算,是与运算加上异常运算除2的结果,即(a and b) + (a xor b )/2,如下:

unsigned average(unsigned a, unsigned b)
{
    return (a & b) + (a ^ b) / 2;// 变体 (a ^ b) + (a & b) * 2

}

至于(a and b) + (a xor b )/2这个等式为什么能满足求平均值的要求,大家根据各种输入的情况都列一下就一目了然了。在这种方案下的计算量是两次位运算、一次加法运算以及一次除法运算来完成。

空间换时间的改进版本

在算法设计当中有一个最基本的常识,空间复杂度与时间复杂度是对跷跷板,上一节的储多算法当中,基本都是牺牲时间复杂度为代价来换取对于溢出的正确处理,那么反过来讲也完全可以用空间换时间,比如现在我们大多数的终端电脑都是64位机了,没必要为了32位长的整形溢出问题而烦恼,直接把类型转换为Long再计算结果就可以了

unsigned average(unsigned a, unsigned b)
{
    // Suppose "unsigned" is a 32-bit type and
    // "unsigned long long" is a 64-bit type.
    return ((unsigned long long)a + b) / 2;
}

但是只要涉及的转换就又要针对不同架构的处理器进行特殊处理了,比如x8664位处理器在进行32位整形转换为64位长整形时会自动将高32位填为0

// x86-64: Assume ecx = a, edx = b, upper 32 bits unknown
    mov     eax, ecx        ; rax = ecx zero-extended to 64-bit value
    mov     edx, edx        ; rdx = edx zero-extended to 64-bit value
    add     rax, rdx        ; 64-bit addition: rax = rax + rdx
    shr     rax, 1          ; 64-bit shift:    rax = rax >> 1
                            ;                  result is zero-extended
                            ; Answer in eax

// AArch64 (ARM 64-bit): Assume w0 = a, w1 = b, upper 32 bits unknown
    uxtw    x0, w0          ; x0 = w0 zero-extended to 64-bit value
    uxtw    x1, w1          ; x1 = w1 zero-extended to 64-bit value
    add     x0, x1          ; 64-bit addition: x0 = x0 + x1
    ubfx    x0, x0, 1, 32   ; Extract bits 1 through 32 from result
                            ; (shift + zero-extend in one instruction)
                            ; Answer in x0

Mips64等架构则会将32位的整形转换为有符号扩展的类型这时候就需要增加rldicl等删除符号的指令做特殊处理。

// Alpha AXP: Assume a0 = a, a1 = b, both in canonical form
    insll   a0, #0, a0      ; a0 = a0 zero-extended to 64-bit value
    insll   a1, #0, a1      ; a1 = a1 zero-extended to 64-bit value
    addq    a0, a1, v0      ; 64-bit addition: v0 = a0 + a1
    srl     v0, #1, v0      ; 64-bit shift:    v0 = v0 >> 1
    addl    zero, v0, v0    ; Force canonical form
                            ; Answer in v0

// MIPS64: Assume a0 = a, a1 = b, sign-extended
    dext    a0, a0, 0, 32   ; Zero-extend a0 to 64-bit value
    dext    a1, a1, 0, 32   ; Zero-extend a1 to 64-bit value
    daddu   v0, a0, a1      ; 64-bit addition: v0 = a0 + a1
    dsrl    v0, v0, #1      ; 64-bit shift:    v0 = v0 >> 1
    sll     v0, #0, v0      ; Sign-extend result
                            ; Answer in v0

// Power64: Assume r3 = a, r4 = b, zero-extended
    add     r3, r3, r4      ; 64-bit addition: r3 = r3 + r4
    rldicl  r3, r3, 63, 32  ; Extract bits 63 through 32 from result
                            ; (shift + zero-extend in one instruction)
                            ; result in r3

不过这种向更高位类型转换的方案也有一定问题,那就是空间的浪费,因为我原本只需要1位去处理溢出就好了,但是做了转换之后我却用了白白消费了31位的空间没有利用。

利用进位处理溢出的改进版本

在现代CPU当中大多都带有Carry bit(这里指进位位,不是C位的意思)功能。通过读取Carry bit的信息,就能达到在不浪费空间的情况下处理溢出的问题。比如在X86-32位处理器的代码如下:


// x86-32
    mov     eax, a
    add     eax, b          ; Add, overflow goes into carry bit
    rcr     eax, 1          ; Rotate right one place through carry

// x86-64
    mov     rax, a
    add     rax, b          ; Add, overflow goes into carry bit
    rcr     rax, 1          ; Rotate right one place through carry

// 32-bit ARM (A32)
    mov     r0, a
    adds    r0, b           ; Add, overflow goes into carry bit
    rrx     r0              ; Rotate right one place through carry

// SH-3
    clrt                    ; Clear T flag
    mov     a, r0
    addc    b, r0           ; r0 = r0 + b + T, overflow goes into T bit
    rotcr   r0              ; Rotate right one place through carry

而对于那些没有Carry  bit功能的处理器来说,也可以通过自定义carry bit变量的方式来解决这个问题。如下

unsigned average(unsigned a, unsigned b)
{
#if defined(_MSC_VER)
    unsigned sum;
    auto carry = _addcarry_u32(0, a, b, &sum);
    sum = (sum & ~1) | carry;
    return _rotr(sum, 1);
#elif defined(__clang__)
    unsigned carry;
    sum = (sum & ~1) | carry;
    auto sum = __builtin_addc(a, b, 0, &carry);
    return __builtin_rotateright32(sum, 1);
#else
#error Unsupported compiler.
#endif
}

对应arm-thumb2的clang 汇编代码如下:


// __clang__ with ARM-Thumb2
    movs    r2, #0          ; Prepare to receive carry
    adds    r0, r0, r1      ; Calculate sum with flags
    adcs    r2, r2          ; r2 holds carry
    lsrs    r0, r0, #1      ; Shift sum right one position
    lsls    r1, r2, #31     ; Move carry to bit 31
    adds    r0, r1, r0      ; Combine

快速幂-逆用二分法

快速幂(Exponentiation by squaring,平方求幂)是一种简单而有效的小算法,它可以以[公式]的时间复杂度计算乘方。快速幂不仅本身非常常见,而且后续很多算法也都会用到快速幂。快速幂算法的核心思想就是每一步都把指数分成两半,而相应的底数做平方运算。这样不仅能把非常大的指数给不断变小,所需要执行的循环次数也变小,而最后表示的结果却一直不会变。

让我们先来看一个简单的例子:

3^10=3*3*3*3*3*3*3*3*3*3

步骤一:先把原乘方运算变为底数平方的幂运算

3^10=(3*3)*(3*3)*(3*3)*(3*3)*(3*3)

3^10=(3*3)^5

3^10=9^5

步骤二:直接迭代步骤1直到有乘方落单,无法凑对。

9^5=(9^4)*(9^1)

最终算出结果:

9^5=(6561^1)*(9^1)

递归实现如下:

int f(int a,int b){   //m^n

    if(b==0) return 1;

    int temp=f(a,b/2);

    return (b%2==0 ? 1 : a)*temp*temp;

}

非递归实现如下:

int pow2(int a,int b){

     if(b==0) return 1;



    int r=1,base=a;

    while(b!=0){

    if(b%2) r*=base;

    base*=base;

    b/=2;

    }

    return r;

}

求平方根-Quake3中神一样的代码

可以看到Raymond的博客先从一个简单问题入手,逐步提出问题并给出解决方案,是一篇阐述编程之道的上乘之作,接下来请允许笔者再推荐一下《Quake3》当中的神级代码。《Quake3》这款3D游戏当年可以在几十兆内存的环境下跑得飞起,和目前动辄要求几十G显存的所谓3A大作形成鲜明对比,而《Quake3》取得这种性价比奇迹的关键在于把代码写得像神创造的一样。

《Quake3》最大的贡献莫过于提出使用平方根倒数速算法,并引入了0x5f3759df这样一个魔法数,目前这段代码的开源地址在:https://github.com/raspberrypi/quake3/blob/8d89a2a3c1707bf0f75b2ea26645b872e97c0b95/code/qcommon/q_math.c

如下:

float Q_rsqrt( float number )

{

floatint_t t;

float x2, y;

const float threehalfs = 1.5F;



x2 = number * 0.5F;

t.f  = number;

t.i  = 0x5f3759df - ( t.i >> 1 );               // what the fuck?

y  = t.f;

y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration

// y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed



return y;

}

这个算法的输入是一个float类型的浮点数,首先将输入右移一次(除以2),并用十六进制“魔术数字”0x5f3759df减去右移之后的数字,这样即可得对输入的浮点数的平方根倒数的首次近似值;而后重新将其作为原来的浮点数,以牛顿迭代法迭代,目前来看迭代一次即可满足要求,这个算法避免了大量的浮点计算,比直接使用浮点数除法要快四倍,大幅提升了平方根倒数运算的效率。

写完本文之后笔者真是思绪万千,别人家的技术讨论要不是由浅入深的编程之道,要不是直接碾压的神级代码,而对比我国IT圈最近的讨论热点则完全被什么员工离职倾向分析器据占据,我们是不是有点过于关注眼前的苟且了,希望笔者本文能让大家有多一分思考吧。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

举轻若重,于无声处听惊雷,微软大师级人物展示平平无奇的伟大算法 的相关文章

  • C++实现推理, 基于Onnx-simplifier、onnx-tensorrt、TensorRT和Cuda编程

    Cuda基础 深度学习模型推理 为啥需要学习tensorRT 因为需要加速 需要C 43 43 部署 为啥又需要学习cuda编程呢 因为有些前处理 后处理需要cuda编程来并行运算进行加速 比如anchor的解码 nms等后处理 1 CUD
  • nuScenes、Culane数据集、Camera选型及内参标定、TX2外接双目相机ZED

    1 xff1a nuScenes数据集 https zhuanlan zhihu com p 295549692 nuScenes mini data 点云数据集 针对cuda10 0 spconv版本 安装cu100版本 torch 1
  • 指针指向0的问题

    问 xff1a 定义指针变量int a 61 0 是指针变量指向地址为零的数据吗 xff1f 答 xff1a 指针赋0 xff0c 就是不指向任何对象 xff0c 相当于NULL
  • pip install basicsr安装时在Preparing metadata (setup.py)卡住

    pip install basicsr安装时在Preparing metadata setup py 卡住 如下图pip安装basicsr时卡在Preparing metadata 查了很多资料也没解决 xff0c 最后从pypi下载包通过
  • Ros 图(节点,消息,话题)命令

    1 启动守护进程 xff1a roscore xff1a ros 43 core这条命令会启动ros的核心模块 xff0c 我感觉这个是一个守护进程 xff0c 会在11311端口 xff08 可能会有不同的配置 xff09 启动一个soc
  • Android 串口通信(二)

    前言 上一篇中主要介绍了串口基础内容和基于谷歌官方android serialport api库 对Android设备进行的串口操作 xff0c 这篇文章主要介绍基于Usb转串口驱动库usb serial for android 的相关内容
  • DeepMind带来了大魔王阿尔法扣,程序员还有前途吗?

    最近 著名的编程竞赛网站C odeforces 发布了一篇名为 AlphaCode DeepMind Solves Programming Problems on CodeForce 的文章 xff0c 将阿尔法扣 xff08 AlphaC
  • 最优控制理论 四、线性二次型最优控制和LQR

    在前面两节最优控制理论 二 哈密尔顿函数法 xff0c 我们利用Hamilton函数法讨论了终端等式约束的非线性控制系统的最优控制 xff0c 它所解决的是 x
  • 多源传感器GNSS INS 视觉 LiDAR 组合导航与SLAM开源项目总结

    多源传感器GNSS INS 视觉 LiDAR 组合导航与SLAM开源项目总结 本文基于 吴桐wutong 微信公众号文章完善而来 开源代码总览 名称传感器类型组合类型滤波方法备注RTKLIBG KFGAMP rtklibexplorer h
  • Integer类超详解

    目录 xff08 点击传送 x1f680 x1f680 xff09 前言 Integer的内部结构 1 MAX VALUE 2 MIN VALUE 3 SIZE 4 TYPE 构造方法 1 Integer xff08 int number
  • PPM信号介绍

    原文地址 xff1a http nicekwell net blog 20161223 ppmxin hao jie shao html 我们知道PWM信号可以控制舵机 xff0c 一般信号周期是20ms xff0c 脉宽在500us 25
  • 算法建模语言比较:C的优势(相比于Matlab)

    数据结构定义 structure 方便组织相关数据 union 方便多角度访问数据 xff08 软件角度 硬件角度等 xff09 bit field 方便硬件角度访问数据 内存分配方式 malloc 预先 灵活申请内存空间 xff0c 拓展
  • 学会使用CMakeLists.txt在VScode中搭建C++工程

    目录 一 Cmake 简介 二 常用命令 1 指定 cmake 的最小版本 2 设置项目名称 3 设置变量 3 1 set 直接设置变量的值 3 2 set 追加设置变量的值 3 3 list 追加或者删除变量的值 4 添加第三方库或链接其
  • WIN10源码编译安装QGC-V3.4

    目录 写在前面 环境 安装VS2015 xff08 采用的是社区版 xff09 安装Git xff08 见GIT安装教程 xff09 并克隆QGC源码 安装Qt xff0c 并用Qt进行编译 运行 安装注意事项 写在前面 最近想起来之前有过
  • 飞控固件烧录方法

    目录 写在前面 方法一 方法二 写在前面 整理两个烧录飞控固件的方法 方法一 1 xff09 waf targets bin ardusub upload 这时两个过程 xff0c 第一个过程target 会产生一个 px4 文件 xff0
  • Ubuntu 20.04 LTS 安装qt4 library

    How to Install Qt4 Libraries in Ubuntu 20 04 LTS July 9 2020 3 Comments The Qt4 framework has been removed from Ubuntu 2
  • STM32接入ONENET-实现数据上传和命令下发

    前言 xff1a 使用ONENET平台进行远程传输数据和远程控制开发板是相对简单的事 xff0c 但由于ONENET官方给的代码只对他家的开发板比较友好 xff0c 对于初学者来说修改这些代码相对麻烦 xff0c 所以我就分享一份STM32
  • 从x86到ARM,代码移植指北

    最近ARM架构的处理器从云到端全面开花 xff0c 比如苹果MAC上的ARM架构处理器M1 MAX就堪称王炸 xff0c 华为的鲲鹏系列ARM处理器也已经稳定服务了很长时间 xff0c 目前业内有口皆碑 xff0c 因此基于x86环境编写的
  • AGV车载控制系统搭建(初学者入门)

    本文转载 xff1a 博主 robinvista的http blog csdn net robinvista article details 78349627 目的 本文介绍 AGV 车载控制系统的实现过程 xff0c 可以分为硬件搭建和软
  • 激光无轨导航AGV,未来智能工厂的必需品

    这篇文章结合一家激光无轨导航AGV公司 xff0c 开启了进入AGV领域的学习与总结 随着国家政策大力支持 xff0c 智能制造 工业4 0 人工智能等等成为了风口 xff0c 热门话题 智能制造中重要的一环 xff0c 物流搬运 分拣和智

随机推荐

  • 【FreeRTOS源码阅读】<2> task.c (1) 任务创建以及TCB、List的结构

    上篇讲述了list c关于链表操作的源码阅读 xff0c 此片文章将开始阅读task c task h相关结构体 由eTaskGetState返回的任务状态 typedef enum eRunning 61 0 一个任务查询自己的状态 xf
  • github仓库添加指定commit版本的子模块

    添加子模块 git submodule add repository url local path 进入子模块目录 xff0c 将子模块回滚到指定commit版本 git reset hard commit number
  • 使用阿里云的k8s部署访问环境

    阿里云推出的kubernetes版本是1 97的 xff0c docker的版本是17 06 2 ce 3 xff0c 用的都是比较新的 xff0c 相比自己搭建集群 xff0c 使用阿里这个还是比较省事的 xff0c 不需要自己研究怎么写
  • 阿里云Ubuntu服务器图形界面配置(详细步骤,萌新看过来)

    刚买完阿里云Ubuntu服务器后 xff0c 发现并没有图形界面 xff0c 就想办法在网上搜集了一些资料配置 xff0c 结果发现一些资料配置过程并不适用于萌新 所以写这篇博客 xff0c 一为记录 xff0c 二为让萌新更快更方便的配置
  • STL简单了解

    STL xff08 Standard Template Library xff0c 标准模板库 xff09 xff1a 是一种类型参数 xff08 type parameterized xff09 的程序设计方法 xff0c 基于模板的标准
  • ROS编译D435i过程中的问题及解决

    请确保已经正确安装了ROS OPENCV realsense viewer 编译出现第一个问题 96 traversing 5 packages in topological order realsense2 camera msgs pla
  • 本科毕业设计 基于ORB SLAM3的多从机SLAM导航系统

    耗时 xff1a 两个月 需求 xff1a 多从机协作 多地图系统 稠密建图 定位 导航 硬件 xff1a 二个D435 一台X86主机 xff08 CPU 13600kf 内存 32G xff09 X86主机环境 xff1a ubuntu
  • 平衡小车之家高配版全向轮小车部分源码分析(蓝牙控制端和运动控制端)

    提前说说 intel杯初选赛过了 xff0c 接下来就是区域决赛 准备时间有两个月 xff0c 时间还是比较紧张 xff0c 必须在这两个月内把所有的知识都消化掉 接下来的打算是想面试几家公司 xff0c 试试自己的水准 xff0c 打好比
  • 当我们在谈SWIFT时,到底在谈什么?

    胜利往往伴随着放弃不切实际的幻想 当地时间2月26日 xff0c 美国 英国 欧盟与加拿大发表共同声明 xff0c 宣布将俄罗斯主要银行从SWIFT体系中剔除 SWIFT凭借着其强大的制裁效果 xff0c 在民间一直有着金融核武器之称 xf
  • 使用Aruco二维码实现定位

    首先使用cv aruco estimatePoseSingleMarkers 函数后得到两个很重要的数据revc和tevc xff0c 分别是旋转向量和平移向量 通过这两个数据就可以得到相机在世界坐标系下的坐标 此处需要了解solvePnP
  • QGC关于SetMode运行流程解析

    QGC与飞控连接后初始化 xff0c 初始飞行模式为手动模式 模式切换在Vehicle中使用 base mode和 custom mode存储模式 xff0c 初始值均为0 当QGC与飞控连接后 xff0c 飞控通过心跳包 xff08 he
  • QGC UDP 建立通讯连接

    QGC关于通信连接建立部分的程序比较复杂 xff0c 但感觉写的很厉害 xff0c 通过读代码把自己的一点理解记录下来 收集接收 空地通信管理 空地通信的管理在 应用程序设置 gt 通讯连接 中进行通信连接创建 xff0c 连接和断开 通讯
  • Win10 Vscode 远程连接Linux 错误解决办法

    错误 xff1a 在网上查找一阵后发现都不能解决我的问题 xff0c 后来发现Vscode的设置里有个 Remote ssh里的Remote SSH Config file路径是空的 尝试把自己的config文件路径放进去 xff0c 发现
  • Ubuntu设置静态IP

    安装Ubuntu后有时候IP会变化导致远程连接不上 xff0c 网上很多都是命令行操作的或者改相应的文件 xff0c 如果能在界面上操作更简单了 右上角 Setting gt Network
  • Qt qml 输出行号 __LINE__ LINENO等信息

    使用 Qt Creator xff0c 按下面的路径可以设置 xff08 Ubuntu Window即可这样设置 xff09 xff1a QT MESSAGE PATTERN file line message 在windows下还可以通过
  • QT5.15.2 View3D 在Ubuntu上无法运行的解决办法

    QT 5 15 2 View3D 在Ubuntu20 04上运行的时候发现无法显示3D模型 xff0c 并报错 经反复折腾发现需要设置 MESA GL VERSION OVERRIDE 61 3 10 xff0c 具体为什么是3 10目前不
  • IMX6ULL + SPI LCD(驱动IC ILI9341)显示简单的QT界面

    1 硬件 xff1a 使用正点原子的IMX6ULL Linux开发板 开发板底板原理图版本 xff1a V2 1 核心板原理图版本 xff1a V1 6 LCD xff1a MSP2402 IC ILI9341 2 查找可用引脚 开发板上引
  • Cmakelist知识总结

    Cmakelist知识总结 文章目录 Cmakelist知识总结程序编译链接过程简介Cmakelist简介Cmakelist写法cmakelist实例解析 程序编译链接过程简介 一个完整的 C 43 43 语言项目可能包含多个 cpp 源文
  • 四个好看的CSS样式表格

    1 单像素边框CSS表格 这是一个很常用的表格样式 源代码 xff1a lt CSS goes in the document HEAD or added to your external stylesheet gt lt style ty
  • 举轻若重,于无声处听惊雷,微软大师级人物展示平平无奇的伟大算法

    近日 微软 神级 人物 Raymond Chen最近 在 个人 博客上 xff0c 发布了 一篇 关于 如何计算平均值 的博 文 这个话题虽然看似平淡无奇 xff0c 却意外在 引爆 xff0c 并带来 无数讨论 xff1a 看完这篇博客之