【算法】LeetCode题解(进度:12+6+2)

392 阅读13分钟

作者的话

当前进度:【简单 12】【中等 6】【困难 2】。

  • 建议在 PC 端阅读该文章,以获得最佳的阅读体验。
  • 该文章中的题解,解法不一定最优,但注释尽量详细。
  • 该文章已迁移至Github,望各位看官多多支持。

1.两数之和

// 题目:https://leetcode-cn.com/problems/two-sum/
// 难度:简单
// 标签:数组、哈希表
// 执行用时:64 ms, 击败了 95.98% 的用户
// 内存消耗:36.8 MB, 击败了 6.78% 的用户

// 解题思路:
// 符合直觉的做法是,通过两层循环找到符合要求的两个数字,并返回他们的下标
// 然而其时间复杂度为O(n^2),效率低
// 故考虑利用哈希表来降低时间复杂度:
// 1.遍历nums,建立key和val分别为数字及其在nums中的下标的哈希表
// 2.再次遍历nums,根据当前数字找到目标数字,再根据哈希表找到目标数字的下标
// 3.返回当前数字的下标和目标数字的下标
// 其中,因为利用哈希表查找数字的下标的时间复杂度为O(1)
// 所以整个流程(仅有单层循环)的时间复杂度仅为O(n)
// 而且实际解题时,前两步可以整合为一步

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
  // 1.定义numsMap,其key和val分别表示数字和数字在nums中的下标
  //   如numsMap.set(2,3)表示:收集数字2及其在nums中的下标3到numsMap中
  const numsMap = new Map();
  // 2.遍历nums
  for (let i = 0; i < nums.length; i++) {
    // 2.1 num1和num2加起来等于target,为符合要求的两个数字
    const num1 = nums[i],
      num2 = target - num1;
    // 2.2 在numsMap中寻找num2在nums中的下标j
    //     如果存在,则返回下标i和下标j
    if (numsMap.has(num2)) {
      const j = numsMap.get(num2);
      return [i, j];
    }
    // 2.3 否则,将num1及其在nums中的下标收集到numsMap中
    numsMap.set(num1, i);
  }
};

2.两数相加

// 题目:https://leetcode-cn.com/problems/add-two-numbers/
// 难度:中等
// 标签:链表、数学
// 执行用时:124 ms, 击败了 88.68% 的用户
// 内存消耗:41.6 MB, 击败了 13.23% 的用户

// 解题思路:
// 以node1=[2,4,3,9],node2=[2,4,8]为例
// 两个节点分别表示数字9342和842,其相加的运算过程:
// (格式:node1的值+node2的值+进位值)
// 1.个位数相加:2+2+0=4
// 2.十位数相加:4+4+0=8
// 3.百位数相加:3+8+0=11,进1位
// 4.千位数相加:9+1+1=10,进1位
// 5.万位数相加:0+0+1=1
// 相加结果为10184,故应返回节点[4,8,1,0,1]
// 由此得到解题思路:
// (nodeRet为结果节点)
// 1.个位数相加:nodeRet.val=node1.val+node2.val+进位值
// 2.十位数相加:nodeRet.next.val=node1.next.val+node2.next.val+进位值
// 3.百位数相加:nodeRet.next.next.val=node1.next.next.val+node2.next.next.val+进位值
// ...
// 重复以上步骤,直到node1和node2的next抵达null,无法再递进下去为止

/**
 * @param {ListNode} node1
 * @param {ListNode} node2
 * @return {ListNode}
 */
var addTwoNumbers = function (node1, node2) {
  // 1.定义nodeRet和nodeCur
  //   nodeRet表示结果节点
  //   nodeCur表示结果节点的递进处,初始指向nodeRet
  const nodeRet = new ListNode(0);
  let nodeCur = nodeRet;
  // 2.定义forward,用于表示进位值,初始为0
  let forward = 0;
  // 3.遍历node1和node2
  while (node1 || node2) {
    // 3.1 sum表示当前位的和,初始值为forward
    let sum = forward;
    // 3.2 如果node1存在,sum加上node1.val,同时node1递进一步
    //     node2同理
    if (node1) {
      sum += node1.val;
      node1 = node1.next;
    }
    if (node2) {
      sum += node2.val;
      node2 = node2.next;
    }
    // 3.3 将sum的个位数赋值给nodeCur.val
    nodeCur.val = sum % 10;
    // 3.4 将sum的十位数赋值给forward
    forward = (sum - nodeCur.val) / 10;
    // 3.5 node1和node2递进后,如果其任一存在,则说明递进尚未结束
    //     故设置nodeCur.next指向一个新节点,同时nodeCur递进一步
    if (node1 || node2) {
      nodeCur.next = new ListNode(0);
      nodeCur = nodeCur.next;
    }
  }
  // 4.递进结束后,如果有进位未处理,则补充一个val为forward的新节点
  if (forward > 0) nodeCur.next = new ListNode(forward);
  // 5.返回结果节点
  return nodeRet;
};

