相关文章推荐
其实,我们都不懂代码(二)

其实,我们都不懂代码(二)

今天聊一些编程过程中遇到的一些小问题,也是很可能平时被我们忽略的问题。


1·平均值


我在公司的面试题里出了一道题,计算两个int类型数的平均值。几乎所有的面试者的答案就是 return (a + b) / 2;如果你的答案也如此,你也走入了我的陷阱。

当然在平时的功能开发中,大多数时候,上面的代码是符合预期的。但是我们有没有想过一种情况,当a或者b非常大的时候,a + b 很可能是大于int.MaxValue(2147483647)或者小于int.MinValue(-2147483648)的。实际上我出这个题目的目的是让面试者考虑到溢出这个情况。当我们写代码的时候,遇到处理的数字非常大的时候,就需要考虑这个问题了。

好了,那么有什么好办法解决一下呢?我先说下答案然后再简单说明一下吧。



代码非常简洁,可读性稍微差一点。基本思想就是先取出a和b相同的部分,不同的部分除2,然后相加即为平均数。

上面的与运算,就是取出了相同的部分。两个相同的数的平均数,就是其本身,所以,不同的部分除以2,这两部分的和,就为两个数的平均值。

首先 a & b,位的与运算,运算规则是同1为1。所以 a & b 的运算是将 a 和 b 的每位相同的数取了下来。举例说明,假设 a = 6, b = 2,那么 a = 110, b = 010,那么 a & b = 010。a ^ b ,位的异或运算。异或运算以前我发的文章里提到过,本质是半加器,即没有进位的加法运算,所以 a ^ b = 100,即取出了两个数不同的部分。右移操作,是除法运算,右移n位等价于除以2的n次幂,所以 (a ^ b) >> 1 = 010。所以平均值就是 010 + 010 = 100,即十进制的4。

我个人推荐,计算两个数的平均值的时候用这个写法,首先不会有溢出的风险,其次位运算要快啊!


2·绝对值


我们知道,在数学上,非负数的绝对值是它本身,非正数的绝对值是它的相反数。在编程中,求一个数的相反数的时候我们直接用系统的库函数,以C#为例,Math.Abs();可是有没有想过这样一个问题:

Math.Abs(int.MinValue);

这句代码的返回值是多少呢?在C#中,这行代码会抛出OverflowException异常,在Java中,并不会抛出异常,而是返回int.MinValue(-2147483648)。等等,绝对值怎么会返回负数??违背我多年学的数学理论啊!!!

先不要着急,听我给你解释。对于这个问题,我们先了解一个知识点,计算机的原码、 反码和补码。int的位数太长了,这里我们用byte型来讲解。


对于有符号数,二进制的最高位(bit)表示的是符号位。1为负数,0为正数。例如我们表示一个数1,二进制为 0 000 0001。这个二进制编码就是所谓的源码。那么-2的原码呢,就是 1 000 0010。那么我们会发现,十进制的运算式

1 + (-2) = -1

这是没问题的,那么我们用二进制表示算一下,就是

0 000 0001 + 1 000 0010 = 1 000 0011 (原码)

很明显这是不对的, 1 000 0011 表示的是十进制的 -3。这时候我们引入了反码来解决这个问题。

反码规定,正数反码与源码相同,负数符号位不变,其余位按位取反。我们再看下上面的例子:

0 000 0001 + 1 111 1101 = 1 111 1110(反码)

1 111 1110 (反码)表示的是 -1(符号位不变,其余为再取反得到)。此时上面的问题就得到了解决。但是又有一个问题,1 + (-1) = 0 这个式子运算的时候怎么表示呢?

0 000 0001 + 1 111 1110 = 1 111 1111(反码)

这个答案是-0(1111 1111 是反码,原码就是1000 0000)。0的二进制原码表示为 0 000 0000,那么 1 000 0000(-0)同样也是0,那么0就有两个编码了。于是,为了解决这个问题,诞生了补码。


我们规定负数的补码就是对反码加一,而正数的补码不变,正数的原码反码补码是一样的。

我们再用补码计算一下1 + (-1) = 0 这个式子。

0 000 0001 + 1 111 1111 = 1 0 000 0000(补码)

这里我没有写补码的详细表示,直接写的结果,不明白的同学可以通过上面给的规则自己推导一下。这样就正常了,最高位1会被舍去(这里有不明白的同学可以私聊我)。细心的同学会发现上面的式子里,我并没有写 1 - 1 = 0 ,而是写的 1 + (-1) = 0。这样写的目的就是告诉大家,计算机中并没有减法,这也是为什么会设计补码的其中一个原因。

好,我们再回到开始的问题,为什么 Abs(int.MinValue) 的返回值是它本身呢?为什么绝对值还能是负数呢?

我们还是用byte来讲解一下。byte.MinValue 是-128,用补码表示出来是多少?是 1000 0000 ,如果取了绝对值,那么符号位变为0,然后将后面位数取反 0111 1111 ,再加1,变成了 1000 0000 ,又回到了它本身。

对于在C#里,Abs(int.MinValue)会抛异常,但是Java里不会这件事,我只能说各有利弊吧,C#里,如果遇到了Math.Abs(x) 抛异常,那么程序员就会认知到这个Bug,但是如果你写Java写了一句 if(Math.Abs(x) >= 0) ,那么你的代码里其实是有Bug的,一定要注意。

再次说一下,要打破自己的思维定式。1的二进制编码为 0000 0001,-1的二进制编码 不是 1000 0001, 而是 1111 1111。

发布于 2020-03-23 17:11
 
推荐文章