前端JS算法,您看我还有机会嘛【持续不间断更新】

550 阅读25分钟


暑假到了...作为算法小渣的我来说
很有必要开启算法的上山之路!
本文章用于记录在路途中的一步一脚印
~不拍照的那种
坚持周期性更新(一天3篇Easy, 一天2篇Mid, 一天1篇Hard)
为了两年后毕业后面试的自己打下基础
奋进吧!少儿郎...

提示: 每一道题均有实例、解题思路、解答代码、执行结果

要是对您有所帮助启发点个小赞噢~
希望你的算法之路,也有我的陪伴~

第一天 - 07.08

前记: 今天是高考的最后一天...望考生们不负自己的努力

Easy

1.两数之和
2.整数反转
3.罗马数字转整数

两数之和

给定一个整数数组 nums 和一个目标值 target,
请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

实例

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解答思路

1.关键在于target-当前数组的剩余值,等于数组剩余值中的某一个
2.利用对象key为值, value为下标

解答代码

var twoSum = function(nums, target) {
  const prevNums = {};  // 存储的对象                         
  for (let i = 0; i < nums.length; i++) {      
    const curNum = nums[i];   // 当前数组值
    const targetNum = target - curNum;    // 当前剩余值
    const targetNumIndex = prevNums[targetNum];   // 在存储对象中key为当前剩余值时候 -> 获取value下标值
    if (targetNumIndex !== undefined) {   // 当存储对象有该下标时
      return [targetNumIndex, i];   // 返回目标下标和当前下标
    }
    prevNums[curNum] = i;   // 否则存储value -> 下标和key -> 数值
  }
}

代码执行结果

输入:
[2,7,11,15]
9

输出
[0,1]

预期结果
[0,1]


整数反转

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
注意反转后整数可能溢出情况 -> 返回0

实例

示例 1:
输入: 123
输出: 321

示例 2:
输入: -123
输出: -321

示例 3:
输入: 120
输出: 21

解答思路

1.反转整数向左移一位同时加上原整数的最后一位 -> 原整数去掉最后一位 -> 至到原整数为0
2.注意需要将反转整数转为32位有符号整数
3.若溢出 -> 反转整数不等于自身 -> 溢出为Infinity

解答代码

var reverse = function(x) {
    let reverseNumber = 0   // 反转整数为0
    while(x) {
        reverseNumber = reverseNumber * 10 + x % 10   // 反转整数向左移一位 再加上x最后一位数字
        x = (x / 10) | 0    // 原整数去掉最后一位数字 x | 0 -> 强制转换为32位有符号整数
    }
    return (reverseNumber | 0) === reverseNumber ? reverseNumber : 0
    // 反转整数强制转换为32位有符号整数 -> 若不等于自身 -> 判断溢出 -> 值为0
};

代码执行结果

输入
12345

输出
54321

预期结果
54321


罗马数字转整数

罗马数字包含以下七种字符: 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:
输入: "IX"
输出: 9

示例 2:
输入: "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.

示例 3:
输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.

解答思路

1.创建一个罗马字符的对象 -> 对应字符对应数值
2.贪心思想 -> 由最大的字符往最小的字符一一转换
3.当前罗马字符 < 右边罗马字符时 -> 整数减去当前罗马字符代表的数值 反之则加
4.遍历整个罗马字符完成

解答代码

var romanToInt = function(s) {
    const roman = {     // 创建罗马字符对象
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }
    const length = s.length   // 罗马字符串长度
    let result = 0    // 转换整数
    for(let i = 0; i < s.length; i++ ) {
        const currentNum = roman[s[i]]    // 当罗马字符对应数值
        if(i < length - 1 && currentNum < roman[s[i+1]]) {    // 判断为倒数第二个字符串 并且当前罗马对应数值小于右边对应数值
            result -= currentNum    // 减去当前值
        } else {
            result += currentNum    // 反之加上当前值
        }
    }
    return result   // 返回转换后的整数
};

代码执行结果

输入
"MCMXCIV"

输出
1994

预期结果
1994