3.无重复字符的最长子串

// 题目:https://leetcode-cn.com/problems/two-sum/
// 难度:中等
// 标签:哈希表、双指针、字符串、Sliding Window
// 执行用时:92 ms, 击败了 92.35% 的用户
// 内存消耗:38.3 MB, 击败了 71.23% 的用户

// 解题思路:
// 以s="abcdadc"为例
// 对其进行遍历,当前下标用i表示
// 当前无重复字符子串用sub表示,当前sub的长度用len表示
// 迄今sub的最大长度用lenMax表示
// i=0时,sub="a",故len=1,lenMax=1
// i=1时,sub="ab",len=2,lenMax=2
// i=2时,sub="abc",len=3,lenMax=3
// i=3时,sub="abcd",len=4,lenMax=4
// i=4时,出现了重复字符"a",sub="bcda",len=4,lenMax=4
// i=5时,出现了重复字符"d",sub="ad",len=2,lenMax=4
// i=6时,sub="adc",len=3,lenMax=4
// 以此为思路,从左向右遍历s,其间判断是否出现重复字符
// 如果未出现,len加1;如果出现,根据重复字符下标和当前下标更新len
// 每次更新len时,需要判断len是否大于lenMax,如果是,则也更新lenMax
// 所以解题重点在于,如何判断是否出现重复字符和获取重复字符下标

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
  // 1.定义ret,用于表示lenMax
  let ret = 0;
  // 2.定义charMap,其key和val分别为字符和字符在s中的下标
  //   其是用于判断是否出现重复字符和获取重复字符下标的关键
  const charMap = new Map();
  // 3.定义left和right,分别用于表示重复字符下标和当前下标
  //   left初始为-1,right初始为0
  let left = -1,
    right = 0;
  // 4.从左向右遍历s
  while (right < s.length) {
    // 4.1 获取当前下标的字符char
    const char = s[right];
    // 4.2 如果charMap中有char,说明出现了重复字符
    //     获取该重复字符的下标left_,当left_大于left时才更新left
    //     如果不判断两者的大小关系就直接更新left,会导致如下情况:
    //     对于"abcba",right=3时,left=left_=1,sub="cb",没问题
    //     而right=4时,left=left_=0,sub="abcba",明显不正确
    //     这是因为left只能向右单向移动,如果往回走,中途可能遇到重复字符
    if (charMap.has(char)) {
      const left_ = charMap.get(char);
      left = Math.max(left, left_);
    }
    // 4.3 收集char及其在s中的下标到charMap中
    charMap.set(char, right);
    // 4.4 此时left为重复字符下标,right为当前字符下标
    //     如"abc"遍历到"c"时,left=-1,right=2
    //     right减去left即为当前无重复字符子串长度len
    ret = Math.max(ret, right - left);
    // 4.5 right自增
    right++;
  }
  // 5.返回结果
  return ret;
};

7.整数反转

// 题目:https://leetcode-cn.com/problems/reverse-integer/
// 难度:简单
// 标签:数学
// 执行用时:84 ms, 击败了 87.75% 的用户
// 内存消耗:35.7 MB, 击败了 93.91% 的用户

