算法一千零一夜 · 第十五夜 一门别扭的外语

本文以童话笔法讲解十六进制的本质:作为二进制的高效速记法,其根基在于16=2⁴,使四位二进制可唯一映射一个十六进制符号(0–9,a–f)。对比十进制与八进制,阐明十六进制在内存表示、调试查看、权限编码等场景不可替代的原因,并揭示其判据——底数必须是2的整数次幂。文中还厘清数据类型(户口)、字节编址(长街)、补码表示等核心概念。

第十五夜 · 一门别扭的外语

回信来得很快。天真的我读完比特王国的故事,只在信末问了一句。你说王国偏要用一门别扭的外语做门牌,可这种外语,究竟是一种什么文字,又为什么偏偏是它。6 他认识,4 他也认识,可它们并排站在一起,他就不认识了。

问得好。这是一种外语,乍一看也确实别扭。王国里只有一个人把它说得最熟,速记官。这一夜我带你进房卡科,见一见他,再把这门外语的字母表,一枚一枚摆给你看。

速记官的柜台后面挂着全王国的地图。地图上没有山河,只有一条长街。上一夜我说它满街是灯,今天不妨说得更直一些,满街都是字节,一个字节八位,每一位不是 0 就是 1。王国的官方文字,就是这些 0 和 1,每个数字的户口,记的都是它的二进制。这种文字有一个好处,数怎么存,它怎么写,永不失真。它也有一个难处,太长。客人 33554432 走进房卡科,它的二进制写出来足足三十二位,速记官要一个字符一个字符往房卡上抄,卡片再长也不够用。

有人出主意,何必抄这些,直接把数字本身写上去不就行了。33554432,八个字符,写得下。速记官把 33554432 写在柜台上,反问那个出主意的人,你盯着它,告诉我它的二进制哪几位是 1。那人答不上来。十进制是给人看的文字,看一眼,你知道这个数有多大,可你一个 1 也看不见;想从十进制还原二进制,得做一长串的除法和余数,房卡科门口的队要排到长街尽头。

速记官的办法,刻在他那台戳机的把手上,只有一行小字。十六,等于二的四次方。四个二进制位,恰好配一枚戳。

四位,从 0000 到 1111,一共十六种组合,从 0 排到 15。给每一种组合铸一枚戳,前十个借用你认识的数字,0 到 9;后六个借字母表开头六个字母,a、b、c、d、e、f,各自顶一个十到十五。这十六枚戳,就是这门外语的全部字母表。

图 15-1 一门外语的全部字母,十六枚戳

翻译从此变成了抄写。把二进制四个一组切开,每组照表查一枚戳,一枚一枚排下去。100 的二进制是 01100100,切成 0110 和 0100 两组,查表,0110 是 6,0100 是 4,房卡写下 64。一个字节八位,恰好两张卡。你念反了也行,房卡 64,逐枚查回去,6 是 0110,4 是 0100,二进制原样回来。全程不做一道算术。速记官的工作其实是抄写,他甚至不太会心算。

这门外语还有一位老前辈。老前辈用八枚戳,三个二进制位配一枚。王国的老房子年代,一间房不是八位,有十二位一间的,有三十六位一间的,三是都能整除的,老前辈的日子过得很滋润。后来全王国的房子统一成八位一间,八被三除不尽,老前辈的戳怎么也对不上新房的窗户,只好退休。他的字迹至今没有走远。一些老语言里留着他的戳,数字开头带一个 0 的写法,就是他在敲门;门禁系统里也有他,三个二进制位管一扇门,能不能看,能不能改,能不能执行,三位一枚戳,所以你偶尔会在配置文件里遇见 755 这样三个数字。

天真的我,读到这里你大概要替十进制喊冤,它被赶出王国了吗。

没有。它只是不再一个人撑场面了。王国的柜台上有三块屏幕。

第一块是十进制屏。数字有户口的时候,它上岗。所谓户口,是这个数说好了按几个字节读、从哪头读、有没有正负。户口在册,屏幕写 100,你一眼知道这个数有多大,人读着舒服。

第二块是速记屏。数字没有户口的岗位,上岗的是它。把长街原文直接摊开给你看的那个窗口,机器与机器之间往来的信,一行一行排开的字节,这些字节不知道自己该按什么读。十进制屏在这样的位置一声不吭,因为它上岗前必须先押注,押这几个字节是一家,押从哪一头读起,押有没有正负。押对了叫展示,押错了叫说谎。速记屏不用押,一个字节两枚戳,逐格直抄,位是什么它写什么,只摆事实,不做解释。

第三块屏幕最有趣,它证明谁也取代不了谁。街上有一排四间房,二进制是 01100100 后面跟着二十四个 0,房卡写的是 64 00 00 00。给这排房押上整数户口,四间房算一家人,屏幕写 100。只把头一间房请出来,押上字符户口,屏幕写出一个字母 d,字符世界的第 100 号恰好是它。同一个字,两副户口,两种读法,都真。十进制屏只能押一种,押了 100,那个 d 就永远隐身。速记屏呢,把 64 00 00 00 摆在台面上,两种读法全留给看的人。所以程序员的屏幕上总有那么一个窗口,满眼是字母混着数字,不是他们偏爱这门外语,是那个窗口展示的是长街原文,原文没有户口,只有 0 和 1。

