造轮子:补码实现与若干分析

这周计算机原理课收到楼sir的一个作业:要自己实现一套整数编码的数码转换与若干运算,并分析
我拿到的是补码,其他的队友分别要实现一套原码、移码或者自行设计一套“帅码”(666)。

编码之前…

编程语言: C

听说规定要用C语言造这个轮子,真是遗憾,要是C++还可以各种运算符重载可以很优雅。

约束

那好吧,在编码之前,先做个约束。

  • 不能使用任何形式的 int 类型,包括中间变量,因为 int 本身就是一个补码实现。
    看起来,这次的任务就是基于 unsigned int 实现一个 int 类型(32位)。

    • 使用 unsigned int 作为 二进制串 word 的容器结构。
      typedef unsigned int word;

    • 不能使用 %d 输出 word , 而要使用 码转数函数 mtoaword 转化为 char*
      当然也不能用 %d 读入 word,而要使用 数转码函数 atomchar* 转化为 word
      本质上使用 %d 就是一个 补码数码转换 的实现,因此我们要避免使用它。

函数原型

函数签名 注释
word atom(char*) 字符串中以带符号十进制的格式读入,转换为补码
char* mtoa(word) 将补码转换为带符号十进制的字符串格式
word madd(word, word) 补码加法
word msub(word, word) 补码减法
word mmul(word, word) 补码乘法
word mdiv(word, word) 补码整除

以上函数原型均为楼sir一手定义,于是我就照着来咯。

数码转换

atom函数与mtoa函数在数学上互逆的,即

(mtoaatom)(x)x,x (atommtoa)(x)x,x 

然而在计算机中受到机器精度限制,并不能好好地做运算。

考虑到这点,我们可以给函数加上定义域与值域的限制。

特别地,对于32位补码,定义:

atom:[231,231)[0,232)
mtoa:[0,232)[231,231)

