算法通关村第十一关——位运算实现加减乘除

在计算机中,位运算的效率比加减乘除效率更高。

1.位运算实现加法

力扣371题, 给你两个整数 ab ,不使用 运算符 + -,计算并返回两整数之和。

分析:不让用运算符,就只能使用位运算。先来看一下两位二进制相加的情况:

0 + 0 = 0
0 + 1 = 0
1 + 0 = 0
1 + 1 = 1 (此处发生了进位,结果应该为 10,可以用 1 << 1 来表示)

两数相加时,我们需要考虑的是进位部分的结果和不进位部分的结果。由上面的例子可以知道,对于ab两个数不进位部分的情况是:相同为0,不相同为1,也就是a ⊕ b。对于进位,其结果就是a & b也就是两位只有都是1的时候才会进位,进位发生的时候数的位数就变成两位了,我们只需要将其左移一位即可,也就是(a & b) << 1。综上得出两条结论:

  1. 不进位部分,计算就是a ⊕ b
  2. 对于进位部分,就是(a & b) << 1

代码如下:

function getSum(a, b) {
	while (b !== 0) {
		// 进位处运算就是 a & b。如果进位了,就左移一位
		let sign = (a & b) << 1;
		// 不进位处按异或进行位运算
		a = a ^ b;
		b = sign;
	}
	return a;
}

2.位运算实现乘法

力扣面试题08.05,递归乘法。 写一个递归函数,不使用*运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。

加入两个正整数分别是A B

分析:

  1. 首先,检查基本情况:如果乘数 B 等于0,那么乘积一定为0,所以返回0。
  2. 接下来,判断乘数 B 的二进制最低位是否为1(奇数检查),可以使用位运算 B & 1 来判断。如果最低位为1,说明 B 是奇数,进入第3步,否则进入第4步。
  3. 如果 B 是奇数,先将 A 加到结果中,然后通过递归计算 A * (B >> 1),这里的 B >> 1 表示将 B 右移一位,相当于除以2。递归结果再左移一位,相当于乘以2,然后将两者相加,即 A + (A * (B >> 1) * 2)
  4. 如果 B 是偶数,直接通过递归计算 A * (B >> 1),然后结果左移一位,即 A * (B >> 1) * 2

代码如下:

/**
 * 递归乘法:计算两个数的乘积
 * @param {number} A
 * @param {number} B
 * @return {number}
 */
var multiply = function(A, B) {
	// 如果B为0,任何数乘以0都为0
	if (B === 0) {
		return 0;
	}
	// 检查B的二进制最低位是否都为1(奇数检查)
	if ((B & 1) === 1) {
		// 如果B是奇数,将A加到结果中,然后将A 乘以(B >> 1)的结果加倍
		return A + (multiply(A, B >> 1) << 1);
	} else {
		// 如果B是偶数,将A乘以(B >> 1)的结果加倍
		return multiply(A, B >> 1) << 1;
	}
};

3.位运算实现除法

力扣29题,给你两个整数,被除数 dividend 和除数 divisor(!== 0)。将两数相除,要求 不使用 乘法、除法和取余运算。

整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8-2.7335 将被截断至 -2

返回被除数 dividend 除以除数 divisor 得到的

**注意:**假设我们的环境只能存储 32 位 有符号整数,其数值范围是$ [−2^{31}, 2^{31} − 1]$。本题中,如果商 严格大于 2 31 − 1 2^{31} − 1 2311 ,则返回 2 31 − 1 2^{31} − 1 2311;如果商 严格小于 − 2 31 -2^{31} 231 ,则返回 − 2 31 -2^{31} 231

分析:这道题有些复杂,细节比较多。刚开始我们最先想到的就是用循环减法,但是这样被除数一变大效率就太低。

首先对于溢出或者容易出错的边界情况进行讨论:

  • 当被除数为32位有符号整数的最小值 − 2 31 -2^{31} 231时:
    • 如果除数为1,直接返回 − 2 31 -2^{31} 231
    • 如果除数为-1,得到的是 2 31 2^{31} 231,会产生溢出,此时需要返回 2 31 − 1 2^{31}-1 2311
  • 当除数为32位有符号整数的最小值 − 2 31 -2^{31} 231时:
    • 如果被除数同样为 − 2 31 -2^{31} 231,直接返回1;
    • 对于其余的情况,返回0。
  • 如果被除数为0,返回0。

对于一般的情况,被除数和除数符号有可能不同,这样就会有4种情况,不便于编码。为了方便编码我们可以采取对被除数和除数取相反数的方法以统一符号,这样就只需要考虑一种情况。

为了防止正整数出现溢出的情况,把符号都统一为负数,在返回答案时也需要取相反数。

使用二分查找来解决,同样需要注意边界问题。被除数x和除数y都是负数,那么他们的商z就是正数,根据除法及余数的定义,可以将其改成乘法的等价形式: z ∗ y > = x > = ( z + 1 ) ∗ y z * y >= x >= (z + 1) * y zy>=x>=(z+1)y,

由于不让用除法就需要用到快速乘方法,本质上就是加法运算。由于较大的z也会导致加法运算溢出,例如判断A + B < C,由于都为负数,A + B可能会产生溢出,所以必须判断A < C - B是否成立。由于任意两个负数的差一定在 [ − 2 31 + 1 , 2 31 − 1 ] [-2^{31} + 1, 2^{31} - 1] [231+1,2311]范围内,这样就不会产生溢出:

完整代码如下:

function divide(divided, divisor) {
	const MAX_VALUE = 2**31 - 1;
	const MIN_VALUE = -(2**31);

	// 考虑被除数为最小值的情况
	if (divided === MIN_VALUE) {
		if (divisor === 1) {
			return MIN_VALUE;
		} else if (divisor === -1) {
			return MAX_VALUE;
		}
	}

	// 考虑除数为最小值的情况
	if (divisor === MIN_VALUE) {
		return divided === MIN_VALUE10;
	}

	// 考虑被除数为0的情况
	if (divided === 0) {
		return 0;
	}

	// 一般情况,使用二分查找
	// 将所有的正数取相反数,这样就只考虑一种情况
	let reversed = false;	// 标记是否取了相反数
	
	if (divided > 0) {
		divided = -divided;
		reversed = !reversed;
	}
	if (divisor > 0) {
		divisor = -divisor;
		reversed = !reversed;
	}

	let left = 0, right = MAX_VALUE, ans = 0;
	while (left <= right) {
		mid = left + ((right - left) >> 1);
		const check = quickAdd(divisor, mid, divided);
		if (check) {
			ans = mid;
			if (mid === MAX_VALUE) {
				break;
			}
			left = mid + 1;
		} else {
			rihgt = mid - 1;
		}
	}
	return reversed ? -ans : ans;
}

/**
 * 快速乘
 * @params y
 * @params z
 * @params x
 * 
 * */
// z * y >= x >= (z + 1) * y  (x和y为负数,z是正数)
function quickAdd(y, z, x) {
	// 需要判断 z * y >= x 是否成立
	let result = 0, add = y;
	while (z !== 0) {
		if ((z & 1) !== 0) {
			// 需要保证 result + add >= x
			if (result < x - add) {
				return false;
			}
			add += add;
		}
		if (z !== 1) {
			// 需要保证 add + add >= x,即两数之和大于等于第三个数,防止加法运算溢出
			if (add < x - add) {
				return false;
			}
			add += add;
		}
		// 不能使用除法,就用右移位运算
		z >>= 1;
	}
	return true;
}

你可能感兴趣的:(算法,javascript,前端,算法)