交换两个变量要借第三个盒子:先存一个,再覆盖,最后放回来。 不借盒子也有两种技巧(加减、异或),但都有代价,日常还是借盒子最清楚。 最大公约数用辗转相除:除数变被除数,余数变除数,直到余数为 0。
这一篇两个题都很小,但各带一个重要的道理:交换讲的是「覆盖会丢数据」, 辗转相除讲的是「算法为什么一定会停」。 后者正好回扣第 1 章的好算法两个条件之一:有穷性。 这一篇收尾,第 4 章就齐了。下一章开始讲排序——那些算法全都是这一章套路的组合。
一、交换:为什么非要借一个盒子
a = 3、b = 5,要换成 a = 5、b = 3。
直觉写法是 a = b 然后 b = a。
跑一遍就知道错了:a = b 之后 a 变成 5,原来的 3 已经被盖掉了,再 b = a 就是 b = 5。
两个都成了 5。
所以必须先把 3 存到别处:t = a、a = b、b = t。借一个盒子,三个动作。
这三行的顺序不能换。 这和 3.3 链表的「先接上再断开」是同一个道理:覆盖之前,先把要丢的东西存起来。 教材讲的就是这一种。它是唯一一种不需要额外条件、不会出意外的写法,所以日常就该用它。
二、两种不借盒子的技巧
面试里常问「不用第三个变量怎么交换」。有两种:加减法和异或法。
| 写法 | 要不要第三个盒子 | 代价 |
|---|---|---|
| 借盒子 | 要 | 没有代价,最清楚 |
| 加减法 | 不要 | 两个大数相加可能超出范围 |
| 异或法 | 不要 | 对同一个变量做会清零 |
加减法是 a = a+b、b = a-b、a = a-b。原理是「和减一个得另一个」。
它的毛病在溢出:两个很大的整数加一下就装不下了。
Python 里不会溢出(大整数自动变长),但在 C 和 Java 里会。
异或法是 a ^= b、b ^= a、a ^= b。原理是异或两次能还原。
它的毛病是如果 a 和 b 是同一个变量,第一句就把它清零了。
所以这两种都是「技巧」,不是「做法」。省一个变量换不来可读性,也换不来安全。
三、辗转相除:除数变被除数,余数变除数
求 1071 和 462 的最大公约数:1071 除 462 余 147;462 除 147 余 21;147 除 21 余 0。 余数到 0 就停,最后那个除数 21 就是答案。 每一轮只做一件事:把「除数变被除数、余数变除数」,然后重算余数。三行代码就够了。
为什么这样能得到最大公约数?
因为 a 和 b 的公约数,
和 b 与 a 除以 b 的余数 的公约数完全一样——公约数集合在缩小,但没丢东西。
这句话是欧几里得两千多年前给出的证明。它也是「算法」这个词最古老的样本之一。
四、为什么它一定停得下来
演示里那句观察很关键:每一步的余数一定比除数小。462 到 147 到 21 到 0,数字严格变小。 严格变小、又不可能是负数,所以最多走到 0。它一定会停。 这就是第 1 章「有穷性」的实例:不是靠数次数保证停,是靠每一步都在向终点靠近。
对照一下第 7 章会讲的递归:没有终止条件的递归也会「一直算下去」,但那不是有穷性, 那是死循环——最后靠内存耗尽才被迫停下。 所以判断一个算法靠不靠得住,先问这一句:它每一步是不是都在靠近终点?
五、换个用法:约分与最小公倍数
1071 和 462 同除 21,得 51 和 22。这就是约分。 分数化简、比例化简,本质都是求最大公约数再同除。 最小公倍数也能从它算出来:两数相乘除以最大公约数。1071 乘 462 再除 21,就是它们的最小公倍数。
为什么?因为两数相乘等于「最大公约数乘最小公倍数」。这条关系在数论里是定理,在这里直接当工具用。 所以这一个算法能解决三类问题:约分、化简、最小公倍数。 算法复用率高,往往是因为它抓住了一个更本质的量。
六、动手 Demo:三种交换写法,加一个辗转相除
先用借盒子的办法交换 3 和 5,再试加减法和异或法,三种结果应该一样。 然后用辗转相除求 1071 和 462 的最大公约数,把每一轮打印出来。 最后拿结果去约分。
a, b = 3, 5
t = a; a = b; b = t
print('借盒子', a, b)
a, b = 3, 5
a = a + b; b = a - b; a = a - b
print('加减法', a, b)
a, b = 3, 5
a ^= b; b ^= a; a ^= b
print('异或法', a, b)
x, y = 1071, 462
while y:
print(x, y, x % y)
x, y = y, x % y
print('最大公约数', x)
print('约分', 1071 // x, '/', 462 // x)
实跑输出:
--------------------------------------------------------------------
交换变量与最大公约数
--------------------------------------------------------------------
① 交换:三种写法
--------------------------------------------------------------------
初始:a = 3,b = 5
借盒子:t = a; a = b; b = t -> a = 5,b = 3
加减法:a = a+b; b = a-b; a = a-b -> a = 5,b = 3
异或法:a ^= b; b ^= a; a ^= b -> a = 5,b = 3
| 写法 | 要不要第三个盒子 | 会不会溢出 |
|---|---|---|
| 借盒子 | 要 | 不会 |
| 加减法 | 不要 | 会(两个大数相加超范围) |
| 异或法 | 不要 | 不会,但对同一个变量做会清零 |
教材讲的是第一种。
后两种是「不用第三个盒子」的技巧,面试常问,实际写代码还是第一种最清楚——省一个变量换不来可读性。
② 最大公约数:辗转相除
--------------------------------------------------------------------
求 1071 和 462 的最大公约数:
| 被除数 | 除数 | 余数 | 下一步 |
|---:|---:|---:|---|
| 1071 | 462 | 147 | 继续 |
| 462 | 147 | 21 | 继续 |
| 147 | 21 | 0 | 停,除数就是答案 |
结果:21
每一轮把「除数变被除数、余数变除数」,直到余数是 0。最后那个除数就是答案。
③ 为什么它停得下来
--------------------------------------------------------------------
每一步的余数一定比除数小:462 -> 147 -> 21 -> 0
数字严格变小,而且不会变成负数,所以最多走到 0。
这是「有穷性」的一个实例:不是靠数次数,是靠每一步都在向终点靠近。
第 1 章讲的那两个条件,在这里变成了能验证的东西。
④ 换个用法:约分
--------------------------------------------------------------------
1071 / 462 同除 21 -> 51 / 22
分数约分、比例化简、求最小公倍数(两数相乘除以最大公约数),都靠这一步。
①三种交换写法最后都输出 5 和 3。结果一样,过程的风险完全不同。
②加减法那行用分号把三句写在一行,只是为了省篇幅。
a = a + b 之后 a 已经不是 3 了,b = a - b 才是 3。
③辗转相除的循环条件 while y 就是「余数不为 0」。
x, y = y, x % y 这一行同时完成了「除数变被除数、余数变除数」。
④打印出来的三行是每一轮的 (被除数, 除数, 余数):1071 462 147;462 147 21;147 21 0。
最后 x 停在 21。
30 秒实验
- 用辗转相除求 12 和 18 的最大公约数,手算一遍再跑一遍。
- 把
while y换成while y != 1,会出什么问题? - 求 1071 和 462 的最小公倍数(相乘除以最大公约数),再用另一个办法验一遍。
常见误区
- 「交换写
a = b; b = a就行。」第一个赋值就把 a 的原值盖掉了,结果两个都成了 b。 - 「异或法更好,因为不用第三个变量。」对同一个变量做会清零,还不易读。
- 「辗转相除会算很多轮。」每轮余数严格变小,最多几十轮就到底。
自测三问(点开对答案)
1. 交换为什么必须借第三个变量? —— 第一个赋值就覆盖了原值。不先存起来,那个值就找不回来了。 2. 辗转相除每一步在做什么? —— 除数变被除数、余数变除数,直到余数为 0。最后的除数就是答案。 3. 为什么它一定停得下来? —— 每一步的余数严格比除数小。数字一直变小、不会为负,最多走到 0。第 4 章到此结束。 下一章换一个题目:让一堆数据按大小站好队。从最简单的桶排序开始,一路到最快的快速排序。
- ☐ (本篇完,下一篇 5.1 排序是什么)