跳转至

进位制

进位制,又称 进位系统(carry system)、进制系统、位置记法(positional notation)、位值记数法(place-value notation)、位置数值系统(positional numeral system),是一种能用有限种符号表示所有自然数的数字系统.一种进位制可以使用的符号数目称为 基数(radix)或 底数(base),基数为 n 的进位制称为 n 进制(n>1),例如我们最常用的十进制,通常只使用 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 这十个符号来记数.进位指的是「当一个数字的某一位达到基数时,将其置为 0 并使高一位的数加一」的操作.

一般地,我们常将一个 n 进制的数记作 (ak⋯a1a0)n、(ak⋯a1a0)(n)、ak⋯a1a0(n)、ak⋯a1a0n 等.若基数隐含在上下文中我们也可以省略下标.注意此处的 ak⋯a1a0 并不是 k+1 个数的乘积,而是一串序列.

对于 k 进制数 an⋯a1a0,其表示的值为 ankn+⋯+a1k1+a0k0=∑i=0naiki;对一个数 m,设其 k 进制下的表示为 an⋯a1a0,则有:

a0=m−q0k,q0=f(m/k),a1=q0−q1k,q1=f(q0/k),⋮⋮an=qn−1−qnk,qn=f(qn−1/k)=0,

其中 f(x)=⌊x⌋.

数 n 在 k 进制下表示的长度为 ⌈logk⁡(n+1)⌉.

