(前端)算法学习笔记

442 阅读19分钟

数组部分

两数求和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标
大概是刷题入门的第一关了 , 我相信百分之九十的人初见算法的魅力 , 都是在这道题 .

var twoSum = function(nums, target) {
    for(let i = 0; i<nums.length -1 ;i++){
        for(let j = i+1; j < nums.length ;j++){
            if(target - nums[i] === nums[j]){
                return [i,j]
            }
        }
    }
};

这是我第一次写的玩意 , 如果你也是这样 , 那你点击执行代码按钮后 , 你会发现转了贼久才出结果 . 反正对是对了吧 . 没啥感觉 .

image.png 但如果你有心看了题解 , 你就会和我一样感受到算法的魅力 .
双重for循环的确没什么操作 . 就像你和传说中的邻居家的孩子一样 , 别人的题解只能用惊艳二字来形容 .
我内层用了for循环 , 更耗时间 , 别人则拿空间换时间 . 所以开始模仿 :

var twoSum = function(nums, target) {
    const res = {}
    const len =nums.length
    for(let i =0; i<len ;i++){
        if(res[target - nums[i]] !== undefined){
                return [res[target-nums[i]],i]
        }
        res[nums[i]] = i 
    }
};

image.png 这次比上次好看多了 , 格局打开了 . 用哈希表来存 , 或者说 , 我需要一个结构来存放我需要的索引号 , 索引号与值也得有对应关系 . 这个结构实现后就是哈希表了 .
通过看别人题解 , 会发现大家都会使用一个变量来存nums.length 而不是在for循环中写 nums.length 所以我也学到了这个 . 然后判断条件可以直接在我们建立的数据结构中查找是否有存在的值 . 如果不存在这个值 , 那就把这个值存入我们的res 以便下次循环时使用 .

删除有序数组重复项

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成

var removeDuplicates = function(nums) {
    let j = 0;
    const len = nums.length
    for (var i = 1; i<len;i++){
        if(nums[i] != nums[i-1]){
            j++;
            nums[j] = nums[i];
        }
    }
    return j+1;
};

image.png 经过第一题 , 我已经很讨厌循环套循环了 . 不过如果实在没有思路的时候 , 为了完成题 ,我还是很乐意用for循环的 .
我的思路是: 既然原地删除数组 , 还是有序数组 , 那我直接搞两个变量 , 对数组的前后两个值进行比较 , 如果后一项不等于前一项 , 那就 i++ , j++ 再比较后边的 . 当遇到相等项的时候 对前一项不做处理 , 直接用更后边的项替换重复的这一项 , 就可以达到去重的目的了 .
在查看题解后 , 才知道这是双指针 , 不过题解总有神人 . 这是题解中看到的大佬写的 :

image.png

class Solution {
    public int removeDuplicates(int[] nums) {
        int index=1;
        for(int i=1;i<nums.length;i++){
            // 重复元素就跳过
            if(nums[i]==nums[i-1])continue;
            else{
            // 不是重复元素就将该元素赋值给index位置,再向右移动指针
                nums[index++]=nums[i];
            }
        }
        return index;
    }
}

作者:chen-yi-qian-c
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shuang-zhi-zhen-by-chen-yi-qian-c-u40v/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

学无止境 . 多学习前辈的知识 , 争取做站在巨人肩膀上的那个人 .

搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。