// 解题思路:
// 以x=-3456为例,考虑如何对其进行反转
// 计算3456%10,可以获取3456的个位数6
// 计算(3456-6)/10,可以将其转换为规模更小的数345
// 而计算345%10,可以获取3456的十位数5
// 累积下来,可以依次获取6、5、4、3这四个数
// 声明一个变量ret,用于表示反转后的数,初始为0
// 每获取到一个数,就对ret进行一次如下形式的更新
// 1.获取到6时,ret=ret*10+6=0+6=6
// 2.获取到5时,ret=ret*10+5=60+5=65
// 3.获取到4时,ret=ret*10+4=65*10+4=654
// 4.获取到3时,ret=ret*10+3=654*10+3=6543
// 最后需要给ret加上负号,并对ret做溢出判断

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function (x) {
  // 1.定义ret,用于表示反转后的整数
  let ret = 0;
  // 2.定义sign,用于表示x的正负
  //   如果x为正数,sign等于1
  //   如果x为负数,sign等于-1
  const sign = Math.sign(x);
  // 3.将x转换为正数,以便于后面的计算
  x *= sign;
  // 4.累积获取x各位上的数字并更新ret
  //   这里以x=3456为例
  while (x > 0) {
    // 4.1 获取x个位上的数6
    const n = x % 10;
    // 4.2 x减去6、再除以10后,x=345
    x = (x - n) / 10;
    // 4.3 ret乘以10、再加上6后,num=6
    ret = ret * 10 + n;
    // 4.4 第1轮,n=6,x=345,ret=6
    //     第2轮,n=5,x=34,ret=60+5=65
    //     第3轮,n=4,x=3,ret=650+4=654
    //     第4轮,n=3,x=0,ret=6540+3=6543
    //     其后,由于x=0,循环中断
  }
  // 5.判断x是否溢出,如果溢出返回0
  if (ret >= Math.pow(2, 31)) return 0;
  // 6.否则,将num带上符号并返回
  return ret * sign;
};

9.回文数

// 题目:https://leetcode-cn.com/problems/palindrome-number/
// 难度:简单
// 标签:数学
// 执行用时:200 ms, 击败了 96.07% 的用户
// 内存消耗:44.5 MB, 击败了 98.00% 的用户

// 解题思路:
// 如果x为负数,那么其必然不是回文数
// 如果x为正数,可以基于第7题,求得反转后的x
// 并根据反转后的x是否和x相等来判断其是否为回文数

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function (x) {
  // 1.如果x为负数,其必然不是回文数
  if (x < 0) return false;
  // 2.否则,需判断反转后的x是否与x相等
  //   如果相等,则x是回文数;如果不相等,则x不是回文数
  return reverse(x) === x;
  // 3.定义函数reverse,用于反转数字
  //   (该函数借由第7题包装而来,不再详述)
  function reverse(x) {
    let y = 0;
    while (x > 0) {
      const n = x % 10;
      x = (x - n) / 10;
      y = y * 10 + n;
    }
    return y;
  }
};

51.N 皇后

// 题目:https://leetcode-cn.com/problems/n-queens/
// 难度:困难
// 标签:回溯算法
// 执行用时:68 ms, 击败了 99.44% 的用户
// 内存消耗:38.2 MB, 击败了 100.00% 的用户

// 解题思路:
// 根据摆放规则,每行仅能且必须摆放一个皇后,所以可以将摆放情况用数组来表示
// 如[2,4,1,3]表示第1~4行的皇后分别摆放在第2、4、1、3列上
// 基于此,采用递归+回溯的方式来摆放皇后

/**
 * @param {number} n
 * @return {string[][]}
 */
