进制转换

一、什么是进制

进制,就是人类用来表示数字的不同计数系统。我们生活中用的就是十进制,基数为10,也就是逢十进一。

在计算机内部,使用的是二进制,基数为 2,逢二进一,也就是说我们在计算机里处理的内容,无论是文字、图片、音频还是视频,最终都是转换为二进制来表示的。

为什么人类会使用十进制?这很可能是因为我们有十个手指,对人类来说,这也是最自然的一种计数方式。而计算机使用二进制,是因为计算机内部的电子电路的开关状态恰好可以用 0 和 1 两个状态来表示,使用二进制最简单,也刚好符合这样的两种状态。

1. 常见的进制系统

  1. 二进制(Binary):基数为 2,逢 2 进 1,计数只使用 0 和 1
  2. 八进制(Octal):基数为 8,逢 8 进 1,计数使用 0 ~ 7
  3. 十进制(Decimal):基数为 10,逢 10 进 1,计数使用 0 ~ 9
  4. 十六进制(Hexadecimal):基数为16,逢 16 进 1,计数使用 0 ~ 9 和 A ~ F

这里要特别说明一下,由于我们目前使用的阿拉伯数字只有 0 ~ 9 这 9 个数,对于十六进制来说,逢 16 才进 1,9 之后的 10 ~ 15 就没办法只用一个数字来表示,因为借用了大写字母 A ~ F 来对应于 10 ~ 15

对于不同的进制,由于基数不同,10 在不同进制中所表示含义也有所不同。

  • 在二进制中,10 表示 2
  • 在八进制中,10 表示 8
  • 在十进制中,10 表示 10
  • 在十六进制,10 表示 16

2. 为什么需要进制转换

在计算机内部,使用的是二进制来计算的,二进制对机器来说比较友好,但对人类来说表达并不方便,但人类习惯使用的十进制对计算机却不太友好(10 不是 2 的整数次方,运算容易出问题),因而出现了八进制和十六进制,因为 23=82^3=824=162^4=16,正好都是 2 的整数次方,这一点很重要,在计算机中,十六进制用更多一些,比如 C++ 中的内存地址表示是用十六进制表示的,计算机中的颜色表示也经常会用十六进制来表示。

那这几种进制之间的转换就有必要了。

实际上,我们在本章会把进制转换分为下面三种情况:

  • 十进制转其它进制
  • 其它进制转十进制
  • 其它进制之间的转换

这里的其它进制主要指的是二、八、十六进制。

二、十进制转其它进制

将十进制转换为其它任意进制,我们采用的是 “短除法”:用十进制不断除以目标进制的基数,记录余数,最后逆序输出这些余数。

例如,将十进制 13 转换为二进制,用短除法,用 13 不断除以二进制基数(2),并记录余数。

  1. 13 除以 2,商为 6,余数为 1
  2. 6 除以 2,商为 3,余数为 0
  3. 3 除以 2,商为 1,余数为 1
  4. 1 除以 2,商为 0,余数为 1

除到商为 0 为止(想一想为什么呢),然后我们将得到的余数逆序输出,得到 13 的二进制表示为 1101

用同样的方法,也可以完成十进制转八进制。

十进制转十六进制也是同样的方法,只是需要处理一下十六进制中的字母表示。

举一个例子,十进制的 126 转换为十六进制,仍然用短除法,用 126 不断除以十六进制基数(16),并记录余数。

  1. 126 除以 16,商为 7,余数为 14
  2. 7 除以 16,商为 0,余数为 7

得到的余数逆序输出,应该是 7 | 14,这里的 14 对应到十六进制中应该是大写字母 E,因为得到 126 的十六进制表示为 7E

如果在用纸笔来描述上面的过程,通常如下图这样。

提示:我们在本章的进制转换中,不涉及到负数的转换,负数的转换以后在讲解原码、反码和补码的时候再讲。

下面我们来看一看,如何通过编程来实现上面的短除法转换。

示例:十进制转任意进制

将一个十进制数 nn 转换为给定的 mm 进制输出。

例如,nn 为 13,mm 为 2,表示将十进制 13 转换为二进制,结果为 1101

输入描述

输入为一行,共两个数,分别为 nnmmnn 代表要转换的十进制数,mm 代表目标进制。

输出描述

输出为一行,转换后的目标进制数。

输入数据

126 16

输出数据

7E

实现代码

#include <bits/stdc++.h>
using namespace std;

/**
 * decimal 表示要转换的十进制数
 * base 表示转换的目标进制基数
 */
string decimal_to_any(int decimal, int base) {
  if (decimal == 0) return "0";
  
  string rlt = "";
  char c;
  while (decimal > 0) {
    int r = decimal % base;
    
    if (r < 10) {
      c = r + '0';
    } else {
      c = r - 10 + 'A';
    }
    // 字符串逆序拼接
    rlt = c + rlt; 
    decimal /= base;
  }
    
  return rlt;
}

int main() {
  int n, m;
  cin >> n >> m;
  
  cout << decimal_to_any(n, m) << endl;
  return 0;
}