我们一般通过添加小数点「.」来表示小数1、添加负号「−」来表示负数、在小数末尾的一段上添加上划线表示无限循环小数等.为了方便阅读,我们可以每隔若干数位添加间隔符号(如空格、,、' 等),如 12 345 表示 12345.

在计算机中,比较常用的进位制有二进制、八进制和十六进制.

不同进位制间的转换

十进制转其他进制

这里以二进制为例来演示,其他进制的原理与其类似.

整数部分,把十进制数不断执行除 2 操作,直至商数为 0,之后从下到上,取所有余数的数字,即为二进制的整数部分数字.小数部分,则用其乘 2,取其整数部分的结果,再用计算后的小数部分依此重复计算,算到小数部分全为 0 为止,之后从上到下,取所有计算后整数部分的数字,即为二进制的小数部分数字.

例子

将 35.25 转化为二进制数.

整数部分:

35/2=17…1,17/2=8…1,8/2=4…0,4/2=2…0,2/2=1…0,1/2=0…1.

小数部分:

0.25×2=0.5…0,0.5×2=1…1.

即 35.25=(100011.01)2.

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <algorithm>
#include <string>

// only non-negative
// 2 <= base && base <= 36
std::string from_dec(int x, int base) {
  if (x == 0) return "0";
  std::string res;
  while (x) {
    int r = x % base;
    x /= base;

    // 写法 1
    res.push_back(r < 10 ? '0' + r : 'A' + r - 10);
    // 写法 2
    // const static std::string digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    // res.push_back(digits[r]);
  }
  std::reverse(res.begin(), res.end());
  return res;
}

其他进制转十进制

还是以二进制为例.二进制数转换为十进制数,只需将每个位的值,乘以 2i 次即可,其中 i 为当前位的位数,个位的位数为 0.

例子

将 (11010.01)2 转换为十进制数.

(11010.01)2=+ 1×24+1×23+0×22+1×21+0×20=+ 0×2−1+1×2−2=26.25.

即 (11010.01)2=(26.25)10.

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <cctype>
#include <string>

// only non-negative
// 2 <= base && base <= 36
int to_dec(std::string const& s, int base) {
  int res = 0;
  for (char c : s) {
    res *= base;
    if (isdigit(c))
      res += c - '0';
    else if (isupper(c))
      res += c - 'A' + 10;
    else if (islower(c))
      res += c - 'a' + 10;
  }
  return res;
}

二进制/八进制/十六进制间的相互转换

一个八进制位可以用 3 个二进制位来表示(因为 23=8),一个十六进制位可以用 4 个二进制位来表示(24=16),反之同理.

补数法

另请参阅:反码与补码

补数法(method of complements)是用正数表示负数,使得可以用和正数加法相同的算法/电路/机械结构来计算减法的方法.补数法广泛用于计算器和计算机的设计中,用以简化结构.

对 b 进制下 n 位数 a,其 数基补数(radix complement,称为 b 的补数)是 bn−a,其 缩减数基补数(diminished radix complement,称为 b−1 的补数,简称 减补数)是 bn−1−a.在二进制下,数基补数叫做 2 的补数(two's complement),又叫做 补码;缩减数基补数叫做 1 的补数(ones' complement),又叫做 反码;在十进制下,数基补数又叫做 10 的补数(ten's complement),缩减数基补数又叫做 9 的补数(nine's complement);其他进制以此类推.

对 b 进制下的 n 位数 x,y,当计算 x−y 时,我们有如下方法(若结果超过 n 位,则舍弃高位):

  1. 考虑 x 的减补数 x′=bn−1−x,计算 x′+y=bn−1−x+y,则该结果的减补数即为答案.
  2. 考虑 y 的减补数 y′=bn−1−y,计算 x+y′=bn−1+x−y,直接加一即为答案.
  3. 考虑 x 的数基补数 x′=bn−x,计算 x′+y=bn−x+y,则该结果的数基补数即为答案.
  4. 考虑 y 的数基补数 y′=bn−y,计算 x+y′=bn+x−y,此即为答案.

另外,对 k 进制下的数,我们设 d=k−1,则 ⋯dd=:d―=∑i=0∞dki=−1,所以对 n 位数 x,设其数基补数在 k 进制下的表示为 an−1⋯a1a0,则 d―an−1⋯a1a0 即为 ∑i=0n−1aiki+∑i=n∞dki=kn−x+(−kn)=−x.这种「无限位的数」的思想可以推广为 p 进数(p-adic number).

此外,我们有一个关于补数和无限循环小数的有趣定理:

Midy 定理

设 a 是正整数,p 是正素数,a/p 在 b 进制下的表示为 0.a1a2⋯al―,其中 l 为(最短)循环节长度.若 l 是偶数4,设 l=2k,则 a1a2⋯ak 是 ak+1ak+2⋯a2k 的减补数,即:

  • ai+ai+k=b,
  • a1a2⋯ak+ak+1ak+2⋯a2k=bk−1.

进一步,若 l 有非平凡因子 k,设 l=nk,则 ∑i=0n−1aik+1aik+2⋯a(i+1)k 是 bk−1 的倍数.

例子

对于 1/19=0.052 631 578 947 368 421―=0.032 745―(8),我们有:

  • 052 631 578+947 368 421=999 999 999,
  • 052+631+578+947+368+421=3×999,
  • 032(8)+745(8)=777(8),
  • 03(8)+27(8)+45(8)=77(8).
证明

对定理中的 a,b,p,l,n,k,不难发现 1≤a<p,b>1 且 (a,p)=(b,p)=1.

对整数 0≤i<l,令 f(i)=bi⋅a/p−⌊bi⋅a/p⌋,我们有

0<f(i)=0.ai+1ai+2⋯anka1a2⋯ai―<1⟹0<pf(i)<p.

注意到 pf(i)∈N+ 且 pf(i)≡abi(modp),因此 pf(i)=abimodp.

令 Sn=∑i=0n−1f(ik)=∑i=0n−10.aik+1aik+2⋯anka1a2⋯aik―,我们可以在各个小数间「交换」若干位(例如 0.142857―+0.285714―+0.571428―=0.141414―+0.282828―+0.575757―=0.14―+0.28―+0.57―=14/99+28/99+57/99=1),则

Sn=∑i=0n−10.aik+1aik+2⋯a(i+1)k―=∑i=0n−1aik+1aik+2⋯a(i+1)k/(bk−1),

进而

pSn=p∑i=0n−1aik+1aik+2⋯a(i+1)k/(bk−1)=∑i=0n−1(abikmodp),

因此

∑i=0n−1aik+1aik+2⋯a(i+1)k=(bk−1)∑i=0n−1(abikmodp)p.

若 p∣(bk−1),注意到

(bk−1)a/p=a1a2⋯ak.ak+1ak+2⋯anka1a2⋯ak―−0.a1a2⋯ank―,

则有 ak+1ak+2⋯anka1a2⋯ak=a1a2⋯ank,进而 a1a2⋯ak=ak+1ak+2⋯a2k=⋯=a(n−1)k+1a(n−1)k+2⋯ank,即 0.a1a2⋯al―=0.a1a2⋯ak―,这与 l 的定义矛盾,因此 p∤(bk−1).

故存在正整数 c=∑i=0n−1(abikmodp)p,使得

∑i=0n−1aik+1aik+2⋯a(i+1)k=c(bk−1).
推论

对上述的 b,n,k,p,有

∑i=0n−1bik≡0(modp).

广义进制系统

标准的进制系统中,基数 b 总是一个固定的正数,每个数位在 b 种不同的符号中选取,用以表示一个非负数(不考虑小数点和负号).实际上仍有许多记数系统和进制系统有类似的特征,但不完全符合进制系统的规定,我们把这样的记数系统称为 广义进制系统 或 非标准进制系统(Non-standard positional numeral systems).下面介绍几种常见的广义进制系统.

双射记数系统

标准的进制系统并不能和其表示的数字建立双射,如 1、01、001 表示的数是相同的2,而 双射记数系统(bijective numeral system)可以和其表示的数字建立双射.

双射 k 进制(k≥1)使用数集 {1,2,…,k} 来唯一表示一个数,规则如下:

  1. 用空串表示 0;
  2. 用非空串 an⋯a1a0 表示数 ankn+⋯+a1k1+a0k0=∑i=0naiki.

对一个正数 m,设其双射 k 进制下的表示为 an⋯a1a0,则有:

a0=m−q0k,q0=f(m/k),a1=q0−q1k,q1=f(q0/k),⋮⋮an=qn−1−qnk,qn=f(qn−1/k)=0,

其中 f(x)=⌈x⌉−1.

例如 Microsoft Excel 的列标签采用的就是双射 26 进制.

在双射记数系统下,我们有 一进制,一进制下的非空串只由 1 构成,串的长度即为其表示的数.

类似 补数法 下的叙述,在 k>1 的双射 k 进制下,我们设 d=k−1,则 ⋯dd=:d―=∑i=0∞dki=−1,进而 d―k=0,所以设 x 在双射 k 进制下的表示为 an−1⋯a1a0,则 d―kan−1⋯a1a0 即为 −x.

下面是双射 k 进制数的一些性质:

  • 长度为 l≥0 的数共有 kl 种.
  • k≥2 时,数 n 在双射 k 进制下表示的长度为 ⌊logk⁡(n+1)(k−1)⌋.
  • k≥2 时,若一个数 n 在 k 进制下的表示中不含 0,则其在 k 进制下的表示和在双射 k 进制下的表示相同.

双射 k 进制转十进制和 k 进制转十进制的代码相同,下面是十进制转双射 k 进制的参考实现

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <algorithm>
#include <string>

// only non-negative
// 1 <= base && base < 36
std::string from_dec_bi(int x, int base) {
  std::string res;
  while (x > 0) {
    int q = (x + base - 1) / base - 1, r = x - q * base;
    x = q;

    // 写法 1
    res.push_back(r < 10 ? '0' + r : 'A' + r - 10);
    // 写法 2
    // const static std::string digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    // res.push_back(digits[r]);
  }
  std::reverse(res.begin(), res.end());
  return res;
}

有符号位数进制

有的进位制系统允许数位中出现负数,例如 平衡三进制.

Gray 码

主条目:格雷码

Gray 码又叫 循环二进制码 或 反射二进制码(reflected binary code,RBC),是一种特殊的二进制数字系统,常用于数据校验中.

非正基数进制

我们知道对于 k 进制数 an⋯a1a0,其表示的值为 ∑i=0naiki,我们稍加修改即可定义 −k 进制数 an⋯a1a0(−k) 表示 ∑i=0nai(−k)i,其中 an,…,a1,a0∈{0,1,…,k−1}.例如 12345(−10)=8265(10).这种进制系统叫做 负底数进制(negative-base system).

类似地,我们也可以定义 复底数进制(complex-base system),如 2i 进制(quater-imaginary base、quater-imaginary numeral system),我们还可以定义 非整数进位制(non-integer base of numeration)用于表示实数的 β 展开(β-expansion)等.

混合基数进制

标准的进制系统中,每一个数位对应的基数都是固定的,而混合基数进制允许每一个数位对应不同的基数.混合基数进制系统最常见的应用就是计时:小时采用 24 进制,分钟和秒采用 60 进制.

an⋯a1a0 在 b 进制下表示的数为 ∑i=0naibi,而在混合基数进制下,其表示的数为 ∑i=0nai∏j=0i−1bj,其中 bj 为 aj 对应的基数.

在算法竞赛中,最常见的混合基数进制系统是 阶乘进制(factorial number system),其中的数可以记作 an⋯a1a0 !,其表示的数为 ∑i=0naii!3.阶乘进制在算法竞赛中的应用可参见 Lehmer 码/Cantor 展开.

实现(十进制转阶乘进制)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <algorithm>
#include <string>

// only non-negative
std::string from_dec_factorial(int x) {
  if (x == 0) return "0";
  std::string res;
  int base = 1;
  while (x) {
    int r = x % base;
    x /= base++;

    // 写法 1
    res.push_back(r < 10 ? '0' + r : 'A' + r - 10);
    // 写法 2
    // const static std::string digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    // res.push_back(digits[r]);
  }
  std::reverse(res.begin(), res.end());
  return res;
}
实现(阶乘进制转十进制)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <cctype>
#include <string>

// only non-negative
int to_dec_factorial(std::string const& s) {
  int res = 0, base = s.size();
  for (char c : s) {
    res *= base--;
    if (isdigit(c))
      res += c - '0';
    else if (isupper(c))
      res += c - 'A' + 10;
    else if (islower(c))
      res += c - 'a' + 10;
  }
  return res;
}

C++ 中的实现

对于非负数,C++ 中用 <前缀><数位><后缀> 表示一个整数字面量,其中 <数位> 和 <后缀> 均可以为空.<后缀> 用于表示字面量的类型,如 u 或 U 表示该字面量为 unsigned、l 或 L 表示该字面量为 long 等.对于 <前缀>:

  • 当 <前缀> 为 0x 或 0X 时表示十六进制字面量,此时 <数位> 中的字符只能在 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, A, b, B, c, C, d, D, e, E, f, F 中选取.例如 0x1234ABCD 为 1234ABCD(16)=305 441 741;
  • 当 <前缀> 为 0 时表示八进制字面量,此时 <数位> 中的字符只能在 0, 1, 2, 3, 4, 5, 6, 7 中选取.例如 01234567 为 1234567(8)=342391;5
  • 当 <前缀> 为 1、2、3、4、5、6、7、8 或 9 时表示十进制字面量,此时 <数位> 中的字符只能在 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 中选取;
  • C++14 起,当 <前缀> 为 0b 或 0B 时表示二进制字面量,此时 <数位> 中的字符只能在 0, 1 中选取.例如 0b11001010 为 11001010(2)=202.

参考资料与注释


  1. 有的地区使用「,」表示小数点. ↩

  2. 我们把最高的非 0 位之前的 0 称为 前导零(leading zero).类似地,我们可以定义 后导零(trailing zero). ↩

  3. ai 对应的基数是 i+1,0≤ai≤i.注意到 (n+1)!−n!=n⋅n!,所以数在阶乘进制下的表示在去除前导零的情况下是唯一的. ↩

  4. 当 a=1 时,在十进制下满足该条件的素数序列是 A028416. ↩

  5. 0 是八进制字面量. ↩