🧠 第三章 编程语言
📄 一句话概括

矩形版 81 个式子,三角版 45 个, 多出来的 36 个全是重复(3*4 和 4*3 各算一遍)。 单个式子的最大显示宽度是 6 格,下三角最宽那一行占 70 格。

📝 本节和前面几节的关系

上一节算数列,这一节练排版。 乘法表本身谁都会打,但「四种版式的行数、个数、宽度各是多少」很少有人真算过。

一、四种版式

Demo 打了四种:矩形、下三角、上三角、右对齐。 - 矩形:九行九列,每行 9 个,共 81 个式子。 - 下三角:第 i 行有 i 个,共 45 个。 - 上三角:第 i 行从 i 数到 9,也是 45 个,只是顺序反了。 - 右对齐:把数值补到固定宽度,位数不齐也能对齐。 前三种是「打多少」的差别,第四种是「怎么对齐」的差别。两类问题得分开处理。

二、81 减 45 等于 36,这 36 个是重复

乘法交换律告诉我们 3*4 和 4*3 是一回事。矩形版把它们各算一遍,所以多出 36 个。 45 是怎么来的?1+2+…+9 = 45。81 - 45 = 36,正好是「重复的个数」。 别小看这个数。做报表、做特征交叉、做 A/B 对比,都涉及「组合数」。 n 个东西两两组合是 n(n-1)/2,本例 36;允许自己跟自己组合就是 n(n+1)/2,本例 45。 这两个公式记混,算出来的样本量会差一倍——而样本量差一倍,成本差一倍。

三、对齐:%-2d 与显示宽度

乘法表要好看,关键是让每个式子的宽度一样。9*9=81 是 6 个字符,1*1=1 只有 5 个。 Demo 用 %-2d 把乘积补到两格宽,于是 1*8= 8 和 9*9=81 都是 6 格。 这就是那张表能对齐的原因。 然后算总宽:下三角最宽的一行是第 9 行,9 个 6 格的式子加 8 个两格的分隔,一共 70 格。 70 格这个数是有用的:终端一般 80 格,70 格刚好放得下;再宽就会折行,一折行整个表就散了。

四、为什么这件事值得较真

有人会说:打个乘法表而已,费这么大劲干什么。 但「输出对齐」是所有日志、报表、命令行工具的基本功。 对齐的背后是「先算宽度、再排版」这个两步动作,少了它就只能靠肉眼挪空格。 而且本例用到的三个数——81、45、36——全是算出来的,不是数出来的。 能用公式算的东西,就不要靠眼睛数。 本书所有 Demo 的表格输出,用的都是同一套「先算宽度再排」的思路。 你看它们齐,不是手工凑的,是代码算的。

五、动手 Demo:四种排版与它的真实宽度

按四种版式打出乘法表的前几行并统计行数;用 %-2d 做右对齐版;统计矩形与三角版的式子个数; 最后算出单个式子的最大显示宽度,以及下三角最宽那一行的总宽度。

def block(rows, title):
    lines = [title]
    for i, js in rows:
        lines.append('  '.join('%d*%d=%-2d' % (j, i, i * j)
                                 for j in js).rstrip())
    return lines

lower = [(i, list(range(1, i + 1))) for i in range(1, 10)]
upper = [(i, list(range(i, 10)))    for i in range(1, 10)]
full  = [(i, list(range(1, 10)))    for i in range(1, 10)]

cnt_full = sum(len(js) for _, js in full)     # 81
cnt_low  = sum(len(js) for _, js in lower)    # 45

wid = max(len('%d*%d=%2d' % (j, i, i * j))
          for i in range(1, 10) for j in range(1, i + 1))

实跑输出:

--------------------------------------------------------------------
实例 4:九九乘法口诀表,四种排版与它的真实宽度
--------------------------------------------------------------------
  【一】九行九列矩形(81 个式子)
  1*1=1   2*1=2   3*1=3   4*1=4   5*1=5   6*1=6   7*1=7   8*1=8   9*1=9
  1*2=2   2*2=4   3*2=6   4*2=8   5*2=10  6*2=12  7*2=14  8*2=16  9*2=18
  1*3=3   2*3=6   3*3=9   4*3=12  5*3=15  6*3=18  7*3=21  8*3=24  9*3=27
  …… 共 9 行
  【二】下三角(45 个式子)
  1*1=1
  1*2=2   2*2=4
  1*3=3   2*3=6   3*3=9
  …… 共 9 行

  【三】上三角(也是 45 个式子,只是顺序反了)
  1*1=1   2*1=2   3*1=3   4*1=4   5*1=5   6*1=6   7*1=7   8*1=8   9*1=9
  2*2=4   3*2=6   4*2=8   5*2=10  6*2=12  7*2=14  8*2=16  9*2=18
  ……

  矩形式个数               81
  三角式个数               45
  省下的个数               36 个
  矩形版重复了 36 个式子(3*4 和 4*3 各算一遍)。

  【四】右对齐版——数字位数不齐时,靠 %-2d 一类的宽度补出来:
  1*8= 8 | 2*8=16 | 3*8=24 | 4*8=32 | 5*8=40 | 6*8=48 | 7*8=56 | 8*8=64
  1*9= 9 | 2*9=18 | 3*9=27 | 4*9=36 | 5*9=45 | 6*9=54 | 7*9=63 | 8*9=72 | 9*9=81
  单个式子的最大显示宽度   6 格
  下三角最宽那一行         70 格(第 9 行)

四种版式打出来的前三行很清楚:矩形每行 9 个,下三角第 2 行 2 个,上三角第 2 行从 2 数到 9。 个数统计:矩形 81、三角 45、多出 36 个。36 正好是「两两组合」的个数。 右对齐版看最后两行最明显:1*9= 9 和 9*9=81 都是 6 格,小数点位置对得整整齐齐。 单个式子最大宽度 6 格;下三角最宽那一行 70 格(9 个 6 格 + 8 个两格分隔)。 70 格在 80 格的终端里刚好不折行。

30 秒实验

  1. 把乘法表改成 12×12,重新算一遍四个数(81/45/36/70 会怎么变)。
  2. 把 %-2d 换成 %-3d,量一下下三角最宽那行变成多少格。
  3. 想一想:为什么「两两组合」的个数是 n(n-1)/2 而不是 n²?

常见误区

  1. 「乘法表随便打打就行」——80 格终端一折行,整个表就散了。
  2. 「81 就是 9×9,没别的数」——三角版只有 45,多出来的 36 是重复。
  3. 「对齐靠手动加空格」——宽度一变就全歪,得先算再排。
自测三问(点开对答案) 1. 矩形版和三角版差多少个式子?为什么? —— 差 36 个,因为 `3*4` 和 `4*3` 在矩形版里各算了一遍。 2. 下三角最宽那一行占多少格? —— 70 格:9 个 6 格式子加 8 个两格分隔。 3. `%-2d` 在这个例子里起什么作用? —— 把乘积补到 2 格宽,让 `1*1=1` 和 `9*9=81` 都是 6 格,才能对齐。

最后一个实例最好玩:写个猜数字游戏,顺便看看两种策略差多少。

相关笔记