var solveNQueens = function (n) {
  // 1.定义rets,用于存放所有的摆放情况ret1、ret2、...
  const rets = [];
  // 2.定义用于摆放皇后的函数arrange,其参数ret为当前的摆放情况
  //   如arrange([3,1])表示,第1~2行的皇后已经分别摆放在第3、1列上了
  //   将基于当前摆放情况,进行下一行(第3行)的摆放
  function arrange(ret) {
    // 2.1 第1~ret.length行的皇后都已经被摆放好了
    //     那么即将进行摆放的行数row为ret.length+1
    const row = ret.length + 1;
    // 2.2 如果row大于n,表示摆放已经结束了
    //     此时将ret添加到rets中,并结束摆放
    if (row > n) {
      rets.push(ret);
      return;
    }
    // 2.3 反之,摆放未结束,需执行摆放
    // 2.3.1 第row行的棋子可在1~n列内摆放
    for (let col = 1; col <= n; col++) {
      // 2.3.2 定义flag,用于表示第row行的皇后摆放在第col列是否可行
      let flag = true;
      // 2.3.3 遍历ret,即遍历已经摆放过的皇后(第1~row_行)
      //       因为row_是从1开始计数的,而ret下标是从0开始计数的
      //       所以第row_行的皇后被摆放在的列用ret[row_-1]表示
      for (let row_ = 1; row_ < row; row_++) {
        // 2.3.3.1 用col_表示第row_行的皇后被摆放在的列数
        const col_ = ret[row_ - 1];
        // 2.3.3.2 因为ret的格式确保了每行只摆放一个皇后
        //         所以无需判断是否有同行的情况
        // 2.3.3.3 判断是否有同列的情况
        if (col === col_) {
          flag = false;
          break;
        }
        // 2.3.3.4 判断是否有同对角线的情况
        if (row - row_ === Math.abs(col - col_)) {
          flag = false;
          break;
        }
      }
      // 2.3.4 当遍历完已经摆放过的皇后时,如果flag仍为true
      //       表示将第row行皇后摆放在第col列是可行的
      if (flag) {
        // 2.3.4.1 既然可行,那么进行摆放
        //         ret[row-1]表示第row行的摆放情况
        ret[row - 1] = col;
        // 2.3.4.2 第row行已经摆放好了,继续下一行的摆放
        //         为了防止不同分支情况互相影响,传递的参数为ret的复制品
        arrange([...ret]);
      }
    }
  }
  // 3.进行初始摆放,即在摆放完第一行的情况下进行第二行的摆放
  for (let col = 1; col <= n; col++) {
    arrange([col]);
  }
  // 4.返回结果前要对rets格式进行转换
  //   如觉得不好理解,可按步骤编号从内向外看
  // 4.2 返回经过拼接的ret们
  return rets.map((ret) => {
    // 4.1 遍历ret,col表示当前行的皇后摆放在第col列上
    return ret.map((col) => {
      // 4.1.1 建立数组,长度为棋盘尺寸,元素均填充为"."
      const charArr = new Array(n).fill(".");
      // 4.1.2 将col-1下标处的元素修改为"Q",表示皇后
      charArr[col - 1] = "Q";
      // 4.1.3 拼接为字符串并返回
      return charArr.join("");
    });
  });
};

52.N 皇后 II

// 题目:https://leetcode-cn.com/problems/n-queens-ii/
// 难度:困难
// 标签:回溯算法
// 执行用时:72 ms, 击败了 78.99% 的用户
// 内存消耗:37.5 MB, 击败了 100.00% 的用户

// 解题思路:
// 该题需求出N皇后的解的个数,而第51题需求N皇后的所有解
// 两题大同小异,可以基于第51题来解答该题

/**
 * @param {number} n
 * @return {string[][]}
 */
var totalNQueens = function (n) {
  // 以下只对不同于第51题的部分做说明
  // 1.定义rets,用于表示解的个数,初始为0
  let rets = 0;
  function arrange(ret) {
    const row = ret.length + 1;
    // 2.结束摆放,解的个数加1
    if (row > n) {
      rets++;
      return;
    }
    for (let col = 1; col <= n; col++) {
      let flag = true;
      for (let row_ = 1; row_ < row; row_++) {
        const col_ = ret[row_ - 1];
        if (col === col_) {
          flag = false;
          break;
        }
        if (row - row_ === Math.abs(col - col_)) {
          flag = false;
          break;
        }
      }
      if (flag) {
        ret[row - 1] = col;
        arrange([...ret]);
      }
    }
  }
  for (let col = 1; col <= n; col++) {
    arrange([col]);
  }
  // 3.直接返回rets
  return rets;
};

58.最后一个单词的长度

// 题目:https://leetcode-cn.com/problems/length-of-last-word/
// 难度:简单
// 标签:字符串
// 执行用时:60 ms, 击败了 93.05% 的用户
// 内存消耗:32.3 MB, 击败了 100.00% 的用户

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLastWord = function (s) {
  // 1.定义ret,表示最后一个单词的长度
  let ret = 0;
  // 2.去除s尾端的空格
  s = s.trimEnd();
  // 3.从后向前遍历s
  for (let i = s.length - 1; i >= 0; i--) {
    // 3.1 如遇到空格,就说明单词结束了,应中断循环
    if (s[i] === " ") break;
    // 3.2 否则,ret加1
    ret++;
  }
  // 4.返回结果
  return ret;
};

66.加一

// 题目:https://leetcode-cn.com/problems/plus-one/
// 难度:简单
// 标签:数组
// 执行用时:56 ms, 击败了 98.83% 的用户
// 内存消耗:32.7 MB, 击败了 100.00% 的用户

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function (digits) {
  // 1.在digits首端插入数字0
  //   以应对可能出现的进位情况
  digits.unshift(0);
  // 2.定义index,用于从后向前遍历digits
  let index = digits.length - 1;
  // 3.末尾数字加1
  digits[index] += 1;
  // 4.遍历digits
  while (index > 0) {
    // 4.1 如果当前数为10,则进位
    if (digits[index] === 10) {
      // 4.2 当前数字从10变为0
      digits[index] = 0;
      // 4.3 前一个数字加1
      digits[index - 1] += 1;
    }
    // 4.4 index减1
    index--;
  }
  // 5.如果digits首端仍为0,则将其删除
  if (digits[0] === 0) digits.shift();
  // 6.返回结果
  return digits;
};

