🧮 第2章 变量和数组
📄 一句话概括

二维数组就是一张表格:第一个下标选行,第二个下标选列。 字符串是字符排成一排,所以数组那一套下标、长度全都能用。 但字符数不等于字节数:中文一个字在 UTF-8 里占 3 个字节,按字节截断会切出乱码。

📝 本章和前面几章的关系

2.4 讲了一排盒子,2.5 讲了什么时候该开一排。这一篇把盒子摞成楼。 教材用的比方是旅馆:grid[1] 是一层楼,grid[1][2] 是那一层的第三个房间。 字符串放在这一节讲,是因为它本质上就是「一排字符」——理解了数组,字符串的下标、切片、 长度全是白送的。

一、二维数组就是一张表格

三个人、四门课的成绩,摆成表格是三行四列。用一维数组只能存一行,要存整张表就得把数组摞起来。 grid = [[80, 90, 70, 85], [60, 75, 88, 92], [95, 65, 72, 58]]——外层三个元素, 每个元素本身又是一个四个数的数组。 所以二维数组不是一种新东西,它是「元素还是数组」的数组。这个说法很重要,三维、 四维都是这么套下去的。

旅馆的比方就是从这里来的:先找到楼层,再找到房间。两层门牌,缺一层都进不去。

二、两个下标各管一件事

grid[1] 给出 [60, 75, 88, 92],是李四的四门课,一整个数组。 grid[1][2] 给出 88,是李四的第三门课。先取行,再取列。 演示里那句提醒别忽略:grid[1][2] 和 grid[2][1] 是两个完全不同的格子。 前者是李四的第 3 门,后者是王五的第 2 门。

一维数组写错下标只是越界,报错还算痛快; 二维数组把两个下标写反,往往不报错,只是取到了别人的数。这种错要查很久。 所以约定一个顺序并且一直用:先行后列,和数学里的矩阵一致。

三、字符串就是字符排成一排

s = '算法入门',可以取 s[0] 得「算」,s[1] 得「法」,s[2:4] 得「入门」。 下标能用、切片能用、 len() 能用——凡是数组能做的,字符串基本都能做。因为它本来就是「一排字符」。 区别在于字符串不能再改。s[0] = '算' 这种写法在 Python 里直接报错,要改只能拼一个新的。 这一点教材没强调,但写代码时天天撞。

演示里 len(s) 得 4,s[2:4] 得两个字符——长度和切片都以「字符」为单位,不是以字节为单位。

四、字符数不等于字节数

同样这四个字,存成 UTF-8 要 12 个字节。中文一个字占 3 个字节,len() 数的是字,不是字节。 所以「字符串长度是 4」和「占 12 个字节」两句都对,它们回答的是不同的问题。 这条区别在截断时会咬人:按字节切到第 6 个字节,正好把一个汉字切成两半,结果就是乱码。 演示里那句提醒说的就是这个。

要按字节处理就用 s.encode('utf-8'),那时 len() 才是 12。 先想清楚你要的是字符还是字节,再选哪一个。

五、求长度的两个办法

教材列了两种:直接问长度,或者一个字符一个字符数到结尾为止。

办法 怎么做 什么时候用
直接问长度 len(s) 一次拿到 有现成函数,几乎总用它
数到结尾为止 一个字符一个字符往后走,走到头 没有长度函数的语言,或边读边处理

演示里第一种一步得 4,第二种数了 4 次才到头。结果一样,代价不同。 第二种在 C 里叫「找结尾的那个 0」:C 的字符串不在开头存长度,只保证最后有个 , 所以求长度必须从头扫到尾。 这就是为什么 C 程序喜欢把长度另外存一份——同一个问题问一万次,不该每次都从头数。

六、动手 Demo:把数组摞成表格,再把字符串拆成一排字

先建一张三人四门的成绩表,用两个下标取值,看看写反会取到什么。 再把一个中文字符串按下标拆开,比较「几个字」和「几个字节」。 最后两种求长度的办法各跑一遍,数一数各走了几步。

grid = [[80, 90, 70, 85],
        [60, 75, 88, 92],
        [95, 65, 72, 58]]
