两道面试题(阿里和腾讯)直接劝退同事!

2,153 阅读6分钟

前言

一个认识同事去面大厂,我问他面了什么,他说印象深刻的有两道题,其实都是网上的题,但是因为人家懂原理,就可以继续深挖,我们先看题:

原问题如下:

  • 跟浮点数相关:阿里面试官说都知道0.1 + 0.2 不等于 0.3,那为啥 0.1 + 0.1 等于 0.2,你能说说其中的原理吗?(他心里想,我只知道 0.1 + 0.2 不等于 0.3。。。咋办。。。)
  • 跟字符编码相关:腾讯面试官说为什么通常一个汉字例如
"你".length //  等于1

"😂".length //  等于2

那么 length 指的到底是什么长度?(他当时心里想,什么鬼啊,长度不一样我哪知道!)

我就从这两个题出发,把问题拆解,形成了如下文章,同时,也更到到自己的github上的前端学习后端知识体系系列,有兴趣的朋友可以加我微信一起进群交流

一个中文占多少字节?Unicode 跟编码有什么关系?js 是什么编码?(北京阿里)

一个中文占多少字节?

一个中文字符占用多少字节跟编码密切相关,不能直接说一个中文占 2 个或者 3 个字节,比如 UTF-8 编码下一个中文字符通常占用 3 个字节。

Unicode 跟编码有什么关系?

Unicode 是一种字符集,它定义了每个字符对应的唯一编号,但是并没有规定如何存储这些字符。编码则是将字符集中的字符转换为字节序列的方法。例如:utf16 可以用两个字节或四个字节来表示一个字符,你也可以设计一种编码 Unicode 的方法,例如统一用 4 个字节表示一个字符。

js 是什么编码?

在 JavaScript 中,通常使用 UTF-16 编码。而我们的网页通常是 UTF-8 编码,所以使用 javascript 内部的字符串方法时,实际上内部会做一个转换,是以 utf-16 为准。 例如:

"😂".length; // 2

因为通常一个汉字的 length 属性是 1,因为在 unicode 字符集中的数字大小,只需要 utf-16 用两个个字节保存即可。有些数字较大,超过了两个字节表示的范围,就需要 4 个字节表示。

再介绍一个概念,叫码元(code unit),是指存储字符的最小单位,这里即 2 个字节。所以 length 就是码元的个数,上面的表情符号用了 4 个字节,所以是 2 个码元,所以 length 是 2。

为什么 0.1 + 0.2 不等于 0.3?为什么 0.1 + 0.1 等于 0.2 ?请结合 IEEE 标准来说,如何避免这种计算误差 (深圳腾讯)

在 IEEE 754 标准中,浮点数的表示是有限的,而 0.1 和 0.2 在二进制下是无限循环小数。为什么是无限循环呢,这就要了解 10 进制小数如何转换为 2 进制小数了,方法如下:

十进制小数转为 n 进制

我们以 2 进制为例,方式是采用“乘 2 取整,顺序排列”法。具体做法是:

  • 用 2 乘十进制小数,可以得到积,将积的整数部分取出
  • 再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出
  • 如此进行,直到积中的小数部分为零,或者达到所要求的精度为止

我们举个例子:

如: 十进制 0.25 转为二进制

  • 0.25 * 2 = 0.5 取出整数部分:0
  • 0.5 * 2 = 1.0 取出整数部分 1

即十进制0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位)

此时我们可以试试十进制0.10.2如何转为二进制,就知道为啥 0.1 + 0.2 不等于 0.3 了

0.1(十进制) = 0.0001100110011001(二进制)
十进制数0.1转二进制计算过程:
0.1*20.2……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*21.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*21.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*20.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*20.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.8*21.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
……
……
所以,得到的整数依次是:“0”,“0”,“0”,“1”,“1”,“0”,“0”,“1”,“1”,“0”,“0”,“1”……。
由此,大家肯定能看出来,整数部分出现了无限循环。

这时,面试官又问我,既然 0.1 的 10 进制不能精确转换为 2 进制,为什么 0.1 + 0.1 == 0.2 是 true ?

为什么 0.1 + 0.1 == 0.2 是 true

因为 64 位浮点数,小数部分最多展示 16 位,因为在 IEEE 754 标准中的 64 位浮点数的小数部分,最多有 53 位, 2 的 53 次方就是 16 位数字,所以小数部分最多展示 16 位。

如下:

(0.1).toPrecision(16);
"0.1000000000000000"(0.1).toPrecision(17);
("0.10000000000000001");

所以 0.1 + 0.1 正好 16 位(四舍五入)截断,等于 0.2。

如何避免

避免方法可以使用将数字转化为字符串,然后模拟加法运算,一些库就是这样实现的,所以更建议使用成熟的第三方库,当然,这种模拟运算性能并不好(相比于支持 decimal 类型的语言)。可以完全避免出现误差。(千万别说可以用乘法转换为整数做运算,也一样会有误差)。

补码有什么用?(上海字节)

使用补码表示法,可以将减法转化为加法,这样加法和减法都可以在加法器上进行运算,简化了计算机的设计和实现。

例如 2 - 4 可以换算为 2 + (-4)。

在补码表示法中,正数的补码与其二进制表示形式相同。负数的补码由对应正数的补码按位取反(即 0 变为 1,1 变为 0),然后再加 1 得到。

基本原理,我们拿时钟举例:

在时钟上,时针加上(正拨)12 的整数位或减去(反拨)12 的整数位,时针的位置不变。14 点钟在舍去模 12 后,成为(下午)2 点钟(14=14-12=2)。从 0 点出发逆时针拨 10 格即减去 10 小时,也可看成从 0 点出发顺时针拨 2 格(加上 2 小时),即 2 点(0-10=-10=-10+12=2)。因此,在模 12 的前提下,-10 可映射为+2。

举例: 在 JavaScript 中,可以使用按位非(~)运算符来实现取反操作,使用按位与(&)运算符来实现加 1 操作。以下是一个实现减法运算的例子:

function subtract(a, b) {
  b = ~b + 1;
  return a + b;
}