图 15-2 同一串字节,三块屏幕

口说无凭,我们把三块屏幕请进同一个程序,让客人 100 和 33554432 各走一遍。

public class Night15ThreeScreens {

    public static void main(String[] args) {
        // 速记官的柜台:同一位客人,三块屏幕各显各的
        int[] guests = {100, 33554432};
        for (int v : guests) {
            System.out.println("guest " + v);
            // 户口在册,十进制屏照常营业
            System.out.println("  decimal screen : " + v);
            // 速记屏,四位一枚戳,逐格直抄,一个字节恰好两枚
            System.out.println("  shorthand card : " + String.format("%08X", v));
            // 二进制,三十二位一位不落
            System.out.println("  binary         : " + String.format("%32s", Integer.toBinaryString(v)).replace(' ', '0'));
        }
        // 头一间房单独请出来,换一副字符户口再读一次
        System.out.println("first room of 100, read as a letter: " + (char) 100);
    }
}

实跑输出如下。

guest 100
  decimal screen : 100
  shorthand card : 00000064
  binary         : 00000000000000000000000001100100
guest 33554432
  decimal screen : 33554432
  shorthand card : 02000000
  binary         : 00000010000000000000000000000000
first room of 100, read as a letter: d

你不必去数那三十二位,速记屏的八个字符替你数了。33554432 的房卡 02000000,八枚戳里只有一枚 2,其余七枚全是 0,客人的全部家当一目了然。这枚 2 藏在哪,十进制屏的 33554432 里半点也看不出来,速记屏一眼就给你。

信写到这里,天真的我又追了一句,FF 是什么,255 还是 −1。

速记官摊手,两可。他只管记号,不管读法。同一个字节 FF,户口上写着不分正负,八位全是家当,读 255;户口上写着分正负,另有一套读法,读 −1。这场官司从头到尾是户口的,跟速记官无关,他连笔都不用换。

不过他确实有一手别人比不了的本事。FF FF FF FF,四间房的房卡一字排开,读一间是 FF,读两间是 FF FF,读四间还是 FF FF FF FF,一格一格的读法全摆在明面上,想看哪层看哪层。换成十进制 4294967295,你只看得见四间房的总数,低处每间房里住着什么,从数字里半点也看不出来。懂行的人还能多看一眼,分正负的 −128,写在卡上是 FFFFFF80,六枚 F 顶着二十四个 1,那是补齐的符号,最后两枚戳 80,才是 −128 的本尊。这些层次,十进制屏统统不给。

最后的考题来了。天真的我问,既然十六行,那二十为什么不行,二十不是更大吗,更大不是更省字符吗。

速记官的考题从来只考一样。把候选的底写成二进制,数一数 1 的个数。

十六,10000,一个 1。十,1010,两个 1。十二,1100,也是两个 1。区别就在这里。底若只有一个 1,四个一组的刀切下去,刀刀落在空隙上,组组整齐;底若有两个 1,怎么切都错位。二十的二进制是 10100,两个 1,一样切不齐。字符省得再多,戳对不上位,这门外语就只是乱码。

这一刀,机器一句话就能试出来。

public class Night15SingleOne {

    public static void main(String[] args) {
        // 速记官的考题:二进制里只有一个 1 的数,才配当这门外语的底
        int[] candidates = {16, 10, 12, 32};
        for (int n : candidates) {
            // n 与 n-1 相与,恰好消去最右边的一个 1
            // 消一轮就全零,说明原本只有这一个 1;否则二进制里还有别的 1
            boolean fit = (n & (n - 1)) == 0;
            System.out.println(n + "  binary " + Integer.toBinaryString(n) + "  fit " + fit);
        }
    }
}

实跑输出如下。

16  binary 10000  fit true
10  binary 1010  fit false
12  binary 1100  fit false
32  binary 100000  fit true

十六和三十二过闸,十和十二留在门外。判据简单到不像学问,可它就是这门外语的国籍法。

信的最后,天真的我终于问出了我最想让他问的那句。那我们的幻方呢,它也住在这条街上。

住着呢。幻方进城也要验档,验的也是 0 和 1,只是验法另有一套。速记官看整个数,幻方看二进制的末尾。把阶数写成二进制,数一数末尾连着几个 0。3、5、7 写成二进制,末尾连一个 0 都没有,这是一档,奇数阶。10 的二进制是 1010,末尾连着一个 0,这是第二档。4、8、16,末尾至少连着两个 0,这是第三档,双偶阶。你还记得第十三夜那部 289 行的手稿,奇数阶一套步法走遍,双偶阶对称倒放就成,唯独第二档动用四分块拼装,篇幅最长,那是瑛姑没考的题。现在你知道它为什么难了,末尾恰好连一个 0 的阶数,正是幻方里最难伺候的客人。

