不用中间变量交换两个数,各种骚操作,一次玩个够

3,836 阅读6分钟

前言

近来看见一些群里面多次出现讨论不用中间变量交换两个数怎么做。虽然这是很古老的问题,大家懂的也懂。但实际上,方法会有很多种,我们一起来看看

何为不用中间变量

如果使用中间变量,那就是这样

var a = 1;
var b = 2;
var temp = b;
b = a;
a = temp;

如果是不用中间变量,那就是无var temp来辅助这个过程。刷算法题的时候,我们通常会看见一些题目有"不借助额外空间"的要求,实际上,不用中间变量 <=> 不借助额外空间 <=> 原地修改。那么也很明显,解决问题的根本方向就是,要把两个变量的信息同时存到一个现有变量上,再实现交换

基于四则运算

相信很多人很快可以写出这个:

var a = 1;
var b = 2;
a = a + b
b = a - b
a = a - b

我们可以看见,a = a + b这句就让a同时把两个变量的信息都存到a上面去了;第二句b = a - b,因为a是a、b两者原本值和,而b交换后结果是a原本的值,那么交换后的b,就是两者原本的值的和减去原本的b。最后一句,既然知道了b,那么a也可以知道了,减一下

举一反三

第一次先相减

var a = 1;
var b = 2;
a = a - b
b = a + b
a = b - a
console.log(a, b)

第一次先相除

// 注意:a、b不能有一个是0
var a = 1;
var b = 2;
a = a / b
b = a * b
a = b / a
console.log(a, b)

第一次先相乘

// 注意:a、b不能有一个是0
var a = 1;
var b = 2;
a = a * b
b = a / b
a = a / b
console.log(a, b)

基于位运算

一个很经典的答案:

var a = 1;
var b = 2;
a ^= b
b ^= a
a ^= b
console.log(a, b)

因为是位运算,我们直接来看看二进制数字的异或运算,看一下如何实现交换

a: 0011  b: 1001
a = a ^ b: 0001
b ^= a => a ^ b ^ b: 0001 ^ 1001 => 0011(等于旧的a)
a ^= b => 0011 ^ 0001 => 1001(等于旧的b)

这就实现交换了a、b。其实也是把ab同时存到一个a变量里面(异或形式),然后反过来分别获取

然而,用类似的方法使用&和|却不能实现。为什么呢?我们观察前面的例子,都是这样的规律:三行代码,第一行让a同时存储两个数(某种运算结果), 第二行使用逆运算从a中拿到旧的a(让新的b等于旧的a),第三行再使用逆运算从a中拿到旧的b(新的a等于旧的b)

如果使用&,那么第一行就是a & b的结果,第二行应该是逆运算|获得旧的a,获得旧的a需要旧a & 旧b | 旧a,但执行第一行后旧a已经不存在了,所以行不通

所以,这里娱乐一下,用~玩玩,其实也就装饰一下前面的四则运算

var a = 1;
var b = 2;
a = ~a + b;
b = ~(a - b);
a = b - ~a;
console.log(a, b)

要求放低一些(娱乐)

如果我们可以把“不用中间变量交换两个数”视为:没看见新定义一个变量的过程(不管执行过程是什么,我只关心我代码有没有写了一句定义一个新的辅助变量的语句),那么就有一些js语法方向的玩法了

基于数组解构

var a = 1;
var b = 2;
[a, b] = [b, a];
console.log(a, b)

// babel编译的结果是
var _a;
var a = 1;
var b = 2;
_a = [b, a], a = _a[0], b = _a[1];

// 或者先赋值给a
var a = 1;
var b = 2;
a = [a, b]
b = a[0]
a = a[1]

基于对象解构

var a = 1;
var b = 2;
var { a: b, b: a } = { a, b }
// 或者
var { a, b } = { a: b, b: a }

console.log(a, b)

// babel结果
var a = 1;
var b = 2;
var _a = { a: a, b: b }, b = _a.a, a = _a.b;

// 还是可以先赋值给a
var a = 1;
var b = 2;
a = { a, b }
b = a.a
a = a.b
console.log(a, b)

如果说,没看见新定义一个变量的过程都算的话,那就可以母猪上树

// 刚刚说的 & | 不行,现在也算了
var [a, b] = [a & b | b, a & b | a]

var [a, b] = [{ [a]: b, [b]: a }[a], { [a]: b, [b]: a }[b]]

var [a, b] = [a << b >> a, a << b >> b]

其他api一起上

还是一样的套路,先想办法让a同时存储两个数,后面再反过来找值。我们来试一下其他api

var a = 1;
var b = 2;
a = `${b}-${a}`;
b = +a.split('-')[1];
a = parseFloat(a);
console.log(a, b)

基于match

var a = 1;
var b = 2;
a = `${b}-${a}`.match(/(\d+).*?(\d+)/);
b = +a[2]
a = +a[1]
console.log(a, b)

字符串个数

var a = 1;
var b = 2;
a = `${'b'.repeat(b)}${'a'.repeat(a)}`;
b = /a+/.exec(a)[0].length
a = /b+/.exec(a)[0].length
console.log(a, b)

套路玩不完,看大家的想象力了......

基于高级数学运算

指数 => 对数

我们知道指数和对数的关系是这样的:如果c = a ** b, 那么b = loga(c)

又有对数换底公式:loga(b) = lnb / lna = log2(b) / log2(a) = logn(b) / logn(a)

所以我们可以这样子交换两个数:

// a、b大于0
var a = 1;
var b = 2;
a = b ** a;
b = Math.log2(a) / Math.log2(b)
a = a ** (1 / b) // 最后一步就是开方: a的b次开根号
console.log(a, b)

三角函数

基于四则运算,包多一层三角函数

// 局限性很大,因为弧度超过两个PI就是一个周期导致不准确
// 而且sin的单调性区间拐点是k * PI / 2,可能结果存在预期之外
// 如果a+b>Math.PI / 2,需要分很多情况讨论,这里简单起见取小一点
// 最终结果并不是彻底准确,但我们可以在误差为EPSILON范围内可视为相等
var a = Math.PI / 4;
var b = Math.PI / 6;

a = Math.sin(a + b)
b = Math.asin(a) - b;
a = Math.asin(a) - b;
console.log(Math.abs(Math.PI / 6 - a) < Number.EPSILON)
console.log(Math.abs(Math.PI / 4 - b) < Number.EPSILON)

同样的,cos、tan、cot,道理都一样

双曲函数

类似三角函数的套路,再来一波。但要求就没那么苛刻了,随意

var a = 2;
var b = 3;
// 双曲正弦
a = Math.sinh(a + b)
b = Math.asinh(a) - b;
a = Math.asinh(a) - b;
console.log(a, b)

// 双曲余弦
a = Math.cosh(a + b)
b = Math.acosh(a) - b;
a = Math.acosh(a) - b;
console.log(a, b)

ln和e

再来一个轻松一些的玩法

var a = 1;
var b = 2;
a = Math.E ** (a + b);
// js的ln就是Math.log
b = Math.log(a) - b;
a = Math.log(a) - b;
console.log(a, b);
var a = 1;
var b = 2;
a = Math.E ** (a - b);
b = Math.log(a) + b;
a = b - Math.log(a);
console.log(a, b);

最后

研究骚操作,其实没什么意义......但是,我又带着大家复习/学习了一遍基本语法、api和数学基础了喔

路过的如果有其他的骚操作也可以分享一波~

关注公众号《不一样的前端》,以不一样的视角学习前端,快速成长,一起把玩最新的技术、探索各种黑科技