首发于微信公众号《前端成长记》,写于 2019.10.28javascript
本文记录刷题过程当中的整个思考过程,以供参考。主要内容涵盖:前端
题目地址java
给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。算法
你能够假设每种输入只会对应一个答案。可是,你不能重复利用这个数组中一样的元素。数组
示例:微信
给定 nums = [2, 7, 11, 15], target = 9 由于 nums[0] + nums[1] = 2 + 7 = 9 因此返回 [0, 1]
这道题首先说明了每种输入只会对应一个答案,而且不能利用数组中一样的元素,也就意味着一个数不能被使用两次,即 [0,0]
这种是不合理的。函数
看到这个题目,我有几个方向去尝试做答:性能
IndexOf
,循环次数最多,很是不推荐HashMap
,减小一次循环Ⅰ.暴力法优化
代码:code
// 暴力点 /** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { for(let i = 0; i < nums.length; i++) { // j 从 i+1 开始,去除一些无用运算 for(let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) { return [i,j]; } } } };
结果:
O(n^2)
Ⅱ.IndexOf
性能最差,每次判断都须要遍历剩余数组(极度不推荐,只是多展现一个实现方案)
代码:
/** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { for(let i = 0; i < nums.length; i++) { const num = nums[i] const dif = target - num const remainArr = nums.slice(i + 1) if (remainArr.indexOf(dif) !== -1) { return [i, remainArr.indexOf(dif) + i + 1] } } };
结果:
O(n^2)
,阶乘的时间复杂度为 O(n)
Ⅲ.HashMap
代码:
/** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { let hash = {} for(let i = 0; i < nums.length; i++) { const num = nums[i] const dif = target - num if (hash[dif] !== undefined) { return [hash[dif], i] } else { hash[num] = i } } };
结果:
O(n)
对比发现,HashMap
方案较暴力法在速度上有明显的提高。
这里看到还有两种方式,咱们一一来尝试一下。
Ⅰ.使用数组替换 HashMap
代码:
/** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { let arr = [] for(let i = 0; i < nums.length; i++) { const num = nums[i] const dif = target - num if (arr[dif] !== undefined) { return [arr[dif], i] } else { arr[num] = i } } };
结果:
O(n)
跟使用 HashMap
性能差别不大。
Ⅱ.两次遍历 HashMap
代码:
/** * @param {number[]} nums * @param {number} target * @return {number[]} */ var twoSum = function(nums, target) { let res = new Map() for(let i = 0; i < nums.length; i++) { res.set(nums[i], i) } for(let i = 0; i < nums.length; i++) { const num = nums[i] const dif = target - num const idx = res.get(dif) if (idx !== undefined && idx !== i) { return [i, idx] } } };
结果:
O(n)
这里我作个了简单的校验:输入 [2,2,2], 4
,发现指望输出是 [0, 2]
,而不是 [0, 1]
,因此上面有几种解法实际上都过不了。若是是为了知足这种输出,个人推荐方案是 两次遍历 HashMap
。可是我我的是以为 HashMap
一次遍历是更合理的。
给出一个 32 位的有符号整数,你须要将这个整数中每位上的数字进行反转。
示例:
输入: 123 输出: 321 输入: -123 输出: -321 输入: 120 输出: 21
注意:
假设咱们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−2^31, 2^31 − 1]
。请根据这个假设,若是反转后整数溢出那么就返回 0。
从题干上来看,有几个要注意的点:
0
0
为首位须要去掉取天然数这里我有两种思路:
reverse
来反转再作天然数转换Ⅰ.数组反转
代码:
/** * @param {number} x * @return {number} */ var reverse = function(x) { const isNegative = x < 0 const rev = Number(Math.abs(x).toString().split('').reverse().join('')) if (isNegative && -rev >= -Math.pow(2, 31)) { return -rev } else if (!isNegative && rev <= Math.pow(2,31) - 1) { return rev } else { return 0 } };
结果:
O(1)
Ⅱ.取余
代码:
/** * @param {number} x * @return {number} */ var reverse = function(x) { const isNegative = x < 0 let res = 0 while(x !== 0) { res = res * 10 + x % 10 x = parseInt(x / 10) } if ((isNegative && res >= -Math.pow(2, 31)) || (!isNegative && res <= Math.pow(2,31) - 1)) { return res } else { return 0 } };
结果:
O(log10(n))
对比发现,使用取余的方式,性能上明显优于数组反转。
思路基本上都是这两种,未发现方向不一样的解法。
对比发现还有一些考虑不周的地方须要补全,好比说一些特殊值可直接返回,避免运算。这里我也作了一个简单的校验:输入 -0
,发现指望输出是 0
而不是 -0
。因此,我这里的代码作一些优化,以下:
/** * @param {number} x * @return {number} */ var reverse = function(x) { if (x === 0) return 0 function isOverflow (num) { return num < -Math.pow(2, 31) || (num > Math.pow(2,31) - 1) } if (isOverflow(x)) return 0 let res = 0 while(x !== 0) { res = res * 10 + x % 10 x = parseInt(x / 10) } return isOverflow(res) ? 0 : res };
判断一个整数是不是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是同样的整数。
示例:
输入: 121 输出: true 输入: -121 输出: false 解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。所以它不是一个回文数。 输入: 10 输出: false 解释: 从右向左读, 为 01 。所以它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
这道题的第一感受有点相似上一题整数反转的拓展,因此咱们从两个方向入手:
在写的过程当中须要考虑到去掉一些运算:把 <0
和 -0
排除,由于负数和 -0
必定不为回文数;一位正整数必定是回文数;除了 0
之外,尾数为 0
的不是回文数。
Ⅰ.转字符串
代码:
/** * @param {number} x * @return {boolean} */ var isPalindrome = function(x) { if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false; if (x < 10) return true; const rev = parseInt(x.toString().split('').reverse().join('')) return rev === x };
结果:
O(1)
这里有用到 ES6
的 Object.is
来判断是否为 -0
,固然 ES5
你也能够这么判断:
function (x) { return x === 0 && 1 / x < 0; // -Infinity }
可能有人会问不须要考虑数字溢出问题吗?
输入的数字不溢出,若是是回文数的话,那么输出的数字必定不溢出;若是不是回文数,无论溢出与否,都是返回 false
。
Ⅱ.取余
代码:
/** * @param {number} x * @return {boolean} */ var isPalindrome = function(x) { if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false; if (x < 10) return true; let div = 1 while (x / div >= 10) { // 用来找出位数,好比121,那么就找到100,获得整数位 div *= 10 } while(x > 0) { let left = parseInt(x / div); // 左侧数起 let right = x % 10; // 右侧数起 if (left !== right) return false; x = parseInt((x % div) / 10); // 去掉左右各一位数 div /= 100; // 除数去两位 } return true; };
结果:
O(log10(n))
这里看到一个更为巧妙的方式,只须要翻转一半便可。好比说 1221
,只须要翻转后两位 21
便可。
Ⅰ.翻转一半
代码:
/** * @param {number} x * @return {boolean} */ var isPalindrome = function(x) { if (x < 0 || Object.is(x, -0) || (x % 10 === 0 && x !== 0)) return false; if (x < 10) return true; let rev = 0; // 翻转的数字 while(x > rev) { rev = rev * 10 + x % 10 x = parseInt(x / 10) } return x === rev || x === parseInt(rev / 10); // 奇数的话须要去掉中间数作比较 };
结果:
O(log10(n))
综上,最推荐翻转一半的解法。
罗马数字包含如下七种字符: I, V, X, L,C,D
和 M
。
字符 | 数值 |
---|---|
I | 1 |
V | 5 |
X | 10 |
L | 50 |
C | 100 |
D | 500 |
M | 1000 |
例如, 罗马数字 2 写作 II
,即为两个并列的 1。12 写作 XII
,即为 X + II
。 27 写作 XXVII
, 即为 XX + V + II
。
一般状况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写作 IIII
,而是 IV
。数字 1 在数字 5 的左边,所表示的数等于大数 5 减少数 1 获得的数值 4 。一样地,数字 9 表示为 IX
。这个特殊的规则只适用于如下六种状况:
I
能够放在 V
(5) 和 X
(10) 的左边,来表示 4 和 9。X
能够放在 L
(50) 和 C
(100) 的左边,来表示 40 和 90。C
能够放在 D
(500) 和 M
(1000) 的左边,来表示 400 和 900。给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。
示例:
输入: "III" 输出: 3 输入: "IV" 输出: 4 输入: "IX" 输出: 9 输入: "LVIII" 输出: 58 解释: L = 50, V= 5, III = 3. 输入: "MCMXCIV" 输出: 1994 解释: M = 1000, CM = 900, XC = 90, IV = 4.
这道题有个比较直观的想法,由于特殊状况有限可枚举,因此我这里有两个方向:
Ⅰ.枚举特殊组合
代码:
/** * @param {string} s * @return {number} */ var romanToInt = function(s) { const hash = { 'I': 1, 'IV': 4, 'V': 5, 'IX': 9, 'X': 10, 'XL': 40, 'L': 50, 'XC': 90, 'C': 100, 'CD': 400, 'D': 500, 'CM': 900, 'M': 1000 } let res = 0 for(let i = 0; i < s.length;) { if (i < s.length - 1 && hash[s.substring(i, i + 2)]) { // 在 hash 表中,说明是特殊组合 res += hash[s.substring(i, i + 2)] i += 2 } else { res += hash[s.charAt(i)] i += 1 } } return res };
结果:
O(n)
Ⅱ.直接遍历
代码:
/** * @param {string} s * @return {number} */ var romanToInt = function(s) { const hash = { 'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000 } let res = 0 for(let i = 0; i < s.length; i++) { if (i === s.length - 1) { res += hash[s.charAt(i)] } else { if (hash[s.charAt(i)] >= hash[s.charAt(i + 1)]) { res += hash[s.charAt(i)] } else { res -= hash[s.charAt(i)] } } } return res };
结果:
O(n)
这里还看到一种方式,所有先按加法算,若是有前一位小于后一位的状况,直接减正负差值 2/20/200
。来看看代码:
Ⅰ.差值运算
代码:
/** * @param {string} s * @return {number} */ var romanToInt = function(s) { const hash = { 'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000 } let res = 0 for(let i = 0; i < s.length; i++) { res += hash[s.charAt(i)] if (i < s.length - 1 && hash[s.charAt(i)] < hash[s.charAt(i + 1)]) { res -= 2 * hash[s.charAt(i)] } } return res };
结果:
O(n)
换汤不换药,只是作了个加法运算而已,没有太大的本质区别。
综上,暂时没有看到一些方向上不一致的解法。我这里推荐字符串直接遍历的解法,性能最佳。
编写一个函数来查找字符串数组中的最长公共前缀。
若是不存在公共前缀,返回空字符串 ""。
示例:
输入: ["flower","flow","flight"] 输出: "fl" 输入: ["dog","racecar","car"] 输出: "" 解释: 输入不存在公共前缀。
说明:
全部输入只包含小写字母 a-z
。
这道题一看以为确定是须要遍历的题,无非是算法上的优劣罢了。我有三个方向来尝试解题:
Ⅰ.遍历每列
代码:
/** * @param {string[]} strs * @return {string} */ var longestCommonPrefix = function(strs) { if (strs.length === 0) return '' if (strs.length === 1) return strs[0] || '' const str = strs.shift() for(let i = 0; i < str.length; i++) { const char = str.charAt(i) for(let j = 0; j < strs.length; j++) { if (i === strs[j].length || strs[j].charAt(i) !== char) { return str.substring(0, i) } } } return str };
结果:
O(n)
Ⅱ.遍历每项
代码:
/** * @param {string[]} strs * @return {string} */ var longestCommonPrefix = function(strs) { if (strs.length === 0) return '' if (strs.length === 1) return strs[0] || '' let str = strs.shift() for(let i = 0; i < strs.length; i++) { while (strs[i].indexOf(str) !== 0) { str = str.substring(0, str.length - 1); if (!str) return '' } } return str };
结果:
O(n)
Ⅲ.分治
代码:
/** * @param {string[]} strs * @return {string} */ var longestCommonPrefix = function(strs) { if (strs.length === 0) return '' if (strs.length === 1) return strs[0] || '' function arrayToString (arr, start, end) { if (start === end) { // 说明数组中只剩一项了 return arr[start] } else { const mid = parseInt((start + end) / 2) const leftStr = arrayToString(arr, start, mid) const rightStr = arrayToString(arr, mid + 1, end) return getCommonPrefix(leftStr, rightStr) } } // 两个字符串取最长前缀 function getCommonPrefix(left, right) { const min = Math.min(left.length, right.length) for(let i = 0; i < min; i++) { if (left.charAt(i) !== right.charAt(i)) { return left.substring(0, i) } } return left.substring(0, min) } return arrayToString(strs, 0, strs.length - 1) };
结果:
O(n)
这里还看见使用二分法,跟分治仍是略有差别,是每次丢弃不包含答案的区间来减小运算量。
Ⅰ.二分法
代码:
/** * @param {string[]} strs * @return {string} */ var longestCommonPrefix = function(strs) { if (strs.length === 0) return '' if (strs.length === 1) return strs[0] || '' // 找到最短字符串长度 let minLen = 0 for(let i = 0; i < strs.length; i++) { minLen = minLen === 0 ? strs[i].length : Math.min(minLen, strs[i].length) } function isCommonPrefix (arr, pos) { const str = arr[0].substring(0, pos) // 取第一项的前一半 for(let i = 0 ; i < arr.length; i++) { if (arr[i].indexOf(str) !== 0) { return false } } return true } let low = 1 let high = minLen // 截取最大数量 while (low <= high) { const mid = parseInt((low + high) / 2) if (isCommonPrefix(strs, mid)) { // 若是前半段是 low = mid + 1 // 继续判断后半段 } else { high = mid - 1 // 前半段继续对半分继续判断 } } return strs[0].substring(0, (low + high) / 2) };
结果:
O(log(n))
具体状况具体分析,好比分治的算法也能够应用在快速排序中。我的比较推荐分治法和二分法求解这道题。
(完)
本文为原创文章,可能会更新知识点及修正错误,所以转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
若是能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork (转载请注明出处:https://chenjiahao.xyz)