1查找表问题
两个数组的交集 II-350
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
为两个数组分别建立 map,用来存储 num -> count 的键值对,统计每个数字出现的数量。
然后对其中一个 map 进行遍历,查看这个数字在两个数组中分别出现的数量,取出现的最小的那个数量(比如数组 1 中出现了 1 次,数组 2 中出现了 2 次,那么交集应该取 1 次),push 到结果数组中即可。
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
let intersect = function (nums1, nums2) {
let map1 = makeCountMap(nums1)
let map2 = makeCountMap(nums2)
let res = []
for (let num of map1.keys()) {
const count1 = map1.get(num)
const count2 = map2.get(num)
if (count2) {
const pushCount = Math.min(count1, count2)
for (let i = 0; i < pushCount; i++) {
res.push(num)
}
}
}
return res
}
function makeCountMap(nums) {
let map = new Map()
for (let i = 0; i < nums.length; i++) {
let num = nums[i]
let count = map.get(num)
if (count) {
map.set(num, count + 1)
} else {
map.set(num, 1)
}
}
return map
}
2 双指针问题
最接近的三数之和-16
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
先按照升序排序,然后分别从左往右依次选择一个基础点 i(0 <= i <= nums.length - 3),在基础点的右侧用双指针去不断的找最小的差值。
假设基础点是 i,初始化的时候,双指针分别是:
left:i + 1,基础点右边一位。
right: nums.length - 1 数组最后一位。
然后求此时的和,如果和大于 target,那么可以把右指针左移一位,去试试更小一点的值,反之则把左指针右移。
在这个过程中,不断更新全局的最小差值 min,和此时记录下来的和 res。
最后返回 res 即可。
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
let threeSumClosest = function (nums, target) {
let n = nums.length
if (n === 3) {
return getSum(nums)
}
// 先升序排序 此为解题的前置条件
nums.sort((a, b) => a - b)
let min = Infinity // 和 target 的最小差
let res
// 从左往右依次尝试定一个基础指针 右边至少再保留两位 否则无法凑成3个
for (let i = 0; i <= nums.length - 3; i++) {
let basic = nums[i]
let left = i + 1 // 左指针先从 i 右侧的第一位开始尝试
let right = n - 1 // 右指针先从数组最后一项开始尝试
while (left < right) {
let sum = basic + nums[left] + nums[right] // 三数求和
// 更新最小差
let diff = Math.abs(sum - target)
if (diff < min) {
min = diff
res = sum
}
if (sum < target) {
// 求出的和如果小于目标值的话 可以尝试把左指针右移 扩大值
left++
} else if (sum > target) {
// 反之则右指针左移
right--
} else {
// 相等的话 差就为0 一定是答案
return sum
}
}
}
return res
}
function getSum(nums) {
return nums.reduce((total, cur) => total + cur, 0)
}
3滑动窗口问题
无重复字符的最长子串-3
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
这题是比较典型的滑动窗口问题,定义一个左边界 left 和一个右边界 right,形成一个窗口,并且在这个窗口中保证不出现重复的字符串。
这需要用到一个新的变量 freqMap,用来记录窗口中的字母出现的频率数。在此基础上,先尝试取窗口的右边界再右边一个位置的值,也就是 str[right + 1],然后拿这个值去 freqMap 中查找:
这个值没有出现过,那就直接把 right ++,扩大窗口右边界。
如果这个值出现过,那么把 left ++,缩进左边界,并且记得把 str[left] 位置的值在 freqMap 中减掉。
循环条件是 left < str.length,允许左边界一直滑动到字符串的右界。
/**
* @param {string} s
* @return {number}
*/
let lengthOfLongestSubstring = function (str) {
let n = str.length
// 滑动窗口为s[left...right]
let left = 0
let right = -1
let freqMap = {} // 记录当前子串中下标对应的出现频率
let max = 0 // 找到的满足条件子串的最长长度
while (left < n) {
let nextLetter = str[right + 1]
if (!freqMap[nextLetter] && nextLetter !== undefined) {
freqMap[nextLetter] = 1
right++
} else {
freqMap[str[left]] = 0
left++
}
max = Math.max(max, right - left + 1)
}
return max
}
4链表问题
两两交换链表中的节点-24
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
这题本意比较简单,1 -> 2 -> 3 -> 4 的情况下可以定义一个递归的辅助函数 helper,这个辅助函数对于节点和它的下一个节点进行交换,比如 helper(1) 处理 1 -> 2,并且把交换变成 2 -> 1 的尾节点 1的next继续指向 helper(3)也就是交换后的 4 -> 3。
边界情况在于,如果顺利的作了两两交换,那么交换后我们的函数返回出去的是 交换后的头部节点,但是如果是奇数剩余项的情况下,没办法做交换,那就需要直接返回 原本的头部节点。这个在 helper函数和主函数中都有体现。
let swapPairs = function (head) {
if (!head) return null
let helper = function (node) {
let tempNext = node.next
if (tempNext) {
let tempNextNext = node.next.next
node.next.next = node
if (tempNextNext) {
node.next = helper(tempNextNext)
} else {
node.next = null
}
}
return tempNext || node
}
let res = helper(head)
return res || head
}
5深度优先遍历问题
二叉树的所有路径-257
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
输入:
1
/ \
2 3
\
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
用当前节点的值去拼接左右子树递归调用当前函数获得的所有路径。
也就是根节点拼上以左子树为根节点得到的路径,加上根节点拼上以右子树为根节点得到的所有路径。
直到叶子节点,仅仅返回包含当前节点的值的数组。
let binaryTreePaths = function (root) {
let res = []
if (!root) {
return res
}
if (!root.left && !root.right) {
return [`${root.val}`]
}
let leftPaths = binaryTreePaths(root.left)
let rightPaths = binaryTreePaths(root.right)
leftPaths.forEach((leftPath) => {
res.push(`${root.val}->${leftPath}`)
})
rightPaths.forEach((rightPath) => {
res.push(`${root.val}->${rightPath}`)
})
return res
}
6广度优先遍历(BFS)问题
您需要在二叉树的每一行中找到最大的值。
输入:
1
/ \
3 2
/ \ \
5 3 9
输出: [1, 3, 9]
这是一道典型的 BFS 题目,BFS 的套路其实就是维护一个 queue 队列,在读取子节点的时候同时把发现的孙子节点 push 到队列中,但是先不处理,等到这一轮队列中的子节点处理完成以后,下一轮再继续处理的就是孙子节点了,这就实现了层序遍历,也就是一层层的去处理。
但是这里有一个问题卡住我了一会,就是如何知道当前处理的节点是哪个层级的,在最开始的时候我尝试写了一下二叉树求某个 index 所在层级的公式,但是发现这种公式只能处理「平衡二叉树」。
后面看题解发现他们都没有专门维护层级,再仔细一看才明白层级的思路:
其实就是在每一轮 while 循环里,再开一个 for 循环,这个 for 循环的终点是「提前缓存好的 length 快照」,也就是进入这轮 while 循环时,queue 的长度。其实这个长度就恰好代表了「一个层级的长度」。
缓存后,for 循环里可以安全的把子节点 push 到数组里而不影响缓存的当前层级长度。
另外有一个小 tips,在 for 循环处理完成后,应该要把 queue 的长度截取掉上述的缓存长度。一开始我使用的是 queue.splice(0, len),结果速度只击败了 33%的人。后面换成 for 循环中去一个一个shift来截取,速度就击败了 77%的人。
/**
* @param {TreeNode} root
* @return {number[]}
*/
let largestValues = function (root) {
if (!root) return []
let queue = [root]
let maximums = []
while (queue.length) {
let max = Number.MIN_SAFE_INTEGER
// 这里需要先缓存length 这个length代表当前层级的所有节点
// 在循环开始后 会push新的节点 length就不稳定了
let len = queue.length
for (let i = 0; i < len; i++) {
let node = queue[i]
max = Math.max(node.val, max)
if (node.left) {
queue.push(node.left)
}
if (node.right) {
queue.push(node.right)
}
}
// 本「层级」处理完毕,截取掉。
for (let i = 0; i < len; i++) {
queue.shift()
}
// 这个for循环结束后 代表当前层级的节点全部处理完毕
// 直接把计算出来的最大值push到数组里即可。
maximums.push(max)
}
return maximums
}
作者:晨曦时梦见兮
链接:https://juejin.im/post/5f05087cf265da22d466f60f
来源:掘金








网友评论