三、其它进制转十进制

将任意一个 nn 进制数转换为十进制,我们使用 “按权展开求和” 的方法。每个数位的权重是该进制的对应次幂,取出每个位置的数字,与该位置的权重相乘,再把所有相乘的结果求和,就得到要转换的十进制。

例如,将二进制数 1101 转换为十进制,从右向左按权展开并求和:

1×20+0×21+1×22+1×23=131 \times 2^0 + 0 \times 2^1 + 1 \times 2^2 + 1 \times 2^3 = 13

上面的方法之所以有效,是因为任何进制的本质都是位置计数法,每个位置代表不同的权重。

如果上面的 1101 是八进制数,要转换为十进制,同样按权展示并求和:

1×80+0×81+1×82+1×83=5771 \times 8^0 + 0 \times 8^1 + 1 \times 8^2 + 1 \times 8^3 = 577

差别只是每个位置代表的权重不同。

示例:任意进制转十进制

将一个 mm 进制数 nn 转换十进制数。

例如,nn 为 1101,mm 为 2,表示将二进制数 1101 转换为十进制,结果为 13

输入描述

输入为一行,共两个数,分别为 nnmmnn 代表要转换为十进制的 mm 进制数。

输出描述

输出为一行,转换后的十进制数。

输入数据

7E 16

输出数据

126

实现代码 - 方法一

#include <bits/stdc++.h>
using namespace std;

int any_to_decimal(string s, int base) {
    int decimal = 0;
    int power = 1; // 代表次方
    
    // 从右向左处理每一位,按权展开并求和
    for (int i = s.size() - 1; i >= 0; i--) {
        int d;
        if (s[i] >= '0' && s[i] <= '9') {
            d = s[i] - '0';
        } else if (s[i] >= 'A' && s[i] <= 'F') {
            d = s[i] - 'A' + 10;
        }
        
        decimal += d * power;
        power *= base;
    }
    
    return decimal;
}

int main() {
  string s;
  int n;
  cin >> s >> n;
  
  cout << any_to_decimal(s, n) << endl;
  return 0;
}

实现代码 - 方法二

方法一是从右到左(从低位开始)来处理每一位,这种方法很直观,比较好理解。实际上我们也可以直接从左到右来计算,同样是按权展开求和,理解上稍微难一点,但写出的代码更简洁一些。

#include <bits/stdc++.h>
using namespace std;

int any_to_decimal(string s, int base) {
    int decimal = 0;
    // 直接从左向右处理每一位,按权展开并求和
    for (int i = 0; i < s.size(); i++) {
        int d;
        if (s[i] >= '0' && s[i] <= '9') {
            d = s[i] - '0';
        } else if (s[i] >= 'A' && s[i] <= 'F') {
            d = s[i] - 'A' + 10;
        }
        
        decimal = decimal * base + d;
    }
    
    return decimal;
}

int main() {
  string s;
  int n;
  cin >> s >> n;
  
  cout << any_to_decimal(s, n) << endl;
  return 0;
}

建议大家用纸和笔手动模拟一下,感觉一下方法一和方法二的差别。

四、其它进制之间转换

对于二、八、十六进制之间的转换,很容易想到,我们可以用十进制作为跳板,先将某一个进制转为十进制,再把十进制转换为目标进制。

在大部分情况下,这种转换方法都是没问题的,把上面的十进制转其它进制和其它进制转十进制的方法结合一下就行了,就是多写一点代码。

但有一些情况,这种方法就不太好办了,比如说要将一个很大的十六进制数转换为八进制数,很可能在转换的过程中,转换出来十进制数就已经超过 long long 可表示的范围了,我们就需要想一些其它办法去完成。

下面讲一讲另外一种快速转换方法。这种方法的原理是建立之前提到过的二、八、十六进制之间的一种特殊对应关系, 23=82^3=824=162^4=16,因此:

  • 每 3 位二进制数可以对应为 1 位八进制数
  • 每 4 位二进制数可以对应为 1 位十六进制数

这种方法在手动转换时尤为方便,编程转换效率也更高,因为是分组后进行的转换,而且中间都是使用字符串及字符,对于前面所说的超大数转换也没问题。

1. 二进制与八进制互转

例如,二进制转八进制,我们可以将每 3 位二进制数对应为 1 位八进制数。

二进制: 101 101 010 八进制: 5 5 2

反过来,八进制转二进制,将每 1 位八进制数对应 3 位二进制数。

八进制: 5 5 2 二进制: 101 101 010

2. 二进制与十六进制互转

二进制转十六进制:每4位二进制数对应 1 位十六进制数

二进制: 1010 1101 十六进制: A D

十六进制转二进制:每1位十六进制数对应 4 位二进制数

十六进制: A D 二进制: 1010 1101

示例:二进制转十六进制

方法一

