LeetCode Hot 100 题解 - JavaScript

LeetCode Hot 100 题解 - JavaScript cover

由于面试需要,决定从零开始刷 LeetCode Hot 100,记录一下刷题过程。

问题都很长,所以只记录题目和解题思路,当前进度(32/100 + 6),因为写了很多题没记录所以等下一遍刷 hot 100 再更新了 = =(很快,一个月内)

准备

先简单了解下数据结构,很不错的视频

你是天才,所以不用学数据结构 | bilibili

然后上 LeetCode 注册账号,开始刷题。

哈希

1. 两数之和

最直接的是使用两层 for 循环,遍历判断两数之和是否等于 target,时间复杂度 O(n^2)

更好的思路是使用哈希表(Map),A + B = C,这里是去找 C - A 的值,如果这个值在哈希表中存在,就说明找到了 A 和 B,时间复杂度为 O(n)

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
  const map = new Map()

  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i]
    if (map.has(need)) return [map.get(need), i]
    map.set(nums[i], i)
  }

  return []
}

49. 字母异位词分组

字母异位词就是两个单词的组成字母完全相同,如 abc 和 cba 是异位词

思路:对每个字符串字母排序,判断排序后字符串是否相等,然后对原字符进行分组

思路有了就找结构,要存原字符串,然后原字符串要对应排序后的字符串,所以哈希表(Map)最好不过了

/**
 * @param {string[]} strs
 * @return {string[][]}
 */
var groupAnagrams = function (strs) {
  const map = new Map()

  for (const s of strs) {
    const key = s.split('').sort().join('')
    if (!map.has(key)) {
      map.set(key, [])
    }
    map.get(key).push(s)
  }

  return [...map.values()]
}

128. 最长连续序列

思路:先对数组去重排序,存两个值:一个当前连续长度,一个最长连续长度,然后遍历一遍新数组,连续的就加当前长度,不连续就重置当前长度,最后返回最长长度

这里去重用的是 Set 结构,去除重复元素,排序用数组的 sort 方法。

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function (nums) {
  if (nums.length === 0) return 0
  const snums = [...new Set(nums)].sort((a, b) => a - b)

  let current = 1
  let result = 1

  for (let i = 0; i < snums.length - 1; i++) {
    const a = snums[i]
    const b = snums[i + 1]

    if (b - a === 1) {
      current++
      result = Math.max(current, result)
    } else {
      current = 1
    }
  }

  return result
}

最开始漏掉了判断数组长度为 0 的情况,每道题都应该去考虑边界情况。

这个能够 AC(Accepted),但是时间复杂度是 O(n logn),题目要求 O(n) 的时间复杂度,所以这个解法不符合要求。

看了题解,是在判断是否连续时,判断当前元素的上一个元素是否存在,如果不存在则说明当前元素是连续序列的起点,然后从当前元素去向后找连续的元素。

更改后:

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function (nums) {
  if (nums.length === 0) return 0

  const set = new Set(nums)
  let longest = 0

  for (const num of set) {
    // 连续起始
    if (!set.has(num - 1)) {
      let current = num
      let length = 1

      while (set.has(current + 1)) {
        current++
        length++
      }

      longest = Math.max(length, longest)
    }
  }

  return longest
}

双指针

283. 移动零

思路:将所有零后移,可以使用双指针,一个指针遍历数组,另一个指针记录非零元素位置,当遍历到非零元素时,将其放到非零元素位置指针上,然后指针后移,直到遍历完数组,最后将非零元素位置指针之后的元素全部设为 0。

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function (nums) {
  let slow = 0

  for (let fast = 0; fast < nums.length; fast++) {
    if (nums[fast] !== 0) {
      nums[slow] = nums[fast]
      slow++
    }
  }

  while (slow < nums.length) {
    nums[slow] = 0
    slow++
  }
}

11. 盛最多水的容器

暴力思路:容积最大,就是 x * y 最大,那就挨个遍历所有可能的组合,取最大值,时间复杂度 O(n^2)

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
  let max = 0
  for (let i = 0; i < height.length; i++) {
    for (let j = i + 1; j < height.length; j++) {
      const x = j - i
      const y = Math.min(height[i], height[j])

      max = Math.max(max, x * y)
    }
  }

  return max
}

这个解法最无脑,但是超时了

思路:原理依旧是 x * y 最大,使用双指针分别指向最左侧和最右侧,这样的话就是 x 最大,接下来指针向内移动,每次移动 x 都会减小,所以要让 y 尽可能大,将两个指针所在的高度比较,将较小的指针向内移动,直到两个指针相遇,时间复杂度 O(n)

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
  let left = 0
  let right = height.length - 1
  let ans = 0

  while (left < right) {
    const h = Math.min(height[left], height[right])
    const w = right - left
    ans = Math.max(ans, h * w)

    if (height[left] < height[right]) {
      left++
    } else {
      right--
    }
  }

  return ans
}