var searchInsert = function(nums, target) {
    let len = nums.length
    let left=0;
    let right=len-1
    let ans=len;
   while (left <= right) {
        let mid = ((right - left) >> 1) + left;
        if (target <= nums[mid]) {
            ans = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return ans;
}

image.png 咱也不知道咋回事 , 一个代码连续交几次 次次结果都不同 .
那么 , 这道题是给了一个排序数组 , 给一个目标值 . 有无值都得返回索引 , 返回它该存在的内个未知的索引 . 所以就不被插入这个词迷惑了 , 这题就是查找索引号 . 也不搞那些奇奇怪怪的解法了 . 我最后实现就是官方的这种 .
二分查找 , 每次都找到一个mid值 , 再与目标值进行比较 , 可以节约大量时间 , 同时找到 mid值后 就将查找范围缩小 , 如果目标值小于 mid 值 那么右边界就是mid , 反之 , 你懂 . 进行到最后 也就可以确定最终目标值的位置了 .

合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

第一眼看到题思路就来了 , 直接for循环开干 , 比较两个数组每一项 , 把大的塞到nums1后边 , 不过一想到这也没法确认得循环几次 , 咋循环 , 还有很多条件得搞 . 想想就麻烦 , 做题是干啥 , 就是用简单的思路实现各种题 . 所以这道题肯定不是拿这玩意做
那就好好再想想吧 . 这就想到了 while , 我只需要判断是否添加 , 然后把比较结果从后面塞就完事了 , 毕竟while自己会干完所有东西 , 那就让他自己干吧 .
通过看题解 , 想思路 ,知道了指针这玩意 . 这东西就是定义一个变量 , 来指向我们所需要的数据 , 方便我们使用 . 毕竟拿个变量保存 , 肯定比调它原本的项要舒服 , 条理清晰 .

const merge = function(nums1, m, nums2, n) {
    let i = m - 1, j = n - 1, k = m + n - 1
    while(i >= 0 && j >= 0) {
        if(nums1[i] >= nums2[j]) {
            nums1[k] = nums1[i] 
            i-- 
            k--
        } else {
            nums1[k] = nums2[j] 
            j-- 
            k--
        }
    }
};

毕竟得合并到nums1里 , 那元素总个数肯定是 m+n了 数组长度那就减个1 , 然后就开始比较 , 存入 . 不过这样提交的话 , 就会报错 . 因为做的时候我是看着例子做的 , 没有考虑到nums1 = [0] 的情况 , 那就得处理一下nums2的添加了 , 此时 j > 0 ,那就再加个while 循环 把nums2加到 nums1 里.

while(j>=0) {
        nums1[k] = nums2[j]  
        k-- 
        j--
    }

这样就ok了 . 搞完这道题 , 以后做题 ,如果想着就感觉自己思路绕了吧唧 , 那多半就是思路跑偏了 , 代码都省了 ,直接跳出这个思维 .

杨辉三角

给定一个非负整数 numRows 生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。
反正看到题我是麻了 , 虽然现在大四 ,但这杨辉三角还是他认识我我忘了他的状态 , 只能去看看度娘那了.

image.png

这么多看的我都麻了 . 反正总结了一下 , 这东西就是个三角形(废话!) , 左右两条边都是 1 填充 , 然后每行每个数字等于上一行左右两个数字的和 , 然后每行元素个数等于行数(很舒服) .
知道这些差不多就能想想解题了 .

image.png
如果我想知道我每一行该填啥 , 首先我得知道我上一行的左右元素是啥 . 这道题结果让我输出一个多维数组 , 如果要push下一行 , 那我得取到结果数组里最后一项 . 然后将某某元素相加 , 得出push行的元素值 . 思路有了 , 开干 .

var generate = function(numRows) {
    let res = [];
    for(let i = 0; i < numRows; i++){
        let row = [];
        row[0] = 1;
        row[i] = 1;
        if(i > 1){
            for(let j = 1; j < i; j++){
                row[j] = res[i - 1][j - 1] + res[i - 1][j];
            }
        }
        res.push(row);
    }
    return res;
};

通过不断试错 , 看题解 . 最终的答案就是这个了 . 试错过程还是不写了 , 也记不清有多少奇妙错误了 . 再回过头来看看题 , 这玩意边边都是1 , 而我们每行都用数组来实现 , 那我可以优先确认 row[0]和row[row.length-1]都是1 . 边都搞好 , 那就剩下其余元素了 . 每个元素都是前一行的相邻元素和 . 我们整个三角使用res来存 , 那么上一行就是res[i-1] , 当前行元素索引我们用j来表示 , 那么上一行的相邻元素肯定是res[i-1][j-1]和res[i-1][j] . 到这也就明了了 . 剩下的就莫得了 . 挺简单的一个玩意 , 忘记杨辉三角也可以在短时间内搞出来 .
老规矩 , 肯定要看看题解了 . 就行看视频第一时间看评论一样 , 题解和评论一样都不能错过 .

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> res={
        {1},
        {1,1},
        {1,2,1},
        {1,3,3,1},
        {1,4,6,4,1},
        {1,5,10,10,5,1},
        {1,6,15,20,15,6,1},
        {1,7,21,35,35,21,7,1},
        {1,8,28,56,70,56,28,8,1},
        {1,9,36,84,126,126,84,36,9,1},
        {1,10,45,120,210,252,210,120,45,10,1},
        {1,11,55,165,330,462,462,330,165,55,11,1},
        {1,12,66,220,495,792,924,792,495,220,66,12,1},
        {1,13,78,286,715,1287,1716,1716,1287,715,286,78,13,1},
        {1,14,91,364,1001,2002,3003,3432,3003,2002,1001,364,91,14,1},
        {1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1},
        {1,16,120,560,1820,4368,8008,11440,12870,11440,8008,4368,1820,560,120,16,1},
        {1,17,136,680,2380,6188,12376,19448,24310,24310,19448,12376,6188,2380,680,136,17,1},
        {1,18,153,816,3060,8568,18564,31824,43758,48620,43758,31824,18564,8568,3060,816,153,18,1},
        {1,19,171,969,3876,11628,27132,50388,75582,92378,92378,75582,50388,27132,11628,3876,969,171,19,1},
        {1,20,190,1140,4845,15504,38760,77520,125970,167960,184756,167960,125970,77520,38760,15504,4845,1140,190,20,1},
        {1,21,210,1330,5985,20349,54264,116280,203490,293930,352716,352716,293930,203490,116280,54264,20349,5985,1330,210,21,1},
        {1,22,231,1540,7315,26334,74613,170544,319770,497420,646646,705432,646646,497420,319770,170544,74613,26334,7315,1540,231,22,1},
        {1,23,253,1771,8855,33649,100947,245157,490314,817190,1144066,1352078,1352078,1144066,817190,490314,245157,100947,33649,8855,1771,253,23,1},
        {1,24,276,2024,10626,42504,134596,346104,735471,1307504,1961256,2496144,2704156,2496144,1961256,1307504,735471,346104,134596,42504,10626,2024,276,24,1},
        {1,25,300,2300,12650,53130,177100,480700,1081575,2042975,3268760,4457400,5200300,5200300,4457400,3268760,2042975,1081575,480700,177100,53130,12650,2300,300,25,1},
        {1,26,325,2600,14950,65780,230230,657800,1562275,3124550,5311735,7726160,9657700,10400600,9657700,7726160,5311735,3124550,1562275,657800,230230,65780,14950,2600,325,26,1},
        {1,27,351,2925,17550,80730,296010,888030,2220075,4686825,8436285,13037895,17383860,20058300,20058300,17383860,13037895,8436285,4686825,2220075,888030,296010,80730,17550,2925,351,27,1},
        {1,28,378,3276,20475,98280,376740,1184040,3108105,6906900,13123110,21474180,30421755,37442160,40116600,37442160,30421755,21474180,13123110,6906900,3108105,1184040,376740,98280,20475,3276,378,28,1},
        {1,29,406,3654,23751,118755,475020,1560780,4292145,10015005,20030010,34597290,51895935,67863915,77558760,77558760,67863915,51895935,34597290,20030010,10015005,4292145,1560780,475020,118755,23751,3654,406,29,1}};
        res.resize(numRows);
        return res;
    }
};

