青训营刷题-13

369 阅读14分钟

4.Base32编码和解码

你需要实现一个 Base32 的编码和解码函数。

相比于 Base32,你可能更熟悉 Base64,Base64 是非常常见的用字符串形式表示二进制数据的方式,在邮件附件、Web 中的图片中都有广泛的应用。

Base32 是 Base64 的变种,与 Base64 不同的地方在于 Base64 以 6 bit 为一组作为索引,而 Base32 以 5 bit 为一组作为索引,每一组用一个 ASCII 字符表示。Base 64 总共需要 64 个字符表示,而 Base32 则只需要 32 个字符表示。

Base32 的编码流程如下:

  • 对二进制数据进行预处理:如果二进制数据的 bit 数目不不是 5 的倍数的话,在末尾补 0 直至为 5 的倍数
  • 以 5 bit 为一组进行分组
  • 将每一组的 5 bit 二进制转换为索引(0 - 31)
  • 在索引 - 字符转换表中查询索引对应的字符
  • 根据原始二进制数据的 bit 数目除以 40 后的余数,确定末尾需要补 0 的数目
    • 如果原始二进制数据 bit 数目除以 40 后的余数是 0 的话,不需要补 +
    • 如果原始二进制数据 bit 数目除以 40 后的余数是 8 的话,补 6 个 +
    • 如果原始二进制数据 bit 数目除以 40 后的余数是 16 的话,补 4 个 +
    • 如果原始二进制数据 bit 数目除以 40 后的余数是 24 的话,补 3 个 +
    • 如果原始二进制数据 bit 数目除以 40 后的余数是 32 的话,补 1 个 +

Base32 的索引 - 字符转换表见下方。

索引:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

字符:9 8 7 6 5 4 3 2 1 0 m n b v c x z a s d f g h j k l p o i u y t

样例说明

编码

  • 输入:字符串 foo
  • 字符 f 的 ASCII 编号为 102,字符 o 的 ASCII 编号为 111
  • 将字符串 foo 以 ASCII 编号形式表达为 102 111 111 的序列
  • 将 102 111 111 的序列转换为二进制表示,即 01100110 01101111 01101111,将二进制字符串以 40 个为一组进行分割
  • 最后一组的二进制字符串长度为 24,不是 5 的倍数,因此在最后补 0,直至其能被 5 整除(在此例子中,补 1 个即可),二进制字符串表示为: 01100110 01101111 01101111 0
  • 每 5 bit 为一组,表示为:01100 11001 10111 10110 11110
  • 将每一组转换为十进制的索引,表示为:12 25 23 22 30,对应字符 b l j h y
  • 由于最终输出字符串长度不是 8 的倍数,在输出最后补充 3 个 +
  • 查询索引 - 字符转换表后,可以得出最终的输出为:bljhy+++

Input data: foo
Input in Unicode: 102 111 111
Unicode in binary (8-bit): 01100110 01101111 01101111 0
Unicode in binary (5-bit): 01100 11001 10111 10110 11110
Decimal: 12 25 23 22 30
Pad: + + +
Output: b l j h y + + +

解码

  • 输入:bljhy+++
  • 查询索引 - 字符转换表后,可知原始的二进制数据组为:01100 11001 10111 10110 11110
  • 由末尾 3 个 + 可知在编码时原始二进制数据最后一组的个数为 24 个,由此可知最后一组数据为:01100110 01101111 01101111
  • 将二进制数据转换为 ASCII 编号后可知,原始字符串的 Unicode 序列为:102 111 111
  • 将 Unicode 序列转换为字符串,即可得出原始字符串,为:foo

解题思路

编码过程详解