第二天 - 07.09

前记: 写了好久小程序,和小妹妹出去散散步

Mid

1.两数相加
2.无重复字符的最长子串

两数相加

给出两个 非空 的链表用来表示两个非负的整数。
其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。  
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

实例

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

解答思路

1.因为倒叙
2.链表取出l1和l2的首个数字相加 -> 为sum
3.sum取余10 -> 存入新链表中(防止为两位数)
4.若sum >= 10 -> 向下一个链表next + 1

解答代码

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    let node = new ListNode('head');   // 表头(上方的ListNode链表方法)
    let temp = node;    // 当前指向表头
    let add = 0;    // 是否进一
    let sum = 0;    // 当前总和值为

    while(l1 || l2){    //遍历,直到最长链表的都为空
        // 当前l1链表值和l2链表值相加(无值时为0)且判断l1+l2 > 10时 -> 需要+1
        sum = (l1 ? l1.val : 0) + (l2 ? l2.val : 0) + add;
        temp.next = new ListNode(sum % 10);     // 当前总和取余存入新链表中
        temp = temp.next;       // 指向下一链表
        add = sum >= 10 ? 1 : 0;        // 判断总和是否 >= 10
        l1 && (l1 = l1.next);       // l1指向下一链表
        l2 && (l2 = l2.next);       // l2指向下一链表
    }
    // 当最后一次add存在时 -> 最后相加 >= 10了 -> 再存入新链表中
    add && (temp.next = new ListNode(add));
    return node.next;       // 返回新链表的next结果
};

代码执行结果

输入
[2,4,3]
[5,6,4]

输出
[7,0,8]

预期结果
[7,0,8]


无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

实例

示例 1:
输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。

请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。。

解答思路

1.遍历字符串每个字符
2.用一个数组存储每个字符
3.若数组中存在当前字符 -> 清除数组当前字符和前的数据
4.判断max取整个过程的最长长度

解答代码

var lengthOfLongestSubstring = function(s) {
    let arr = [], max = 0       // 存储到数组中,无重复字符的最长长度
    for(let i = 0; i < s.length; i++) {     // 遍历每一个字符
        let index = arr.indexOf(s[i])       // 数组中是否有当前字符
        if(index !== -1) {
            arr.splice(0, index+1);    // 若已存在 -> 清除已有字符前面的
        }
        arr.push(s.charAt(i))       // 存入当前字符 -> 至少长度为1
        max = Math.max(arr.length, max)     // 判断当前数组长度与max大小
    }
    return max      // 返回最长长度
};

代码执行结果

输入
"dvdf"

输出
3

预期结果
3


第三天 - 07.10

前记: 打了会游戏,困难题肝了快3小时,还是不太清除官方做法...

Hard

1.正则表达式匹配

正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。  
'.' 匹配任意单个字符  
'*' 匹配零个或多个前面的那一个元素  
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。  
说明:  
s 可能为空,且只包含从 a-z 的小写字母。  
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。  
注意可不要使用JS的正则表达式呀~那这道题将毫无意义

实例

示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 3:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 4:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

解答思路