好家伙 , 面向脑容量编程 . 快乐的源泉 . 不过哪都有大佬 , 总有人会用你看不懂的代码完成这道题

var generate = function(numRows) {
    return Array(numRows).fill().map((_, i, r) => r[i] = Array(i + 1).fill(1).map((v, j) => j > 0 && j < i ? r[i - 1][j - 1] + r[i - 1][j] : v))
};

作者:mantoufan
链接:https://leetcode-cn.com/problems/pascals-triangle/solution/xiang-lin-dui-cheng-1xing-dai-ma-2jie-fa-ndok/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

大佬题解就写了 , 一行实现 , 超 99% , 咱虽然没这本事 ,但也得看看 . 因为大佬有两种解法 , 还有描述与图 , 还是直接移步力扣看吧 .

最长增长序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

又是被动态规划折磨的一天 , 题解里都是动态规划 . 不过这道题难度不高 , 还是能花点时间写出来的 .

看到题的第一想法就是搞个循环 , 再判断数组前后项是否保持递增关系 , 在只有递增关系时 , 再让输出值++ . 否则则不变 , 进行下次循环 .

var findLengthOfLCIS = function(nums) {
   if (!nums.length) return 0;
    let res = 1, count = 1;
    for (let i = 0; i < nums.length - 1; i++) {
        if (nums[i] < nums[i + 1]) {
            count++;
        } else {
            count = 1;
        }
        res = res > count ? res : count;
    }
    return res;
}