这里用到了贪心算法,贪心算法就是局部最优 -> 全局最优。

15. 三数之和

虽然这个依然能暴力,但是 3 个 for 循环,时间复杂度飙到 O(n^3)了,所以还是要用双指针。

思路:固定一个数,然后使用双指针找另外两个数,跟 11. 盛最多水的容器 类似,两个指针向内移动。

容易漏的点是去重,外层 for 索引和两个指针都要去重。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function (nums) {
  const ans = []
  let snums = nums.sort((a, b) => a - b)

  for (let i = 0; i < snums.length; i++) {
    if (i > 0 && snums[i] === snums[i - 1]) continue
    const a = snums[i]
    let left = i + 1
    let right = snums.length - 1

    while (left < right) {
      const b = snums[left]
      const c = snums[right]
      const sum = a + b + c

      if (sum === 0) {
        ans.push([a, b, c])
        while (left < right && snums[left] === snums[left + 1]) left++
        while (left < right && snums[right] === snums[right - 1]) right--

        left++
        right--
      } else if (sum < 0) {
        left++
      } else {
        right--
      }
    }
  }

  return ans
}

42. 接雨水

思路:每个位置的雨水是被两侧围住的,能分析出来每个位置的雨水量 = min(左边最高高度, 右边最高高度) - 当前高度。

暴力解法是每个位置向两侧遍历找最高高度,时间复杂度 O(n^2);稍微好一点的话,遍历一遍数组,用两个数组分别存储每个位置的左边最高高度和右边最高高度,然后再遍历一遍数组计算雨水量,时间复杂度 O(n)。

更好的办法是使用双指针,两个指针分别指向数组的两端,在线更新左右两侧的最高高度,移动较低的指针,直到两个指针相遇。因为从最左侧和从最右侧出发,对于一侧经过的位置是逐渐增加的,所以可以在线更新最高高度。

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
  let l = 0
  let r = height.length - 1
  let leftMax = 0
  let rightMax = 0
  let ans = 0

  while (l < r) {
    if (height[l] <= height[r]) {
      if (height[l] >= leftMax) {
        leftMax = height[l]
      } else {
        ans += leftMax - height[l]
      }
      l++
    } else {
      if (height[r] >= rightMax) {
        rightMax = height[r]
      } else {
        ans += rightMax - height[r]
      }
      r--
    }
  }

  return ans
}

滑动窗口

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

思路:滑动窗口,[left, right],找连续无重复最长子串,让 left 先固定,right 向右移动,right 移动过程用 Map 记录字符索引,这样当 right 移动到重复字符时,将 left 移动到重复字符的下一个位置,名副其实的 “滑动窗口”。

有个容易漏的点,移动 left 应该比较重复字符的索引和 left 当前大小,取最大值,这样才能保证 left 不会回退过去。

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
  if (s.length <= 1) return s.length
  let ans = 0
  const map = new Map()
  let left = 0

  for (let right = 0; right < s.length; right++) {
    if (map.has(s[right])) {
      left = Math.max(left, map.get(s[right]) + 1)
    }
    map.set(s[right], right)
    ans = Math.max(ans, right - left + 1)
  }

  return ans
}

438. 找到字符串中所有字母异位词

首先是最无脑打法:

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, p) {
  const n = p.length
  const target = p.split('').sort().join()
  let left = 0
  const ans = []

  for (let right = n - 1; right < s.length; right++) {
    if (
      s
        .slice(left, right + 1)
        .split('')
        .sort()
        .join() == target
    ) {
      ans.push(left)
    }
    left++
  }

  return ans
}

提交运行,超出时间限制,只好另寻方法

看了题解,使用滑动窗口的同时统计字符频次,一个 need Map + 一个 window Map + 一个 valid 计数器,滑动窗口的同时更新 window Map,然后对 valid++ 或 valid—,当 valid === need Map 大小 (need.size) 时,说明找到了一个异位词。

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, p) {
  const ans = []
  const need = new Map()
  const window = new Map()

  for (const ch of p) {
    need.set(ch, (need.get(ch) || 0) + 1)
  }

  let left = 0
  let right = 0
  let valid = 0

  while (right < s.length) {
    const ch = s[right]
    if (need.has(ch)) {
      window.set(ch, (window.get(ch) || 0) + 1)
      if (need.get(ch) === window.get(ch)) {
        valid++
      }
    }
    right++

    if (right - left === p.length) {
      if (valid === need.size && right - left === p.length) {
        ans.push(left)
      }

      const l = s[left]
      if (need.has(l)) {
        if (need.get(l) === window.get(l)) {
          valid--
        }
        window.set(l, window.get(l) - 1)
      }

      left++
    }
  }

  return ans
}