// 二进制转十六进制
string bin2hex(string bin) {
  // 确保二进制数长度是4的倍数
  // 不够的话就补 0
  while (bin.size() % 4 != 0) {
    bin = "0" + bin;
  }

  string hex = "";
  char c;
  for (int i = 0; i < bin.size(); i += 4) {
    string group = bin.substr(i, 4);
    int t = 0;
    for (int j = 0; j < 4; j++) {
      t = t * 2 + (group[j] - '0');
    }

    if (t < 10) {
      c = t + '0';
    } else {
      c = t - 10 + 'A';
    }

    hex = c + hex; // 逆序拼接
  }

  return hex;
}

方法二

// 二进制转十六进制
string bin2hex(string bin) {
  // 确保二进制数长度是4的倍数
  // 不够的话就补 0
  string hex = "";
  char c;
  while (bin.size() % 4 != 0) {
    bin = "0" + bin;
  }

  for (int i = 0; i < bin.size(); i += 4) {
    int t = (bin[i] - '0') * 8 + (bin[i+1] - '0') * 4
              + (bin[i+2] - '0') * 2 + (bin[i+3] - '0');

    if (t < 10) {
      c = t + '0';
    } else {
      c = t - 10 + 'A';
    }

    hex = c + hex; // 逆序拼接
  }

  return hex;
}

示例:十六进制转二进制

方法一:预设数组值

string hex_to_binary(string hex) {
  string r[] = {
    "0000", "0001", "0010", "0011",
    "0100", "0101", "0110", "0111",
    "1000", "1001", "1010", "1011",
    "1100", "1101", "1110", "1111"
  };
  string binary = "";
  for (char c : hex) {
    int value;
    if (c >= '0' && c <= '9') {
      value = c - '0';
    } else if (c >= 'A' && c <= 'F') {
      value = c - 'A' + 10;
    } 
    
    // 转换为4位二进制
    binary = binary + r[value];
  }
  
  while (binary[0] == '0') {
    binary.erase(0, 1);
  }
  
  if (binary == "") binary = "0";
  
  return binary;
}

方法一:位运算

string hex_to_binary(string hex) {
  string binary = "";
  for (char c : hex) {
    int value;
    if (c >= '0' && c <= '9') {
      value = c - '0';
    } else if (c >= 'A' && c <= 'F') {
      value = c - 'A' + 10;
    } 
    
    // 转换为4位二进制
    for (int i = 3; i >= 0; i--) {
      binary += ((value >> i) & 1) ? '1' : '0';
    }
  }
  // 去除前导 0 
  while (binary[0] == '0') {
    binary.erase(0, 1);
  }
  
  if (binary == "") binary = "0";
  
  return binary;
}

位运算在后面的章节会学到,这里大家如果不太明白,可以略过,学了位运算后再回过来看。

示例:十六进制转八进制

对于一个非常大的十六进制数,要将其转为八进制,我们的核心思路是:

  1. 十六进制转二进制:每个十六进制字符对应4位二进制。
  2. 二进制补位:将二进制字符串长度补为3的倍数,便于后续转换。
  3. 二进制转八进制:每3位二进制对应1位八进制。
  4. 去除前导零:确保最终结果符合格式要求。

实现代码

#include <bits/stdc++.h>
using namespace std;

int main() {
    string hex;
    cin >> hex;
    
    // 十六进制到二进制转换表(每个字符对应4位二进制)
    const string hexToBin[] = {
        "0000", "0001", "0010", "0011",
        "0100", "0101", "0110", "0111",
        "1000", "1001", "1010", "1011",
        "1100", "1101", "1110", "1111"
    };
    
    string bin;
    for (char c : hex) {
        int val;
        if (isdigit(c)) 
            val = c - '0';
        else 
            val = 10 + (c - 'A');
        bin += hexToBin[val];
    }
    
    // 补前导0使二进制长度为3的倍数
    int len = bin.size();
    int add_zero = (3 - len % 3) % 3;
    bin = string(add_zero, '0') + bin;
    
    // 三位一组转八进制
    string oct;
    for (int i = 0; i < bin.size(); i += 3) {
        string part = bin.substr(i, 3);
        int num = (part[0]-'0')*4 + (part[1]-'0')*2 + (part[2]-'0')*1;
        oct += (num + '0');
    }
    
    // 去除前导0(注意全0情况)
    int first_non_zero = oct.find_first_not_of('0');
    // if (first_non_zero != string::npos) 
    if (first_non_zero != -1) 
        oct = oct.substr(first_non_zero);
    else 
        oct = "0";
    
    cout << oct << endl;
    return 0;
}

提示:这里使用了一个字符串中不常用的方法 find_first_not_of(),这个方法是找出第一个不是 0 的位置。

小结

本章我们主要了解了不同的进制及相互转换,涉及到的主要转换方法包括:

  1. 任意进制转十进制:按权展开法
  2. 十进制转任意进制:短除法(除基取余法)
  3. 特殊进制间直接转换:分组法

掌握进制转换不仅有助于理解计算机工作原理,还能在日常编程中解决许多问题。希望这篇详解能帮助你全面理解进制转换的原理与应用!