292.Nim 游戏

// 题目:https://leetcode-cn.com/problems/nim-game/
// 难度:简单
// 标签:脑筋急转弯、极小化极大
// 执行用时:68 ms, 击败了 57.99% 的用户
// 内存消耗:32.3 MB, 击败了 100.00% 的用户

// 解题思路:
// 首先,分情况进行讨论:
// 1.当n=1,2,3时【我方赢】
// 2.当n=4时,拿1剩3,拿2剩2,拿3剩1。对方面临情况1【我方输】
// 3.当n=5,6,7时,有5拿1剩4,有6拿2剩4,有7拿3剩4。我方能使对方面临情况2【我方赢】
// 4.当n=8时,拿1剩7,拿2剩6,拿3剩5。对方面临情况3,故对方能使我方面临情况2【我方输】
// 5.当n=9,10,11时,同理,我方能使对方面临情况4【我方赢】
// 6.当n=12时,同理,对方能使我方面临情况4【我方输】
// ...
// 发现规律:
// 当n为4的倍数时,最终对方会面临着n=1,2,3的情况,对方赢
// 否则,最终我方会面临着n=1,2,3的情况,我方赢
// 证明:
// 1.设我方初始面临n=4x的情况,拿完可能剩4x-1,4x-2,4x-3
// 2.对方再拿,能使我方面临4x-4,即n=4(x-1)的情况
// 3.再一个来回,我方面临n=4(x-2)的情况
// 4.最终,我方将面临x-2=1,即n=4的情况,我方输
// (否则,对方最终将面临n=4的情况,我方赢)

/**
 * @param {number} n
 * @return {boolean}
 */
var canWinNim = function (n) {
  return n % 4;
};

319.灯泡开关

// 题目:https://leetcode-cn.com/problems/bulb-switcher/submissions/
// 难度:中等
// 标签:脑筋急转弯、数学
// 执行用时:64 ms, 击败了 79.25% 的用户
// 内存消耗:32.3 MB, 击败了 100.00% 的用户

// 解题思路:
// 据题意,相当于在第i轮时,每第i个灯泡就切换状态
// 第1轮亦是,每1个灯泡就切换状态,即每个灯泡都从关切换成开
// 试分析灯泡x在整个过程中的切换次数为P(x),1<=x<=n
// 1.首先,对于灯泡x,i=1,x时,是必切换的
//   比如,对于灯泡8,i=1,8时,是必切换的,即2次切换
// 2.推之,如果存在数k能被x整除,那么x/k也一定能被x整除
//   即对于灯泡x,i=k,x/k时,灯泡x会切换
// 2.1 一般情况下,k和x/k总是成对出现,所以总共会有偶数次切换
//     比如,对于灯泡15,存在k=1,3,5,15,1与3、5与15成对出现
// 2.2 但是有一类特殊情况,当x为完全平方数时,会有奇数次切换
//     比如,对于灯泡16,存在k=1,2,4,8,16,1与16、2与8成对出现
//     这是因为k=4时,x/4=4,与k重合,4不像1和2一样,有它自己的小伙伴
// 总而言之,对于灯泡x,如x为完全平方数,则有奇数次切换,否之有偶数次切换
// 而且,若一灯泡若历经偶数次切换,那么它最终保持初始状态,为“关”;
//      若一灯泡若历经奇数次切换,那么它最终不同于初始状态,为“开”
// 所以在1~n范围内,若有m个完全平方数,最终就会有m个灯泡为“开”
// m的值易求,为“n开方后向下取整”

/**
 * @param {number} n
 * @return {number}
 */
var bulbSwitch = function (n) {
  return Math.floor(Math.sqrt(n));
};

415.字符串相加