子串

560. 和为 K 的子数组

思路:前缀和 + 哈希表,遍历数组,计算前缀和 sum,同时把前缀和记录到哈希表,值是出现的次数,然后判断 sum - k 是否在哈希表中(看到了两数之和的影子),如果存在就 ans++。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySum = function (nums, k) {
  let ans = 0
  let sum = 0
  const map = new Map()
  map.set(0, 1)

  for (const num of nums) {
    sum += num
    const target = sum - k
    if (map.has(target)) {
      ans += map.get(target)
    }
    map.set(sum, (map.get(sum) || 0) + 1)
  }

  return ans
}

239. 滑动窗口最大值

~~休息一会~~ 休息完了,先来个暴力解法(bushi)

思路:又是新东西,这次用单调递减队列 q(ueue) 去获取最大值,最开始是用队列存的值,结果发现如果有重复值,删除的时候就会出问题,改成存索引简直完美。新增元素时,从队尾不断向前比较,然后填入元素,同时根据当前最大值的索引判断是否过期,在窗口成型后,每次推送 q[0] 的值到结果数组中。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  const n = nums.length
  if (n === 0 || k === 0) return []

  const res = []
  const q = []

  for (let i = 0; i < n; i++) {
    while (q.length && nums[i] >= nums[q[q.length - 1]]) {
      q.pop()
    }

    q.push(i)

    if (q[0] <= i - k) {
      q.shift()
    }

    if (i >= k - 1) {
      res.push(nums[q[0]])
    }
  }

  return res
}

76. 最小覆盖子串

思路:这一个跟找字母异位词类似…依旧使用滑动窗口 + 哈希表,统计字符频次,一个 need Map + 一个 window Map + 一个 valid 计数器,滑动窗口的同时更新 window Map,然后对 valid++ 或 valid—,当 valid === need Map 大小 (need.size) 时,说明找到了一个覆盖子串,然后左侧不断收缩,记录最小长度,直到 valid !== need.size 跳出循环,继续右侧扩张。

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function (s, t) {
  if (s.length === 0 || t.length > s.length) return ''

  const need = new Map()
  const window = new Map()
  let valid = 0

  let start = 0
  let minLen = Infinity

  for (const s of t) {
    need.set(s, (need.get(s) || 0) + 1)
  }

  let left = 0
  let right = 0
  while (right < s.length) {
    const b = s[right]

    if (need.has(b)) {
      window.set(b, (window.get(b) || 0) + 1)
      if (need.get(b) === window.get(b)) {
        valid++
      }
    }
    right++

    while (valid === need.size) {
      if (right - left < minLen) {
        minLen = right - left
        start = left
      }

      const a = s[left]
      if (need.has(a)) {
        if (window.get(a) === need.get(a)) {
          valid--
        }
        window.set(a, window.get(a) - 1)
      }
      left++
    }
  }

  if (minLen === Infinity) return ''
  return s.substring(start, start + minLen)
}

普通数组

53. 最大子数组和

思路:找到连续子数组最大的和,想到前缀和,这个和一定是一个前缀和减去前面最小的前缀和。那就很简单了,for 遍历一遍数组,计算前缀和 pre,同时记录最小前缀和 min,取 pre - min 的最大值。

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function (nums) {
  let max = -Infinity
  let min = 0
  let pre = 0
  for (let i = 0; i < nums.length; i++) {
    pre += nums[i]
    max = Math.max(max, pre - min)
    min = Math.min(min, pre)
  }

  return max
}

还有一种思路是动态规划,dp[i] 表示以 nums[i] 结尾的最大子数组和,那么 dp[i] = max(dp[i - 1] + nums[i], nums[i]),也就是让当前元素加上前一个最大子数组和,与当前元素本身比较,取最大值。有点像走楼梯问题,走楼梯只能走一阶或者两阶,那么走最后一阶的时候一定是从倒数第二阶或倒数第一阶走上来的。

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function (nums) {
  let cur = nums[0]
  let res = nums[0]

  for (let i = 1; i < nums.length; i++) {
    cur = Math.max(cur + nums[i], nums[i])
    res = Math.max(res, cur)
  }

  return res
}

56. 合并区间

思路:先对区间左端点排序,然后遍历区间数组,这里用 ans 存储合并后的区间。

这里有个点是 last 是对 ans 的最后一个数组元素的引用,直接修改 last[1] 就是修改了 ans 中的最后一个区间。原本一股脑写了 ans.pop(),然后再 push()

