刷Leetcode算法题儿的新方法:抄袭Unix的源代码①——415. 字符串相加

50 阅读4分钟

今天我们继续抄袭 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 V3 版本中 dc 命令的手册

直到 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 的大整数加法题儿搞定了。

🔚