// 题目:https://leetcode-cn.com/problems/add-strings/
// 难度:简单
// 标签:字符串
// 执行用时:72 ms, 击败了 94.59% 的用户
// 内存消耗:37.2 MB, 击败了 25.00% 的用户

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var addStrings = function (num1, num2) {
  // 1.定义ret,用于表示结果字符串
  //   index1和index2分别表示遍历num1和num2时的下标
  //   因为要末尾对齐,所以index1指向num1末尾,index2同理
  //   next表示是否进位,比如计算9+9=18时需将next设为true
  let ret = "",
    index1 = num1.length - 1,
    index2 = num2.length - 1,
    next = false;
  // 2.以num1="35",num2="9"为例
  //   从末尾向开头方向遍历num1和num2
  while (index1 >= 0 || index2 >= 0) {
    // 2.1 第一轮中,index1为1,index2为0,故n1为5,n2为9
    //     同时,index1和index2都减1
    const n1 = Number(num1[index1--] || "0"),
      n2 = Number(num2[index2--] || "0");
    // 2.2 计算n1和n2的和,如果next为true,表示要进位
    //     刚好Number(false)等于0,Number(true)等于1
    //     此时next为false,不用进位,故sum等于14
    const sum = Number(next) + n1 + n2;
    // 2.3 sum大于等于10,表示下一轮时要进位了,故将其设为true
    next = sum >= 10;
    // 2.4 连接"4"和"",并赋值给ret
    ret = String(sum % 10).concat(ret);
    // 2.5 第二轮过程:
    // 2.5.1 index1为0,index2为-1(越界),故n1为3,n2为0
    // 2.5.2 next为true,表示进位,故sum=1+3+0=4
    // 2.5.3 sum小于10,下一轮不进位,将next设为false
    // 2.5.4 连接"4"和"4",并赋值给ret
    // 2.6 第二轮结束后,因为index2不再大于等于0,所以会结束循环
  }
  // 3.如果next为true,得在ret前面加上"1"
  if (next) ret = String("1").concat(ret);
  // 4.返回结果
  return ret;
};

441.排列硬币

// 题目:https://leetcode-cn.com/problems/arranging-coins/
// 难度:简单
// 标签:数学、二分查找
// 执行用时:92 ms, 击败了 92.75% 的用户
// 内存消耗:37.1 MB, 击败了 100.00% 的用户

// 解题思路:
// 要达到第k行,需要1+2+...+k-1+k个硬币
// 即需要求f(k)=1/2*k(1+k)=n的根
// 根据一元二次方程求根公式,k=(-1+(1+8n)^(1/2))/2
// 将k向下取整,即为本题结果

/**
 * @param {number} n
 * @return {number}
 */
var arrangeCoins = function (n) {
  return Math.floor((Math.sqrt(1 + 8 * n) - 1) / 2);
};

539.最小时间差

// 题目:https://leetcode-cn.com/problems/minimum-time-difference/
// 难度:中等
// 标签:字符串
// 执行用时:92 ms, 击败了 87.69% 的用户
// 内存消耗:38.6 MB, 击败了 100.00% 的用户

/**
 * @param {string[]} timePoints
 * @return {number}
 */
var findMinDifference = function (timePoints) {
  // 1.为便于比较,先遍历timePoints,将时间转换为分钟
  //   并存放在timeMinutes中,如2:15=>135,12:30=>750
  const timeMinutes = timePoints.map((timePoint) => {
    // 1.1 获取hour和minute,注意,这里两者均为String类型
    const [hour, minute] = timePoint.split(":");
    // 1.2 先将hour和minute转换为Number类型,再计算时间
    return Number(hour * 60) + Number(minute);
  });
  // 2.对timeMinutes从小到大进行排序
  timeMinutes.sort((a, b) => a - b);
  // 3.在timeMinutes末尾添加“最小时间+一圈”
  //   这是为了应对23:59和0:00这种情况
  timeMinutes.push(timeMinutes[0] + 24 * 60);
  // 4.找出timeMinutes中最小的相邻时间间隔
  // 4.1 定义dMin,用于表示最小时间差
  let dMin = 24 * 60;
  // 4.2 从下标1开始,遍历timeMinutes
  for (let i = 1; i < timeMinutes.length; i++) {
    // 4.2.1 计算时间差
    const d = timeMinutes[i] - timeMinutes[i - 1];
    // 4.2.2 如果d小于dMin,则将d赋值给dMin
    dMin = Math.min(dMin, d);
  }
  // 5.返回结果
  return dMin;
};

557.反转字符串中的单词 III

// 题目:https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/
// 难度:简单
// 标签:字符串
// 执行用时:60 ms, 击败了 100.00% 的用户
// 内存消耗:36.6 MB, 击败了 100.00% 的用户