编码步骤:

  1. 字符到二进制的转换:

    • 将输入字符串中的每个字符转换为其对应的 ASCII 编码值。
    • 将每个 ASCII 编码值转换为 8 位二进制表示,并将所有二进制字符串连接成一个连续的二进制字符串。
  2. 补齐二进制字符串:

    • 检查二进制字符串的长度是否是 5 的倍数。
    • 如果不是,末尾补 0 直到长度为 5 的倍数。
  3. 分组和索引转换:

    • 将二进制字符串每 5 位一组进行分割。
    • 将每组 5 位二进制转换为十进制索引(0-31)。
    • 使用预定义的 Base32 字符映射表,将索引转换为对应的 Base32 字符。
  4. 填充字符 + 的添加:

    • 根据编码后的字符数量对 8 取余,确定需要补充的 + 字符数量,以使最终编码字符串的长度为 8 的倍数。

    • 具体规则如下:

      • 余数为 0,不需要补充。
      • 余数为 1,补充 7 个 +
      • 余数为 2,补充 6 个 +
      • 余数为 3,补充 5 个 +
      • 余数为 4,补充 4 个 +
      • 余数为 5,补充 3 个 +
      • 余数为 6,补充 2 个 +
      • 余数为 7,补充 1 个 +
  5. 生成最终编码字符串:

    • 将 Base32 字符和补充的 + 字符连接,形成最终的编码结果。

关键点:

  • 二进制处理: 使用 StringBuilder 高效地构建和操作二进制字符串,避免频繁的字符串拼接操作。
  • 填充逻辑: 确保填充 + 字符的数量正确,以满足编码长度为 8 的倍数的要求。

解码过程详解

解码步骤:

  1. 去除填充字符 +

    • 从编码字符串的末尾开始,统计连续的 + 字符数量,这些字符仅用于填充。
    • 移除所有末尾的 + 字符,得到实际的 Base32 编码部分。
  2. 将 Base32 字符转换为二进制:

    • 使用预定义的字符到索引的映射表,将每个 Base32 字符转换为对应的 5 位二进制表示。
    • 将所有 5 位二进制字符串连接成一个连续的二进制字符串。
  3. 调整二进制字符串长度:

    • 计算二进制字符串的总长度。
    • 如果长度不是 8 的倍数,末尾多余的 0 位是由于编码时的填充操作,这些需要被移除以恢复原始数据。
  4. 将二进制字符串转换为 ASCII 字符:

    • 将调整后的二进制字符串每 8 位一组进行分割。
    • 将每组 8 位二进制转换为对应的 ASCII 字符,组合成最终的原始字符串。

关键点:

  • 填充字符处理: 确保只移除末尾的 + 字符,不要影响中间的编码数据。
  • 位数调整: 准确地去除因填充而添加的多余 0 位,避免影响解码结果。
  • 异常处理: 对于无效字符(如内部的 + 字符),应抛出异常或进行错误处理,确保解码过程的准确性。

算法实现

import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;

public class Main {
    // Base32 索引 - 字符转换表
    private static final char[] BASE32_CHARS = {
        '9', '8', '7', '6', '5', '4', '3', '2', '1', '0',
        'm', 'n', 'b', 'v', 'c', 'x', 'z', 'a', 's', 'd',
        'f', 'g', 'h', 'j', 'k', 'l', 'p', 'o', 'i', 'u',
        'y', 't'
    };

    // 创建字符到索引的映射表
    private static final Map<Character, Integer> CHAR_TO_INDEX_MAP = new HashMap<>();
    static {
        for (int i = 0; i < BASE32_CHARS.length; i++) {
            CHAR_TO_INDEX_MAP.put(BASE32_CHARS[i], i);
        }
    }                 
    
    // 编码函数
    public static String encodeBase32(String input) {
        // 使用StringBuilder构建二进制字符串,预估容量提高效率
        StringBuilder binaryBuilder = new StringBuilder(input.length() * 8);
        for (char c : input.toCharArray()) {
            // 每个字符转换为8位二进制,并附加到binaryBuilder
            String binaryString = String.format("%8s", Integer.toBinaryString(c)).replace(' ', '0');
            binaryBuilder.append(binaryString);
        }

        String binary = binaryBuilder.toString();
        int originalBitLength = binary.length();

        // 计算需要补齐的位数,使其成为5的倍数
        int paddingBits = (5 - (originalBitLength % 5)) % 5;

        for(int i=0;i<paddingBits;i++) {
            binaryBuilder.append('0');
        }

        // 更新二进制字符串
        binary = binaryBuilder.toString();

        // 预估Base32字符数,减少ArrayList扩容
        int base32Length = (binary.length() + 4) / 5; // 向上取整
        List<Integer> indices = new ArrayList<>(base32Length);

        // 分组并转换为索引
        for (int i = 0; i < binary.length(); i += 5) {
            String group = binary.substring(i, i + 5);
            int index = Integer.parseInt(group, 2);
            indices.add(index);
        }

        // 使用StringBuilder构建编码后的Base32字符串,预估容量
        StringBuilder encodedBuilder = new StringBuilder(base32Length + 8); // 8为最大补齐数

        // 索引转换为Base32字符
        for (int index : indices) {
            if (index >= 0 && index < BASE32_CHARS.length) {
                encodedBuilder.append(BASE32_CHARS[index]);
            } else {
                throw new IllegalArgumentException("Invalid index during encoding: " + index);
            }
        }

        // 确定需要补充的 '+' 数量,使编码长度为8的倍数
        int encodedLength = encodedBuilder.length();
        int padCount = (8 - (encodedLength % 8)) % 8;

        for (int i = 0; i < padCount; i++) {
            encodedBuilder.append('+');
        }


        return encodedBuilder.toString();
    }

