教材里并行处理机那一章,很多人的第一反应是跳过。原因很实在:前面讲流水线、讲Cache,至少有性能公式和时序图可以算;到了互连网络这一节,忽然冒出来一堆函数名字——交换、立方体置换、PM2I、均匀洗牌、蝶形、反位序——再配上一张密密麻麻的连线图,看上去像是纯记忆题,背完就忘。可真要往并行计算、向量机、FFT硬件加速、片上网络这些方向走一步,就会发现这些基本互连函数是绕不开的地基:它们决定了数据怎么从一堆处理单元重新排到另一堆处理单元,中间需要几级网络、每级怎么连线、布线和延迟的代价有多大。我把这一块来回推导了很多遍,也自己写代码把每个函数在N=8、N=16下全部跑过一遍,把常见的坑几乎踩全了。这篇就把基本互连函数从头拆到脚,给出数学定义、二进制位解释、手算全表、可复现的代码核对,最后把几个容易混淆的点摊开讲清楚。不管你是在应付考试、做课程设计,还是真的要在FPGA或仿真器里搭一个互连网络,这套推导过程都能直接抄。
1. 互连函数到底是干什么的
1.1 从多处理机互连网络的现实约束说起
一台单核机器里,数据在寄存器和内存之间流动,靠的是总线或交叉开关,路径非常短。可一旦处理单元数量涨到几十、几百甚至上千个,问题就变了味:不可能让每一对处理单元之间都拉一根专用线,因为N个节点全连接的线数是N(N-1)/2,节点翻倍,线数涨四倍,布线和芯片面积立刻失控。工程上真正可行的做法,是用若干级简单的、规律性的连接叠出一个复杂的网络——每一级只做一种固定的、好实现、好复用的重排动作,靠多级叠加去逼近"任意节点到任意节点"的灵活性。
互连函数就是用来精确描述"一级连接做什么"的数学工具。它不关心这一级是用开关、多路选择器还是光交叉实现的,只关心输入端编号和输出端编号之间的映射关系。你可以把它理解成一副牌的洗牌规则:输入是排好序的0号到N-1号牌,函数告诉你每张牌洗完之后落到第几个位置。把若干种洗牌规则串起来,就能设计出蝶形网络、Omega网络、间接二进制n立方体网络这些经典结构。所以互连函数不是凭空造出来的抽象符号,它的每一个定义背后都对应着一块真实的、可以画进电路图里的连线。
理解这一点之后再回头看那些函数名字,就不容易懵了。均匀洗牌之所以叫"洗牌",是因为它的输入输出次序真的像把一叠牌对半错开;蝶形之所以叫"蝶形",是因为把它的连接关系画出来,形状像蝴蝶的一对翅膀;立方体置换之所以带"立方体"三个字,是因为它和超立方体拓扑的边一一对应。名字是记忆的钩子,但真正要抓住的是二进制位怎么变。
1.2 互连函数是一种下标重排的数学语言
形式化地讲,一个有N个输入和N个输出的互连网络,它的连接可以用一个置换函数 f 来描述:f(x) = y 表示编号为x的输入端连到编号为y的输出端。这里的x和y都在0到N-1之间取值,f是[0, N-1]到自身的一个双射——也就是说,每个输出端恰好被一个输入端占据,不会有两个输入撞到同一个输出。双射这个约束很重要,它保证了网络是"无损"的,不会出现数据合并或冲突,这在设计一级连接时是基本要求。
为了让函数的形式尽可能整齐,教材统一约定N取2的整数次幂,即N = 2^n,n是网络级数或地址位数。这个约定不是随便定的:一旦N是2的幂,输入端编号就能用恰好n位二进制完整表示,于是"重排"这件事就能翻译成"对n位二进制做某种操作",而位运算恰恰是硬件最容易并行实现的那类操作。交换函数是翻某一位,洗牌是整体移位,蝶形是首末位对调——你会发现,所有这些函数最后都落到了位操作上,这就是为什么它们能被做成对称、规整、可级联的硬件结构。
我一开始学的时候总想找"统一公式",后来才明白这类函数的价值恰恰在于它们各自简单、各自只动很小一块。真正的复杂网络是这些简单函数的复合,而不是某个花哨的单一公式。
1.3 学这块内容需要先建立哪些直觉
往前学之前,有三件事最好先在心里立住。第一,编号从0开始,不是从1开始,因为二进制表示要"对齐"——0号写成全0,N-1号写成全1,中间每一位都有意义。第二,n = log2(N),这是后面所有函数参数个数的来源,交换函数有n个,PM2I有2n个,记不住个数时先算n。第三,任何给定的函数,都要能立刻在脑子里把它对N=8(n=3)展开成一张完整的输入输出对照表,这张表是所有推导的落脚点。
如果你对这些函数的印象还停留在"名字挺多",下面的逐个拆解会把它们还原成一张张具体的表,你照着算两遍,手感就出来了。
2. 统一记忆:用二进制位理解所有函数
2.1 N=2^n 与 n 位二进制编号
把N个输入端从0编到N-1之后,每个编号都能写成n位二进制。以N=8为例,n=3,编号0到7对应:
- 0 = 000
- 1 = 001
- 2 = 010
- 3 = 011
- 4 = 100
- 5 = 101
- 6 = 110
- 7 = 111
这里有个非常关键的约定:习惯上我们把最左边那一位叫最高位(对应2的n-1次方),最右边那一位叫最低位(对应2的0次方,也就是1)。写成 p_{n-1} p_{n-2} ... p_1 p_0,其中 p_{n-1} 是最高位,p_0 是最低位。后面描述所有函数时都会用到这套位记号,一旦你把每个函数都翻译成"对哪几位、做什么操作",记忆量会直接砍掉一大半。
为什么反复强调位?因为硬件实现互连网络的成本,跟你动了几位、动的位之间是不是规律直接相关。只翻一位的网络,走线和开关可以做得极规整,级联起来也好看;一下子把整个下标搅乱的网络,布线会立刻变成一团乱麻。理解了这一点,你就能理解为什么教材要先教这些"温和"的函数,再谈它们的复合。
2.2 三种基本位操作套路
几乎所有基本互连函数,本质都逃不出三种位操作套路:
- 翻转某一位:把第i位从0变1或从1变0,其余位不动。交换函数和立方体置换都属于这一类。
- 整体循环移位:把n位当成一个环,统一左移或右移若干位。均匀洗牌就是循环左移一位。
- 位置对调:把某两位(常见是最高位和最低位,或整段逆序)交换位置,其余保持。蝶形和反位序属于这一类。
还有一类是算术型的,比如PM2I,它不直接做位操作,而是对编号做加减2的幂次再取模,但你把它展开成二进制表之后,会发现它对应的同样是一种有规律的重排。把这四种套路在脑子里分类存放,遇到新函数先判断它属于哪一类,理解速度会快很多。
提示:不要试图死记每个函数对N=16的结果表,那张表有16行,记不住也没必要记。要记的是"它对应哪种位操作",用的时候现推,推得又快又不会错。
2.3 输入输出方向容易搞反
这里必须提前打一针:互连函数有"方向约定"问题。同一个物理连接,你从输入端看是 f(x)=y,从输出端反着看就是 f 的逆函数。教材在某一章可能统一按 f(x)=y 来定义,考试题如果给了对照表让你反推函数,方向一旦搞反,全盘皆错。我的建议是:每做一道题,先在草稿纸上写清楚"f(x)=y 表示输入x连到输出y",然后所有推导都按这个方向来,不要中途换。下面所有内容我也统一按这个方向讲。
3. 六大基本互连函数逐个拆透
3.1 交换函数:只翻一位,共 n 个
交换函数是里面最简单的一类。定义:Cube_i(x) 把x的n位二进制表示中的第i位取反,其余位不变。用异或写出来就是 Cube_i(x) = x ⊕ 2^i,i 取 0 到 n-1,所以一共有n个交换函数。
以N=8(n=3)为例逐个展开:
- Cube_0:翻最低位(p_0)。0→1,1→0,2→3,3→2,4→5,5→4,6→7,7→6。
- Cube_1:翻中间位(p_1)。0→2,1→3,2→0,3→1,4→6,5→7,6→4,7→5。
- Cube_2:翻最高位(p_2)。0→4,1→5,2→6,3→7,4→0,5→1,6→2,7→3。
观察一下就能发现一个漂亮的性质:交换函数是对合函数,也就是自己和自己复合等于恒等——Cube_i(Cube_i(x)) = x。因为把同一位翻两次自然就回来了。这个性质在硬件上意味着"这一级网络是双向对称的",一根线正着接完反着接都成立,布线非常规整,这也是它被大量用在中低层互连里的原因。
再看Cube_0:它把0和1配成对、2和3配成对、4和5配成对、6和7配成对。Cube_1把间隔为2的成对连起来,Cube_2把间隔为4的成对连起来。你把这n个函数各当成"沿一个维度连接相邻立方体顶点",它们合起来就画出了一个n维超立方体——下一节专门说这个。
实操心得:判断Cube_i到底翻哪位时,记住 2^i 就是那个权重。Cube_0 权重1,翻最低位;Cube_2 权重4,翻第三位。先把 2^i 的公司列出来再动位,比硬记"第几个"靠谱。
3.2 立方体置换与超立方体网络的对应关系
交换函数族 {Cube_0, Cube_1, ..., Cube_{n-1}} 合在一起,和n维超立方体(也叫n立方体)的边是一一对应的。超立方体的每个顶点用一个n位二进制串标号,两个顶点之间有一条边当且仅当它们的标号恰好差一位——而"差一位"正是Cube_i干的事。所以Cube_i就是把所有顶点沿第i个维度连接到它的邻居。
这个对应带来一个直接的好处:在超立方体网络里做路由时,从源点到目标点要用哪几步、每步翻哪位,就是把源和目标标号做异或,结果里哪些位是1,就依次翻哪些位。这是一个非常实用的小技巧,很多并行计算课程设计里让你实现超立方体路由,核心就这一句话。
举个具体例子,N=8下从5(101)走到2(010)。5 ⊕ 2 = 101 ⊕ 010 = 111,三位全是1,说明要翻三次。可以翻的顺序很多,比如先翻p_0(5→4),再翻p_1(4→6),再翻p_2(6→2);也可以先翻p_2(5→1),再翻p_1(1→3),再翻p_0(3→2)。路径长度都是3,正好等于标号不同的位数,也就是汉明距离。这说明超立方体路由的最短步数就等于两端标号的汉明距离,这是个能直接用的结论。
3.3 PM2I 函数:加减 2 的幂次再取模
PM2I 是"Plus-Minus 2^i"的缩写,它一族有2n个函数,分正负两组:
- PM2+i(x) = (x + 2^i) mod N
- PM2-i(x) = (x − 2^i) mod N
其中i取0到n-1。N=8时,n=3,共6个:
- PM2+0:加1再模8。0→1,1→2,...,7→0。
- PM2−0:减1再模8。0→7,1→0,...,7→6。
- PM2+1:加2再模8。0→2,1→3,...,6→0,7→1。
- PM2−1:减2再模8。
- PM2+2:加4再模8。0→4,1→5,...,4→0。
- PM2−2:减4再模8。
PM2+0 和 PM2−0 一个本质作用是把整条序列整体循环平移一格,连续叠加PM2+0若干次,就能实现任意位数的循环移位。这一族函数最贴近"环"或者"移位寄存器"的结构,因为它就是均匀地绕圈走。
这里有一个极易搞错的点:取模的方向。正函数是加,负函数是减,减完还要加N再模N来保证结果落在[0, N-1]。很多人在手算时忘记减完取模,比如PM2−2算了0−4=−4,直接写−4就错了,正确答案是(−4+8) mod 8 = 4。手算时一律用"(结果 + N) mod N"保险,不用纠结数学上mod的符号约定。
把PM2I和交换函数对比着看很有意思:交换函数动的是二进制的"位",PM2I动的是数值的"大小",两者视角不同但都能描述规整的重排。有些网络用PM2I来构造,因为它天然实现了对处理单元编号的"邻域访问",做环形算法(比如循环分布的数据)时特别顺手。
3.4 均匀洗牌函数:把序列对半错开
均匀洗牌是这组里最"有名"的。它的直观描述是:把N个输入分成前后两半,前一半是0到N/2−1,后一半是N/2到N−1,然后从两半里交替取牌交错排列,效果上等于把整个序列按"对半错开"的方式重排。
用二进制解释更精确:均匀洗牌就是把n位标号整体循环左移一位,也就是把最高位p_{n-1}挪到最低位的位置,其余各位都向左移一格。公式化的版本是:
- f(x) = (2x) mod (N−1),当 x 不等于 N−1;
- f(N−1) = N−1。
至于为什么N−1要单独拿出来处理,是因为全1那个标号在循环左移下不动(111左移还是111),而(2x) mod (N−1)这个公式在x=N−1时会算成(2N−2) mod (N−1) = 0,和实际不符,所以必须单独规定。
N=8时展开:0→0,1→2,2→4,3→6,4→1,5→3,6→5,7→7。按输入顺序读一遍输出:0、2、4、6、1、3、5、7,正好是前一半的偶序列接后一半的序列,肉眼可见的"对半错开"。
均匀洗牌有一个很值得记的性质:连续做n次,任何标号都会回到原位。因为每做一次就是循环左移一位,做n次正好整个循环一圈,回到起点。工程含义是,如果你想用纯洗牌级堆出一个"看似任意"的置换,得搭配别的函数,因为单独只靠洗牌只能生成n个循环位上的一族置换,绕来绕去跳不出这个循环。这也解释了为什么实际网络(比如Omega网络)要用洗牌和交换交替叠加。
3.5 蝶形函数:首末两位对调
蝶形函数的定义很干脆:把标号的最高位和最低位互换位置,中间各位不动。用位记号写就是 p_{n-1} p_{n-2} ... p_1 p_0 变成 p_0 p_{n-2} ... p_1 p_{n-1}。
N=8时展开:0(000)→0,1(001)→4(100),2(010)→2,3(011)→6(110),4(100)→1,5(101)→5,6(110)→3,7(111)→7。
画成连线图,输入在线排0到7,输出在线排0到7,把连接关系画出来,是中间一堆交叉、两端收束的对称形状,像一对翅膀对着,这就是"蝶形"名字的来源。蝶形也是常见的FFT硬件里那一级"蝶形运算"背后连接的数学叫法,虽然严格说FFT蝶形和这里的互连函数不完全等价,但名字和形态上的联系让它们经常被放在一起讲。
一个容易忽略的细节:当 n≤3 时,蝶形和反位序是同一个函数。因为n位里只有三位(n=3)时,把最高位和最低位对调、中间只剩一位,结果就是对整串逆序,和反位序一模一样。只有n≥4,中间有两位及以上时,两者才分化开来。这个点我当年做练习题时错过一次,一开始还以为两个函数写错了。
3.6 反位序函数:整个标号逆序
反位序(也叫位序颠倒、bit reversal)把整个n位标号左右翻个,p_{n-1} p_{n-2} ... p_1 p_0 直接变成 p_0 p_1 ... p_{n-2} p_{n-1}。N=8时:0(000)→0,1(001)→4(100),2(010)→2,3(011)→6,4(100)→1,5(101)→5,6(110)→3,7(111)→7。注意这一组结果和上一节蝶形在N=8下完全相同,原因就是刚才说的n=3时两者重合。
反位序是FFT和各种分治变换里非常重要的一个重排:很多按"输入自然顺序、输出位倒序"或者反过来组织的算法,最后都要靠它把结果摆正。它也是这几族函数里唯一一个把整串位"彻底打乱"的,一条线的去向可能跨越大半个网络,布线代价比较高。所以真正拿它做硬件时,一般是放在软件后处理里,或者用多级洗牌近似,不会真的拉一条全交叉线。
把六个函数放在一起,n=3的对照表如下,建议自己动手在东边列输入、西边列输出各画一遍,加深印象:
| 输入(二进制) | Cube_0 | Cube_1 | Cube_2 | PM2+1 | Shuffle | Butterfly | Bit-reverse |
|---|---|---|---|---|---|---|---|
| 0 (000) | 1 | 2 | 4 | 2 | 0 | 0 | 0 |
| 1 (001) | 0 | 3 | 5 | 3 | 2 | 4 | 4 |
| 2 (010) | 3 | 0 | 6 | 4 | 4 | 2 | 2 |
| 3 (011) | 2 | 1 | 7 | 5 | 6 | 6 | 6 |
| 4 (100) | 5 | 6 | 0 | 6 | 1 | 1 | 1 |
| 5 (101) | 4 | 7 | 1 | 7 | 3 | 5 | 5 |
| 6 (110) | 7 | 4 | 2 | 0 | 5 | 3 | 3 |
| 7 (111) | 6 | 5 | 3 | 1 | 7 | 7 | 7 |
4. 手算加代码核对:把每个函数真正跑一遍
4.1 N=16 时两者才分化,动手推一遍
N=8的对照表只是热身,真正能暴露理解漏洞的是N=16。n=4,标号用4位。我这里重点说蝶形和反位序的分化,因为其他函数在N=8和N=16下规律一致,唯独这两个在n=4时会分家。
N=16下的反位序,把4位整体逆序:
- 3 = 0011,逆序后 1100 = 12
- 5 = 0101,逆序后 1010 = 10
- 6 = 0110,逆序后 0110 = 6
N=16下的蝶形,只把最高位和最低位对调、中间两位不动:
- 3 = 0011(p3=0,p2=0,p1=1,p0=1),换首末位后为 p0 p2 p1 p3 = 1 0 1 0 = 10
- 5 = 0101(p3=0,p2=1,p1=0,p0=1),换后为 1 1 0 0 = 12
- 6 = 0110(p3=0,p2=1,p1=1,p0=0),换后为 0 1 1 0 = 6
看,3这个标号:反位序给12,蝶形给10,明显不同了。这正是我之前踩过的坑——拿着N=8的思路不管三七二十一推N=16,结果全错。手算时务必先确认n是几,然后把位串写全,别偷懒省略前导零。
4.2 用代码把函数和手算结果对上
手算容易眼花,我一般会写一小段代码核对。下面这段Python把六个函数都实现了,可以直接复制去跑:
N = 16 n = 4 mask = (1 << n) - 1 def cube(x, i): # 翻转第 i 位 return x ^ (1 << i) def pm2_plus(x, i): return (x + (1 << i)) % N def pm2_minus(x, i): return (x - (1 << i)) % N def shuffle(x): # n 位循环左移一位 highest = (x >> (n - 1)) & 1 return ((x << 1) & mask) | highest def butterfly(x): # 最高位与最低位对调,中间位不变 low = x & 1 high = (x >> (n - 1)) & 1 mid = x & ~((1 << (n - 1)) | 1) return mid | (low << (n - 1)) | high def bit_reverse(x): r = 0 for i in range(n): if (x >> i) & 1: r |= 1 << (n - 1 - i) return r for x in range(N): print(x, bin(x)[2:].zfill(n), "cube0=", cube(x, 0), "shuffle=", shuffle(x), "butterfly=", butterfly(x), "bitrev=", bit_reverse(x))跑完之后,把屏幕上的butterfly和bit_reverse两列对比,你会亲眼看到它们在N=16下从第3个标号(也就是3、5这类标号)开始就拉开差距。这一步非常值得做——手推一遍、代码再验证一遍,两个函数的分化点很难再忘。
注意:写shuffle函数时一定要先取最高位再移位,最后把最高位塞回最低位,别直接用 (x<<1)%N 之类,那样会丢掉溢出的最高位,结果全错。这是我自己写第一版时踩的坑。
4.3 复合函数与函数等价关系
单看每个函数都很简单,但真正决定网络的,是它们之间的复合关系。我列几个值得记住的结论。
第一,Shuffle复合n次等于恒等。因为它是循环左移,转一圈就回来了。
第二,Shuffle和Cube_0组合,能生成一族很有用的置换。经典教材里会讨论"均匀洗牌加交换"能构造出Omega网络,其实质就是每一级在洗牌的基础上再翻最低位,把原来只能沿一个循环跳的洗牌补成可以在不同分支间切换。
第三,PM2+0连续复合k次,等价于把整个序列循环平移k位,这在对数据做环形分布调整时比逐个交换方便得多。
第四,蝶形和反位序在n=2和n=3时等价,n≥4时不等价。这个等价关系既是理解它们的捷径,也是很多人做题时栽跟头的点。
把这些复合关系整理成一张"关系备忘录",复习的时候只看这张表比翻整章书更快:
| 关系 | 结论 | 适用条件 |
|---|---|---|
| Cube_i ∘ Cube_i | 恒等(对合) | 任意 n |
| Shuffle 连续 n 次 | 恒等 | 任意 n |
| PM2+0 连续 k 次 | 循环平移 k 位 | 任意 n |
| Butterfly 是否等于 Bit-reverse | n≤3 相等,n≥4 不等 | 取决于 n |
| Shuffle(N−1) | 仍为 N−1 | 全1标号固定点 |
5. 常见问题速查与踩坑记录
5.1 位数没对齐,前导零被省略
这是出现频率最高的错误,没有之一。手算时图快,把6写成"110"而不是"0110",然后拿3位去推4位的规则,结果必然塌方。我的习惯是:只要题目给的N对应n位,动笔第一步就在草稿纸顶上写一排"p3 p2 p1 p0",然后每个标号都补齐到n位,一个零都不省。看着啰嗦,但比返工强得多。特别是反位序和蝶形这种"动首末位"的函数,少一位整个方向都反了。
5.2 均匀洗牌在全1标号处算错
前面强调过,均匀洗牌函数在x = N−1时是个固定点,不能套用(2x) mod (N−1)。很多人算N=8的7、N=16的15时,顺手用公式一算就对不上,然后怀疑自己整个函数的理解有问题——其实只是漏了这个特殊值。我建议记洗牌函数时用"循环左移"的位操作版本,天然就不会在这个点上出错;公式版本只用来做快速验证。
5.3 PM2I 取模方向与负数处理
PM2+i是加,PM2−i是减,减完要回到[0, N−1]。手算减法的结果是负数时,一律加N再取模,不要凭直觉写负数。另一种错误是i的范围记错:PM2I的i从0到n-1,一共2n个函数;N=8时是6个,不是3个,也不是8个。搞不清个数时就用2n这个公式现推。
5.4 互连函数和互连网络混为一谈
这是概念层面的混淆:互连函数描述的是一级连接的映射关系,是"规则";互连网络是由若干级这样的连接级联而成的"结构"。一个函数可以单独成为一级网络,网络则由多个函数的复合构成。考试里如果问"均匀洗牌是几级网络"这类问题,本质是在问这个函数在某个具体网络结构里被当做第几级用,而不是函数本身有级数。把"规则"和"由规则搭出来的结构"这两个层次分开,很多绕人的题目会瞬间清晰。
5.5 输入输出方向记反导致全盘错
最后再强调一次方向。f(x)=y里的x是输入端、y是输出端,这个约定在整章里要前后一致。如果题目给了一张表让你反推函数名,先确认表的列头是"输入"还是"输出",方向搞反的话,交换函数还能自己蒙对(它是对称的),洗牌、蝶形、反位序就全乱了,因为这些函数不是自反的。
6. 这些函数在真实系统里的落点
6.1 在超立方体与多级互连网络中的角色
超立方体网络之所以在并行机里出现频率极高,一个原因就是它的连线规则可以直接用交换函数描述:沿第i维连接就是Cube_i,任意两点间的最短路就是把源和目标异或,按为1的位依次走。这个特性让路由算法简单到可以用位运算一句话说清,硬件实现也规整。中间级的多级互连网络(Omega、间接二进制n立方体、基准网络等)多数是"均匀洗牌加交换"这种组合一层层叠出来的,用N=2^n的位解释去模拟每一级的连线,是设计和仿真这些网络的基本功。
6.2 在快速变换与数据重排中的落点
反位序在FFT和各类快速变换里几乎无处不在。按自然顺序输入、位倒序输出的组织方式,最后一步就是把结果按反位序摆正,这一步如果做成硬件一整块全交叉,面积和延迟都很吃亏,工程上更常见的是用多级洗牌或专门的小型重排网络去近似。理解反位序的位规律,能帮你在写软件层重排代码时写出比朴素循环更快的实现——直接按位操作构造索引比逐项搬移数据快得多。
6.3 在课程设计与仿真里的用法
如果你正在做并行计算或系统结构的课程设计,需要搭一个互连网络仿真,我的建议是:先用前面那段Python代码把每个基本函数的映射表打印出来做单元测试,确认每一步映射都和自己手算的一致;再往上叠加多级网络,每叠一级就验证一次映射表。这样一旦某级接错,能立刻定位到是哪个函数、哪个参数的问题,而不是拿一整张网络图去瞎找。多个单元测试堆起来,整个网络的正确性就是自底向上被"钉"死的,比最后整体调试省太多时间。
我自己做的时候还养成一个习惯:每个函数都写一个"正向映射表"和"反向映射表"两个版本,正向用来做路由,反向用来做逆操作验证。正反两张表互为印证,能顺带把方向搞反的低级错误全部挡在门外。
回过头看这套东西,其实没有一个函数是难的,难的是它们长得太像,加上位数、方向、特殊值这几个变量一搅,就容易出错。把每个函数都还原成"哪几位、怎么变"的位操作,再配上N=8、N=16两组手推表和一段可以跑起来的代码做交叉验证,基本就没有死角了。我个人的体会是,这章内容动手算的效率远高于用眼睛看:看十遍不如亲手把N=8那张表推一遍,推完再看N=16,理解会扎实得多。后续如果要往Omega网络、Banyan网络这些结构深入,这些基本函数就是拼图的每一块,先把它们磨利了,后面拼起来才顺手。