盛最多水的容器

给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 说明:你不能倾斜容器。

这可能是搞得第一道中等难度的题了 .

image.png 由示例还是较好理解的 , 求最大面积的问题 . 找出元素能涵盖的最大范围 . 那就开始好好解题 , 先得好好想想思路 .

首先 , 最大面积 , 那宽度是索引号差值 , 高度则是最大能实现的高 , 也就是数组项理想最大值 . 那么就需要比较了 , 先确保宽度大 , 那么就需要两个指针 , 一左一右 , 再搞个max存放结果 . 最优先的判断就是 左指针得小于右指针 , 然后再算出当前面积 , 存入max , 再进行循环判断 .

var maxArea = function(height) {
    let max = 0
    let i = 0
    let j = height.length - 1
    while(i < j){
        if(height[i]>height[j]){
            j--
            max = Math.max(max,(j-i)*height[i])
        }else{
            i++
            max = Math.max(max,(j-i)*height[j])
        }
    }
    return max
};

然后就开始解答错误 , 当输入[1,1]时 , 代码输出 0 o(╥﹏╥)o

var maxArea = function(height) {
    let max = 0
    let i = 0
    let j = height.length - 1
    while(i < j ){
       max = Math.max(max, Math.min(height[i], height[j]) * (j - i))
    if (height[i] < height[j]) {
      i++
    } else {
      j--
    }
  }
  return max
};

代码倒是好搞 , 要是思路走入死胡同就会像我这样

image.png

所以还是得保持思路 , 走进死胡同会越走越深 . 及时看题解也是很有必要的选择 .

和为k的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k的连续子数组的个数。

照例 , 题越短 , 肯定越难 . 不能说毫无思路吧 , 只能想到最笨的 , 把所有连续子数组取出来 , 然后判断值 . 然后就整了三层循环 , 那就看题解吧 .

const subarraySum = (nums, k) => {
  const map = { 0: 1 };
  let prefixSum = 0;
  let count = 0;

  for (let i = 0; i < nums.length; i++) {
    prefixSum += nums[i];

    if (map[prefixSum - k]) {
      count += map[prefixSum - k];
    }

    if (map[prefixSum]) {
      map[prefixSum]++;
    } else {
      map[prefixSum] = 1;
    }
  }
  return count;
};

作者:xiao_ben_zhu
链接:https://leetcode-cn.com/problems/subarray-sum-equals-k/solution/dai-ni-da-tong-qian-zhui-he-cong-zui-ben-fang-fa-y/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

大佬提出了一个前缀和的概念 , 大体意思就是反正要求连续子数组和与目标值比较 , 那就把每个项所对应的前缀和搞出来 , k就是两个前缀和的差值 . 因为前缀和定义是从0开始到当前元素的和 , k则是数组内部一段连续数组的和 , 所以相减就是我们所求的值 .