    // 解码函数
    public static String decodeBase32(String input) {
        // 1. 去除所有末尾的 '+' 填充字符
        int padCount = 0;
        int length = input.length();
        while (padCount < length && input.charAt(length - 1 - padCount) == '+') {
            padCount++;
        }

        // 移除 '+' 字符
        String base32Str = input.substring(0, length - padCount);

        // 2. 将 Base32 字符转换为二进制字符串
        StringBuilder binaryBuilder = new StringBuilder(base32Str.length() * 5);
        for (char c : base32Str.toCharArray()) {
            Integer index = CHAR_TO_INDEX_MAP.get(c);
            if (index == null) {
                throw new IllegalArgumentException("Invalid character in encoded string: " + c);
            }
            // 每个索引转换为5位二进制,并附加到binaryBuilder
            String binaryString = String.format("%5s", Integer.toBinaryString(index)).replace(' ', '0');
            binaryBuilder.append(binaryString);
        }

        String binary = binaryBuilder.toString();
        
        // 3. 计算总长度并调整为8的倍数
        int totalBitLength = binary.length();
        int excessBits = totalBitLength % 8;

        if (excessBits != 0) {
            // 去除末尾的多余的0
            binary = binary.substring(0, totalBitLength - excessBits);
        }

        // 4. 将二进制字符串每8位转换为ASCII字符
        StringBuilder decodedBuilder = new StringBuilder(binary.length() / 8);
        for (int i = 0; i < binary.length(); i += 8) {
            String byteStr = binary.substring(i, i + 8);
            int ascii = Integer.parseInt(byteStr, 2);
            decodedBuilder.append((char) ascii);
        }

        return decodedBuilder.toString();
    }


    public static String solution(String rawStr, String encodedStr) {
        // Please write your code here
        String encodeResult = encodeBase32(rawStr);
        String decodeResult = decodeBase32(encodedStr);

        return encodeResult + ":" + decodeResult;
    }