/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
var merge = function (intervals) {
  if (intervals.length === 0) return []

  const ans = []
  intervals.sort((a, b) => a[0] - b[0])
  ans.push(intervals[0])

  for (let i = 1; i < intervals.length; i++) {
    const cur = intervals[i]
    const last = ans[ans.length - 1]

    if (cur[0] <= last[1]) {
      last[1] = Math.max(last[1], cur[1])
    } else {
      ans.push(cur)
    }
  }

  return ans
}

189. 轮转数组

思路:用一个额外数组存储轮转后的数组,很简单直接

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var rotate = function (nums, k) {
  const n = nums.length
  k = k % n
  const res = new Array(n)
  for (let i = 0; i < n; i++) {
    res[(i + k) % n] = nums[i]
  }
  for (let i = 0; i < n; i++) {
    nums[i] = res[i]
  }
}

看题解还有原地反转算法,空间复杂度为 O(1),先将整个数组反转,然后将前 k 个元素反转,再将后 n - k 个元素反转。反转函数用的是双指针。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var rotate = function (nums, k) {
  const n = nums.length
  k = k % n
  if (k === 0) return

  const reverse = (arr, l, r) => {
    while (l < r) {
      const tmp = arr[l]
      arr[l] = arr[r]
      arr[r] = tmp
      l++
      r--
    }
  }

  reverse(nums, 0, n - 1)
  reverse(nums, 0, k - 1)
  reverse(nums, k, n - 1)
}

238. 除了自身以外数组的乘积

思路:不让用除法,拆解问题,除了自身以外数组的乘积,也就是左侧数乘积 * 右侧数乘积,先遍历一遍数组,声明 pre 前缀乘积变量,把左侧数乘积放到 ans 结果数组中,ans[0] 就是 nums[0] 左侧数的乘积,没有问题。然后是 suf 后缀乘积,将 suf 乘进 ans 数组里。

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var productExceptSelf = function (nums) {
  const ans = new Array(nums.length)

  let pre = 1
  for (let i = 0; i < nums.length; i++) {
    ans[i] = pre
    pre *= nums[i]
  }

  let suf = 1
  for (let i = nums.length - 1; i >= 0; i--) {
    ans[i] *= suf
    suf *= nums[i]
  }

  return ans
}

41. 缺失的第一个正数

接下来转变下策略,先刷高频题,不按顺序刷了 = =

矩阵

73. 矩阵置零

54. 螺旋矩阵

48. 旋转图像

240. 搜索二维矩阵 II

链表

160. 相交链表

206. 反转链表

思路:一个 prev 一个 curr,用 temp 存中间值

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
  let prev = null
  let curr = head

  while (curr) {
    const temp = curr.next
    curr.next = prev
    prev = curr
    curr = temp
  }

  return prev
}

234. 回文链表

141. 环形链表

哈希表思路:用 Set 存储访问过的节点,如果访问过就说明有环,直到访问到 null 说明没有环。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
  const visited = new Set()
  let cur = head
  while (cur !== null) {
    if (visited.has(cur)) {
      return true
    }
    visited.add(cur)
    cur = cur.next
  }
  return false
}

快慢指针思路:快指针每次走两步,慢指针每次走一步,如果有环,快指针一定会追上慢指针,如果没有环,快指针会先到 null。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
  if (head === null || head.next === null) {
    return false
  }

  let slow = head
  let fast = head

  while (fast !== null && fast.next !== null) {
    slow = slow.next
    fast = fast.next.next

    if (slow === fast) {
      return true
    }
  }

  return false
}

142. 环形链表 II

21. 合并两个有序链表

思路:新建一个链表 + 两个指针,两个指针分别指向两个列表的头部,比较值然后在新链表中插入较小的值,指针后移,直到一个链表遍历完,然后将另一个链表剩余的部分接到新链表后面。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} list1
 * @param {ListNode} list2
 * @return {ListNode}
 */
var mergeTwoLists = function (list1, list2) {
  const dummy = new ListNode(-1)
  let curr = dummy

  let p1 = list1
  let p2 = list2

  while (p1 && p2) {
    if (p1.val < p2.val) {
      curr.next = p1
      p1 = p1.next
    } else {
      curr.next = p2
      p2 = p2.next
    }

    curr = curr.next
  }

  curr.next = p1 ? p1 : p2

  return dummy.next
}

2. 两数相加

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

24. 两两交换链表中的节点

25. K 个一组翻转链表