/**
 * @param {string} s
 * @return {string}
 */
var reverseWords = function (s) {
  // (这里为了分步骤讲解,未采用链式调用)
  // 1.以"Let's take LeetCode contest"为例,对其进行分割
  //   得到单词数组["Let's","take","LeetCode","contest"]
  let words = s.split(" ");
  // 2.遍历words
  words = words.map((word) => {
    // 2.1 以"take"为例,其被分割为["t","a","k","e"]
    const letters = word.split("");
    // 2.2 颠倒["t","a","k","e"]为["e","k","a","t"]
    letters.reverse();
    // 2.3 将["e","k","a","t"]合并为"ekat"
    return letters.join("");
  });
  // 3.合并颠倒的word们,并返回结果
  return words.join(" ");
};

709.转换成小写字母

// 题目:https://leetcode-cn.com/problems/to-lower-case/
// 难度:简单
// 标签:字符串
// 执行用时:48 ms, 击败了 99.64% 的用户
// 内存消耗:32.4 MB, 击败了 100.00% 的用户

/**
 * @param {string} str
 * @return {string}
 */
var toLowerCase = function (str) {
  // 1.定义ret,表示结果字符串
  let ret = "";
  // 2.遍历str
  for (let i = 0; i < str.length; i++) {
    // 2.1 获取下标i字符的ASCII码
    let code = str.charCodeAt(i);
    // 2.2 如果code在[65,90]区间内,说明其是大写字符
    //     大小写字符的编码差为32,所以加32将其转为小写字符
    if (code >= 65 && code <= 90) code += 32;
    // 2.3 将code转回字符,并添加到ret末端
    ret += String.fromCharCode(code);
  }
  // 3.返回结果
  return ret;
};

777.在 LR 字符串中交换相邻字符

// 题目:https://leetcode-cn.com/problems/swap-adjacent-in-lr-string/
// 难度:中等
// 标签:脑筋急转弯
// 执行用时:68 ms, 击败了 100.00% 的用户
// 内存消耗:36.6 MB, 击败了 100.00% 的用户

// 解题思路:
// 以一符合要求的情况为例:
// (X相当于空地,L向左走,R向右走,L和R不能穿越彼此)
// start: RXXLRXRXL => R--LR-R-L => RLRRL
// end:   XRLXXRRLX => -RL--RRL- => RLRRL
// 发现,要符合要求,应符合以下规律:
// 1.start和end中,L的数量相等
// 2.start和end中,R的数量相等
// 3.start和end中,L和R的排序相同,即有一一对应关系
// 3.1 start中的L的位置要比end中的对应的L的位置要后(或相同)
// 3.2 start中的R的位置要比end中的对应的R的位置要前(或相同)

/**
 * @param {string} start
 * @param {string} end
 * @return {boolean}
 */
var canTransform = function (start, end) {
  // 1.声明countL和countR
  //   countL: end比start多出的L的数量
  //   countR: start比end多出的R的数量
  //   countL和countR的值能用于判断是否符合规律1和2
  let countL = 0,
    countR = 0;
  // 2.遍历字符串
  for (let i = 0; i < start.length; i++) {
    // 2.1 因为L是向左移动的,end中L的位置比start中的要前(或相同)
    //     所以先判断end[i],再判断start[i]
    // 2.1.1 如果end[i]为L,countL加1
    if (end[i] === "L") countL++;
    // 2.1.2 如果start[i]为L,countL减1
    if (start[i] === "L") {
      countL--;
      // 2.1.3 R!==0这个条件,是用于判断L和R是否穿越了彼此
      //       countR<0这个条件,是用于判断start中L的数量是否超额
      //       如果这两个条件中任一为真,则中断循环
      //       且中断循环时,countL和countR其一不为0
      if (countL < 0 || countR !== 0) break;
    }
    // 2.2 同理,对R进行一系列判断
    if (start[i] === "R") countR++;
    if (end[i] === "R") {
      countR--;
      if (countR < 0 || countL !== 0) break;
    }
  }
  // 3.经过上面一系列的判断后,如果countL和countR均为0
  //   则表示符合规律1、2、3,即符合要求
  return countL === 0 && countR === 0;
};

1033.移动石子直到连续

// 题目:https://leetcode-cn.com/problems/moving-stones-until-consecutive/
// 难度:简单
// 标签:脑筋急转弯
// 执行用时:68 ms, 击败了 77.78% 的用户
// 内存消耗:33.1 MB, 击败了 100.00% 的用户

