目录

  • 引言
  • 题目
    • 1.两数之和

      • 题目
      • 解题笔记
    • 7.反转整数
      • 题目
      • 解题笔记
    • 9.回文数
      • 题目
      • 解题笔记
    • 13.罗马数字转整数
      • 题目
      • 解题笔记
    • 14.最长公共前缀
      • 题目
      • 解题笔记
    • 20.有效的括号
      • 题目
      • 解题笔记
    • 26.删除排序数组中的重复项
      • 题目
      • 解题笔记
    • 27.移除元素
      • 题目
      • 解题笔记
    • 35.搜索插入位置
      • 题目
      • 解题笔记
    • 38.报数
      • 题目
      • 解题笔记
    • 53.最大子序和
      • 题目
      • 解题笔记
    • 67.二进制求和
      • 题目
      • 解题笔记

引言

虽然说前端设计的算法复杂度并不高,但是像我这种懒龙,还是希望能通过算法优化来解决问题,并且升职加薪,调戏女媛,忽悠实习生的。所以学习算法成了我日常撩妹,偶尔装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             1V             5X             10L             50C             100D             500M             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.     12.     113.     214.     12115.     1112211 被读作  "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刷题记录相关推荐

  1. 算法记录 牛客网 leetcode刷题记录

    算法记录 & 牛客网 & leetcode刷题记录 解题思路 STL容器 常用算法模板 堆排序 插入排序 快速排序 BFS层序遍历 二叉树 JZ55 二叉树的深度 BST(binary ...

  2. LeetCode刷题记录8——605. Can Place Flowers(easy)

    LeetCode刷题记录8--605. Can Place Flowers(easy) 目录 LeetCode刷题记录8--605. Can Place Flowers(easy) 题目 语言 思路 ...

  3. wy的leetcode刷题记录_Day66

    wy的leetcode刷题记录_Day66 声明 本文章的所有题目信息都来源于leetcode 如有侵权请联系我删掉! 时间:2023-4-10 前言 目录 wy的leetcode刷题记录_Day66 ...

  4. LeetCode刷题记录15——21. Merge Two Sorted Lists(easy)

    LeetCode刷题记录15--21. Merge Two Sorted Lists(easy) 目录 LeetCode刷题记录15--21. Merge Two Sorted Lists(easy) ...

  5. LeetCode刷题记录14——257. Binary Tree Paths(easy)

    LeetCode刷题记录14--257. Binary Tree Paths(easy) 目录 前言 题目 语言 思路 源码 后记 前言 数据结构感觉理论简单,实践起来很困难. 题目 给定一个二叉树, ...

  6. LeetCode刷题记录13——705. Design HashSet(easy)

    LeetCode刷题记录13--705. Design HashSet(easy) 目录 LeetCode刷题记录13--705. Design HashSet(easy) 前言 题目 语言 思路 源 ...

  7. LeetCode刷题记录12——232. Implement Queue using Stacks(easy)

    LeetCode刷题记录12--232. Implement Queue using Stacks(easy) 目录 LeetCode刷题记录12--232. Implement Queue usin ...

  8. LeetCode刷题记录11——290. Word Pattern(easy)

    LeetCode刷题记录11--290. Word Pattern(easy) 目录 LeetCode刷题记录11--290. Word Pattern(easy) 题目 语言 思路 源码 后记 题目 ...

  9. LeetCode刷题记录10——434. Number of Segments in a String(easy)

    LeetCode刷题记录10--434. Number of Segments in a String(easy) 目录 LeetCode刷题记录9--434. Number of Segments ...

最新文章

  1. [置顶] Objective-C ,ios,iphone开发基础:命名规范
  2. python【力扣LeetCode算法题库】53- 最大子序和
  3. 【Deep Learning笔记】神经网络基础
  4. 算法一:求两个数的和
  5. spring-cloud-sleuth+zipkin追踪服务实现(一)
  6. c++ 线程池_基础篇:高并发一瞥,线程和线程池的总结
  7. $_POST[] name
  8. rest_framework09:自动生成接口文档(简略)
  9. DB2常用傻瓜问题1000问(之一)
  10. 重邮计算机导师评价,李章勇_重庆邮电大学研究生导师信息
  11. C++ STL 容器 string
  12. ajax常见特效,用ajax实现正在载入的特效-Ajax编程
  13. android打开xlsx文件,如何在Android中打开.xlsx文件?
  14. (8.1)基于牛顿-欧拉公式的动力学方程
  15. 深入解析MySQL索引原理
  16. Scala 函数式编程(一) 什么是函数式编程?
  17. Delayed Project(下)
  18. python3 简单爬虫实战|使用selenium来模拟浏览器抓取选股宝网站信息里面的股票
  19. 关于X509证书和密钥的概念
  20. C++基础(持续更新)

热门文章

  1. windows 设置 rocketmq环境变量_RocketMQ 开篇
  2. 实体与电商,有啥区别?
  3. 很多餐饮店都会倒闭,最主要的是有2大原因
  4. 经常用everything对硬盘有伤害吗?
  5. windows部署tomcat服务自动启动,同时解决服务无法启动的问题
  6. sql azure 语法_如何:Azure中SQL Server文件快照备份
  7. 如何使用PowerShell保护密码
  8. ssas 分层维度_通过SSAS维度层次结构增强数据分析
  9. python 目标直方图_深入了解扩展事件–直方图目标
  10. t–sql pl–sql_SQL串联正确完成–第1部分–可疑做法