1.定义基础返回条件,p的长度为0时,s的长度为0则表示匹配,s的长度不为0则表示不匹配
2.定义匹配状态match,即为第一个字符的互相比较,如果相等或p[0]为'.'则为true
3.当p没有模式的时候,我们根据match的状态进行返回,如果match为false,直接返回false,如果为true,那么进行下一步的判断getIsMactch(s.slice(1), p.slice(1)
4.当p有模式的时候,有两种情况:一是s匹配0个字符,则进行getIsMactch(s, p.slice(2)),二是s匹配1个字符,递归下去,用来表示s匹配多个s,这样如果match为false,直接返回false,如果为true,那么进行getIsMactch(s.slice(1), p)。这两种只要一个能成立就够了
5.返回匹配结果

解答代码

var isMatch = function (s, p) {
  let getIsMactch = (s, p) => {
    // 判断,如果传入p的长度为0,那么,必须s的长度也为0才会返回true
    if (p.length === 0) {
      return !s.length
    }
    // 判断第一个字符是否相等
    let match = false
    if (s.length > 0 && (s[0] === p[0] || p[0] === '.')) {
      match = true
    }
    //p有模式的
    if (p.length > 1 && p[1] === "*") {
      // 如果有"*"字符,回溯字符
      // 第一种情况:s*匹配0个字符
      // 第二种情况:s*匹配1个字符,递归下去,用来表示s*匹配多个s*
      return getIsMactch(s, p.slice(2)) || (match && getIsMactch(s.slice(1), p))
    } else {
      return (match && getIsMactch(s.slice(1), p.slice(1)))
    }
  }
  return getIsMactch(s, p)  // 返回最终调用方法匹配结果
}

/**
 * 官方解答代码
 */
var isMatch = function(s, p) {
  function matches(s, p, i, j) {
    if (i == 0) {
      return false;
    }
    if (p.charAt(j - 1) === '.') {
      return true;
    }
    return s[i - 1] === p[j - 1];
  }
  let m = s.length
  let n = p.length
  let f = []
  for (let i = 0; i <= m; i++) {
    f.push(new Array(n + 1).fill(false))
  }
  f[0][0] = true
  for (let i = 0; i <= m; ++i) {
    for (let j = 1; j <= n; ++j) {
      if (p[j - 1] == '*') {
        f[i][j] = f[i][j - 2];
        if (matches(s, p, i, j - 1)) {
          f[i][j] = f[i][j] || f[i - 1][j];
        }
      }
      else {
        if (matches(s, p, i, j)) {
          f[i][j] = f[i - 1][j - 1];
        }
      }
    }
  }
  return f[m][n]
}

代码执行结果

输入
"mississippi"
"misisp*."

输出
false

预期结果
false


第四天 - 07.11

前记: 噢吼吼吼吼吼,写了一天小程序。等会看小白船

Easy

1.回文数
2.最长公共前缀
3.有效的括号

两数之和

判断一个整数是否是回文数。  
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 

实例

示例 1:
输入: 121
输出: true

示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。

解答思路

1.将整数转为字符串
2.字符串倒置
3.判断字符串倒置和原整数字符串是否一致

解答代码

var isPalindrome = function(x) {
    const reserveStr = x.toString().split("").reverse().join("")
    return reserveStr === x.toString()
}

代码执行结果

输入:
121
9

输出
true

预期结果
true


最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。  
如果不存在公共前缀,返回空字符串 ""。

实例

示例 1:
输入: ["flower","flow","flight"]
输出: "fl"

示例 2:
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

说明:
所有输入只包含小写字母 a-z 。

解答思路

1.初始化任意一个数组字符串(原理: 公共前缀小于任意一个字符串)
2.开始遍历和对比字符串数组 -> 若公共前缀与当前不相等 -> 截取之前的公共前缀
3.遍历完成返回公共前缀 -> 若公共前缀为空字符 -> 提前返回

解答代码

var longestCommonPrefix = function(strs) {
   if(strs.length == 0)     // 如果字符数组为空数组 -> 返回为空
        return "";
    let ans = strs[0];      // 初始化公共前缀第一个数组字符串
    for(let i = 1; i < strs.length; i++) {      // 遍历字符数组
        let j = 0;
        // 公共前缀小于初始化字符串长度 并且 小于当前字符串长度
        for(;j < ans.length && j < strs[i].length; j++) {
            // 公共前缀字符 和 当前数组字符串字符不相等时 -> break
            if(ans[j] != strs[i][j])
                break;
        }
        // 截取目前相等的公共前缀
        ans = ans.substr(0, j);
        if(ans === "")      // 若公共前缀为空 -> 直接返回无需遍历
            return ans;
    }
    return ans;     // 遍历完成,返回公共前缀
};

代码执行结果

输入
["flower","flow","flight"]

输出
"fl"

预期结果
"fl"


有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

实例

示例 1:
输入: "()[]{}"
输出: true

示例 2:
输入: "(]"
输出: false

示例 3:
输入: "([)]"
输出: false

示例 4:
输入: "{[]}"
输出: true

解答思路

1.因为空字符串可被认为是有效字符串 -> 利用对象的key和value只提取出括号字符
2.利用栈思想 -> 先近后出原理 -> 这里将括号字符定义为开、关两种
3.若 开 字符 -> 存入栈中对应 闭 字符
4.若 闭 字符 -> 取出栈顶字符 -> 判断是否相等
5.若不相等 -> 返回false / 若遍历结束后栈为空 -> 返回true

解答代码

var isValid = function(s) {
    // 括号字符分为 开、关 两种
    const brackets = {       // 定义括号对象 -> key为开、value为闭
        '(': ')',
        '{': '}',
        '[': ']'
    }
    let stack = []      //定义空栈
    let top = undefined     // 栈顶
    for(let char of s) {        // 遍历字符
        let value
        if((value = brackets[char])) {      // 若是对象key的 开 字符 -> 赋值给value
            stack.push(value)       // 存入栈中对应 闭 字符
        } else {        // 若是 闭 字符
            top = stack.pop()       // 取出栈顶
            if(top !== char) {      // 判断是否栈顶值等于对应当前 闭 字符
                return false        // 若不相等 -> 返回false
            }
        }
    }
    return stack.length === 0       // 遍历结束,栈为空 -> 返回true
};

代码执行结果

输入
"([)]"

输出
false

预期结果
false


第五天 - 07.13

前记: emmm这个暑假有点懒散

Mid

1.最长回文子串
2.Z 字形变换

两数相加

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

实例

示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:
输入: "cbbd"
输出: "bb"

解答思路

1.找最长回文字符串 -> 使用动态规划思想
2.从一个字符往两边延伸开 -> 且两边字符串也是回文串 -> 仍标为true

解答代码

var longestPalindrome = function(s) {
  // babad
  // tag : dp
  if (!s || s.length === 0) return "";
  let res = s[0];       // 若不为空,初始化回文子串为第一个

  const dp = [];

  // 倒着遍历简化操作, 这么做的原因是dp[i][..]依赖于dp[i + 1][..]
  for (let i = s.length - 1; i >= 0; i--) {
    dp[i] = [];
    for (let j = i; j < s.length; j++) {
      // 如果为第一个字符 -> 标记为true
      if (j - i === 0) dp[i][j] = true;
      // 如果第二个字符且相等字符 -> 标记为true
      else if (j - i === 1 && s[i] === s[j]) dp[i][j] = true;
      // 如果一个字符串是回文串且两边字符串也是回文串,那么它一定还是一个回文串
      else if (s[i] === s[j] && dp[i + 1][j - 1]) {
        dp[i][j] = true;
      }

      // 标记位为true 且 长度大于现回文子串长度
      if (dp[i][j] && j - i + 1 > res.length) {
        // 更新截取回文子串
        res = s.slice(i, j + 1);
      }
    }
  }
  return res;
};

代码执行结果

输入
"babad"

输出
"aba"

预期结果
"bab"


Z 字形变换

将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下:
L   C   I   R
E T O E S I I G
E   D   H   N

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。

实例

示例 1:
输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN"

示例 2:
输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"
解释:
L     D     R
E   O E   I I
E C   I H   N
T     S     G

解答思路

1.题目最后是需要转换后的字符串newStr -> newStr是根据Z字形的行数重新拼接
2.可以根据输入行数来移动数组下标(上升或下降) -> 存入对应同一行的字符
3.最后再拼接数组中每个字符串 -> 新的Z字形变换

解答代码

var convert = function(s, numRows) {
    // 行数为1直接返回字符串
    if(numRows === 1) return s

    // 判断字符长度是否大于行数取最小
    const len = Math.min(s.length, numRows)
    // 每行字符串数组
    let rowStr=[]
    // 初始化字符串数组为空字符串
    for(let i = 0; i < len; i++) {
        rowStr[i] = ""
    }
    // 定义索引和位置为上升还是下降
    let index = 0, down = false
    // 遍历字符串
    for (const c of s) {
        rowStr[index] += c
        // 若在顶部或在最底部 -> 更换标示
        if(index === 0 || index === numRows - 1) down = !down
        // 索引改变
        index += down ? 1 : -1
    }
    
    // 提取每行字符串 -> 拼接新字符串
    let res = ""
    for(const str of rowStr) {
        res += str
    }
    return res
};

代码执行结果

输入
"PAYPALISHIRING"
3

输出
"PAHNAPLSIIGYIR"

预期结果
"PAHNAPLSIIGYIR"


第六天 - 07.14

前记: 今天看到了掘金上的广告,琢磨了一天,第一次应聘了字节跳动的夏令营,有点兴奋!希望自己可以幸运一点~

Hard

1.合并K个排序链表

合并K个排序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

实例

示例 1:
输入:
[
  1->4->5,
  1->3->4,
  2->6
]

输出: 1->1->2->3->4->4->5->6

解答思路

1.注意有k个已经排序过的链表 -> 可以采用暴力遍历一遍整个链表放入数组中 -> 再数组sort排序值 -> 再数组变为合并的排序链表
2.看了官方解答 -> 采用双指针逐一遍历k个链表
3.简单来说有k个链表[a, b, c, d]和新new链表 -> a+b为new -> new+c为new -> new+d为new -> 遍历完成

解答代码

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    let len  = lists.length;        // 链表长度
    if(len == 0) return null;       // 若空
    if(len == 1) return lists[0];       // 若只有一条(无须合并)
    let heap = new ListNode();      // 创建新链表
    heap.next = lists[0];           // 指向第一个链表
    for(let i = 1; i < len; i++){       // 遍历链表
        let origh = heap;       // 初始化新的合并链表
        let cur1 = heap.next;       // 新合并链表的next
        let cur2 = lists[i];        // lists下一个链表
        while(cur1 != null && cur2 != null){      // 均不为空时
            // origh指向小的值同时移动cur1或cur2的next
            if(cur1.val >= cur2.val) {
                origh.next = cur2;
                cur2 = cur2.next;
            }else{
                origh.next = cur1;
                cur1 = cur1.next;
            }
            // 移动合并列表next
            origh = origh.next;
        }
        // 最后若存在 -> 该链表中最大的值
        if(cur1) origh.next = cur1;
        if(cur2) origh.next = cur2;
    }
    return heap.next;       // 遍历完成 -> 返回合并链表的next
};