image.png 代码中很难理解的就是 map{0:1} 这一段 .
因为是两前缀和相减 , 当i= 0 时 前一项就是 i= -1 , 可是数组中并没有这一项 , 且此时前缀和为0 , 所以我们就提前把和为 0 的值设置为 1 ,代表0已经出现了一次 .

得把解题思路记下来 , 拓宽自己思维

移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
var moveZeroes = function(nums) {
    let i = 0, j = 0;
    while (i < nums.length) {
        if (nums[i] != 0) {
            [nums[i], nums[j]] = [nums[j], nums[i]];
            i++;
            j++;
        } else {
            i++;
        }
    }
};

嗯 , 重点肯定不在我的代码里 , 精髓总在大佬的代码中

var moveZeroes = function(nums) {
    nums.sort((a,b) => b? 0: -1)
};

作者:suan-huang-suan-ge
链接:https://leetcode-cn.com/problems/move-zeroes/solution/javascript-shu-zu-sort-yi-xing-gao-ding-by-suan-hu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

没错 , 只有一行 , 体现了大佬对sort的了解之深 .

Array.prototype.sort()

sort()  方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的,sort方法允许传入一个函数来指定元素的排列 .
在本题中 , 三元表达式会判断b值是否为0 , 0即是false , 对应则为 -1 , 反之为 0 .而sort的返回值决定着元素的排序 , 小于零时 a在前 , b在后 . 反之同理 .
MDN详解

只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明: 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例1:
输入: [2,2,1]
输出: 1

示例2:
输入: [4,1,2,1,2]
输出: 4

闲话不说 , 先上代码

var singleNumber = function(nums) {
  nums = nums.sort();
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] !== nums[i - 1] && nums[i] !== nums[i + 1]) return nums[i];
  }
};

题目中建议不使用额外空间 , 这无疑加大了思考的难度 . 这段代码中利用了js中数组越界会返回undefined的特性 .使得if判断语句可以成立 .
js中 , 数组实际上是哈希表实现的 , 这也是为什么js数组中元素可以不是同一个类型的原因 .
那么开始解析 , 如果要排查出一个独特的元素 , 那么最容易想到的就是 , 搞一个结构 , 然后取出对应值 , 再使用indexof()判断 . 但是这样空间复杂度就不满足要求了 . 所以我们得想别的方法 .
那就进行比较嘛 , 先进行冒泡 , 然后对前后元素进行比较 , 如果不相同(题目中固定重复元素最多俩,无疑方便我们钻漏洞) , 那就return , 如果相同 , 那就进行下一步 . 由于得同时和前后两个元素所比较 , 所以数组边界的0和nums.length-1 会涉及越界 , 所以得在if判断中多花点心思 , 所以最后代码就是上面啦 .

位异或 ^

var singleNumber = function(nums) {
    let res = 0;
    for(const num of nums) {
        res ^= num;
    }
    return res;
};

这时该回忆位异或运算了 . 我推荐参考以下文章 . 深入研究js中的位运算及用法

位异或运算 , 对于相同的值进行比较 , 会返回 0 .

image.png

本题中 , res = 0^nums[0]^nums[1]......^nums[nums.length-1]

只要有涉及相同元素 , 那么就会抵消为 0 . 所以最后返回的就是不同元素了 .

字符串

判断一个字符串是否是回文字符串

回文字符串 , 就像' 上海自来水来自海上 ' 一样 , 颠倒也是一样的 , 同时也有对称性

function isPalindrome(str) { 
    const reversedStr = str.split('').reverse().join('') 
    return reversedStr === str 
}

对称性

function isPalindrome(str) { 
    const len = str.length  
    for(let i=0;i<len/2;i++) { 
        if(str[i]!==str[len-i-1]) { 
            return false 
        } 
    } 
return true 
}

最多删除一个字符得到回文

给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。

题目一上手 , 首先就想遍历删除字符 , 然后判断剩下字符串是否是回文串 , 但这个方法想想也知道效率低 , 所以再考虑考虑 . 再就想到了对称性 , 对称的东西就好解决点了 , 可以试着首尾进行比较 , 如果相等就比较下一位 , 那么就可以不断减小范围 . 如果不相等的话 , 那就删除一个字符进行判断 , 删了后还不行? 那这肯定就不是回文串了 , 题目可是限定最多删一个元素 .

