• 大整数算法[05] 移位操作


             上一篇博文讲到了大整数位的相关操作,这次的内容依然和位的操作相关:移位操作。需要说明的是,这里所讲的大整数移位操作都是算术移位(左移的话精度会增加,而不是把移出的位丢掉)。

             ★ 两种移位的区别

             在移位操作当中存在两种不同的操作方式:逻辑移位和算术移位。在逻辑移位当中,移出的位丢失,移入的位取 0。在算术移位当中,移出的位丢失,左移入的位取 0,右移入的位取符号位,即最高位代表的数据符号保持不变。举个例子:有如下两个类型的变量(32位系统下)被定义:

             unsigned int u = 0x80000000;   (8 后面有 7 个 0)

             int s = 0x80000000;

             用二进制表示都是:1000 0000 0000 0000 0000 0000 0000 0000

             u 跟 s 的区别在于 s 的最高位是符号位(0 代表正数,1 代表负数),所以如果用以下语句打印这两个变量,结果会是:2147483648, -2147483648。

             printf(" u = %u ", u);
             printf(" s = %d ", s);

             第一个结果好说,无符号数就是 2^31,但是为什么第二个是 - 2^31 呢?按理说应该是 - 0 才对呀。原因是这些数都是用补码表示的。

             先来看看 - 2^31 - 1 (-2147483647)这个数用补码怎么表示。

             原码:1111 1111 1111 1111 1111 1111 1111 1111,注意最高位是符号位。要求补码,先计算反码,反码的计算是符号位不变,其余位取反。

             反码:1000 0000 0000 0000 0000 0000 0000 0000,得到反码之后,将反码加 1 就得到补码了。

             补码:1000 0000 0000 0000 0000 0000 0000 0001。所以 - 2^31 - 1 用补码表示就是 0x80000001。

             要表示 - 2^31,只需要将 - 2^31 - 1 的补码减 1 即可,所以是 0x80000000,这就得到 32 位有符号整形下可表示的最小负整数。

             下面看看进行移位后的结果是怎样的。

             对于左移操作,不管是逻辑左移还是算术左移,左边移出的位都丢失,右边移入的位都取 0,所以如果执行如下两条语句:u <<= 1; s <<= 1; 其结果都是 0。

             对于右移操作,逻辑右移左边补 0,算术右移左边补符号位。所以如果执行如下两条语句:u >>= 1; s >>= 1; 其结果:1073741824, -1073741824。结果的补码用二进制表示就是:

              u:0100 0000 0000 0000 0000 0000 0000 0000

              s:1100 0000 0000 0000 0000 0000 0000 0000

              上面的右移 1 位的操作,相当于在计算:2147483648 / 2 = 1073741824;  - 2147483648 / 2 = - 1073741824

              对于移位操作,左移 n 位相当于乘以 2^n,右移 n 位相当于除以 2^n。

              前面废话那么多就是想说明逻辑移位和算术移位是有区别嘀。对于大整数,算术移位操作不需要用什么补码,因为 dp 指向的内存保存的是大整数的绝对值,符号是用 sign 来跟踪的。另外和C里面不同的是,如果左移后位数不够,大整数的精度会增加,而不像C那样丢弃移出来的位。

             ★ 左移和右移 b 个数位

              简单来说就是乘以或除以 2 ^ (biL *b),移位操作是以整个数位为单元进行的。

              左移 b 个数位:

    int bn_lshd(bignum *x, size_t b)
    {
        int ret;
        size_t i;
        bn_digit *top, *bottom;
    
        x->used += b;
        BN_CHECK(bn_grow(x, x->used));
    
        top = x->dp + x->used - 1;
        bottom = x->dp + x->used - b - 1;
    
        for(i = x->used; i > b; i--)
            *top-- = *bottom--;
    
        top = x->dp;
    
        for(i = 0; i < b; i++)
            *top++ = 0;
    
    clean:
    
        return ret;
    }
    

              左移 b 个数位后,数位增加 b 位,所以 used 值要增加 b。使用 bn_grow 函数增加 bignum 的精度。然后用 top 指针指向 bignum 左移后的最高位,bottom 指针指向 bignum 当前的最高位,然后循环 used - b 次把每一个数位往左移动 b 个数位。操作结束后,让 top 指针指向最低位,循环 b 次把最低的 b 个数位置 0,完成移位操作。

                右移 b 个数位:

    int bn_rshd(bignum *x, size_t b)
    {
        int ret = 0;
        size_t i;
        bn_digit *top, *bottom;
    
        if(x->used <= b)
        {
            BN_CHECK(bn_set_word(x, 0));
            return ret;
        }
    
        bottom = x->dp;
        top = x->dp + b;
    
        for(i = 0; i < x->used - b; i++)
            *bottom++ = *top++;
    
        for(; i < x->used; i++)
            *bottom++ = 0;
    
        x->used -= b;
    
    clean:
    
        return ret;
    }
    

              和左移不同的是,右移精度不会增加,但如果 used 的值小于等于 b,则 bignum 的最高位都会从右边被移出去,结果为 0。如果 used > b,让 bottom指向 bignum 的最低数位,top 指针指向第 b + 1 位,然后循环 used - b 次将每个数位往右挪动 b 个数位,最后将左边剩余的 b 位取 0,完成右移操作。

             ★ 左移和右移 1 个比特位

             很好理解,就是乘以 2 或者除以 2。下面的算法完成后会把 x 的值赋值给 y。

             左移 1 位:

    int bn_lshift_1(bignum *y, const bignum *x)
    {
        int ret;
        size_t i, olduse, shift;
        bn_digit r, rr, *px, *py;
    
        BN_CHECK(bn_grow(y, x->used + 1));
    
        olduse = y->used;
        y->used = x->used;
    
        px = x->dp;
        py = y->dp;
    
        r = 0;
        shift = (size_t)(biL - 1);
    
        for(i = 0; i < x->used; i++)
        {
            rr = *px >> shift;
            *py++ = (*px++ << 1) | r;
            r = rr;
        }
    
        if(r != 0)
        {
            *py = 1;
            y->used++;
        }
    
        py = y->dp + y->used;
    
        for(i = y->used; i < olduse; i++)
            *py++ = 0;
    
        y->sign = x->sign;
    
    clean:
    
        return ret;
    }
    

             首先算法默认将 y 的精度增加到 x->used + 1 个数位,之所以要加 1,因为有可能刚好把最高位移到下一个数位中去。olduse 记录当前 y 的数位,然后将 y 的数位增加到 x 的数位,如果最终还有进位,y->used 还要加一。shift 变量表明每个数位要往右移动的比特位数,例如在 32 位系统下,shift = 31,64 位系统下 shift = 63。变量 r 存储上一个数位最左边的比特位,变量 rr 存储当前数位最左边的比特位。在循环当中,先将当前数位右移 shift 位来获得最左边的比特位,然后再将这个数位左移 1 位并且与右边数位的最左边比特位做 OR 操作,这样当前数位中的每一比特位就往左边移动了 1 位,并且右边数位的最左边比特位也移动到当前数位的最右位置。循环结束后,如果 r 不为 0,表明原来 bignum 最左边数位的最左边比特位为 1,在左移中被移到了新的数位中,所以 used 加一,新的数位值为 1。最后将 y 的高位设置为 0,把 x 的符号给 y 完成所有操作。

                右移 1 位:

    int bn_rshift_1(bignum *y, const bignum *x)
    {
        int ret;
        size_t i, olduse, shift;
        bn_digit r, rr, *px, *py;
    
        BN_CHECK(bn_grow(y, x->used));
    
        olduse = y->used;
        y->used = x->used;
    
        px = x->dp + y->used - 1;
        py = y->dp + y->used - 1;
    
        r = 0;
        shift = (size_t)(biL - 1);
    
        for(i = y->used; i > 0; i--)
        {
            rr = *px & 1;
            *py-- = (*px-- >> 1) | (r << shift);
            r = rr;
        }
    
        py = y->dp + y->used;
    
        for(i = y->used; i < olduse; i++)
            *py++ = 0;
    
        y->sign = x->sign;
    
        bn_clamp(y);
    
    clean:
    
        return ret;
    }
    

              右移 1 位精度不会增加,不过仍然要调用 bn_grow 函数增加精度,因为一开始 y 的精度可能不够。右移 1 位的操作跟左移 1 位的操作比较类似,只是方向相反。在第一个循环当中,先获取当前数位的最右比特位存放到变量 rr 中,然后当前数位右移 1 位,接着将左边数位的最右比特位左移 shift 位后与当前数位做 OR 操作,最后将 rr 的值存放到变量 r 中,这样当前位的所有比特都往右移动了 1 位,并且左边数位的最右边比特也移动到当前数位的最左边。完成循环后将高位设置为 0,然后设置符号位,最后压缩多余位完成操作。

             ★ 左移和右移 n 个比特位

              如果说前面的移位操作都带有特殊性,那么下面这两个操作就是将其一般化而已。左移或右移 n 位相当于乘以或除以 2^n。

              左移 n 位:

    int bn_lshift(bignum *y, const bignum *x, size_t count)
    {
        int ret;
        size_t i, d, shift;
        bn_digit r, rr, *py;
    
        if(y != x)
            BN_CHECK(bn_copy(y, x));
    
        BN_CHECK(bn_grow(y, y->used + count / biL + 1));
    
        if(count >= biL)
            BN_CHECK(bn_lshd(y, count / biL));
    
        d = count & (biL - 1);
    
        if(d != 0)
        {
            py = y->dp;
            shift = (size_t)(biL - d);
    
            r = 0;
    
            for(i = 0; i < y->used; i++)
            {
                rr = (*py >> shift);
                *py = (*py << d) | r;
                py++;
                r = rr;
            }
    
            if(r != 0)
                y->dp[y->used++] = r;
        }
    
    clean:
    
        return ret;
    }
    

             首先算法检查并增加 y 的精度,接着如果左移的位数 count 大于或等于一个数位的比特数,调用 bn_lshd 函数将 x 左移 count / biL 个数位,然后检查是否有剩余的比特位:d = count & (biL - 1),如果 d 不为 0,表明还有剩余位,按照左移 1 位的原理提取剩余位向左移动完成左移 n 位的操作。

               右移 n 位:

    int bn_rshift(bignum *y, const bignum *x, size_t count)
    {
        int ret = 0;
        size_t i, d, shift;
        bn_digit r, rr, *py;
    
        if(y != x)
            BN_CHECK(bn_copy(y, x));
    
        if(count >= biL) BN_CHECK(bn_rshd(y, count / biL));
    
        d = count & (biL - 1);
    
        if(d != 0)
        {
            py = y->dp + y->used - 1;
            shift = (size_t)(biL - d);
    
            r = 0;
    
            for(i = y->used; i > 0; i--)
            {
                rr = *py;
                *py = (*py >> d) | (r << shift);
                py--;
                r = rr;
            }
        }
    
        bn_clamp(y);
    
    clean:
    
        return ret;
    }
    

            和左移 n 位一样,先做整个数位的右移,然后再按照右移 1 位的原理往右移动剩余的比特位。要注意的是,右移之后,需要压缩多余位来更新 used 的值。

             ★ 时间复杂度分析

             本文所讲到的移位操作,都是在一重循环内完成的,算法的复杂度与 bignum 的精度和大小有关,时间复杂度大致为 O(n)。

             ★ 总结

             移位操作对于某些特殊的计算是十分有效的,比如乘以或除以 2,因此碰到这类计算,最好使用移位操作而不是乘法或除法。讲完了大整数的位操作,接下来就要开始讲讲四则运算了。下一篇文章将介绍绝对值加法。

         【回到本系列目录】 

    版权声明
    原创博文,转载必须包含本声明,保持本文完整,并以超链接形式注明作者Starrybird和本文原始地址:http://www.cnblogs.com/starrybird/p/4357222.html

  • 相关阅读:
    solidity 学习笔记(5)接口
    solidity 学习笔记(4)library库
    以太坊开发教程(二) 利用truffle发布宠物商店 DAPP 到 以太坊测试环境Ropsten
    以太坊开发教程(一) truffle框架简介/安装/使用
    软分叉/硬分叉/重放攻击/重放保护
    linux安装node.js
    jq获取元素偏移量offset()
    js:把字符串转为变量使用; js下将字符串当函数去执行的方法
    JS中json数组多字段排序方法(解决兼容性问题)(转)
    javascript之取余数、去商、天花板取整、地板取整
  • 原文地址:https://www.cnblogs.com/starrybird/p/4357222.html
Copyright © 2020-2023  润新知