思路:分段 + 反转链表 + 拼接

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var reverseKGroup = function (head, k) {
  if (!head || k === 1) return head

  const dummy = new ListNode(0, head)
  let pre = dummy

  while (true) {
    // 1. 从 pre 开始向后走 k 步,看看够不够一组
    let end = pre
    for (let i = 0; i < k && end !== null; i++) {
      end = end.next
    }
    if (end === null) break

    // 2. 记录这一组的开始和下一组的开始
    let start = pre.next
    let nextGroupStart = end.next

    // 3. 断开这一段,单独反转 [start, end]
    end.next = null

    // 反转整段,返回新头
    let newHead = reverseList(start)

    // 4. 接回到原链表
    pre.next = newHead
    start.next = nextGroupStart

    // 5. pre 移动到当前组的尾部
    pre = start
  }

  return dummy.next
}

// 反转整条链表:返回新头
function reverseList(head) {
  let prev = null
  let curr = head
  while (curr !== null) {
    const temp = curr.next
    curr.next = prev
    prev = curr
    curr = temp
  }
  return prev
}

138. 随机链表的复制

148. 排序链表

23. 合并 K 个升序链表

146. LRU 缓存

思路:LRU 要存的值是 key-value,然后要求删除最早未使用的元素,所以这道题用 Map 再合适不过。这里获取最早使用元素用的是 map.keys() 方法,返回一个迭代器对象,用 next() 方法获取第一个元素。

/**
 * @param {number} capacity
 */
var LRUCache = function (capacity) {
  this.map = new Map()
  this.cap = capacity
}

/**
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function (key) {
  const map = this.map
  if (!map.has(key)) return -1

  const value = map.get(key)
  map.delete(key)
  map.set(key, value)
  return value
}

/**
 * @param {number} key
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function (key, value) {
  const map = this.map

  if (map.has(key)) {
    map.delete(key)
  }

  map.set(key, value)

  if (map.size > this.cap) {
    const first = map.keys().next().value
    map.delete(first)
  }
}

还有经典解法,哈希表 + 双向链表,两个头尾哨兵节点,但是没有 Map 简单好用,这里不写了。

二叉树

94. 二叉树的中序遍历

递归思路:中序遍历的顺序是左子树 -> 根节点 -> 右子树,所以先递归访问左子树,然后访问根节点,最后递归访问右子树。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
  const res = []
  function dfs(node) {
    if (!node) return
    dfs(node.left)
    res.push(node.val)
    dfs(node.right)
  }

  dfs(root)
  return res
}

迭代思路:使用栈,先将左子树入栈,然后访问根节点,最后访问右子树。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
  const res = []
  const stack = []
  let cur = root

  while (cur || stack.length) {
    if (cur) {
      stack.push(cur)
      cur = cur.left
    } else {
      cur = stack.pop()
      res.push(cur.val)
      cur = cur.right
    }
  }

  return res
}

104. 二叉树的最大深度

226. 翻转二叉树

101. 对称二叉树

543. 二叉树的直径

102. 二叉树的层序遍历

108. 将有序数组转换为二叉搜索树

98. 验证二叉搜索树

230. 二叉搜索树中第 K 小的元素

思路:二叉搜索树中左子树 < 根节点 < 右子树,这里递归访问左子树 -> 根节点 -> 右子树,访问的顺序就是从小到大,使用 visited 计数器记录访问了多少个节点,当 visited === k 时,说明找到了第 k 小的元素。

/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {number}
 */
var kthSmallest = function (root, k) {
  let ans = null
  let visited = 0

  function dfs(node) {
    if (!node || visited >= k) return

    dfs(node.left)

    visited++
    if (visited === k) {
      ans = node.val
      return
    }

    dfs(node.right)
  }

  dfs(root)
  return ans
}

199. 二叉树的右视图

114. 二叉树展开为链表

105. 从前序与中序遍历序列构造二叉树

思路:理解前序和中序遍历的特点,前序遍历的第一个元素是根节点,然后在中序遍历中找到根节点的位置,左边的就是左子树,右边的就是右子树,然后递归构建左右子树。利用哈希表存储中序遍历的值和索引,方便快速查找根节点在中序遍历中的位置。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function (preorder, inorder) {
  if (preorder.length === 0) return null

  const indexMap = new Map()
  for (let i = 0; i < inorder.length; i++) {
    indexMap.set(inorder[i], i)
  }

  function build(preL, preR, inL, inR) {
    if (preL > preR) return null

    const rootVal = preorder[preL]
    const root = new TreeNode(rootVal)

    const idx = indexMap.get(rootVal)
    const leftSize = idx - inL

    root.left = build(preL + 1, preL + leftSize, inL, idx - 1)
    root.right = build(preL + leftSize + 1, preR, idx + 1, inR)

    return root
  }

  return build(0, preorder.length - 1, 0, inorder.length - 1)
}

437. 路径总和 III

236. 二叉树的最近公共祖先

124. 二叉树中的最大路径和

图论

200. 岛屿数量