    public static void main(String[] args) {
        // You can add more test cases here
        System.out.println(solution("foo", "b0zj5+++").equals("bljhy+++:bar"));
        // System.out.println(solution("The encoding process represents 40-bit groups of input bits as output strings of 8 encoded characters.  Proceeding from left to right, a 40-bit input group is formed by concatenating 5 8bit input groups. These 40 bits are then treated as 8 concatenated 5-bit groups, each of which is translated into a single character in the base 32 alphabet.  When a bit stream is encoded via the base 32 encoding, the bit stream must be presumed to be ordered with the most-significant- bit first. That is, the first bit in the stream will be the high- order bit in the first 8bit byte, the eighth bit will be the low- order bit in the first 8bit byte, and so on.", "bljhy+++b0zj5+++").equals("maf3m164vlahyl60vlds9i6svuahmiod58l3mi6sbglhmodfcbz61b8vb0fj1162c0jjmi6d58jhb160vlk2mu89b0fj1il9b4ls9oogcak2mu89cvp25pncbuls9oo359i79lncbvjh1ln558ahzknsb4aj1lnscbj7917zc0jh3ln4bafhill9bll3yo09vashbu89cajs9id0buf21n89b5z61b8vb0fj1160vlk2mu89bul3yunz58fj3163vul3pln558a2s166vuj33knfbgj37u60vlds9v0928a3su89v4j29unf58dj5oogc8lsi17fv8sj3l093zk79kd0cals9knsbfz21p64vkz21id4b4p3ml89b4ls9c89bvjhiko8cashiknfbgs79v0vb0fj1162c0jjmi6d4zz3mkn6v9z3yla9cuf3sko158fj316fc0zhiiobb4p3ml89v4j21ol9b5z23pncbuh3m166v8zj5kn6casj5160vkz21p6458a37io459ld5168vak3zkn7bgp7i189muf3moa9b5z35pnf58lj1id4b4hs9pnd58shikoxbash116hv4zs9u61bfz35kndbfz63ba9bgj33oo5v4j3cn89caf3m167v4p79iofc0sh7o09vgpj3u89b0ss9i6sbgljmon4bzz21ol9b0ss9oosbasj5ln558ohsu6158p3zl09vgjj3u8vcvfhcod0blfh3kncczhs9kd0czz3bpnscvp7i17fv8zj1160cbh79u61bfz3bpnscvp79kd0czz3soa9caf3m16dcal3mknv58ohso6b58a3m16fv8ss9p60buf7p16xc0s3mia9b0fj1160vkz21p6458d3siddczz6zkd0czz35ynfbfh79u61bfz3mpn2v8p3z167v4p79uo0vah79kd458p3zl09vajjcn09vul31lns58a3su89v4j79u61bfz3bpnscvp79c67v4p79kdlcassk168vls79iox58jhinz+:foobar"));
        System.out.println(solution("The encoding process represents 40-bit groups of input bits as output strings of 8 encoded characters.  Proceeding from left to right, a 40-bit input group is formed by concatenating 5 8bit input groups. These 40 bits are then treated as 8 concatenated 5-bit groups, each of which is translated into a single character in the base 32 alphabet.  When a bit stream is encoded via the base 32 encoding, the bit stream must be presumed to be ordered with the most-significant- bit first. That is, the first bit in the stream will be the high- order bit in the first 8bit byte, the eighth bit will be the low- order bit in the first 8bit byte, and so on.", "bljhykd8c1++++++").equals("maf3m164vlahyl60vlds9i6svuahmiod58l3mi6sbglhmodfcbz61b8vb0fj1162c0jjmi6d58jhb160vlk2mu89b0fj1il9b4ls9oogcak2mu89cvp25pncbuls9oo359i79lncbvjh1ln558ahzknsb4aj1lnscbj7917zc0jh3ln4bafhill9bll3yo09vashbu89cajs9id0buf21n89b5z61b8vb0fj1160vlk2mu89bul3yunz58fj3163vul3pln558a2s166vuj33knfbgj37u60vlds9v0928a3su89v4j29unf58dj5oogc8lsi17fv8sj3l093zk79kd0cals9knsbfz21p64vkz21id4b4p3ml89b4ls9c89bvjhiko8cashiknfbgs79v0vb0fj1162c0jjmi6d4zz3mkn6v9z3yla9cuf3sko158fj316fc0zhiiobb4p3ml89v4j21ol9b5z23pncbuh3m166v8zj5kn6casj5160vkz21p6458a37io459ld5168vak3zkn7bgp7i189muf3moa9b5z35pnf58lj1id4b4hs9pnd58shikoxbash116hv4zs9u61bfz35kndbfz63ba9bgj33oo5v4j3cn89caf3m167v4p79iofc0sh7o09vgpj3u89b0ss9i6sbgljmon4bzz21ol9b0ss9oosbasj5ln558ohsu6158p3zl09vgjj3u8vcvfhcod0blfh3kncczhs9kd0czz3bpnscvp7i17fv8zj1160cbh79u61bfz3bpnscvp79kd0czz3soa9caf3m16dcal3mknv58ohso6b58a3m16fv8ss9p60buf7p16xc0s3mia9b0fj1160vkz21p6458d3siddczz6zkd0czz35ynfbfh79u61bfz3mpn2v8p3z167v4p79uo0vah79kd458p3zl09vajjcn09vul31lns58a3su89v4j79u61bfz3bpnscvp79c67v4p79kdlcassk168vls79iox58jhinz+:foobar"));

    }
}