/**
 * 一个heap链表 -> 空间复杂度O(1)
 * 时间复杂度 -> 若每个k链表中的元素有n个 -> O(kn)
 */

代码执行结果

输入
[[1,4,5],[1,3,4],[2,6]]

输出
[1,1,2,3,4,4,5,6]

预期结果
[1,1,2,3,4,4,5,6]


第七天 - 07.16

前记: 今天把小程序简历给完结了,打算出篇文章介绍一下

Easy

1.合并两个有序链表
2.删除排序数组中的重复项
3.移除元素

合并两个有序链表

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

实例

输入:1->2->4, 1->3->4

输出:1->1->2->3->4->4

解答思路

1.很像第六天Hard题的简化版可以参考一下噢
2.照常链表迭代即可
3.最后还会剩一个全场最大值,要记得next进去噢

解答代码

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    const list = new ListNode(-1)       // 初始化合并链表第一个数 -1
    let merge = list        // 浅拷贝

    while(l1 && l2) {       // 当l1和l2链中均有值时
        if(l1.val >= l2.val) {
            merge.next = l2     // 合并链表next指向较小值
            l2 = l2.next
        } else {
            merge.next = l1
            l1 = l1.next
        }
        merge = merge.next      // 移动合并链表指针
    }

    merge.next = l1 === null ? l2 : l1      // 最后还会有一个最大值
    return list.next        // 返回该链表的next
};