思路:深度优先搜索 DFS + “淹没”岛屿,遍历访问,遇到 1 则将周围的 1 都淹没成 0,岛屿数量 +1。

/**
 * @param {character[][]} grid
 * @return {number}
 */
var numIslands = function (grid) {
  if (!grid || grid.length === 0) return 0

  const m = grid.length
  const n = grid[0].length
  let count = 0

  const dfs = (i, j) => {
    if (i < 0 || i >= m || j < 0 || j >= n) return
    if (grid[i][j] === '0') return

    grid[i][j] = '0'

    dfs(i - 1, j)
    dfs(i + 1, j)
    dfs(i, j - 1)
    dfs(i, j + 1)
  }

  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] === '1') {
        count++
        dfs(i, j)
      }
    }
  }

  return count
}

994. 腐烂的橘子

207. 课程表

208. 实现 Trie (前缀树)

回溯

回溯算法是一种通过尝试所有可能的解决方案来解决问题的算法。它通常用于组合、排列、子集等问题。回溯算法的核心思想是通过递归来构建解空间树,并在每个节点上做出选择,然后继续递归探索,直到达到终止条件。如果当前路径不满足条件,就回溯到上一个节点,尝试其他选择。

46. 全排列

思路:用回溯生成全排列。用 path 记录当前排列,用 used 标记每个元素是否已在本轮使用。每次递归,当 path 长度等于 nums 长度时,将 path 的拷贝加入结果集;否则遍历 nums,对于未使用的元素,将其加入 path 并标记 used 后继续递归,回溯时再撤销选择(从 path 移除并重置 used)。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function (nums) {
  const res = []
  const path = []
  const used = new Array(nums.length).fill(false)

  function backtrack() {
    if (path.length === nums.length) {
      // 推送拷贝
      res.push(path.slice())
      return
    }

    for (let i = 0; i < nums.length; i++) {
      if (used[i]) {
        continue
      }

      used[i] = true
      path.push(nums[i])

      backtrack()

      // 撤销选择:恢复状态,回到上一层
      path.pop()
      used[i] = false
    }
  }

  backtrack()
  return res
}

78. 子集

17. 电话号码的字母组合

39. 组合总和

22. 括号生成

79. 单词搜索

131. 分割回文串

51. N 皇后

二分查找

35. 搜索插入位置

74. 搜索二维矩阵

34. 在排序数组中查找元素的第一个和最后一个位置

33. 搜索旋转排序数组

153. 寻找旋转排序数组中的最小值

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

20. 有效的括号

思路:栈的基本应用,先进后出,比较括号是否匹配就是与栈顶比较

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  if (s.length % 2 === 1) return false

  const stack = []
  const map = {
    ')': '(',
    ']': '[',
    '}': '{',
  }

  for (const ch of s) {
    if (ch in map) {
      const top = stack.length ? stack.pop() : '#'
      if (top !== map[ch]) {
        return false
      }
    } else {
      stack.push(ch)
    }
  }

  return stack.length === 0
}

155. 最小栈

394. 字符串解码

思路:栈的应用,遇到数字和字符就存储下来,遇到 [ 就把当前数字和字符串入栈,遇到 ] 就出栈,拼接字符串。

/**
 * @param {string} s
 * @return {string}
 */
var decodeString = function (s) {
  const nums = []
  const strs = []
  let num = ''
  let str = ''

  for (const ch of s) {
    if (ch >= '0' && ch <= '9') {
      num += ch
    } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
      str += ch
    } else if (ch === '[') {
      nums.push(Number(num))
      strs.push(str)
      num = ''
      str = ''
    } else if (ch === ']') {
      str = strs.pop() + str.repeat(nums.pop())
    }
  }

  return str
}

739. 每日温度

84. 柱状图中最大的矩形

215. 数组中的第 K 个最大元素

思路:nums.sort,然后返回 nums[k - 1] O(n) 复杂度要用到堆排序,建立大根堆,堆顶就是最大值,第 K 个最大值就弹出堆顶 K - 1 次,之后堆顶就是第 K 个最大值。重点在于 heapify() 函数,倒着排序建堆,理解原理就能看明白函数了。

堆排序,看的这个视频:堆与堆排序 | bilibili

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
  const n = nums.length

  function heapify(i, heapSize) {
    while (true) {
      let largest = i
      let left = 2 * i + 1
      let right = 2 * i + 2

      if (left < heapSize && nums[left] > nums[largest]) {
        largest = left
      }
      if (right < heapSize && nums[right] > nums[largest]) {
        largest = right
      }

      if (i === largest) break
      ;[nums[i], nums[largest]] = [nums[largest], nums[i]]
      i = largest
    }
  }

  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapify(i, n)
  }

  let heapSize = n
  for (let i = 0; i < k - 1; i++) {
    ;[nums[0], nums[heapSize - 1]] = [nums[heapSize - 1], nums[0]]
    heapSize--
    heapify(0, heapSize)
  }

  return nums[0]
}