print(len(grid), len(grid[0]))
print(grid[1], grid[1][2], grid[2][1])
print(sum(sum(row) for row in grid))

s = '算法入门'
print(s[0], s[1], s[2:4], len(s), len(s.encode('utf-8')))

实跑输出:

--------------------------------------------------------------------
二维数组与字符串
--------------------------------------------------------------------


  ① 二维数组就是表格
  --------------------------------------------------------------------
  三个人、四门课的成绩表:
    张三  [80, 90, 70, 85]
    李四  [60, 75, 88, 92]
    王五  [95, 65, 72, 58]
  行数 3,列数 4
  教材打的比方是旅馆:grid[1] 是 1 号房那一层,grid[1][2] 是那一层的 3 号房。
  两个下标,先定位层,再定位房间。

  ② 两个下标各管一件事
  --------------------------------------------------------------------
  grid[1]     = [60, 75, 88, 92]   (李四的四门课)
  grid[1][2]  = 88            (李四第 3 门课)
  全班总分 = 930,共 12 个格子
  注意 grid[1][2] 和 grid[2][1] 完全是两个格子。二维数组的下标顺序不能凭感觉换。

  ③ 字符串就是字符排成一排
  --------------------------------------------------------------------
  s = '算法入门'
  s[0] = '算'   s[1] = '法'   s[2:4] = '入门'
  len(s) = 4 个字符
  但存成 UTF-8 要 12 个字节
  「字符数」和「字节数」不是一回事。
  中文一个字在 UTF-8 里占 3 个字节,所以按字节截断中文,十有八九会切出乱码。

  ④ 求长度的两个办法
  --------------------------------------------------------------------

| 办法 | 怎么做 | 什么时候用 |
|---|---|---|
| 直接问长度 | len(s) 一次拿到 | 有现成函数,几乎总用它 |
| 数到结尾为止 | 一个字符一个字符往后走,走到头 | 没有长度函数的语言,或边读边处理 |

  用第一种:len(s) -> 4,一步
  用第二种:数了 4 次才到头
  两种结果一样,代价不同。
  第二种在 C 里叫「找 \0」,字符串一长就慢,这也是为什么 C 程序喜欢把长度另外存一份。

①len(grid) 得 3 是行数,len(grid[0]) 得 4 是列数。二维数组的「长度」有两个答案。 ②第二行三个值分别是:李四那一整行、李四的第 3 门、王五的第 2 门。 后两个都是 88 和 65 这种具体分数,但它们不在一处。 ③sum(sum(row) for row in grid) 得 930,是十二个格子的总和。 外层 sum 加的是三行的和,内层加的是每行四个数。 ④最后一行对比最关键:len(s) 是 4 个字符,len(s.encode('utf-8')) 是 12 个字节。 同一句话,两个长度。

30 秒实验

  1. 把这个 3×4 的表改成 4×3(四个学生三门课),看要改哪几行。
  2. 写一个双层循环求每行的平均分,注意外层和内层各管什么。
  3. 把 s[2:4] 改成 s.encode('utf-8')[0:6],看输出变成什么样。

常见误区

  1. 「二维数组是一种新的数据类型。」它是「元素还是数组」的数组,一层套一层。
  2. 「grid[1][2] 和 grid[2][1] 差不多。」前者李四第 3 门,后者王五第 2 门,差得远。
  3. 「字符串长度就是它占多少字节。」len() 数字符。要数字节得先编码。
自测三问(点开对答案) 1. 二维数组的两个下标分别管什么? —— 第一个管行(第几层),第二个管列(第几个房间)。约定先行后列。 2. 为什么 `len('算法入门')` 是 4,而 UTF-8 下它占 12 个字节? —— `len()` 数的是字符个数。中文一个字占 3 个字节,四个字 12 个字节。 3. C 为什么求字符串长度要从头扫到尾? —— C 的字符串不在开头存长度,只保证末尾有个 0。不愿每次重数就另存一份长度。

第 2 章最后一节换了个角度:不讲数据怎么摆,讲名字怎么起——一张对照表, 外加一个能替你挑毛病的检查器。

相关笔记