概述
目录
- 引言
- 题目
- 1.两数之和
- 题目
- 解题笔记
- 7.反转整数
- 题目
- 解题笔记
- 9.回文数
- 题目
- 解题笔记
- 13.罗马数字转整数
- 题目
- 解题笔记
- 14.最长公共前缀
- 题目
- 解题笔记
- 20.有效的括号
- 题目
- 解题笔记
- 26.删除排序数组中的重复项
- 题目
- 解题笔记
- 27.移除元素
- 题目
- 解题笔记
- 35.搜索插入位置
- 题目
- 解题笔记
- 38.报数
- 题目
- 解题笔记
- 53.最大子序和
- 题目
- 解题笔记
- 67.二进制求和
- 题目
- 解题笔记
- 1.两数之和
引言
虽然说前端设计的算法复杂度并不高,但是像我这种懒龙,还是希望能通过算法优化来解决问题,并且升职加薪,调戏女媛,忽悠实习生的。所以学习算法成了我日常撩妹,偶尔装X的关键。同时那种解题的高潮快感,还能让我忘记单身的烦恼。感觉学好算法的话,我的人生都能变得丰富多彩(美女环绕)。但是我光学不写,不能拿出来装X,岂不是失去了学习的乐趣,少了撩妹的渠道。所以,必须记录,必须写出来!没准睡醒一觉,就有妹子关注我了,嘿嘿嘿~
题目
1.两数之和
题目
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
解题笔记
这道题,我总共用时1小时。写出了2钟思路,3钟写法。首先上第一种思路,也是最容易被想到的思路。
1)思路1,写法1——暴力循环法。测试用时大概在160ms上下浮动。
function twoSum(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i,j];
}
}
}
}
这种写法,相信是大多数人脑子里蹦出的第一种写法。两次循环,再给索引加上一点冒泡排序的思想,大功告成。
2)思路1,写法2——隐性暴力循环法。测试用时大概在170ms上下浮动。
function twoSum(nums, target) {
for (let i = 0;i < nums.length;i++) {
let index = nums.indexOf(target - nums[i],i+1);
if(index !== -1) return [i,index];
}
}
这种写法,跟第一种暴力循环在方式和思想上是一样的。也是循环两次,只不过第二次是通过indexOf循环索引值。
实际用时的话,比暴力循环要慢上10ms左右,所以也就是看着代码少而已,性能并不高。
3)思路2,写法1——对象属性查找。测试用时大概在70ms上下波动,减少了一倍运行时间,最快用时64ms。
function twoSum(nums, target) {
let obj = {};
for (let i = 0;i < nums.length;i++) {
if (obj[nums[i]] !== undefined) {
return [obj[nums[i]],i];
}
obj[target - nums[i]] = i;
}
}
这种思路是通过将数组中的每个元素以{key : value} = {target - nums[index] : i}的形式保存在对象中,所以只要obj[nums[i]]存在,就说明target - nums[index] = nums[i],那么nums[index] + nums[i] = target就成立。
所以此时obj[nums[i]]对应的value值及当前的循环的i值,就是对应的目标的索引值,返回它们的数组,就是我们想要得到的结果。
优势在于只执行了一次遍历,虽然if判断也等同于在对象中遍历属性,但性能比for循环要高。而且,第二次的对象属性遍历,是一个由少增多的过程。所以只要不出现目标值位于数组两端的情况,对象遍历的次数是一定小于前两种方法的。(就算真要出现正好位于数组两端的情况,
也不能更换暴力循环法,必须保持代码的装X优雅)
7.反转整数
题目
给定一个 32 位有符号整数,将整数中的数字进行反转。
示例 1:
输入: 123
输出: 321
示例 2:
输入: -123
输出: -321
示例 3:
输入: 120
输出: 21
注意:
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31,
2^31 − 1]。根据这个假设,如果反转后的整数溢出,则返回 0。
解题笔记
这道题用了多长时间我已经忘了,因为试了很多种方法和不同写法,但实际在用时上,差距都没有实质上的飞跃,最快能到84ms,最慢也不过在120ms前后。所以这道题我着重对各种方法进行了理解,而对于性能上并没有太多的关注。
1)数组方法
var reverse = function(x) {
let arrX = x.toString().split(""),
newX = arrX.reverse().join("");
return newX[newX.length - 1] === "-" ? Number("-" + parseInt(newX)) >= Math.pow(-2, 31) && Number("-" + parseInt(newX)) <= Math.pow(2, 31) ? Number("-" + parseInt(newX)) : 0 : Number(newX) >= Math.pow(-2, 31) && Number(newX) <= Math.pow(2, 31) ? Number(newX) : 0;
}
这种方法是我最先想到的方法了,将数字转换成字符串,再通过split()将字符串分割成数组,然后用数组的reverse()倒排序,再将reverse()后的数组join()成字符串,最后判断翻转后的数组最后一位是不是负号(-),再判断是不是在规定范围内,返回结果。
最先想到的往往也最悲催就是了……
2)数组方法·改
var reverse = function(x) {
let min = Math.pow(-2, 31),
max = Math.pow(2, 31) - 1;
let str = Math.abs(x).toString().split("").reverse().join("");
let result = x > 0 ? +str : -str;
return result >= min && result <= max ? result : 0;
}
核心方法还是split()、reverse()、join()。改进在于,通过Math.abs()直接取绝对值,单纯对数字进行反转操作,然后通过判断参数x本来的正负号,再给反转后的数字添加正负号,最后判断是否在规定范围内,返回结果。
在原数组方法的基础上,避免了正负号对反转操作的影响,并且通过判断参数的正负号更直接。
3)官方提示算法
var reverse = function(x) {
let min = Math.pow(-2, 31);
let max = Math.pow(2, 31) - 1;
let newX = Math.abs(x);
let len = newX.toString().length;
let arr = [];
for (let i = 0; i < len; i++) {
arr.push(newX % 10);
newX = Math.floor(newX / 10);
}
result = arr.join("");
if (x > 0 && +result <= max) return +result;
if (x < 0 && -result >= min) return -result;
return 0;
}
官方提示的方法,用到的是取余的操作。实现思路就是通过对10取余,得到个位数的数值。然后再将结果真的除以10,并取整。实现的思路和倒计时相似,通过取余再取整,将数值不断的减少一位,然后获得个位数数值。
在我第一次实现官方提示算法时,还是利用了数组去操作,没有用到数据操作的特性,所以从方法上看,思路是优化了,但是写法还没有优化。
4)官方提示算法·改
var reverse = function(x) {
let min = Math.pow(-2, 31);
let max = Math.pow(2, 31) - 1;
let len = x.toString().length;
let result = 0;
for (let i = x > 0 ? len - 1 : len - 2; i >= 0; i--) {
result += x % 10 * Math.pow(10, i);
x = x > 0 ? Math.floor(x / 10) : Math.ceil(x / 10);
}
if (result > 0 && result <= max) return result;
if (result < 0 && result >= min) return result;
return 0;
}
其实算不上是优化,只是又换了种更装X的写法。直接将取余后的数再乘以相应的位数对应的10的i次方,然后相加,达到直接将数值反转的结果。麻烦点就是正数与负数的取整不同。
5)性能最佳写法
var reverse = function(x) {
let min = Math.pow(-2, 31);
let max = Math.pow(2, 31) - 1;
let strX = Math.abs(x).toString();
let result = "";
for (let i = strX.length - 1; i >= 0; i--) {
result += strX[i];
}
if (x > 0 && +result <= max) return +result;
if (x < 0 && -result >= min) return -result;
return 0;
}
写完官方提示方法之后,实在想不出其他办法了,就去看了下最佳的写法。其实最佳写法跟官方写法,在性能上是不分上下的,差距非常小。
但最佳方法应用到了字符串拼接的特性,就让人眼前一亮。
通过反向遍历,将字符串倒拼接,就实现了反转字符串的效果。最后再判断正负及数值范围,返回结果。
9.回文数
题目
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
解题笔记
这道题,我总共用时1小时,只写出了纯数学方法的思路。经测试,最佳时间在前99%里面,所以我开始还比较肯定我的思路和写法。直到我看了最佳写法,感受到了暴击,思路跟大神还是没法比。
1)我的方法——最佳用时264ms
var isPalindrome = function(x) {
if (x < 0) return false;
if(x >=0 && x < 10) return true;
const len = Math.floor(Math.log10(x) + 1);
const count = len % 2 ? Math.floor(len / 2) : len / 2,
flag = len % 2 ?
Math.ceil(len / 2) : len / 2,
target = Math.floor(x / Math.pow(10,flag));
let num = 0;
for (let i = count;i > 0 ;i--) {
num += (x % 10) * Math.pow(10,i - 1);
x = Math.floor(x / 10);
}
if (num === target) return true;
return false;
}
从我的写法上面可以看出,我是受了前面几道题的方法影响的,尤其是受了第7题反转整数的影响;
这道题在思路上,我的方向是没错的,只需要取对称长度,然后对后一半的数字进行反转,与前一半的数值去进行比较,如果相等就是回文数;
但是在方法上,我还是没有脱离字符串和数组的方法逻辑——取长度,然后根据长度循环。对传入数字参数取余后再乘以相应的位数值,将参数减一位,通过循环实现数字的反转。这完全就是第7题官方思路的实践;
唯一的难点在于对数字长度的获取,这个地方卡了半小时左右,最后想到通过log10的方法获取长度。只要长度知道了,后边的操作就比较简单了;
总结这一方法,思路还是太固定了。可以说没有扩大思考空间。虽然没有使用字符串和数组方法,但是思路上还是太拘泥于惯性。可以说即没有方法上的突破,也没有写法上的突破。就是死板的按照条件实现了结果。
没想到的点是:1)其实target变量就是循环后的x;2)可以通过判断len的奇偶性,直接操作循环后的x;3)循环次数可以是一个值,不必分奇偶性。
所以count、flag、target其实都是没必要的。
2)我的方法·改
var isPalindrome = function (x) {
if (x < 0 || x % 10 === 0 && x !== 0) return false;
if (x >= 0 && x < 10) return true;
let length = parseInt(Math.log10(x));
let num = 0;
for (let i = parseInt((length - 1) / 2); i >= 0; i--) {
num += x % 10 * Math.pow(10, i);
x = parseInt(x / 10);
};
length % 2 ? null : x = parseInt(x / 10);
if (num === x) return true;
return false;
}
3)最佳性能写法——最佳用时236ms
var isPalindrome = function(x) {
if(x < 0 || ( x !==0 && x % 10 === 0)) return false;
if(x > = 0 && x < 10) return true;
let intNum = 0;
while (x > intNum){
intNum = intNum * 10 + x % 10;
x = parseInt(x / 10);
}
return x === intNum || x===parseInt(intNum / 10);
}
虽然说性能上的差别说不上质的提升,但是思路上真是让我感受到了人与人之间的差距;
首先:
在开始的if判断上,只要<0或者是10的倍数,就判定不是回文;
然后:
对于循环的定义:比较出彩的地方是对两个数字的循环操作;
因为对循环次数不确定,所以用while来循环;然后通过一个数字位数相加,另一个数字位数相减来控制循环结束;
如果是回文数字,只会出现两种情况——1)num === intNum;此时参数x的长度是偶数;2)num < intNum;此时参数x的长度是奇数,当intNum比num多一位时,循环肯定停止;
而至于不是回文数的情况,不论奇偶性,只要出现intNum的位数比num多一位,循环就会停止。所以循环的次数,最多就是x参数长度的一半再+1。
再来看循环内部的操作:
首先intNum * 10,实际上是给intNum当前值增加一位,将个位数的位置腾出来给num % 10,同时也给intNum其他数字往前进一位。
然后num%10则是把num的个位数取出来,通过intNum * 10的进位操作,num&10的个位数,将调整到它应该在的位置。
再然后将num / 10并取整,等同于将num长度-1,不断抹除个位数的数字。这实际上就是反转num。对数字反转的思路上,我想到了,但是写法上,真是差距太大了。
最后: 如果是回文数,并且x的长度是偶数,则num === intNum。如果是x的长度是奇数,那么intNum会比num多出处于x中间的一位数,这个数位于intNum的尾部。 所以将intNum / 10取整,抹除intNum的最后一位数字后,再进行比较。
如果不是回文数,则不论x的奇偶性,两种情况都有可能出现,但是都不可能相等。
这种方法,相比于我自己想出来的方法,在循环、数字反转以及数字长度的操作上,完全是碾压性的。自己在算法上的提升,看来还有十分巨大的空间。
13.罗马数字转整数
题目
罗马数字包含以下七种字符: 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 的范围内。
示例 1:
输入: "III"
输出: 3
示例 2:
输入: "IV"
输出: 4
示例 3:
输入: "IX"
输出: 9
示例 4:
输入: "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
示例 5:
输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4
解题笔记
这道题,我总共用时1小时,只写出了纯数学方法的思路。经测试,最佳时间在前99%里面,所以我开始还比较肯定我的思路和写法。直到我看了最佳写法,感受到了暴击,思路跟大神还是没法比。
1)向后比较法
var romanToInt = function(s) {
const obj = {
"I" : 1,
"V" : 5,
"X" : 10,
"L" : 50,
"C" : 100,
"D" : 500,
"M" : 1000
}
let result = 0;
for (let i = 0;i < s.length;i++) {
if (obj[s[i]] < obj[s[i + 1]]) {
result += obj[s[i + 1]] - obj[s[i]];
i++;
}
else {result += obj[s[i]]};
}
return result;
}
动手之前,先找规律。
其实题干已经解释的很清楚了,特殊情况分为3类。而根据示例中给出的种种个例,可以找出其中的规律——除了3种特殊情况外,罗马数字从左到右是数值是依次减小的。也就是正常写法的话,按顺序是1000-500-100-50-10-5-1,即M-D-C-L-X-V-I。
唯独不按照大小顺序书写的情况,就是那特殊的3大类6种情况中的3种——也就是4,40,400的情况。
那么由最后的实例5,可以明显看出数字转换的计算方法——根据数值逐项累加。而遇到特殊的3种情况时,则将紧邻的2个字母看做是一个(通过i++跳过循环实现),而在数值上,则是后一项减去当前项。
最后返回累加后的结果。
2)最佳性能写法
var romanToInt = function(s) {
if (s === null || s === undefined || s.length === 0) {
return 0;
}
var romanNumber = {
"I":1,
"V":5,
"X":10,
"L":50,
"C":100,
"D":500,
"M":1000
}
let total = 0;
let prevVal = Number.MAX_VALUE;
for (let i = 0; i < s.length; i++) {
let currentNum = romanNumber[s[i]];
total = total + currentNum;
if (currentNum <= prevVal) {
prevVal = currentNum;
}
else {
total = total - (2 * prevVal)
}
}
return total;
}
思路上与向后比较法是一个思路,只是写法稍有不同。
首先在函数最前加了特殊情况的判断。
然后,在比较上也是一个思路,只不过最佳写法这里用了一种效率更高的写法。将一个罗马数字首位对应值赋值给一个变量,如果当前值比这个变量的值小,就再将当前值赋值给变量,以此类推,如果出现当前值大于变量值的情况,就减去当变量值的2倍。
实际上就是比较上一个值和当前值哪个大,如果当前值大,说明出现了4,40,400的3钟情况。
最后因为上一次循环已经将IV,XL,CD前面的数值加上了,所以要减去2倍的变量值。比如:10 + 50 - 10 - 10 = 40。
最后返回结果。
14.最长公共前缀
题目
编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入: ["flower","flow","flight"]
输出: "fl"
示例 2:
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
说明:所有输入只包含小写字母 a-z 。
解题笔记
这道题,我总共用时1小时,在最开始的思路上,方向就找对了。只是写法上还是不如最佳性能的方案。
1)单循环重复法——最佳用时64ms
var longestCommonPrefix = function(strs) {
if (!strs.length) return "";
if (strs.length === 1) return strs[0];
let str = strs[0];
for (let i = 1;i < strs.length;i++) {
if (!strs[i]) str = "";
if (strs[i].indexOf(str) === 0) continue;
str = str.slice(0,--str.length);
if (!str) break;
i--;
}
return str;
}
通过不停地修改、测试,尝试出来的一种写法;
核心思路是先找出数组第一项与第二项的公共字符串。然后往下逐项匹配。通过indexOf检查字符串str是否处于当前数组项的开头位置,然后进行str字符串删除末尾字符的操作,对数组进行循环;
之所以叫单循环重复法,是因为每执行一次str字符串的末尾删除操作,就将i--,重复循环当前数组项,直到符合条件为止;
与最佳的提交代码,在思路上一致;
2)最佳性能写法
var longestCommonPrefix = function(strs) {
if (strs.length == 0) return "";
let str = strs[0];
for (let i = 1; i < strs.length; i++)
while (strs[i].indexOf(str) != 0) {
str = str.substring(0, str.length - 1);
if (str.length == 0) return "";
}
return str;
}
思路一致,相比于单循环重复法,缺少了特殊条件的判断,但是利用了while的循环次数不确定的特性,自我感觉是各有千秋,平分秋色。
20.有效的括号
题目
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
解题笔记
这道题目前看来是碰到的第一道,没有独立写出答案,最后靠着官方提示才完成的题目。断断续续写了两天写出了两种错误思路,然后看了提示,最终写出正确答案。
1)错误写法
var isValid = function(s) {
const obj = {
"(" : ")",
"{" : "}",
"[" : "]"
}
let flag = true;
for (let i = 0;i < s.length;i++) {
if (obj[s[i]] === undefined) break;
if (!s[i]) continue;
if (obj[s[i]] === s[i + 1]) {
i++;
continue;
}
if (obj[s[i]] === s[s.length - i - 1]) continue;
flag = false;
}
return flag;
}
写出第一个答案时,单纯的我,认为括号的匹配只有两种情况——相邻或者字符串首尾对称。最后挂掉……
2)错误写法·plus
var isValid = function(s) {
if (!s) return true;
const obj = {
"(": ")",
"{": "}",
"[": "]"
}
if (obj[s[0]] === undefined) return false;
let len = s.length;
while (s.length === len) {
if (!s[0]) {
s = s.slice(1);
len -= 1;
} else if (s.length === 1) {
return false;
} else {
let index = 1;
while (len === s.length) {
if (s[index] === undefined) return false;
if (s[index] === obj[s[0]]) {
s = index === 1 ? s.slice(2) : s.slice(1, index) + (s[index + 1] ? s.slice(index + 1) : "");
}
index += 2;
}
len -= 2;
}
}
return !s;
}
在有了第一个错误之后,聪明的我灵光一闪,构思了另一种思路。这次我觉得匹配模式不应该只有相邻和首尾对称,应该是相隔偶数项(包括0)。并且找到一对就拆散一对删除一对。为此我不惜又在循环了加入了一个循环,通过index+=2的方式,隔2项查找。最后通过s是否为空字符串来进行判断。
这次一定可以成功,哼哼哼……扑街……
"[([]])"这种错误情况无法识别……
3)官方提示写法
var isValid = function(s) {
if (!s) return true;
if (s.length === 1 && s[0] !== " ") return false;
const obj = {
"(": ")",
"{": "}",
"[": "]"
}
const arr = [];
for (let i = 0;i < s.length;i++) {
if (s[i] === " ") continue;
if (obj[arr[arr.length - 1]] === s[i]) {
arr.pop();
continue;
}
arr.push(s[i]);
}
return !arr.length;
}
看完官方提示之后,我觉得我的第二种思路其实已经接近正确写法,但可惜就差临门一脚,最终还是不得入;
思路的最终导向,还是回到对称本身上;
往数组arr中push的元素,最终会以同样的顺序再被pop掉,如果arr数组为空,那么说明字符串是符合输入规则的;
4)最佳性能写法
var isValid = function(s) {
if (s !== " " && s.length === 1) return false;
if (!s || s === " ") return true;
const obj = new Map();
obj.set(")","(");
obj.set("]","[");
obj.set("}","{");
const arr = [];
for (let i of s) {
if (i === " ") continue;
if (!obj.has(i)) {
arr.push(i);
}
else {
if (obj.get(i) !== arr[arr.length - 1]) return false;
arr.pop();
}
}
return !arr.length;
}
思路不变,只是用到了新的对象Map以及for-of循环,可以说是充分利用了最新的特性。
对于Map对象,在写这道题前,并没有接触到过,关于Map的方法,请看下图。
26.删除排序数组中的重复项
题目
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
解题笔记
总共用时1个半小时,写出两种方法,其中第二种方法为最佳写法。
1)对象循环法
var removeDuplicates = function(nums) {
const obj = new Map();
for (let i of nums) {
if (!obj.has(i)) {obj.set(i,i)};
}
nums.splice(0,obj.size,...Array.from(obj.keys()))
return obj.size;
}
通过Map对象配合for-of循环遍历数组,将不重复的值添加进对象;
然后通过splice方法,将nums数组头部元素替换成Map对象的值;
用到了Array.from转数组,以及扩展运算符这两种语法糖;
2)最佳性能写法
var removeDuplicates = function(nums) {
let index = 0;
for (let i = 0;i < nums.length;i++) {
if (nums[i] !== nums[index]) {
nums[++index] = nums[i]
}
}
return index + 1;
}
通过声明一个index变量存储索引;
因为index初始值为0,所以nums[index]的初试值就是nums[0];
通过遍历nums数组,只要跟nums[index]的值不相同,那就让nums[index]的下一值,等于这个与nums[index]不相等的值;
核心思想用到了去重以及数组赋值;
3)最佳性能写法·官方答案
var removeDuplicates = function(nums) {
let index = 0;
nums.forEach(n => {
if (nums[index] !== n) {
nums[++index] = n;
}
})
return index + 1;
}
用forEach减少了点代码量,更有bigger。
27.移除元素
题目
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
说明:为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
解题笔记
这道题比较简单,总共用时半小时,写出最普通的写法,后来看了官方统计数据,写法的思路还是比较单一的,就是O(n)遍历。
1)通常写法
var removeElement = function(nums, val) {
for (let i = 0;i < nums.length;i++) {
if (nums[i] === val) {
nums.splice(i,1),
i--;
}
}
};
思路比较简单,遍历nums数组,如果跟val值相等的话,就通过splice方法删除。
2)通常写法·plus
var removeElement = function(nums, val) {
for (let i = nums.length - 1;i >= 0;i--) {
if (nums[i] === val) { nums.splice(i,1); }
}
};
相比较于上一种写法,增加了一点小思路。如果倒序查找的话,因为查找nums元素时是left-to-right的,所以就算删除了元素,也不需要再对索引值进行操作。
3)冒泡查找
var removeElement = function(nums, val) {
let i = 0;
let j = nums.length - 1;
while (i <= j) {
if (nums[i] === val) {
[nums[i],nums[j]] = [nums[j],nums[i]]; //位置互换
j--;
}
else {
i++;
}
}
};
这种写法没有对原数组进行删除,而是将重复值都抛到数组项的末尾,有点类似于冒泡排序与数组去重时的位置互换思想。
再循环次数上,这3钟方法没有区别,所以性能上难分伯仲。解题思路上,由于题目比较简单,也没有出彩的地方。所以从题目角度看,这道题价值不大。
35.搜索插入位置
题目
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
解题笔记
这道题难度不大,写法也比较单一,通过率很高。总共用时1小时,最终结果为性能最佳写法之一。
1)循环计数
var searchInsert = function(nums, target) {
let count = 0;
while (nums[count] < target) {
count++;
}
return count;
}
这是我最终提交的答案,思路很简单。就不过多解释了
2)循环数组
var searchInsert = function(nums, target) {
for (let i=0; i<nums.length; i++){
if (nums[i] === target || nums[i] > target) {
return i;
}
}
return nums.length;
}
这种应该是最容易被想到的写法,切着题干的描述
3)快排思路
let searchInsert = function(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
let mid = ~~((left + right) / 2);
if (nums[mid] === target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
利用的快排算法的思想,不停取中间值,缩小范围,最终定靶。
38.报数
题目
报数序列是一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:
1.
1
2.
11
3.
21
4.
1211
5.
111221
1 被读作
"one 1"
("一个一") , 即 11。
11 被读作 "two 1s" ("两个一"), 即 21。
21 被读作 "one 2",
"one 1" ("一个二" ,
"一个一") , 即 1211。
给定一个正整数 n(1 ≤ n ≤ 30),输出报数序列的第 n 项。
注意:整数顺序将表示为一个字符串。
示例 1:
输入: 1
输出: "1"
示例 2:
输入: 4
输出: "1211"
解题笔记
用时3小时,耗时不在思路上,主要在写法与自我怀疑上。总觉得应该会有规律,但是看到最后各种答案的时候,心里还是万草泥马奔腾的。整体看,这道题思路比较单一,而且写法暴力,写完不是很过瘾。
1)双循环
var countAndSay = function(n) {
let arr = ["1"];
let str = "1";
let count = 1;
for (let i = 0;i < n - 1;i++) {
let copy = "";
for (let j = 0; j < str.length; j++) {
if (arr[0] === str[j + 1]) {
count++;
continue;
}
copy += count + arr[0];
arr = [str[j + 1]];
count = 1;
}
str = copy;
arr = [str[0]];
}
return str;
}
我在写的时候,主要的卡点就在第一个字符和最后一个字符的判断上。主要思考点在——1)如果第一个字符就是个单独的字符,那么该怎么才能返回第一个字符的报数情况,然后继续判断第二个;2)最后一个循环时,如何把最后的报数添加到字符串中;
比较绕的地方就是n - 1、数据的比较、count的初始化及计数、以及arr的赋值:
首先n - 1,循环少一次的逻辑。 因为初始化,给str赋值了1,所以n = 1的情况是跳过的, 通过初始化已经实现。那么整体的循环就随之少了1次;
然后是数据的比较。 如果跟字符串当前值进行比较的话,那么count的初始化不好操作。因为count的初始化会出现在两个位置,一个是字符串的首位(即首位与第二位不相等的情况)、另一个出现不相等值,需要停止计数时。 这两种情况的本质区别是第一种是相等判断,第二种是不等判断。
第一种情况因为出现在字符串的首位,如果跟当前索引值比较,那么肯定是要判断相等的,这样如果初始化count值是0的话,才能通过+1来计数。那么为什么count不直接初始化为1呢? 因为从写法逻辑上来看,每判断一次相等,count应该+1, 所以从首位的情况看,count的初始化值,应该为0,那么这就与第二种情况矛盾了;
第二种情况的出现,要执行两步操作,首先将之前的计数情况插入到字符串中,给arr重新赋值,然后初始化count。这里就需要注意了,因为比较的是当前的索引值,所以必须将count初始化为1,因为本次已经是下一循环的第一次了。
而如果count初始化仍为0的话,通过 --操作,再执行一次当前循环,又耗费了性能。不难看出,跟当前索引值进行比较,实现起来过于复杂。 所以,通过跟当前索引的下一值进行比较,达到统一的效果。并且最后一次的判断必定为false,可以执行添加的操作;
再然后是count的初始化及计数。 因为是与当前索引值的下一个值进行比较,所以实际上每次的查找都是少了一次的。而且如果出现当前值仅有一个的情况下,相等判断必为flase,而对当前值的报数又为1。所以在这种逻辑下,count的初始化值必须为1,每次判断相等时,count就+1;
最后是arr的赋值。 分两种情况——1) 是字符串循环内的赋值;2) 是对于报数次数循环内的赋值。首先是第一种, 对于字符串循环内的赋值,因为比较的是当前索引值的下一项,而且count计数为1。所以每次一个count的判断周期结束,都将arr的赋值为下一个要计数的字符值;第二种是报数次数循环内的赋值。 因为报数循环每循环一次,意味着str更新了一次,那么将从首位开始循环。那么根据逻辑,每个count计数开始时,当前索引值都是跟数组值相等的, 所以每次报数循环结束,都将arr赋值为新str的首位(即str[0]);
对于字符串的初始化与赋值,逻辑简单,不再过多赘述;
2)双函数
var countAndSay = function(n) {
let str = "1";
for (let i = 2; i <= n; i++) {
str = saySomething(str);
}
return str;
};
var saySomething = function (s) {
s += '';
let str = "";
let j;
for (let i = 0; i < s.length;) {
j = i + 1;
while (s[i] == s[j]) {
j++;
}
str += ((j - i) + s[i]);
i = j;
}
return str;
}
双函数与双循环,思路都是一样的,只是拆分成2个函数
3)暴力法
暴力法就不上代码了,太长了。
因为限定了输入范围,所以最快的写法就是把所有的结果都写在了一个数组里,数组第一项为空,然后直接返回以传入参数为索引的对应值。
可以说是相当暴力了……哈哈哈
53.最大子序和
题目
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
解题笔记
断断续续写了大概两天,最后还是看了别人的答案……有点伤。
1)最佳性能写法
var maxSubArray = function(nums) {
let result = Number.MIN_SAFE_INTEGER;
let num = 0;
// nums.forEach(function(val) {
//
num += val;
//
result = Math.max(result,num);
//
if (num < 0) num = 0;
// })
// for (let i in nums) {
//
num += nums[i];
//
result = Math.max(result,num);
//
if (num < 0) num = 0;
// }
// for (let val of nums) {
//
num += val;
//
result = Math.max(result,num);
//
if (num < 0) num = 0;
// }
for (let i = 0;i < nums.length;i++) {
num += nums[i];
result = Math.max(result,num);
if (num < 0) num = 0;
}
return result;
}
整个的思路是围绕两个点——全局最大值和局部最大值。
全局最大值result的确立:
首先,每次循环都将局部值num加上数组nums的当前值,然后将全局最大值result与局部值num进行比较,取最大值;
这样操作,首先保证了全局最大值的连续性——因为num是连续相加的;
其次保证了全局最大值在连续性上是最大的,因为一旦局部值num变小,全局值result将不再变化,直到局部值再次超过全局最大值;
局部(最大)值num的确立:
局部值num相对于全局最大值result,在逻辑上稍微难理解一点。关键点在于任何值加上负数都是变小的;
所以一旦局部值num为负时,不管下次循环的数值是多少,两个值的相加,对于下个值来讲,都是变小的;
所以不管下次循环的数值是什么,之后再连续求和时,都不需要再加上本次的局部值num。此时可以理解为,结束了一次局部最大值查找;
所以,一旦局部值num出现负值的情况,就将局部值num重新赋值为0,开始下一次的局部最大值确立;
通过全局最大值与局部最大值的不断比较,最后就得到了想要的结果。
最后通过测试,for循环的性能是大于其他遍历方法的!
67.二进制求和
题目
给定两个二进制字符串,返回他们的和(用二进制表示)。
输入为非空字符串且只包含数字 1 和 0。
示例 1:
输入: a = "11", b = "1"
输出: "100"
示例 2:
输入: a = "1010", b = "1011"
输出: "10101"
解题笔记
用时2小时,停留在一种思路上,最后没能突破。
1)所有情况判断法
var addBinary = function(a, b) {
let result = "";
let num = 0;
let i = a.length - 1,
j = b.length - 1;
while (a[i] || b[j]) {
if (!a[i]) {
if (num) {
if (+b[j] + num === 2) {
result = "0" + result;
num = 1;
} else {
result = (+b[j] + num) + "" + result;
num = 0;
}
j--;
continue;
}
result = b[j] + result;
j--;
continue;
}
if (!b[j]) {
if (num) {
if (+a[i] + num === 2) {
result = "0" + result;
num = 1;
} else {
result = (+a[i] + num) + "" + result;
num = 0;
}
i--;
continue;
}
result = a[i] + result;
i--;
continue
}
if (a[i] === "0" && b[j] === "0") {
if (+a[i] + +b[j] + num === 0) {
result = "0" + result;
} else {
result = "1" + result;
num = 0;
}
i--;
j--;
continue;
}
if (a[i] !== "0" || b[j] !== "0") {
if (+a[i] + +b[j] + num === 2) {
result = "0" + result;
num = 1;
} else if (+a[i] + +b[j] + num === 3) {
result = "1" + result;
num = 1;
} else {
result = "1" + result;
}
i--;
j--;
continue;
}
}
if (num === 1) return "1" + result;
return result;
}
这是我第一次提交的答案,由于判断直接,毫无技巧,尝试了不知道多少次,可以说这个写法是相当暴力了…
核心思路遵循了二进制的计算方法,然后把所有情况进行判断计算;
二进制的计算方法,百度一下~
2)所有情况判断法·plus
var addBinary = function(a, b) {
let result = "";
let num = 0;
let i = a.length - 1,
j = b.length - 1;
while(i >= 0 || j >= 0 || num) {
let sum = ~~a[i] + ~~b[j] + num;
if (sum > 1 && sum < 3) {
sum = 0;
num = 1;
}
else if (sum === 3) {
sum = 1;
num = 1;
}
else num = 0;
result = sum + result;
i--;
j--;
}
return result;
}
相比于第一次的写法,优化了判断机制,利用了 ** ~~ 计算符对于字符串中字符不全为数字的情况,返回0的特性。这样的话,当i或j<0时,通过 ~~ a[i]或 ~~ **b[j]将返回0;
在while判断中加入了对num的判断,这样当进行最后一次字符串的计算时,将根据num的值选择是否再执行一次循环,加上最后num的值;
尴尬的地方是,虽然写法优化了,但是性能降低了……
3)最佳性能写法
var addBinary = function(a, b) {
let s = '';
let c = 0, i = a.length-1, j = b.length -1;
while(i >= 0 || j >= 0 || c === 1) {
c += i >= 0 ? a[i--] - 0 : 0;
c += j >= 0 ? b[j--] - 0 : 0;
s = ~~(c % 2) + s;
c = ~~(c/2);
}
return s;
}
看大神的写法,总会产生一种自我怀疑,我这脑子是怎么长的??
最佳写法将二进制的进位值与两个字符串值的求和合并为一个变量,通过~~操作符配合取余%以及/操作,判断了求和值及进位值,数学方法用的真是666~
转载于:https://www.cnblogs.com/keepStudying/p/9951619.html
最后
以上就是负责耳机为你收集整理的算法进阶之Leetcode刷题记录引言题目的全部内容,希望文章能够帮你解决算法进阶之Leetcode刷题记录引言题目所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复