1.位运算:
1-1.计算机最基本的操作单元是字节(byte),一个字节由8个位(bit)组成,一个位只能存储一个0或1,其实也就是高低电平。无论多么复杂的逻辑、庞大的数据、酷炫的界面,最终体现在计算机最底层都只是对0101的存储和运算。因此,了解位运算有助于提升我们对计算机底层操作原理的理解。
2.位运算实现加法运算:
2.1.十进制的加法运算:13 + 9 = 22。拆分运算过程:
①不考虑进位,分别对各位数进行相加,结果为 sum(1):个位数 3 加上 9 为 2,十位数 1 加上 0 为 1;最终结果为 12; ②只考虑进位,结果为 carry(1):3 + 9 有进位,进位的值为 10; ③如果步骤 ② 所得进位结果 carry 不为 0,对步骤 ① 所得 sum(1),步骤 ②所得 carry(1) 重复步骤 ①、②、③;如果 carry(n) 为 0 则结束,最终结果为步骤 ① 所得 sum(n) :
α:对 sum(1) = 12 和 carry(1) = 10 重复以上三个步骤:不考虑进位:sum(2) = 22;只考虑进位:carry(2) = 0;carry(2) = 0,结束;sum(2) = 22。 2.2.二进制的加法运算:13 的二进制为 0000 1101,9 的二进制为 0000 1001:00001101 + 00001001 = 0001 0110;拆分运算过程:
①不考虑进位:sum(1) = 0000 1101 + 0000 1001 = 0000 0100; ②考虑进位:有两处进位,第 0 位和第 3 位,只考虑进位的结果为:carry(1) = 0001 0010; ③步骤 ② carry(1) 不为 0,重复步骤 ①、②、③;carry(n) 为 0 则结束,结果为 sum(n):
α:不考虑进位 sum(2) = 0001 0110;只考虑进位:carry(2) = 0;carry(2) = 0,结束,最终结果为 sum(2) = 0001 0110。 2.3.第一步不考虑进位的加法其实就是异或运算;第二步只考虑进位就是 与运算并左移一位;第三步就是重复前面两步操作直到第二步进位结果为 0。 2.4.代码实现:计算机其实就是通过位运算实现加法运算的(通过加法器,加法器就是使用下面方法实现加法的)。
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
add(1284, 18898); // 20182
function addSum(a, b) {
let sum = a ^ b;
let carry = (a & b) << 1;
while(carry !== 0) {
let a = sum;
let b = carry;
sum = a ^ b;
carry = (a & b) << 1;
}
return sum;
}
addSum(1284, 189998) // 191282
3.位运算实现减法运算:减法运算变形为一个正数加上一个负数:
3.1.将数字的正负号变号的方式叫补码,求取步骤:总结为取反加 1:
①将一个二进制位都取相反值,0 变 1,1 变 0(即反码)。 ②将上一步得到的值(反码)加 1。 3.2.代码实现:
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
function substract(a, b) {
let unB = add(~b, 1); // 先求减数的补码并加 1(取反加1)
let result = add(a, unB); // 求和加法运算
return result;
}
substract(100, 99) // 1
4.位运算实现乘法运算:
4-1.乘数和被乘数的绝对值的乘积,再根据它们的符号确定最终结果的符号即可:代码实现:规定每个字节的最高位为符号位。1为负。被乘数 * 乘数 = 积 4-2.乘数与被乘数的绝对值的乘积使用累加计算。
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
function multiply(a, b) {
let multiplicand = a < 0 ? add(~a, 1) : a;
let multiplier = b < 0 ? add(~b, 1) : b;
let product = 0;
let count = 0;
while(count < multiplier) {
product = add(product, multiplicand); // 累加
count = add(count, 1); // 计数器
};
if((a ^ b) < 0) {
product = add(~product, 1);
}
return product;
}
multiply(98, 99); // 9702
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
function multiply(a, b) {
let multiplicand = a < 0 ? add(~a, 1) : a; // 被乘数
let multiplier = b < 0 ? add(~b, 1) : b; // 乘数
let product = 0;
while(multiplier > 0) {
if(multiplier & 1) {// 每次考察乘数的最后一位
product = add(product, multiplicand);
}
multiplicand = multiplicand << 1;// 每运算一次,被乘数要左移一位
multiplier = multiplier >> 1;// 每运算一次,乘数要右移一位(可对照上图理解)
};
if((a ^ b) < 0) {
product = add(~product, 1);
}
return product;
}
multiply(98, 99); // 9702
5.位运算实现除法:
5-1.转换成减法运算,即不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商。被除数÷除数=商 5-2.性能慢:
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
function substract(a, b) {
let unB = add(~b, 1); // 先求减数的补码并加 1(取反加1)
let result = add(a, unB); // 求和加法运算
return result;
}
function divide(a, b) {
let dividend = a < 0 ? add(~a, 1) : a; // 被除数
let divisor = b < 0 ? add(~b, 1) : b; // 除数
let quotient = 0; // 商
let remainder = 0; // 余数
while(dividend >= divisor){// 当被除数小于除数时,即除不尽了。
quotient = add(quotient, 1); // 商,累加计数器
dividend = substract(dividend, divisor); // 被除数 - 除数
}
if((a ^ b) < 0) {
quotient = add(~quotient, 1);
}
remainder = b > 0 ? dividend : add(~dividend, 1);
return quotient;
}
5-3.优化:int整型的表示范围是-2147483648~+2147483647, [2^0, 21,…,31]。采用类似二分法的思路,从除数*最大倍数开始测试,如果比被除数小,则要减去。下一回让除数的倍数减少为上一次倍数的一半,这样的直到倍数为1时,就能把被除数中所有的除数减去,并得到商。只有当a >= b时才可以上商,又因为是二进制,所以商每次只会多1,在每次上1之后a都要减去一次b。
function add(a, b) {
if(b === 0) {
return a;
}
let sum = a ^ b;
let carry = (a & b) << 1;
return add(sum, carry);
}
function substract(a, b) {
let unB = add(~b, 1); // 先求减数的补码并加 1(取反加1)
let result = add(a, unB); // 求和加法运算
return result;
}
function divide(a, b) {
let dividend = a < 0 ? add(~a, 1) : a; // 被除数
let divisor = b < 0 ? add(~b, 1) : b; // 除数
let quotient = 0; // 商
let remainder = 0; // 余数
for(let i = 31; i >= 0; i--) {
//比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较,
//效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor
if((dividend >> i) >= divisor) { // dividend >= (dividend << i) 更容易理解
console.log('被除数大于除数未处理',i, dividend,divisor,quotient)
quotient = add(quotient, 1 << i);
dividend = substract(dividend, divisor << i);
console.log('被除数大于除数',i, dividend,divisor,quotient)
}
console.log('被除数小于除数',i, dividend,divisor,quotient)
}
if((a ^ b) < 0) {
quotient = add(~quotient, 1);
}
remainder = b > 0 ? dividend : add(~dividend, 1);
return quotient;
}
divide(9, 3)
被除数小于除数 31 9 3 0
被除数小于除数 30 9 3 0
被除数小于除数 29 9 3 0
被除数小于除数 28 9 3 0
被除数小于除数 27 9 3 0
被除数小于除数 26 9 3 0
被除数小于除数 25 9 3 0
被除数小于除数 24 9 3 0
被除数小于除数 23 9 3 0
被除数小于除数 22 9 3 0
被除数小于除数 21 9 3 0
被除数小于除数 20 9 3 0
被除数小于除数 19 9 3 0
被除数小于除数 18 9 3 0
被除数小于除数 17 9 3 0
被除数小于除数 16 9 3 0
被除数小于除数 15 9 3 0
被除数小于除数 14 9 3 0
被除数小于除数 13 9 3 0
被除数小于除数 12 9 3 0
被除数小于除数 11 9 3 0
被除数小于除数 10 9 3 0
被除数小于除数 9 9 3 0
被除数小于除数 8 9 3 0
被除数小于除数 7 9 3 0
被除数小于除数 6 9 3 0
被除数小于除数 5 9 3 0
被除数小于除数 4 9 3 0
被除数小于除数 3 9 3 0
被除数小于除数 2 9 3 0
被除数大于除数未处理 1 9 3 0
被除数大于除数 1 3 3 2
被除数小于除数 1 3 3 2
被除数大于除数未处理 0 3 3 2
被除数大于除数 0 0 3 3
被除数小于除数 0 0 3 3