atom(x)={x,232+x,0x<231231x<0

构成双射函数,于是求这个函数的反函数得到:
mtoa(x)={x,232+x,0x<231231x<232

atom,mtoa的定义是等价的,都可以称为补码的定义。

接下来看一下在0-1串内部的一些位运算:

二进制码按位取反函数的代数代换

设有0-1串

m=(m1m2...mn)2[0,2n)

则有
Not(m)=(Not(m1)Not(m2)...Not(mn))2

显然有
m+Not(m)=(11...1)2=2n1

于是
Not(m)=2n1m[0,2n)

按位取反运算:Not(m)在 [0,2n) 中封闭。

补码:取相反数的算法证明

m0 是一个 n 位的0-1串,证明 mtoa(Not(m)+1)=mtoa(m)
证:
代入按位取反公式:

mtoa(Not(m)+1)=mtoa(2n1m+1)

简单化简:
mtoa(2n1m+1)=mtoa(2nm)

  1. m(0,2n1)2nm(2n1,2n)
    根据补码的定义

    mtoa(m)=mmtoa(2nm)=2n+(2nm)=m

    所以:
    mtoa(2nm)=mtoa(m)

  2. m[2n1,2n)2nm(0,2n1]
    根据补码的定义

    mtoa(m)=(2n+m)=2nmmtoa(2nm)=2nm

    所以:
    mtoa(2nm)=mtoa(m)

综上所述,

mtoa(Not(m)+1)=mtoa(m)

证毕。

所以,非0数的补码按位取反再加一即为其相反数的补码。
特别地,0的相反数的补码是其本身,这个可以单独验证。

补码加法

对于n位0-1串 m1,m2 证明

mtoa(m1)+mtoa(m2)mtoa((m1+m2)mod2n)(mod2n)

引理2 mtoa(x)x(mod2n),x[0,2n)
证明:
1. 若 x[0,2n1)
mtoa(x)=xx(mod2n)
2. 若 x[2n1,2n)
mtoa(x)=2n+xx(mod2n)
证毕。

证:
由引理2:

mtoa(m1)m1(mod2n)mtoa(m2)m2(mod2n)mtoa((m1+m2)mod2n)m1+m2(mod2n)

由同余定理得
mtoa(m1)+mtoa(m2)m1+m2(mod2n)

所以,
mtoa(m1)+mtoa(m2)mtoa((m1+m2)mod2n)(mod2n)

证毕。

这个部分证明了:两数的和的补码两数的补码的和(高位丢弃)对 2n 同余。

补码减法

对于n位0-1串 m1,m2 证明 mtoa(m1)mtoa(m2)mtoa((m1m2)mod2n)(mod2n)
证:
由引理2:

mtoa(m1)m1(mod2n)mtoa(m2)m2(mod2n)mtoa((m1m2)mod2n)m1m2(mod2n)

由同余定理:
mtoa(m1)mtoa(m2)m1m2(mod2n)

所以,
mtoa(m1)mtoa(m2)mtoa((m1m2)mod2n)(mod2n)

这个部分证明了:两数的差的补码两数的补码的差 2n 同余。

补码乘法

终于到乘法了,这个部分的数学部分就比较麻烦了,二进制串(向量)乘法本质是一个离散卷积。关于其算法,普通可以使用 O(n2) 的朴素做法,也可以用FFT1算法进行优化到 O(nlog(n))

// TODO
鉴于楼sir尚未要求实现乘法/除法,我暂时先不做这部分的工作。

编码实现

数码转换

数转码:

word atom(char* str){
    word res;
    sscanf(str, "%d", &res);
    return res;
}

码转数:

char* mtoa(word w){
    char* res = (char*) malloc(sizeof(char) * 12);
    sprintf(res, "%d", w);
    return res;
}

以上是错误示范。尽管完美地完成了补码的数码转换,但是这样就违反了一开始的约束了,用了 C 标准库中的轮子。
mtoa中给res开的空间是比较宽松的。32位整数的十进制表示不会超过11位字符,加上字符串结束符\0 最多12位。

数转码

正如之前的错误示范的正确思路,扫描字符串即可。

word atom(char* str){
    word res = 0, flag = 0;
    for(word i = 0; str[i]; i++){
        if('0' <= str[i] && str[i] <= '9')
            res = 10 * res + str[i] - '0';
        else if(i == 0 && str[i] == '-')
            flag = 1; // negative flag
        else break; // illegal character
    }
    // if res != 0 and negative flag 
    // consider about the input "-0"
    if(res && flag) 
        res = ~res + 1;
    return res;
}

码转数

这个需要将码逐位取出(或者一次取多位)然后转化为字符串即可。

char* mtoa(word w){
    char* res = (char*) malloc(sizeof(char) * 12);
    word flag = w >> 31; // get the topest digit
    word cur = 0;
    if(flag) { // if negative
        res[cur++] = '-';
        w = ~w + 1;
    }
    word startCur = cur;
    while(w){
        res[cur++] = w % 10 + '0';
        w /= 10;
    }
    word endCur = cur - 1;
    // let's reverse 
    while(startCur < endCur){
        char temp = res[startCur];
        res[startCur++] = res[endCur];
        res[endCur--] = temp;
    }
    if(cur == 0) 
        res[cur++] = '0'; // if 0
    res[cur] = '\0';
    return res;
}

这个操作相对低效一些并没有太大的关系,这个通常跟I/O相关,I/O瓶颈带来的时间代价远大于此函数。

补码加/减法

word madd(word a, word b){
    return a + b;
}
word msub(word a, word b){
    return a - b;
}

……没错,就是这么简单,补码就是这么方便。

附录:源代码

complement.c

Compile Mode : C99

#include <stdio.h>
#include <stdlib.h>

typedef unsigned int word;

word atom(char* str){
    word res = 0, flag = 0;
    for(word i = 0; str[i]; i++){
        if('0' <= str[i] && str[i] <= '9')
            res = 10 * res + str[i] - '0';
        else if(i == 0 && str[i] == '-')
            flag = 1; // negative flag
        else break; // illegal character
    }
    // if res != 0 and negative flag 
    // consider about the input "-0"
    if(res && flag) 
        res = ~res + 1;
    return res;
}

char* mtoa(word w){
    char* res = (char*) malloc(sizeof(char) * 12);
    word flag = w >> 31; // get the topest digit
    word cur = 0;
    if(flag) { // if negative
        res[cur++] = '-';
        w = ~w + 1;
    }
    word startCur = cur;
    while(w){
        res[cur++] = w % 10 + '0';
        w /= 10;
    }
    word endCur = cur - 1;
    // let's reverse 
    while(startCur < endCur){
        char temp = res[startCur];
        res[startCur++] = res[endCur];
        res[endCur--] = temp;
    }
    if(cur == 0) 
        res[cur++] = '0'; // if 0
    res[cur] = '\0';
    return res;
}

word madd(word a, word b){
    return a + b;
}

word msub(word a, word b){
    return a - b;
}


int main(){
    int t, x, y;
    char S[1000], X[100], Y[100];
    while(scanf("%d", &t))
        switch(t){
            case 0:
                scanf("%s", S);
                puts(mtoa(atom(S)));
                break;
            case 1:
                scanf("%s%s", X, Y);
                puts(mtoa(madd(atom(X), atom(Y))));
                break;
            case 2:
                scanf("%s%s", X, Y);
                puts(mtoa(msub(atom(X), atom(Y))));
                break;
            default:
                return 0;
        }
}

结语

数学真有趣。

  1. 快速傅里叶变换。 ↩

你可能感兴趣的:(编码,数学,计算机)