JavaScript如何实现加法?

 

这篇文章是我在LeetCode刷题时写的一篇题解,

因为我的解题思路非常独特,网上完全没看到过类似的实现,所以专门发上CSDN

其中有种解法,可以不用任何算术运算符,位运算符或Math对象实现整数加法

 

题目要求

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:

输入: a = 1, b = 1
输出: 2

代码模板:

/**
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
var add = function(a, b) {

}

输入输出限制:

a, b 均可能是负数或 0
结果不会溢出 32 位整数

 

常规解法

 

1.偏要做加法

既然要实现整数相加,为什么不用原生加法呢?

虽然题目要求我们不能在函数体内使用加法,但是我们可以反其道而行之,直接返回两数相加的结果:

var add = function(a, b) {
  return a + b
}

执行用时 / 内存消耗 超过 50% / 100% 的用户

照理说使用原生加法应该是效率最高的解法,但是可能判题系统有分析函数源码并特地降低成绩,因此成绩不好

 

2.普通的代码混淆法

既然题目中不能出现 “+”、“-”、“*”、“/” 四则运算符号,

那么我们可以用编码的方式,将四则运算符号编码成判题系统不能识别出的字符串,

然后再利用JavaScript动态语言的特性,将解码后的符号放入代码内,然后执行代码:

var add = function(a, b) {

  //return eval([a,b].join(decodeURIComponent('%2b')))

  return Function('a','b',`return a${String.fromCharCode(43)}b`)(a,b)

}

执行用时 / 内存消耗 超过 100% / 100% 的用户

可以明显的看到,尽管我们兜了个圈子,但执行用时和内存消耗都大幅减小,说明判题系统很有可能做了代码识别的操作

上下两种方法可以任选其一。虽然通常来说出于性能和安全性考虑要少用eval执行代码,但是它在本题中效率不错

 

3.不错的递归位运算解法

先讲讲加法的二进制原理

学过计算机组成原理的都知道,计算机硬件里的半加器是通过异或逻辑门(XOR gate)和与逻辑门(AND gate)实现的,

放几张计算机科学速成课里的截图:

从异或门的真值表可以看出,两个二进制位经过异或得出的结果位,看起来很像是执行了二进制加法,

即0+0=0,0+1=1,1+0=0,1+1=10

当XOR门的两个输入都是1时,输出为0,但我们想实现的二进制加法的结果应该是10,少了前面的一个进位1,

因此我们需要在我们的XOR门旁边加上一个AND门,只有当两个输入都是1时,输出1,表示进位:

这样我们就能造出了一个半加器,能够实现两个二进制位的加法。

但是我们的这个半加器现在还不够用,因为我们的半加器只能处理当前位的加法,而不能接收之前位的加法结果的进位

(即只能处理一位加法,不能处理多位加法)

 

举个例子,我们有两个整数A和B,有个半加器负责处理第0位的加法A0+B0,还有个半加器负责处理A1+B1,

那么显然,处理A1+B1的半加器只有两个输入位,还缺一个输入位处理上一位的进位,因此我们需要引入全加器

全加器负责将这一位的sum和上一位的carry加起来,再将这一位算出的carry和前面算出的carry进行或(OR)运算,

就能得出新的carry:

(图中的A,B指两个二进制位的输入,C,S分别表示Carry In进位和Sum和)

然后将一个半加器(第0位没有进位输入)和七个全加器(后面的位都有可能需要进位)连起来,就是一个八位的二进制加法器

那么讲这么久,对我们的解题有什么帮助呢?

我们知道,任何数字在(现代)计算机中都是以二进制形式进行传输和处理的,

我们的加法函数,在二进制视角上,就是在对整数a和b的每个二进制位都调用一次二进制加法,

将对应二进制位相加的结果,再加上上一个位的进位,

然后再将这个位的进位传入下一个位的加法过程中,再调用二进制加法,再将进位传入下一个。。。直到没有进位

那么我们怎么用JavaScript代码实现呢?

 

首先我们看看JavaScript语言中XOR,AND,和左移位运算符的使用(直接抄MDN的描述)

^(按位异或):对于每一个比特位,当两个操作数相应的比特位有且只有一个1时,结果为1,否则为0。

&(按位与):对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。

<<(按位左移):将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充

提示1:在JavaScript中,位运算符将其操作数当成32位有符号整数看待

提示2:按位,指的是对于32位整数的每一位都执行相同的操作

 

那么我们可以发现,对于我们题目要求输入的两个32位整数a,b而言,

 

a ^ b ,相当于对所有的位求对应的结果位,将所有位的值都变成对应位相加所得的 sum 值,

a & b,相当于对所有的位求对应的进位,将所有位的值变成对应位相加所得的 carry in 值,

 

我们在半加器中可以看到,进位和结果位并不在同一位置上,

进位因为需要加到下一位上,所以在二进制表示上要比结果位更前,也就是需要左移一位,

所以我们的进位(a & b)在二进制中需要表示成(a & b)<< 1,

那么我们可以很容易想到:a + b = 对应位所有的 sum + 对应位所有的 carry in,即:

 

a + b = ( a ^ b ) + ( ( a & b ) << 1 )

 

注意在JavaScript中左移<<的优先级要低于加法+,为了只让进位左移,我们需要在右边的进位外再套一个括号

上面的这个公式看起来很完美,但我们这题用不了,因为我们的题目要求是,不能在代码内使用“+”符号,

因此我们必须消灭等号右侧的加号,但这似乎又陷入了一个死胡同:

 

有什么办法能够不用加法,将所有的进位和所有的结果位相加呢?

 

答案是:将进位和结果位任意作为a,b,放进之前的 a ^ b 和 ( ( a & b ) << 1 ) 里再算多次,直到进位为0

 

我们先说明,我们没有假定a,b谁是进位,谁是结果位,

只要代码逻辑符合上述所说,那么不管进位和结果位有没有互换位置,最终结果都是一样的

 

为什么答案是这样的呢?思路其实非常巧妙,读一读下面的步骤你就懂了:

1.输入两个数 a,b

2.计算两个数相加得到的进位和结果位

3.需要将进位和结果位相加

4.输入两个数进位,结果位

5.计算进位和结果位相加得到的进位①和结果位①

6.需要将进位①和结果位①相加

......

n-1.输入两个数进位m和结果m

n.没有进位,直接返回结果位

 

只要你读明白了,代码就能轻松写出来了,

我们可以使用递归的方式实现,也可以使用交换值的方式实现,我们先给出递归形式的解法:

var add = function(a,b){
  return b == 0 ? a : add( a ^ b, (a & b) << 1 )
}

执行用时 / 内存消耗 超过 70% / 100% 的用户

JavaScript使用64位浮点数储存数字,由于位运算会将操作数强制转换为32位有符号整数,

所以不同于其他语言,在JavaScript中使用位运算会有部分性能损失

 

4.不错的循环位运算解法

有了上面的解释,相信你也不难想到循环形式的解法,这种解法可能看起来会更好懂一些:

var add = function(a,b){

  while(b!=0){
    [a,b] = [ a ^ b, (a & b) << 1 ]
  }

  return a
}

执行用时 / 内存消耗 超过 70% / 100% 的用户

这里主要使用了 ES6 里的解构赋值,来避免使用临时变量,结构上看起来更清晰一些

 

非常规解法

 

5.不用任何算术运算符,位运算符或Math对象实现整数加法

这真的可能吗?不用之前提到过的任何方法,连位运算符和Math对象也不用?当然可以。

这里的算术运算符指的是:(来自百度百科)

+(加号) 加法运算 (3+3)

–(减号) 减法运算 (3–1) 负 (–1)

*(星号) 乘法运算 (3*3)

/(正斜线) 除法运算 (3/3)

%(百分号) 求余运算10%3=1 (10/3=3·······1)

^(乘方) 乘幂运算 (3^2)  (这个符号在这里不是我们之前说的异或)

! (阶乘) 连续乘法 (3!=3*2*1=6)

|X| x为任何数 (绝对值) 求正 (|1|)

位运算符指:(还是来自百度百科)

& 按位与

| 按位或

^ 按位异或

~取反

<<左移

>>右移(包括逻辑右移和算术右移)

 

废话不多说,直接上代码。

var add = function(a,b){

  return (function(a,b){

    if(a==0 || b == 0){
        return a || b
    }

    function negative(num){//将正数变为负数
        return Number([ [].indexOf('wth').toString()[0],num ].join(''))
    }

    function abs(num){//取绝对值
        return num >= 0 ? num : Number( num.toString().slice(1) )
    }

    if( a>0 && b>0 ){ //正数相加
        return Array( abs(a) ).concat( Array(abs(b)) ).length
    }
    if( a<0 && b<0 ){ //负数相加
        return negative( Array(abs(a)).concat(Array(abs(b))).length )
    }
    if( a > b ){

        if( abs(a) > abs(b) ){ //大正数+小负数
            let t = Array(a)
            t.splice(b)
            return t.length
        }
        else if( abs(a) < abs(b) ){ //小正数+大负数,即大负数绝对值-小正数取反
            let t = Array(abs(b))
            let tmp = t.splice(a)
            return negative(tmp.length)
        }

    }
    
    else{//提示:在严格模式下无法在匿名函数内调用自身
        return arguments.callee(b,a)
    }

  })(a,b)

}

你看懂了吗?

 

先说明一下,有的人可能会问,你不是说不用绝对值吗?为什么还有个abs函数?

绝对值的计算是非常必要的,如果没有绝对值,就无法比较两数距离原点0的距离,那么就无法处理两数在0两侧的情况,

JavaScript本来也没有原生计算绝对值的运算符,如果你觉得绝对值函数不对劲,你把它当成防抱死函数不就好了嘛

 

接下来我解释一下我的思路。

 

1.在JavaScript内如果不用任何算术运算符,位运算符或Math对象来实现整数加法,就只可能通过原生数据结构来实现

2.再读一遍题,整数加法,输入可以是正数,负数或者0,数据结构本身必须可变,而且能映射成数值

3.在JavaScript中算上 Symbol 只有七种基本数据类型,只有数组 Array 类型满足要求

4.数组可以合并,模拟正数相加,可以切片,模拟正数作差,但最关键的是:这两个数组操作无法产生负值

5.既要产生负数,代码里又不能出现负号(题目要求),就只能通过数组索引获得负数

6.由于代码内不能出现加号(题目要求),因此只能通过数组的join方法实现字符串拼接

7.a和b如果是一正一负,那么理论上需要处理4种情况,浪费时间而且没有必要

8.为了减少代码量,将一正一负的相反的情况(一负一正)的a,b换位再传入自身参数中,问题解决

 

思路是不是很巧妙呢?

 

这种方法缺陷显而易见,需要频繁分配大量内存空间,性能极差,

尽管如此,这玩意还是能在Leetcode上顺利提交通过的,是不是很神奇?

 

如果你喜欢我这篇文章,点个赞吧!原创不易,求多支持!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值