速记官在旁边听完,补了一句他的版本。他的考题更苛刻,不看末尾,看整个数的二进制里是不是只有一个 1。10 在幻方籍上是第二档的正牌居民,到他这儿过不了闸,1010,两个 1。16 更妙,它在幻方籍上住在第三档,跟 4 和 8 是街坊,到他这儿却是上宾,10000,全数只此一个 1。同一个数,两本户籍,量的都是 0 和 1,尺子不一样。

所以这一夜的判词只有一句。一个数能不能当这门外语的底,不看它在幻方籍上住哪一档,只看它的二进制里是不是只有一个 1。十进制没有不好,它只是在 0 和 1 的世界里,永远切不齐。

幻方旅行团还睡在长街上,房卡 08、01、06,一字排开,你现在读得懂了。这门外语一点也不别扭,它只是不肯拣好听的说。学会它,不是为了跟机器套近乎,是为了有一天,能亲眼看见数字睡着的样子。


① 真伪核对 逐条照实登记。

 Ⅰ 十六记号 十六进制使用 0 至 9 与 a 至 f 共十六个符号,a 至 f 依次表示十至十五;一枚戳对应四位二进制,一个字节(8 位)恰用两枚戳(真,通行记数规范)。

 Ⅱ 八进制与老房子 PDP-8 字长 12 位,分四组、每组 3 位,以八进制记地址与数据,前面板指示灯三位一组(真,已联网核查);同期机器字长 12、18、24、36 位均能被 3 整除,八进制与之相配(真,已联网核查);字节统一为 8 位后,8 不能被 3 整除,八进制错位出局,十六进制一字节两枚戳接位(真,算术)。C 语言保留八进制字面量(前导 0,如 0123)与八进制转义(如 \121),是老前辈的化石(真,语言规范)。

 Ⅲ 门禁里的八进制 Unix 文件权限每 3 位一组(读、写、执行),一枚八进制数字记一组,755 的 7 是三位全 1(能读、能写、能执行),5 是两个 1(能读、能执行)(真)。

 Ⅳ 三块屏幕 调试器对已声明的变量默认按十进制显示;内存窗口与十六进制查看器不问类型、按字节以十六进制显示(真,通行行为)。字符世界第 100 号为 d(真,第十四夜核对在案,ASCII)。

 Ⅴ 同一字节两读 一字节 FF,按无符号读 255,按 8 位补码有符号读 −1;32 位有符号 −128 写作 FFFFFF80,前六枚 F 为 24 位符号扩展(真,算术与补码定义)。

 Ⅵ 判据 n 与 n−1 按位与为零,当且仅当 n 是 2 的整数次幂且 n 大于 0(真,实跑验证)。用数学话说,速记官的国籍法是底的质因数必须只有 2,$10 = 2 \times 5$,坏在那个 5;幻方验档数的是阶数里因子 2 的含量,恰为二进制末尾连着的 0 的个数(真,算术)。

 Ⅶ 幻方三分类 奇数阶、单偶阶($4k+2$)、双偶阶($4k$)三档完备;4、8、16 为正牌幻方阶,居双偶档;二阶幻方无解,是唯一缺席的 2 的幂(真,第十三夜勘误在案)。第十三夜手稿 289 行、单偶阶段篇幅最长(真,实跑在案)。

 Ⅷ 演示件 Night15ThreeScreens 与 Night15SingleOne 实跑输出与文中一致(真,JDK 21 实跑验证)。

② 术语对照卡 家常名与学名对照,定义不含比喻。这些家常名只是记号,机器里没有官员,也没有纸卡,只有约定。

 Ⅰ 长街,学名内存。按字节编址的存储序列,每字节 8 位。

 Ⅱ 戳与速记,学名十六进制。以 16 为基数的记数法,使用 0 至 9 与 a 至 f,每一位对应二进制四位。

 Ⅲ 外语的底,学名基数。记数法使用的符号个数,有 2、8、10、16 等,几进制即以几为基数。

 Ⅳ 户口,学名数据类型。约定一段数据按几个字节一组、从哪一端开始读、是否带符号的规则。

 Ⅴ 位与字节。位是二进制的一个数位,非 0 即 1;字节是 8 位一组,是内存的最小编址单位。

③ 今夜习题 三道,评论区见。

 Ⅰ 2026 的房卡。上一夜的习题你写过它的二进制,这一夜给它办房卡。四位一组切二进制,写出速记写法,再逐枚戳对回四位,自查一遍。

 Ⅱ 只有一个 1 的国籍法。64、6、28、96,写出各自二进制,数一数 1 的个数,谁够格当这门外语的底。

 Ⅲ FF 的两副面孔。一个字节的 FF,按不分正负读是 255,按分正负读是 −1。这场官司该找谁判,速记官管不管,说出你的理由。


本书目录导航 上一篇:第十四夜 幻方与奇妙的二进制 下一篇:第十六夜 十只白鼠 起点:卷首语·算法童话


版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值