Search This Blog

2013-05-01

说数制转换

目录  [+]

1 原文引用

《数据结构(C语言版)》(严蔚敏、吴伟民编著),3.2.1 数制转换,把十进制转换为其他进制的原理:
N = (N div d) x d + N mod d(其中:div为整除运算,mod为求余运算)
例如:(1348)10 = (2504)8,其运算过程如下:
NN div 8N mod 8
13481684
168210
2122
202
对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数。
void conversion() {
    InitStack(S); // 构造空栈
    scanf("%d", N);
    while (N) {
        Push(S, N % 8);
        N = N / 8;
    }
    while (!StackEmpty(S)) {
        Pop(S, e);
        printf("%d", e);
    }
}

2 对上面引用的说明

说是十进制,实际上是二进制,输入的十进制整数字符串被scanf转换成了二进制。

上面的公式很直观,是对的,但是怎么想到用这样一个公式?
以上面的数字为例,根据进制的定义,得到如下算式:
(1348)10 = 1*(10^3)+3*(10^2)+4*(10^1)+8*(10^0) = S(k)*(8^k)+S(k-1)*(8^(k-1))+...+S(1)*(8^1)+S(0)*(8^0)

观察上面的算式,发现只有S(0)*(8^0)是落单的,其他的都是8的倍数,那S(0)*(8^0)就是N%8了,即S(0)=N%8,N-S(0)是8的倍数,从而得出,N=N%8+8*(S(1)+S(2)*(8^1)+...+S(k-1)*(8^(k-2))+S(k)*(8^(k-1))),这次S(1)成了落单的,S(1)=((N-N%8)/8)%8=(N/8)%8,剩下的可以用一样的方法得出。这就是上面的做法。

也可以先找到手动计算S(k),S(k-1),...,S(1),S(0)的方法。如果8^(k+1)>=N,那k就是最高指数,给8^k加倍,直至超过N,超过之前的那个倍数就是S(k),可以对N-S(k)*(8^k)及8^(k-1)做类似的计算,剩下的也一样,最后得到2*(8^3)+5*(8^2)+0*(8^1)+4*(8^0)。

这个手动计算过程是有规律的,可以以此写一个程序,先不论好坏。

通过前面的过程容易发现,S(k)=N/(8^k),余数(k)=N%(8^k),S(k-1)=余数(k)/(8^(k-1)),余数(k-1)=余数(k)%(8^(k-1)),可以以此修改一下前面写的那个程序。继续往前推导,S1=余数(2)/(8^1),余数(1)=余数(2)%(8^1)=N%8,S(0)=余数(1)/(8^0)=余数(1)=N%8,反过来计算就是上面的做法了。

3 整数的进制转换例子

3.1 java.lang.Integer.toString

GPL license(代码不允许商业使用);通用算法跟书上介绍的一样,8/10/16进制有专门的优化实现。
Pre[-]
public static String toString(int i, int radix) {
    if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) // MIN_RADIX=2, MAX_RADIX=36
        radix = 10;
    /* Use the faster version */
    if (radix == 10) {
        return toString(i);
    }
    char buf[] = new char[33]; // 使用足够大的字符数组保存结果,而不是栈
    boolean negative = (i < 0);
    int charPos = 32;
    if (!negative) {
        i = -i; // 如果是正整数,那转化为负整数,下面统一处理
    }
    while (i <= -radix) {
        buf[charPos--] = digits[-(i % radix)]; // digits: [0-9a-z]范围内的字符数组,正好36个字符
        i = i / radix;
    }
    buf[charPos] = digits[-i];
    if (negative) {
        buf[--charPos] = '-';
    }
    return new String(buf, charPos, (33 - charPos));
}

3.2 groff itoa

GPL license(代码不允许商业使用);算法跟书上介绍的一样。
Pre[-]
#define INT_DIGITS 19        /* enough for 64 bit integer */
char *itoa(i)
     int i;
{
  /* Room for INT_DIGITS digits, - and '\0' */
  static char buf[INT_DIGITS + 2]; // 使用足够大的静态局部字符数组保存结果,多线程调用这个函数的话,应该会有问题
  char *p = buf + INT_DIGITS + 1;    /* points to terminating '\0' */
  if (i >= 0) {
    do {
      *--p = '0' + (i % 10);
      i /= 10;
    } while (i != 0);
    return p;
  }
  else {            /* i < 0 */
    do {
      *--p = '0' - (i % 10);
      i /= 10;
    } while (i != 0);
    *--p = '-';
  }
  return p;
}
--引用自groff itoa.c - Author: James Clark - www.opensource.apple.com

3.3 Linux itoa

GPL license(代码不允许商业使用);这个算法跟额外介绍的其中一个方法一样。
namespace linux
{
    void itoa( int i,char* string) 
    {
        int power, j;
        j=i; 
        for (power=1;j>=10;j/=10) 
            power*=10; 
        for (;power>0;power/=10)
        {
            *string++='0'+i/power; i%=power; 
        }
        *string='\0';
    } 
}
--引用自itoa的两种实现 - Author: yanglinbo - www.cppblog.com

3.4 Solaris itoa

GPL license(代码不允许商业使用);算法跟书上介绍的一样。
Pre[-]
namespace solaris
{
    char * itoa(long n, int base) // 从下面的代码来看,base允许的最大取值是16
    {
        register char *p; 
        register int minus; 
        static char buf[36]; 
        p = &buf[36]; 
        *--p = '\0'; 
        if (n < 0) { minus = 1; n = -n; } else minus = 0; 
        if (n == 0)  *--p = '0'; 
        else while (n > 0) { *--p = "0123456789abcdef"[n % base]; n /= base; } 
        if (minus) *--p = '-';
        return p;
    }
}
--引用自itoa的两种实现 - Author: yanglinbo - www.cppblog.com

