今天我们继续抄袭 Unix 的源代码,通过借鉴近半个世纪之前的思路,再来解决一道 Leetcode 算法题儿。
这次要挑战的题目是 Leetcode 415.字符串相加 —— 也就是用字符串模拟大整数相加。说到“大整数”,*nix 用户第一个想到的大概就是 bc 命令了吧?
$ bc -e 602214076987654321987654321987+198847362947000123567890123456789
199449577023987777889877777778776
有趣的是,bc 其实只是另一个古老程序 dc 的“友好版“。dc 是贝尔实验室的鲍勃·莫里斯(Bob Morris)编写的任意精度计算器,采用了后缀表达式作为输入,对新人极不友好。后来,洛琳达·彻丽(Lorinda Cherry)编写了 bc 命令,作为 dc 的“前端翻译器”。于是,人们又可以使用惯用的中缀表达式来输入算式了。
dc 命令似乎最早出现在距今 53 年的 Unix V3 版本中。
直到 Unix 第六版,dc 还是用汇编语言写的。到了 Unix 第七版,终于出现了 C 语言版的 dc。
接下来,我们就从 V7 版本的 dc 源码里找找有什么可以抄袭、借鉴的地方。
说回到大整数相加,用字符串存储整数似乎是最直接的方式,Leecode 415 这道题给出的原型函数也是 char* addStrings(char*, char*),即用字符串表示两个加数和计算结果。
但 dc 并没有直接用字符串,而是引入了一个带有 4 个 char 指针的结构体 struct blk 来表示一个数字。
struct blk {
char *rd;
char *wt;
char *beg;
char *last;
};
简单来说,beg(begin)和 last 可看作是一个 char[] (其实就是字符串)的左右边界,而 rd(read)和 wr(write)读写双指针都可以在 beg 和 last 圈定的范围内自由左右移动。
这就巧妙地实现了加法运算中同时用到的两种方向:输出时按从左到右,即从高位到低位的方向移动 rd 指针;计算时按照从右向左,即从低位到高位的方向移动 wr 指针。
另外,由于这个 char[] 数组中每个元素都可以存放 1 字节,即 0~255 或-128~127 内的整数,所以数组中每个元素不是一个 0~9 之间的个位数,而是 0~99 之间的两位数。进行加法运算时,也是两位两位地加(这里的“位”是指十进制数的个数,不是 bit),所以处理进位时是逢百进一,而不是我们习惯的逢十进一。
dc 命令源代码中的 add() 函数实现了任意精度的整数相加,
#define length(p) ((p)->wt-(p)->beg)
#define rewind(p) (p)->rd=(p)->beg
#define sfeof(p) (((p)->rd==(p)->wt)?1:0)
#define sfbeg(p) (((p)->rd==(p)->beg)?1:0)
#define sgetc(p) (((p)->rd==(p)->wt)?EOF:*(p)->rd++)
#define sbackc(p) (((p)->rd==(p)->beg)?EOF:*(--(p)->rd))
#define sputc(p,c) {if((p)->wt==(p)->last)more(p); *(p)->wt++ = c; }
#define fsfile(p) (p)->rd = (p)->wt
// https://github.com/dspinellis/unix-history-repo/blob/Research-V7-Snapshot-Development/usr/src/cmd/dc/dc.c#L1416
// struct blk *
// add(a1,a2)
// struct blk *a1,*a2;
struct blk* add(struct blk *a1, struct blk *a2)
{
register struct blk *p;
register int carry,n;
int size;
int c,n1,n2;
size = length(a1)>length(a2)?length(a1):length(a2);
p = salloc(size);
rewind(a1);
rewind(a2);
carry=0;
while(--size >= 0){
n1 = sfeof(a1)?0:sgetc(a1);
n2 = sfeof(a2)?0:sgetc(a2);
n = n1 + n2 + carry;
if(n>=100){
carry=1;
n -= 100;
}
// ... 省略处理负数的逻辑 ...
else carry = 0;
sputc(p,n);
}
if(carry != 0)sputc(p,carry);
fsfile(p);
// ... 省略删除前导0的逻辑 ...
// ... 省略处理负数的逻辑 ...
return(p);
}
若只看其中 while 循环的部分,
while(--size >= 0){
n1 = sfeof(a1)?0:sgetc(a1);
n2 = sfeof(a2)?0:sgetc(a2);
n = n1 + n2 + carry;
if(n>=100){
carry=1;
n -= 100;
}
// ... 省略处理负数的逻辑 ...
else carry = 0;
会感觉这大体上和我们熟悉的写法差不多,无非也是数位对齐,对应位置上的数字(两位十进制数)相加,较短的数字前面补 0 后再与较长数字中的数位相加,然后小心处理进位 carry 即可。
今天我们又用几行 Unix 中的老代码,就把 LeetCode 的大整数加法题儿搞定了。
🔚