代码执行结果

输入:
[1,2,4]
[1,3,4]

输出
[1,1,2,3,4,4]

预期结果
[1,1,2,3,4,4]


删除排序数组中的重复项

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 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。

解答思路

1.使用indexOf判断元素第一次出现的下标,不等于当前则重复,删除它
2.删除数组长度减1,所以下标要往前移动一位

解答代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    for(let i = 0; i < nums.length; i++) {
        if(nums.indexOf(nums[i]) !== i) {
            nums.splice(i, 1)
            i --
        }
    }
    return nums.length
};

代码执行结果

输入
[0,0,1,1,1,2,2,3,3,4]

输出
[0,1,2,3,4]

预期结果
[0,1,2,3,4]


移除元素

给你一个数组 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。

注意这五个元素可为任意顺序。

解答思路

1.简单的迭代判断一下是否有该val值
2.有 -> nums.splice一下
3.无 -> 返回新数组长度

解答代码

var removeElement = function(nums, val) {
    let i       // 定义返回索引
    while (nums.indexOf(val) !== -1) {      // 若数组中存在该值
        i = nums.indexOf(val)
        nums.splice(i, 1)       // 移除该值
    }
    return nums.length      // 返回长度
};

代码执行结果

输入
[3,2,2,3]
3

输出
[2,2]

