• 算法进阶之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~

  • 相关阅读:
    C语言指针专题——如何理解指针
    C语言指针专题——序
    Java 第14章 字符串
    Java截图笔记
    Java 第五章 循环结构1
    Java 第六章 循环结构2
    Java 第13章 带参数的方法
    Java 第11章 类的无参方法
    Java 第四章 选择结构2
    Java 第三章 选择结构1
  • 原文地址:https://www.cnblogs.com/keepStudying/p/9951619.html
Copyright © 2020-2023  润新知