首发于微信公众号《前端成长记》,写于 2019.11.21
本文记录刷题过程当中的整个思考过程,以供参考。主要内容涵盖:javascript
题目地址前端
给定两个二进制字符串,返回他们的和(用二进制表示)。java
输入为非空字符串且只包含数字 1
和 0
。算法
示例:数组
输入: a = "11", b = "1" 输出: "100" 输入: a = "1010", b = "1011" 输出: "10101"
这道题又是一道加法题,因此记住下,直接转数字进行加法可能会溢出,因此不可取。因此咱们须要遍历每一位来作解答。我这有两个大方向:补0后遍历,和不补0遍历。可是基本的依据都是本位相加,逢2进1便可,相似手写10进制加法。微信
Ⅰ.补0后遍历,先算先推函数
代码:性能
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let times = Math.max(a.length, b.length) // 须要遍历次数 // 补 0 while(a.length < times) { a = '0' + a } while(b.length < times) { b = '0' + b } let res = [] let carry = 0 // 是否进位 for(let i = times - 1; i >= 0; i--) { const num = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0) carry = num >= 2 ? 1 : 0 res.push(num % 2) } if (carry === 1) { res.push(1) } return res.reverse().join('') };
结果:测试
O(n)
Ⅱ.补0后遍历,按位运算优化
代码:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let times = Math.max(a.length, b.length) // 须要遍历次数 // 补 0 while(a.length < times) { a = '0' + a } while(b.length < times) { b = '0' + b } let res = [] let carry = 0 // 是否进位 for(let i = times - 1; i >= 0; i--) { res[i] = carry + (a.charAt(i) | 0) + (b.charAt(i) | 0) carry = res[i] >= 2 ? 1 : 0 res[i] %= 2 } if (carry === 1) { res.unshift(1) } return res.join('') };
结果:
O(n)
Ⅲ.不补0遍历
固然处理方式仍是能够选择上面两种,我这就采用先算先推来处理了。
代码:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { let max = Math.max(a.length, b.length) // 最大长度 let min = Math.min(a.length, b.length) // 最大公共长度 // 将长字符串拆成两部分 let left = a.length > b.length ? a.substr(0, a.length - b.length) : b.substr(0, b.length - a.length) let right = a.length > b.length ? a.substr(a.length - b.length) : b.substr(b.length - a.length) // 公共长度部分遍历 let rightRes = [] let carry = 0 for(let i = min - 1; i >= 0; i--) { const num = carry + (right.charAt(i) | 0) + (((a.length > b.length ? b : a)).charAt(i) | 0) carry = num >= 2 ? 1 : 0 rightRes.push(num % 2) } let leftRes = [] for(let j = max - min - 1; j >= 0; j--) { const num = carry + (left.charAt(j) | 0) carry = num >= 2 ? 1 : 0 leftRes.push(num % 2) } if (carry === 1) { leftRes.push(1) } return leftRes.reverse().join('') + rightRes.reverse().join('') };
结果:
O(n)
看到一些细节上的区别,我这使用 '1' | 0
来转数字,有的使用 ''1' - '0''
。另外还有就是初始化结果数组长度为最大长度加1后,最后判断首位是否为0须要剔除的,我这使用的是判断最后是否还要进位补1。
这里还看到用一个提案中的 BigInt
类型来解决的
Ⅰ.BigInt
代码:
/** * @param {string} a * @param {string} b * @return {string} */ var addBinary = function(a, b) { return (BigInt("0b"+a) + BigInt("0b"+b)).toString(2); };
结果:
O(1)
经过 BigInt
的方案咱们能看到,使用原生方法确实性能更优。简单说一下这个类型,目前还在提案阶段,看下面的等式基本就能知道实现原理本身写对应 Hack
来实现了:
BigInt(10) = '10n' BigInt(20) = '20n' BigInt(10) + BigInt(20) = '30n'
虽然这种方式很友好,可是仍是但愿看到加法题的时候,能考虑到遍历按位处理。
实现 int sqrt(int x)
函数。
计算并返回 x 的平方根,其中 x 是非负整数。
因为返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例:
输入: 4 输出: 2 输入: 8 输出: 2 说明: 8 的平方根是 2.82842..., 因为返回类型是整数,小数部分将被舍去。
一样,这里类库提供的方法 Math.sqrt(x)
就不说了,这也不是本题想考察的意义。因此这里有几种方式:
Ⅰ.暴力法
代码:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0) return 0 let i = 1 while(i * i < x) { i++ } return i * i === x ? i : i - 1 };
结果:
O(n)
Ⅱ.二分法
代码:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0) return 0 let l = 1 let r = x >>> 1 while(l < r) { // 这里要用大于判断,因此取右中位数 const mid = (l + r + 1) >>> 1 if (mid * mid > x) { r = mid - 1 } else { l = mid } } return l };
结果:
O(log2(n))
这里看见了两个有意思的解法:
Ⅰ.幂次优化
稍微解释一下,二分法须要作乘法运算,他这里改用加减法
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { let l = 0 let r = 1 << 16 // 2的16次方,这里我猜是由于上限2^32因此取一半 while (l < r - 1) { const mid = (l + r) >>> 1 if (mid * mid <= x) { l = mid } else { r = mid } } return l };
结果:
1017/1017 cases passed (72 ms)
Your runtime beats 98.46 % of javascript submissions
Your memory usage beats 70.66 % of javascript submissions (35.4 MB)
O(log2(n))
Ⅱ.牛顿法
算法说明:
在迭代过程当中,以直线代替曲线,用一阶泰勒展式(即在当前点的切线)代替原曲线,求直线与 xx 轴的交点,重复这个过程直到收敛。
首先随便猜一个近似值 x
,而后不断令 x
等于 x
和 a/x
的平均数,迭代个六七次后 x
的值就已经至关精确了。
公式能够写为 X[n+1]=(X[n]+a/X[n])/2
代码:
/** * @param {number} x * @return {number} */ var mySqrt = function(x) { if (x === 0 || x === 1) return x let a = x >>> 1 while(true) { let cur = a a = (a + x / a) / 2 // 这里是为了消除浮点运算的偏差,1e-5是我试出来的 if (Math.abs(a - cur) < 1e-5) { return parseInt(cur) } } };
结果:
O(log2(n))
这里就提一下新接触的牛顿法吧,其实是牛顿迭代法,主要是迭代操做。因为在单根附近具备平方收敛,因此能够转换成线性问题去求平方根的近似值。主要应用场景有这两个方向:
假设你正在爬楼梯。须要 n
阶你才能到达楼顶。
每次你能够爬 1
或 2
个台阶。你有多少种不一样的方法能够爬到楼顶呢?
注意:给定 n
是一个正整数。
示例:
输入: 2 输出: 2 解释: 有两种方法能够爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 输入: 3 输出: 3 解释: 有三种方法能够爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
这道题很明显能够用动态规划和斐波那契数列来求解。而后咱们来看看其余正常思路,若是使用暴力法的话,那么复杂度将会是 2^n
,很容易溢出,可是若是可以优化成 n
的话,其实还能够求解的。因此这道题我就从如下三个方向来做答:
Ⅰ.哈希递归
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { let hash = {} return count(0) function count (i) { if (i > n) return 0 if (i === n) return 1 // 这步节省运算 if(hash[i] > 0) { return hash[i] } hash[i] = count(i + 1) + count(i + 2) return hash[i] } };
结果:
O(n)
Ⅱ.动态规划
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { if (n === 1) return 1 if (n === 2) return 2 // dp[0] 多一位空间,省的后面作减法 let dp = new Array(n + 1).fill(0) dp[1] = 1 dp[2] = 2 for(let i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2] } return dp[n] };
结果:
O(n)
Ⅲ.斐波那契数列
其实斐波那契数列就能够用动态规划来实现,因此下面的代码思路很类似。
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { if (n === 1) return 1 if (n === 2) return 2 let num1 = 1 let num2 = 2 for(let i = 3; i <= n; i++) { let count = num1 + num2 num1 = num2 num2 = count } // 至关于fib(n) return num2 };
结果:
O(n)
查看题解发现这么几种解法:
Ⅰ.斐波那契公式
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { const sqrt_5 = Math.sqrt(5) // 因为 F0 = 1,因此至关于须要求 n+1 的值 const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2, n + 1) return Math.round(fib_n / sqrt_5) };
结果:
O(log(n))
Ⅱ.Binets 方法
算法说明:
使用矩阵乘法来获得第 n 个斐波那契数。注意须要将初始项从 fib(2)=2,fib(1)=1
改为 fib(2)=1,fib(1)=0
,来达到矩阵等式的左右相等。
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { function pow(a, n) { let ret = [[1,0],[0,1]] // 矩阵 while(n > 0) { if ((n & 1) === 1) { ret = multiply(ret, a) } n >> 1 a = multiply(a, a) } return ret; } function multiply(a, b) { let c = [[0,0], [0,0]] for (let i = 0; i < 2; i++) { for(let j = 0; j < 2; j++) { c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] } } return c } let q = [[1,1], [1, 0]] let res = pow(q, n) return res[0][0] };
结果:
测试用例能够输出,提交发现超时。
这个笔者还没彻底理解,因此很抱歉,暂时没有 js 相应代码分析,后续会补上。也欢迎您补充给我,感谢!
Ⅲ.排列组合
代码:
/** * @param {number} n * @return {number} */ var climbStairs = function(n) { // n 个台阶走 i 次1阶和 j 次2阶走到,推导出 i + 2*j = n function combine(m, n) { if (m < n) [m, n] = [n, m]; let count = 1; for (let i = m + n, j = 1; i > m; i--) { count *= i; if (j <= n) count /= j++; } return count; } let total = 0; // 取出全部知足条件的解 for (let i = 0,j = n; j >= 0; j -= 2, i++) { total += combine(i, j); } return total; };
结果:
O(n^2)
这种叠加的问题,首先就会想到动态规划的解法,恰好这里又知足斐波那契数列,因此我是推荐首选这两种解法。另外经过查看他人解法学到了斐波那契公式,以及站在排列组合的角度去解,开拓了思路。
给定一个排序链表,删除全部重复的元素,使得每一个元素只出现一次。
示例:
输入: 1->1->2 输出: 1->2 输入: 1->1->2->3->3 输出: 1->2->3
注意一下,给定的是一个排序链表,因此只须要依次更改指针就能够直接得出结果。固然,也可使用双指针来跳太重复项便可。因此这里有两个方向:
若是是无序链表,我会建议先获得全部值而后去重后(好比经过Set)生成新链表做答。
Ⅰ.直接运算
代码:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { // 复制一个用作操做,因为对象是传址,因此改指针指向便可 let cur = head while(cur !== null && cur.next !== null) { if (cur.val === cur.next.val) { // 值相等 cur.next = cur.next.next } else { cur = cur.next } } return head };
结果:
O(n)
Ⅱ.双指针法
代码:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { // 新建哨兵指针和当前遍历指针 if (head === null || head.next === null) return head let pre = head let cur = head while(cur !== null) { debugger if (cur.val === pre.val) { // 当前指针移动 cur = cur.next } else { pre.next = cur pre = cur } } // 最后一项若是重复须要把head.next指向null pre.next = null return head };
结果:
O(n)
忘记了,这里确实还可使用递归来做答。
Ⅰ.递归法
代码:
/** * @param {ListNode} head * @return {ListNode} */ var deleteDuplicates = function(head) { if(head === null || head.next === null) return head if (head.val === head.next.val) { // 值相等 return deleteDuplicates(head.next) } else { head.next = deleteDuplicates(head.next) } return head };
结果:
O(n)
关于链表的题目通常都是经过修改指针指向来做答,区分单指针和双指针法。另外,遍历也是能够实现的。
给定两个有序整数数组 nums1
和 nums2
,将 nums2
合并到 nums1
中,使得 num1
成为一个有序数组。
说明:
nums1
和 nums2
的元素数量分别为 m
和 n
。nums1
有足够的空间(空间大小大于或等于 m + n
)来保存 nums2
中的元素。示例:
输入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 输出: [1,2,2,3,5,6]
以前咱们作过删除排序数组中的重复项,其实这里也相似。能够从这几个方向做答:
可是因为题目有限定空间都在 nums1
,而且不要写 return
,直接在 nums1
上修改,因此我这里主要的思路就是遍历,经过 splice
来修改数组。区别就在于遍历的方式方法。
Ⅰ.从前日后
代码:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 两个数组对应指针 let p1 = 0 let p2 = 0 // 这里须要提早把nums1的元素拷贝出来,要否则比较赋值后就丢失了 let cpArr = nums1.splice(0, m) // 数组指针 let p = 0 while(p1 < m && p2 < n) { // 先赋值,再进行+1操做 nums1[p++] = cpArr[p1] < nums2[p2] ? cpArr[p1++] : nums2[p2++] } // 已经有p个元素了,多余的元素要删除,剩余的要加上 if (p1 < m) { // 剩余元素,p1 + m + n - p = m + n - (p - p1) = m + n - p2 nums1.splice(p, m + n - p, ...cpArr.slice(p1, m + n - p2)) } if (p2 < n) { // 剩余元素,p2 + m + n - p = m + n - (p - p2) = m + n - p1 nums1.splice(p, m + n - p, ...nums2.slice(p2, m + n - p1)) } };
结果:
O(m + n)
Ⅱ.从后往前
代码:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 这种 nums1.length > nums2.length 而且 m = 0 nums1.splice(m, nums1.length - m) // 两个数组对应指针 let p1 = m - 1 let p2 = n - 1 // 数组指针 let p = m + n - 1 while(p1 >= 0 && p2 >= 0) { // 先赋值,再进行-1操做 nums1[p--] = nums1[p1] < nums2[p2] ? nums2[p2--] : nums1[p1--] } // 可能nums2有剩余,因为指针是下标,因此截取数量须要加1 nums1.splice(0, p2 + 1, ...nums2.slice(0, p2 + 1)) };
结果:
O(m + n)
Ⅲ.合并后排序再赋值
代码:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { arr = [].concat(nums1.splice(0, m), nums2.splice(0, n)) arr.sort((a, b) => a - b) for(let i = 0; i < arr.length; i++) { nums1[i] = arr[i] } };
结果:
O(m + n)
这里看到一个直接用两次 while
,而后直接用 m/n
来计算下标的,没有额外空间,可是本质上也是从后往前遍历。
Ⅰ.两次while
代码:
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ var merge = function(nums1, m, nums2, n) { // 避免 nums1 = [0,0,0,0], nums2 = [1,2] 这种 nums1.length > nums2.length 而且 m = 0 // nums1.splice(m, nums1.length - m) // 从后开始赋值 while(m !== 0 && n !== 0) { nums1[m + n - 1] = nums1[m - 1] > nums2[n - 1] ? nums1[--m] : nums2[--n] } // nums2 有剩余 while(n !== 0) { nums1[m + n - 1] = nums2[--n] } };
结果:
O(m + n)
碰到数组操做,会优先考虑双指针法,具体指针方向能够由题目逻辑来决定。
(完)
本文为原创文章,可能会更新知识点及修正错误,所以转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
若是能给您带去些许帮助,欢迎 ⭐️ star 或 ✏️ fork
(转载请注明出处: https://chenjiahao.xyz)