var validPalindrome = function(s) {
    const len = s.length
    let left=0, right=len-1
    
    while(left<right&&s[left]===s[right]) {
        left++ 
        right--
    }
    
    if(isPalindrome(left+1,right)) {
      return true
    }
    if(isPalindrome(left,right-1)) {
        return true
    }
    function isPalindrome(start, end) {
        while(start<end) {
            if(s[start] !== s[end]) {
                return false
            }
            start++
            end--
        } 
        return true
    }
    return false 
};

字符串转换整数 (atoi)

image.png

根据经验来看 , 题越长 , 给的信息越多 , 越简单 . 那就仔细读题理思路吧

var myAtoi = function(s) {
    const int = parseInt(s, 10);
    if(isNaN(number)) {
        return 0;
    } else if (number < Math.pow(-2, 31) || number > Math.pow(2, 31) - 1) {
        return number < Math.pow(-2, 31) ? Math.pow(-2, 31) : Math.pow(2, 31) - 1;
    } else {
        return number;
    }
};

parseInt(stringradix)   解析一个字符串并返回指定基数的十进制整数, radix 是2-36之间的整数,表示被解析字符串的基数。 强制类型转换真是方便 , 省的写正则了 , 正则是真的烦 , 虽然简单的还能写一写 , 但是还是不想写 . 想到了parseInt 也就成功百分之八十了 剩下就是判断边界了 .

最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

LeetCode热题100 的第五题 , 也是一道极容易出现在前端面试中的算法题 . 命题关键字:字符串、动态规划

image.png

不高的通过率让大家都望而却步 . 学习前端 , 我们不仅有艺术性的思维 , 也得有精妙的逻辑 , 而这道题 , 便可以让我们历练一番 .

**示例 1:**

输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。

**示例 2:**

输入: s = "cbbd"
输出: "bb"

那么解题开始 .

定义两个指针 i 和 j,用这两个指针嵌套两层循环,尝试枚举出给定字符串对应的所有可能的子序列,判断每一个子序列是否回文:若回文且长度已经超越当前的长度最大值,则更新长度最大值,并记录该子串的两个端点。当所有的子串枚举结束时,我们也就得到了最大长度的回文子串。

//建议跳过
var longestPalindrome = function(s) {
    function isPalindrome(str) {
    var len  = str.length
    var middle = parseInt(len/2)
    for(var i = 0;i<middle;i++){
        if(str[i]!=str[len-i-1]){
            return false
        }
    }
    return true
    }
    var ans = '';
    var max = 0;
    var len = s.length
    for(var i = 0;i<len;i++){
        for(var r = i+1;r<=len;r++){
            var tmpStr = s.substring(i,r)
            if(isPalindrome(tmpStr) && tmpStr.length > max){
                ans = s.substring(i,r)
                max = tmpStr.length;
            }
        }
    }
    return ans;
};

但是咱得优雅 , 暴力不是解决问题的唯一方法 .

题干中的 '最长' 二字 , 见到最值 , 就得开始考虑动态规划了 . 而给我们一个字符串 , 假如这字符串就是一个回文串 , 那么最长回文子串就是他本身了 , 而他的内部又会有着 1/2 他长度的回文子串 .

所以思路来了 .

  1. 以最中央的点为核心 , 设俩指针不断向两边发散 , 我们就可以对每次发散的字符串进行判断 , 从而找到以该点而形成的回文串
  2. 那么再进行发散 , 我们把每个点都当做中心点 , 开始进行发散 , 那么我们就可以得到每个点上所对应的回文串 .
  3. 到了这一步 , 那就剩下代码复现了 . 在不断试错中解决边界问题与一些小瑕疵 .