/**
 * @param {number} a
 * @param {number} b
 * @param {number} c
 * @return {number[]}
 */
var numMovesStones = function (a, b, c) {
  // 1.对石子位置进行排序:左记为x,中间记为y,右记为z
  const [x, y, z] = [a, b, c].sort((a, b) => a - b);
  // 2.区分可能出现的3种情况
  // 2.1 当x、y、z连续时,无需移动
  if (z - x === 2) {
    return [0, 0];
  }
  // 2.2 当x和y间隔或y和z间隔小于等于2时
  //     最少只需移动1步,如[1,3,5]、[2,3,5]
  //     最多需移动(z-y-1)+(y-x-1)=z-x-2步
  else if (y - x <= 2 || z - y <= 2) {
    return [1, z - x - 2];
  }
  // 2.3 其他情况下,最少需移动2步,如[1,4,7]
  //     最多需移动的步数同2.2
  else {
    return [2, z - x - 2];
  }
};

1078.Bigram 分词

// 题目:https://leetcode-cn.com/problems/occurrences-after-bigram/
// 难度:简单
// 标签:哈希表
// 执行用时:68 ms, 击败了 60.00% 的用户
// 内存消耗:32.3 MB, 击败了 100.00% 的用户

/**
 * @param {string} text
 * @param {string} first
 * @param {string} second
 * @return {string[]}
 */
var findOcurrences = function (text, first, second) {
  // 1.以空格为隔板,对text进行分割,将其转换为单词数组
  const words = text.split(" ");
  // 2.遍历words,筛选出所有的符合要求的词,并返回结果
  return words.filter((word, index) => {
    // 2.1 wordIndex为0或1时,前面的词数不够两个
    if (index < 2) return false;
    // 2.2 判断前两个词是否分别为first和second
    if (words[index - 2] === first && words[index - 1] === second) return true;
  });
};

1227.飞机座位分配概率

// 题目:https://leetcode-cn.com/problems/airplane-seat-assignment-probability/
// 难度:中等
// 标签:脑筋急转弯、数学、动态规划
// 执行用时:64 ms, 击败了 78.57% 的用户
// 内存消耗:32.1 MB, 击败了 100.00% 的用户

// 解题思路:
// 参考:https://leetcode-cn.com/problems/airplane-seat-assignment-probability/solution/ju-ti-fen-xi-lai-yi-bo-by-jobhunter-4/
// 易知,P(1)=1,P(2)=0.5
// 当n>=3时,设有乘客1~n,对应座位1~n
// 1.分析乘客1
// 1.1 坐到位置1的概率为1/n,此后乘客n能坐到位置n
// 1.2 坐到位置n的概率为1/n,此后乘客n不能坐到位置n
// 1.3 坐到位置k∈[2,n-1]的概率为(n-2)/n
//     此后乘客2~k-1能坐到正确位置上,乘客k待分析
// k.如果出现了情况1.3,则分析乘客k
//   此时剩下的位置数量为n-k+1,记为a
// k.1 坐到位置1的概率为1/a,此后乘客n能坐到位置n
// k.2 坐到位置n的概率为1/a,此后乘客n不能坐到位置n
// k.3 坐到位置m∈[k+1,n-1]的概率为(a-2)/a
//     此后乘客k+1~n-1能坐到正确位置上,乘客m待分析
// 会发现,乘客1和乘客k面临的选择是一样的
// 只是选择规模不一样,前者为n,后者为a=n-k+1
// 因k在[2,n-1]内均匀分布,故a在[2,n-1]内均匀分布
// 由此推出:
// 1.P(n) = 1/n + 1/n * (P(2) + ... + P(n-1))
//        = 1/n * (1 + P(2) + ... + P(n-1))
// 2.P(n+1) = 1/(n+1) * (1 + P(2) + ... + p(n-1) + p(n))
// 3.(n+1) * p(n+1) - n * P(n) = P(n)
// 4.P(n+1) = P(n)
// 因为P(3) = 1/3 * (1 + 0.5) = 0.5
// 所以n>=3时,P(n) = P(3) = 0.5
// 整理上述式子,得:
// n=1时,P(n) = 1
// n>1时,P(n) = 0.5

/**
 * @param {number} n
 * @return {number}
 */
var nthPersonGetsNthSeat = function (n) {
  return n === 1 ? 1 : 0.5;
};