最近刚刚开始学习算法,无论书上还是网上的答案,很多都是优化过的,的确把这个问题整明白后觉得那样的代码真的很棒很优雅~
但是当我第一次接触的时候,理解起来真的还是有点难,我现在在试着理解他们的思路之后,按照自己的逻辑一步一步写了一遍,可能没那么完美,但一定是便于理解的。
写这篇内容也是想让自己输出些东西,在成长的路上留些足迹哈哈,之后回看的时候也有迹可循。如果觉得自己还挺厉害的,证明我是退步了😂,如果看自己好蠢哟,那可能是我真的有进步了哈哈哈哈
ps.题目来自《剑指offer》,都附上了leetcode网址
第一章-整数
1、整数除法
题目:给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%' 。
leetcode.cn/problems/xo…
我直接用代码+文字,来理清做这道题的逻辑
1、当被除数大于除数时,再比较被除数是否大于2倍的除数、4倍的除数、8倍的除数...... 记录此时的倍数。当被除数最多大于2^n的除数时,也就是跳出循环的时候,用被除数减去2^n的除数,再用这个新的被除数继续重复前面的步骤。
while(被除数 >= 除数){
while(被除数 >= 2倍的除数、4倍的除数、8倍的除数){
倍数 = ?; //记录此时的倍数
}
新被除数 = 被除数 - 除数;
结果 = 第一次的倍数 + 第二次的倍数 + ……;
}
因为函数传进来的值不可以变,但是循环中,被除数与除数都会变,所以需要有个东西存储函数输入的被除数(dividen)与除数(divisor);还需要有东西记录倍数(quotient)与结果(result);还发现在循环中,有用到2、4、8……倍的除数,但每次都是从本来的除数开始的,所以还需要有个东西记录2、4、8……倍的除数(value)
var divide = function(a, b){
var dividend = a;
var divisor = b;
var quotient;
var result = 0;
while(dividend >= divisor){
let value = divisor;
while(dividend >= 2倍的value、4倍的value、8倍的value){
quotient = ?; //记录此时的倍数
}
dividend = dividend - divisor;
result = result + quotient;
}
return result;
}
如何表示2、4、8……倍的除数?——首先要明确,被除数要减去的value是被除数最多大于的2^n的除数。(用36/7举例,要减去的是28而不是56)
内部 while 的第一次循环就是判断 dividend 是否大于2倍的 value。2倍的 value 有两种方式,1、声明value的时候,let value = divisor + divisor ; 2、判断时 while(dividend > value + value);
循环内部只用 value = value + value,就可以让下次循环是4倍的value了。
第一种方法的逻辑是,比较后,相加,拿去比较,如果不符合,此时的 value 已经是相加过的,也就是 36/7 时的 56,所以需要用方法二。
倍数quotient,第一次出循环是2(1+1),第二次是4(2+2),第三次是8(4+4)……所以初始的他是1,之后自己加自己就可以
var divide = function(a, b){
var dividend = a;
var divisor = b;
var quotient = 1;
var result = 0;
while(dividend >= divisor){
let value = divisor;
while(dividend >= value + value){
value += value;
quotient += quotient;
}
dividend = dividend - divisor;
result += quotient;
}
return result;
}
函数此时已经可以满足基本逻辑了,接下来需要考虑特殊的情况
2、当输入的某个整数为负数时
可以将负数先转变为正数计算,再根据需要添加符号。
var divide = function(a, b){
var dividend = a;
var divisor = b;
var quotient = 1;
var result = 0;
if (dividend < 0) {
dividend = -dividend;
}
if (divisor < 0) {
divisor = -divisor;
}
while(dividend >= divisor){
let value = divisor;
while(dividend >= value + value){
value += value;
quotient += quotient;
}
dividend = dividend - divisor;
result += quotient;
}
return result;
}
如何判断符号呢?想起来了小学老师教过的“负负为正”,“异号为负,同号为正”哈哈哈哈哈,我们可以引入一个变量,初始值为2,一个数是负数时,他就减一。那么他可能的结果就是'0'、'1'、'2',为 1 时,代表是一正一负,结果为负数
var divide = function(a, b){
var dividend = a;
var divisor = b;
var quotient = 1;
var result = 0;
var negative = 2;
if (dividend < 0) {
negative--;
dividend = -dividend;
}
if (divisor < 0) {
negative--;
divisor = -divisor;
}
while(dividend >= divisor){
let value = divisor;
while(dividend >= value + value){
value += value;
quotient += quotient;
}
dividend = dividend - divisor;
result += quotient;
}
return negative == 1 ? -result : result;
}
3、还有最后一个问题,就是32位的整数,最大值是2^31 - 1,最小值是-2^31,那么当转变最小值的时候,就会发生溢出问题,处理后,就是完整的答案了
/**
* @param {number} a
* @param {number} b
* @return {number}
*/
var divide = function (a, b) {
var dividend = a;
var divisor = b;
var quotient = 1;
var result = 0;
if (dividend === -0x80000000 && divisor === -1) {
return 0x80000000 - 1;
}
var negative = 2;
if (dividend < 0) {
negative--;
dividend = -dividend;
}
if (divisor < 0) {
negative--;
divisor = -divisor;
}
while (dividend >= divisor) {
let value = divisor;
quotient = 1;
while (dividend >= value + value) {
value += value;
quotient += quotient;
}
dividend = dividend - value;
result += quotient;
}
return negative == 1 ? -result : result;
};
2、二进制加法
题目:给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制 字符串 的形式输出。
输入为 非空 字符串且只包含数字 1 和 0。
leetcode.cn/problems/JF…
逻辑与十进制加法相似,从后往前计算,二进制需要遇2进1。
1、获取两个数的最后一位(charAt() 方法可返回指定位置的字符),并把它们相加。
var addBinary = function (a, b) {
let i = a.length - 1;
let j = b.length - 1; // i、j就表示字符串的第几位,.length - 1 就是最后一位
let digitA = a.charAt(i);
let digitB = b.charAt(j);
let sum = digitA + digitB; // 调试时会发现这一步执行的是字符串拼接
};
想办法将digitA与digitB的数据类型从 string 转为 number
var addBinary = function (a, b) {
let i = a.length - 1;
let j = b.length - 1;
let digitA = a.charAt(i) - '0';
let digitB = b.charAt(j) - '0';
let sum = digitA + digitB; // ok了
};
2、如果相加结果 >= 2,进位(用carry记录),那就需要把这一位减去 2 ,如果没有 >= 2,这一位不变。然后把结果用数组的形式先记下来
var addBinary = function (a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let result = [];
let digitA = a.charAt(i) - '0';
let digitB = b.charAt(j) - '0';
let sum = digitA + digitB;
if (sum >= 2) {
sum = sum - 2;
carry = 1;
} else {
sum = sum;
carry = 0;
}
result.unshift(sum); // 因为是从最后一位往前计算的,所以结果也用 unshift,向数组的开头添加一个或多个元素
};
3、开始算倒数第二位,那首先要知道进入循环的条件,就是两个字符串,有一个没有遍历完。两个数的长度可能不相等,那就要把短的那个前面都当作 0 和另一个进行计算。
这一步算两个数之和的时候,还要加上上一步进位的数
var addBinary = function (a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let result = [];
while (i >= 0 || j >= 0) {
let digitA = i >= 0 ? a.charAt(i) - '0' : 0;
let digitB = j >= 0 ? b.charAt(j) - '0' : 0; //第0位算完后,其他都按0去和另一个数计算
let sum = digitA + digitB + carry; // 加上上一步进位的数
if (sum >= 2) {
sum = sum - 2;
carry = 1;
} else {
sum = sum;
carry = 0;
}
result.unshift(sum);
i--;
j--;
}
};
4、全部算完后,要知道第0位是否有进位,有的话,也加入到数组。最后将结果数组转换为字符串即可。
/**
* @param {string} a
* @param {string} b
* @return {string}
*/
var addBinary = function (a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let result = [];
while (i >= 0 || j >= 0) {
let digitA = i >= 0 ? a.charAt(i) - '0' : 0;
let digitB = j >= 0 ? b.charAt(j) - '0' : 0; //第0位算完后,其他都按0去和另一个数计算
let sum = digitA + digitB + carry; // 加上上一步进位的数
if (sum >= 2) {
sum = sum - 2;
carry = 1;
} else {
sum = sum;
carry = 0;
}
result.unshift(sum);
i--;
j--;
}
if (carry == 1) {
result.unshift(carry);
}
return result.join("");
};
3、前n个数字二进制形式中 1 的个数
题目:给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。leetcode.cn/problems/w3…
1、
var countBits = function(n) {
// 因为最后需要输出一个数组,所以先声明
let result = [];
// 遍历出 0 ~ n
for(let i = 1 ; i < n ; i++){
}
};
2、写出几位二进制,可以发现,
1 左移一位得到 2 ,左移一位加 1 得到 3,(1 与 2 二进制形式中 1 的个数相同,3 比 1 中的 1 的个数多 1)
2 左移一位得到 4 ,左移一位加 1 得到 5,(2 与 4 二进制形式中 1 的个数相同,5 比 2 中的 1 的个数多 1)
3 左移一位得到 6 ,左移一位加 1 得到 7,
……
说明只要得到 1 的二进制中 1 的个数,其他数字都可以得到
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
6 --> 110
7 --> 111
8 --> 1000
9 --> 1001
10 --> 1010
11 --> 1011
12 --> 1100
13 --> 1101
14 --> 1110
15 --> 1111
16 --> 10000
所以得到代码
/**
* @param {number} n
* @return {number[]}
*/
var countBits = function(n) {
let result = [];
result[0] = 0;
result[1] = 1;
for(let i = 2 ; i < n ; i++){
result[i] = result[i >> 1] + (i & 1); // 右移一位,找到i/2的二进制中1的个数,在判断i是基数还是偶数,偶数中1的个数与i/2相同,基数中1的个数比i/2多1
}
return result;
};
书中说到i >> 1与i/2相同,i & 1与i % 2相同,但是位运算比除法运算和取余运算更高效,而且这个题目是关于位运算的,所以要尽量使用位运算来优化代码。
4、只出现一次的数字
题目:给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。 请你找出并返回那个只出现了一次的元素。leetcode.cn/problems/WG…