预期结果
[2,2]


第八天 - 07.17

前记: 今天也是召唤师峡谷的一天

Mid

1.字符串转换整数 (atoi)
2.盛最多水的容器

字符串转换整数 (atoi)

请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
在任何情况下,若函数不能进行有效的转换时,请返回 0 。

实例

示例 1:
输入: "42"
输出: 42

示例 2:
输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。

示例 3:
输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
     因此无法执行有效的转换。

示例 4:
输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 
     因此返回 INT_MIN (−231) 。

解答思路

1.利用Js的parseInt()函数 -> 无视开头空格 -> 返回有符号整数 -> 无视整数部分后的字符
2.我们需要判断的是 -> 范围在32位内(含) -> 其他情况返回0

解答代码

var myAtoi = function(str) {
    const number = parseInt(str, 10);

    if(isNaN(number)) {     // 其他情况返回0
        return 0;
    }
    // 判断在32位内
    return result = number <= Math.pow(-2, 31) ?  Math.pow(-2, 31) : number >= Math.pow(2, 31) ? Math.pow(2, 31) - 1 : number
}

代码执行结果

输入
"+-3241aac22"

输出
0

预期结果
0


盛最多水的容器

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

说明:你不能倾斜容器,且 n 的值至少为 2

图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。
在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

实例

示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49

解答思路

1.双指针 -> 指开头和结尾
2.当开头指针 > 结尾指针时 -> 遍历结束
3.判断高度较短的指针向中间移动
4.当前容积和最高容积进行比较并赋值

解答代码

var maxArea = function (height) {
    let res = 0, i = 0, j = height.length - 1, cur = 0;
    while (i < j) {     // i为左指针,j为右指针
        // 高度为两边较短一边
        let h = height[i] < height[j] ? height[i] : height[j];
        cur = h * (j - i);      // 当前容积
        res = cur > res ? cur : res;        // 判断最高容积
        if (height[i] < height[j]) {        // 移动较短的指针
            i++;
        } else {
            j--;
        }
    }
    return res;     // 返回最高容积
};

代码执行结果

输入
[1,8,6,2,5,4,8,3,7]

输出
49

预期结果
49


第九天 - 07.18

前记: 今天参加codeJump的夏令营笔试,一直没有看邮箱!!等短信通知...结果最后40分钟才看到了考试的短信
唉总体都是算法的题目,自己还是弱了...而且150分钟,自己迟到作死,只有40分钟笔试。
给自己一个警钟,不单要看官网应聘流程和短信,还要看邮箱

Hard

1.寻找两个正序数组的中位数

寻找两个正序数组的中位数

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。

请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

实例

示例 1:
nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0

示例 2:
nums1 = [1, 2]
nums2 = [3, 4]

则中位数是 (2 + 3)/2 = 2.5

解答思路

1.暴力算法 -> 合并 -> 排序 -> 判断奇偶 -> 返回(时间复杂度O(m+n) -> 不符合题意)
2.二分法(时间复杂度O(log(min(m, n))))-> 个人琢磨了个把小时还是没太理解,只能参照代码放上
3.将自己理解注释在代码中