4 数值运算规范

上面那些程序的一个关键是数值的整除及求余运算规范,前面的分析是基于对整除及求余的基本认识。语言规范决定数值运算规范。
我不熟悉其他语言,仅以Java为例。

4.1 Java Remainder Spec

In C and C++, the remainder operator accepts only integral operands, but in the Java programming language, it also accepts floating-point operands.
The remainder operation for operands that are integers after binary numeric promotion (§5.6.2) produces a result value such that (a/b)*b+(a%b) is equal to a.
--引用自JLS # 15.17.3. Remainder Operator % - docs.oracle.com

在Java里边,整除负数有个特例,这里就不说了。正整数的运算是比较直观的,一般也不会有问题,有负数就得小心了。如果要做任意数字的整除或求余,那得把相关的数值运算规范好好看完,其他运算也一样。

5 任意非二进制之间的转换

非二进制一般是human-readable的需要,这种情况下,都是可打印ASCII字符串,这一步只讨论这种情况。
分两步:
(1)先把其中一种进制转换为二进制。这一步就是把字符串转换为数字,字符跟数字的联系是字符编码,比如ASCII。
(2)再把二进制转换为另一种进制。

6 进制的引申含义,以及进制跟Base系列编码的区别

上面的(1348)10跟(2504)8反映的是一种等价映射关系,代表的是同一个整数。

对于数字的二进制表示跟数字的2^k(k>1)进制表示之间的等价映射关系,有一种很直观的映射,就是用一个字符来表示k-bit,能无损地相互转换,所以是等价的,但未必是进制转换(开头那句是假设)。实现起来很方便,逻辑右移k、按位与(2^k-1)就行了,其实,逻辑右移k就是对2^k做整除运算,按位与(2^k-1)就是对2^k求余,如果不映射那些残余的无意义0bit(比如0x0000007C中前面的那些0),那还是符合上面的那个公式,属于进制转换,否则,那就算别的编解码方式,比如Base64、Base32(结尾可能有=)。

7 数字压缩

7.1 介绍

)用更高进制表示的数字长度更短,可以节约存储空间。当然,二进制排除在外,因为最终的数据都是以二进制的形式存储,是否节约存储空间在于编码方法。如果把二进制输出为字符串,比如,0xFFFF对应的是11111111,每个'1'都至少需要一个字节存储,那就有可比性了。
)2/3/4/5/6/7/8-bit就是Base4/8/16/32/64/128/256,如果不映射那些残余的无意义0bit,那就是进制转换。
)如果输出的字符需要human-readable,那64进制最节约空间。如果要用10或16进制存储数字,那可以考虑转换为64进制存储。
)如果输出的字符不需要human-readable,那256进制最节约空间。比如,int32类型的24需要4个字节,转换为256进制只需要1个字节。
)当然,上面说的是单个数字,如果需要连续保存很多数字,那需要有方法分离出连续的数字。

举两个业界的例子,都不是human-readable编码。

7.2 UTF-8编码

UTF-8使用1到4个八位字节来表示Unicode code point。Unicode code point的范围是0到U+10FFFF。在编程语言里边,int32/uint32就可以表示了,UTF-32就直接使用4个八位字节表示Unicode code point。
UTF-8编码格式(来自UTF-8 definition - tools.ietf.org):
Char. number range  |        UTF-8 octet sequence
  (hexadecimal)     |              (binary)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
The letter x indicates bits available for encoding bits of the character number.
数字1/0是固定的前缀,解决了连续保存的数字之间的边界问题,边界对受损的数据有限制损失的功效。

7.3 Protocol Buffers

Protocol Buffers的数字编码很紧凑,编码文档在https://developers.google.com/protocol-buffers/docs/encoding

)Base 128 Varints
  • Varints是一种使用一到多字节的整数序列化方法,越小的数字使用越少的字节。
  • 对于varint的每个字节,最后一个字节除外,都是用最高位用来表示后面还有没有字节属于这个varint,1代表有,0代表没有,所有字节的低7位代表原始数字的二进制补码表示,低字节中的数据放在varint的高位。
  • 以300为例,二进制字符串为100101100(前面的0都已省略),按照7位一组分开(低位优先)并交换顺序,得到0101100 10,补上指示后续字节的最高位并扩充不足的位,得到10101100 00000010,这就是300的varint编码。
    • 这里扩充的最高位解决了连续保存的varints之间的边界问题。

)ZigZag encoding
用二进制补码表示的负数,最高位是1,相应的varint会有很多字节;所以有必要把负整数映射为比较小的正整数,以提高压缩效率。对于sint32 n,这里提供的映射方式是这样的:(n << 1) ^ (n >> 31)。

(n << 1)^(n >> 31)不够直观,无法直接看出设计意图。
这个算式的等价表示:
  • 如果n>=0,那(n << 1)=2n,(n >> 31)=0,(n << 1)^(n >> 31)的结果是2n;
  • 如果n<0,那(n << 1)=2n,(n >> 31)=0xFFFFFFFF,(n << 1)^(n >> 31)的结果是-2n-1。证明如下:在二进制补码系统中,对于int32 n, -n=~n+1, ~n=n^0xFFFFFFFF, -> n^0xFFFFFFFF=-n-1, -> (2n)^0xFFFFFFFF=-2n-1。

sint64同理。

8 附录:二进制跟64进制互相转换的Java实现

Radix64Converter.java - sandynz.blogspot.com

=文章版本=

201303
20130501 修改一点格式,顺便增减一些内容
20130506 改为html格式

No comments:

Post a Comment