虽然我并不是聪明的那一个 , 但我是比较喜欢汲取他人经验的那一个 , 总是在看题解中学习 .

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
        if (s.length<2){
            return s
        }
        let res = ''
        for (let i = 0; i < s.length; i++) {
            // 回文子串长度是奇数
            helper(i, i)
            // 回文子串长度是偶数
            helper(i, i + 1) 
        }

        function helper(m, n) {
            while (m >= 0 && n < s.length && s[m] == s[n]) {
                m--
                n++
            }
            // 注意此处m,n的值循环完后  是恰好不满足循环条件的时刻
            // 此时m到n的距离为n-m+1,但是mn两个边界不能取 所以应该取m+1到n-1的区间  长度是n-m-1
            if (n - m - 1 > res.length) {
                // slice也要取[m+1,n-1]这个区间 
                res = s.slice(m + 1, n)
            }
        }
        return res
};

链表

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]

从之前复习的数据结构中 , js链表是对象形式的 .

function ListNode(val, next) {
     this.val = (val===undefined ? 0 : val)
     this.next = (next===undefined ? null : next)
 }

leetcode也为我们准备好了 , 所以可以new一个头结点 , 用来访问链表 , 然后再准备一个指针 , 就像穿针引线一样把两个有序链表连起来 , 链表中考察的主要是对结点的操作 , 指针可是基操了 .

image.png 这是官方给的示例图 , 简单明了 .

var mergeTwoLists = function(l1, l2) {
    let head = new ListNode()
    let cur = head
    while(l1 && l2) {
        if(l1.val<=l2.val) {
            cur.next = l1
            l1 = l1.next
        } else {
            cur.next = l2
            l2 = l2.next
        }
      cur = cur.next 
    }
    cur.next = l1!==null?l1:l2
    return head.next
};

链表初见 , 代码还是蛮简单的 , 只要搞清楚每个结点之间的关系 , 就有解题的方法 .

删除排序链表中的重复元素

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。

返回同样按升序排列的结果链表。

输入: head = [1,1,2]
输出: [1,2]

链表的删除操作 , 就是将待删除结点的前驱结点的next指向他的后继结点 . 就相当于把待删除的结点跳过 .

var deleteDuplicates = function(head) {
    let cur = head
    while(cur!== null&&cur.next!==null){
        if(cur.val == cur.next.val){
            cur.next = cur.next.next
        }else{
            cur = cur.next
        }
    }
    return head
};

删除排序链表中的重复元素

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。

返回同样按升序排列的结果链表。

输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]

删除的升级版 , 现在咱得删除多个元素了 , 只要重复 , 就全都删除 . 那么我们所需要的指针也不只一个了 , 我们一层一层查值时 , 到了第二个重复的元素那才能察觉到该元素重复 , 而此时进行普通的删除操作只能删除后一个结点 , 所以我们得设置一个指针充当第一个元素的前驱结点 .

var deleteDuplicates = function(head) {
    if(!head || !head.next) {
        return head
    }
    let dummy = new ListNode() 
    dummy.next = head   
    let cur = dummy 
    while(cur.next && cur.next.next) {
        if(cur.next.val === cur.next.next.val) {
            let val = cur.next.val
            while(cur.next && cur.next.val===val) {
                cur.next = cur.next.next 
            }
        } else {
            cur = cur.next
        }
    }
     return dummy.next;
};

由于涉及多个结点 , 所以我们外层while循环得首先确认指针指向的结点后至少还有结点 , 而内层while循环则进行反复判断与删除结点 .

删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点。

进阶: 你能尝试使用一趟扫描实现吗?

输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]

输入: head = [1], n = 1
输出: []

这波是删除固定的结点 , 固定倒数第n个 , 可惜我们不知道到底有多少结点 . 此时第一次遇到这种题的我反正懵了 , 那咋办 , 看题解呗 . 看题解后 , 才知道有快慢指针 , 既然所求是倒数第n个结点 , 那让一个快指针 , 先到最后边的结点 , 而慢指针比快指针少走n步 , 那不就到了该删除的结点那里吗?