解答代码

/**
 * 暴力解法
 * 时间复杂度O(m+n)
 * 空间复杂度O(0)
 */
var findMedianSortedArrays = function(nums1, nums2) {
    // 合并 -> 排序 -> 避免负数
    let nums3 = nums1.concat(nums2).sort((a,b)=>a-b);
    let length = nums3.length;
    if(length%2 == 0){
        return (nums3[length/2-1] + nums3[length/2])/2
    }else{
        return nums3[Math.floor(length/2)]
    }
};

/**
 * 二分法
 * 时间复杂度O(log(min(m, n)))
 * 空间复杂度O(0)
 */
var findMedianSortedArrays = function(nums1, nums2) {
  // 保证m是最短的数组长度
  if (nums1.length > nums2.length) {
    [nums1, nums2] = [nums2, nums1]
  }
  const m = nums1.length
  const n = nums2.length
  let low = 0
  let high = m
  while(low <= high) {
    const i = low + Math.floor((high - low) / 2)    // nums1的指针
    const j = Math.floor((m + n + 1) / 2) - i       // nums2的指针

    // 判断数组越界
    const maxLeftA = i === 0 ? -Infinity : nums1[i-1]
    const minRightA = i === m ? Infinity : nums1[i]
    const maxLeftB = j === 0 ? -Infinity : nums2[j-1]
    const minRightB = j === n ? Infinity : nums2[j]

    if (maxLeftA <= minRightB && minRightA >= maxLeftB) {
      return (m + n) % 2 === 1    // 判断奇偶性
        ? Math.max(maxLeftA, maxLeftB)
        : (Math.max(maxLeftA, maxLeftB) + Math.min(minRightA, minRightB)) / 2
    } else if (maxLeftA > minRightB) {    // 当nums1左数字大于nums2右数字时
      high = i - 1    // 往high指针左移一位
    } else {
      low = low + 1   // 往low指针右移一位
    }
  }
};

代码执行结果

输入
[3]
[-2,-1]

输出
-1.0

预期结果
-1.0


第十天 - 07.19

前记: 总体将ES6 2015~2019复习了一遍新特性
总结: 感觉这样记录算法很累赘 20题左右就要拉很长了
还是决定题解放别的地方 -> 每50道算法发一篇思路带题解链接的文章

Easy

1.实现 strStr()
2.搜索插入位置
3.外观数列


第十一天 - 07.20

前记: 想逃离生活这一切

Mid

1.三数之和
2.电话号码的字母组合


第十二天 - 07.21

前记: 滴滴滴

Hard

1.缺失的第一个正数


第十三天 - 07.22

前记: 睡了好久好久 - 最近喜欢看爽文小说了,我去

Easy

1.最大子序和
2.加一
3.x 的平方根
4.爬楼梯


第十四天 - 07.23

前记: 小帅哥加量不加价噢

Mid

1.删除链表的倒数第N个节点
2.括号生成
3.两数相除


第十五天 - 07.24

前记: 明天要和一个小伙伴去惠州潇洒几天咯

Hard

1.接雨水 2.通配符匹配


第十六天 - 07.30

前记: 我回来了!去惠州潇洒了几天,深感要赚钱

Easy

1.合并两个有序数组
2.对称二叉树
3.二叉树的最大深度


第十七天 - 07.31

前记: 继续加油

Mid

1.搜索旋转排序数组
2.在排序数组中查找元素的第一个和最后一个位置
3.有效的数独


第十八天 - 08.01

前记: 八月你好呀!麻烦对我好一点哈哈~ 要坚持努力去大厂

Hard

1.最小覆盖子串


尾言

提前前挂一个尾言嘻嘻~
会坚持持续更新,可能做题方向不太正确时
各位大佬们还望不吝赐教一下!✨
拍照嘛,小编摄影光线贼好


小屋随时欢迎你们到来~
也欢迎各位dalao们的建议噢~