347. 前 K 个高频元素

295. 数据流的中位数

贪心算法

121. 买卖股票的最佳时机

思路:记录购入最小价格,遍历更新最小价格以及计算利润。

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let minPrice = prices[0]
  let maxProfit = 0

  for (let i = 1; i < prices.length; i++) {
    const profit = prices[i] - minPrice
    maxProfit = Math.max(profit, maxProfit)
    minPrice = Math.min(prices[i], minPrice)
  }

  return maxProfit
}

55. 跳跃游戏

45. 跳跃游戏 II

763. 划分字母区间

动态规划

70. 爬楼梯

118. 杨辉三角

198. 打家劫舍

279. 完全平方数

322. 零钱兑换

139. 单词拆分

300. 最长递增子序列

思路:二分查找 + 贪心算法。lowerBound() 函数是二分查找,返回第一个大于等于 target 的索引,tail 数组存储当前最长递增子序列的末尾元素,遍历 nums 数组,使用 lowerBound() 找到 x 在 tail 中的位置 i,如果 i === tail.length,说明 x 比 tail 中所有元素都大,将 x 添加到 tail 末尾,否则将 tail[i] 替换为 x,这样可以保证 tail 中的元素尽可能小,从而为后续的元素提供更多的选择。最后返回 tail.length 即为最长递增子序列的长度。

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
  const lowerBound = (nums, target) => {
    let left = 0
    let right = nums.length

    while (left < right) {
      const mid = left + Math.floor((right - left) / 2)

      if (nums[mid] < target) {
        left = mid + 1
      } else {
        right = mid
      }
    }

    return left
  }

  const tail = []
  for (const x of nums) {
    const i = lowerBound(tail, x)
    if (i === tail.length) {
      tail.push(x)
    } else {
      tail[i] = x
    }
  }

  return tail.length
}

152. 乘积最大子数组

416. 分割等和子集

32. 最长有效括号

思路:栈存储左括号的索引,遇到右括号就弹出栈顶索引,如果栈为空,说明当前右括号没有匹配的左括号,将当前索引入栈作为新的起点(分界线),否则计算当前有效括号长度为 i - stack[stack.length - 1],更新最大长度 maxLen。

/**
 * @param {string} s
 * @return {number}
 */
var longestValidParentheses = function (s) {
  const stack = [-1]
  let maxLen = 0

  for (let i = 0; i < s.length; i++) {
    const ch = s[i]

    if (ch === '(') {
      stack.push(i)
    } else {
      stack.pop()

      if (stack.length === 0) {
        stack.push(i)
      } else {
        const currLen = i - stack[stack.length - 1]
        maxLen = Math.max(currLen, maxLen)
      }
    }
  }

  return maxLen
}

多维动态规划

62. 不同路径

64. 最小路径和

思路:动态规划,dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j],先计算第一行和第一列的最小路径和,然后从 (1, 1) 开始计算每个位置的最小路径和,最后返回右下角的值。

/**
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function (grid) {
  const m = grid.length
  const n = grid[0].length

  for (let i = 1; i < m; i++) {
    grid[i][0] += grid[i - 1][0]
  }

  for (let j = 1; j < n; j++) {
    grid[0][j] += grid[0][j - 1]
  }

  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1])
    }
  }

  return grid[m - 1][n - 1]
}

5. 最长回文子串

1143. 最长公共子序列

72. 编辑距离

技巧

136. 只出现一次的数字

169. 多数元素

75. 颜色分类

31. 下一个排列

287. 寻找重复数

其他(非 Hot 100)

8. 字符串转换整数 (atoi)

思路:题目说的很明白了,trim() 去零,用 sign 记录正负号,res 存储结果,遍历字符串,遇到非数字就 break,遇到数字就计算 res = res * 10 + digit,同时判断是否溢出。这里判断溢出原理是每次 res = res * 10 + digit,就是 res * 10 + digit > INT_MAX,变形一下就是 res > (INT_MAX - digit) / 10,floor() 是因为 res 是整数,INT_MAX - digit 可能不是 10 的倍数。

/**
 * @param {string} s
 * @return {number}
 */
var myAtoi = function (s) {
  s = s.trim()
  if (s === '') return 0

  const INT_MAX = 2 ** 31 - 1
  const INT_MIN = -(2 ** 31)

  const n = s.length
  let i = 0

  let sign = 1
  if (s[i] === '-') {
    sign = -1
    i++
  } else if (s[i] === '+') {
    i++
  }

  let res = 0
  while (i < n) {
    const ch = s[i]
    if (ch < '0' || ch > '9') break

    const digit = ch.charCodeAt(0) - '0'.charCodeAt(0)

    if (res > Math.floor((INT_MAX - digit) / 10)) {
      return sign === 1 ? INT_MAX : INT_MIN
    }

    res = res * 10 + digit
    i++
  }

  return sign * res
}