var removeNthFromEnd = function(head, n) {
    const dummy = new ListNode()
    dummy.next = head

    let fast = dummy
    let slow = dummy
    while(n!==0){
        fast = fast.next
        n--
    }
    while(fast.next){
        fast = fast.next
        slow = slow.next
    }
    slow.next = slow.next.next
    return dummy.next
};

我看到dummy一开始懵了 , 但转念一想 , 毕竟删除结点 , 万一删除的刚好是第一个 , 那岂不是取不到这个链表了 . 所以遇到删除操作 , 还是搞个dummy比较好 .

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]

翻转链表 , 也是基操了 , 可以实现的方式也多 , 多指针翻转 , 或者拿个栈存着再翻转 , 还有用空节点倒腾的 . 基操嘛 , 多练练 .

var reverseList = function(head) {
    let pre = null;
    let cur = head;
    while (cur !== null) {
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre
};

反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

输入: head = [1,2,3,4,5], left = 2, right = 4
输出: [1,4,3,2,5]

局部翻转 , 首先得确定翻转的那部分 , 那就得搞个结点指向翻转部分的前驱结点 , 然后就能取到第一个翻转的结点 , 再利用指针与for循环 , 来不断改变翻转区间内结点的指向关系 , 最后再把后一段结点接上 , 局部翻转就完成了 . 先得想到如何去进行翻转 , 只有死路清晰 , 写代码才能不迷糊 , 要是画个图啥的那就更清晰了 , 看着图都能把代码写出来 , LeetCode上有很多大佬做的动态图解 , 建议大家看可以多看看 .

var reverseBetween = function(head, left, right) {
    let pre,cur,leftHead
    const dummy = new ListNode()  
    dummy.next = head
    let p = dummy  
    for(let i=0;i<left-1;i++){
        p = p.next
    }
    leftHead = p
    let start = leftHead.next  
    pre = start
    cur = pre.next
    for(let i=left;i<left;i++){
        let next = cur.next
        cur.next = pre
        pre = cur
        cur = next
    }
    leftHead.next = pre
    start.next=cur
    return dummy.next
};

环形链表

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶: 你能用 O(1)(即,常量)内存解决此问题吗?
提示:

  • 链表中节点的数目范围是 [0, 100000]
  • -1000000 <= Node.val <= 1000000
  • pos 为 -1 或者链表中的一个 有效索引 。
输入: head = [3,2,0,-4], pos = 1
输出: true
解释: 链表中有一个环,其尾部连接到第二个节点。

这个搞个环就很头痛 , 我是能想到这个结构 , 但我想不到这玩意在内存里咋存的 . 反正后半截就循环嘛 , 搞个指针 , 那这指针就一直绕一直绕 , 和个死循环一样吗 , 我本想打印出来看看 , 可打印不出来呀 . 还是在脑子里想吧 . 反正刚刚要循环嘛 , 而且LeetCode还给了提示 , 结点数目有上限 . 那就蛮干呗 , 要是循环 , 那就循环肯定会突破上限 .

const hasCycle = function(head) {
  let i = 0
  const size = 100000
  let node = head
  while (i <= size) {
    i++
    if(!node) return false
    node = node.next
  }
  return true;
};

成功倒是成功了 , 结果也和预料的一样 , 时间久嘛 , 还可以 .
再想想别的解法 , 反正要循环 , 那就每个点搞个标记 , 要是循环的链表 , 那就肯定会重复触发标记 .

const hasCycle = function(head) {
    while(head){
        if(head.flag){
            return true;
        }else{
            head.flag = true;
            head = head.next;
        }
    }
    return false;
};

然后就可以心满意足看题解了 , 因为只能想到这了 .

var hasCycle = function(head) {
    try {
        JSON.stringify(head)
        return false
    } catch {
        return true
    }
};

作者:cherishit
链接:https://leetcode-cn.com/problems/linked-list-cycle/solution/qiao-yong-jsji-bai-5yong-hu-de-jie-fa-by-cherishit/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

看了评论才知道为啥这么写 : JSON.stringify当在循环引用时会抛出异常TypeError ("cyclic object value")(循环对象值)

题解处处都大佬 , 看一次学一次 .