算法进阶之Leetcode刷题记录

目录

引言

       虽然说前端设计的算法复杂度并不高,但是像我这种懒龙,还是希望能通过算法优化来解决问题,并且升职加薪,调戏女媛,忽悠实习生的。所以学习算法成了我日常撩妹,偶尔装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的方法,请看下图。

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~

猜你喜欢

转载自www.cnblogs.com/keepStudying/p/9951619.html
今日推荐