43. 字符串相乘

思路:模拟竖式乘法,创建 n1 + n2 长度(最大位)的数组 res,其中 res[i + j + 1] 为计算中当前位,res[i + j] 为进位,最后将 res 前置 0 去除。

/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
var multiply = function (num1, num2) {
  if (num1 === '0' || num2 === '0') return '0'

  const n1 = num1.length
  const n2 = num2.length
  const res = new Array(n1 + n2).fill(0)

  for (let i = n1 - 1; i >= 0; i--) {
    const x = num1.charCodeAt(i) - '0'.charCodeAt(0)
    for (let j = n2 - 1; j >= 0; j--) {
      const y = num2.charCodeAt(j) - '0'.charCodeAt(0)
      const sum = res[i + j + 1] + x * y

      res[i + j + 1] = sum % 10
      res[i + j] += Math.floor(sum / 10)
    }
  }

  let k = 0
  while (k < res.length - 1 && res[k] === 0) k++

  return res.slice(k).join('')
}

93. 复原 IP 地址

思路:回溯递归,利用 path.pop() 进行回溯,同时递归终止条件是 path 长度为 4 且 start === n,说明已经找到了一个合法的 IP 地址。同时判断剩余字符数是否满足剩余段数的要求,如果不满足就直接返回,剪枝优化。

/**
 * @param {string} s
 * @return {string[]}
 */
var restoreIpAddresses = function (s) {
  const res = []
  const n = s.length

  if (n < 4 || n > 12) return res

  function backtrack(start, path) {
    if (path.length === 4) {
      if (start === n) {
        res.push(path.join('.'))
      }
      return
    }

    const remainChars = n - start
    const remainSegs = 4 - path.length
    if (remainChars < remainSegs || remainChars > 3 * remainSegs) {
      return
    }

    for (let len = 1; len <= 3; len++) {
      if (start + len > n) break
      const part = s.substring(start, start + len)

      if (part[0] === '0' && part.length > 1) break

      const num = Number(part)
      if (num < 0 || num > 255) break

      path.push(part)
      backtrack(start + len, path)
      path.pop()
    }
  }

  backtrack(0, [])
  return res
}

165. 比较版本号

思路:先将版本号按 ’.’ 分割成数组,然后同时遍历两个数组,获取当前版本号的整数值,如果遍历到长度外就用 0 补,根据大小返回结果。

/**
 * @param {string} version1
 * @param {string} version2
 * @return {number}
 */
var compareVersion = function (version1, version2) {
  const a = version1.split('.')
  const b = version2.split('.')

  const n = Math.max(a.length, b.length)
  for (let i = 0; i < n; i++) {
    const v1 = i < a.length ? parseInt(a[i], 10) : 0
    const v2 = i < b.length ? parseInt(b[i], 10) : 0
    if (v1 > v2) return 1
    if (v1 < v2) return -1
  }

  return 0
}

442. 数组中重复的数据

思路:根据题目限制,数组中的整数都在 1 到 n 之间,这里用原数组标记是否访问,很巧妙地不改变数组的(绝对)值,同时能够标记访问过的元素,遍历数组,根据值获取索引 x - 1,如果 nums[x - 1] < 0,说明 x 已经访问过了,将 x 添加到结果数组中,否则将 nums[x - 1] 取负数标记访问过。

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findDuplicates = function (nums) {
  const res = []

  for (let i = 0; i < nums.length; i++) {
    const x = Math.abs(nums[i])
    if (nums[x - 1] < 0) {
      res.push(x)
    } else {
      nums[x - 1] = -nums[x - 1]
    }
  }

  return res
}

LCR 180.文件组合

思路:滑动窗口

/**
 * @param {number} target
 * @return {number[][]}
 */
var fileCombination = function (target) {
  const res = []
  let l = 1,
    r = 1,
    sum = 0

  while (r < target) {
    sum += r

    while (sum > target) {
      sum -= l
      l++
    }

    if (sum === target && r > l) {
      const temp = []
      for (let x = l; x <= r; x++) {
        temp.push(x)
      }
      res.push(temp)
    }

    r++
  }

  return res
}

122. 买卖股票的最佳时机 II

思路:利润累计,遍历价格数组,如果当前价格大于前一天的价格,就卖出,累加利润。

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let profit = 0
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) {
      profit += prices[i] - prices[i - 1]
    }
  }
  return profit
}

评论

加载中...