写给前端的算法进阶指南,我是如何两个月零基础刷200题(1)

示例:

输入:nums = [-1,2,1,-4], target = 1

输出:2

解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

提示:

3 <= nums.length <= 10^3``-10^3 <= nums[i] <= 10^3``-10^4 <= target <= 10^4

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/3sum-closest

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


先按照升序排序,然后分别从左往右依次选择一个基础点 i0 <= i <= nums.length - 3),在基础点的右侧用双指针去不断的找最小的差值。

假设基础点是 i,初始化的时候,双指针分别是:

  • lefti + 1,基础点右边一位。

  • rightnums.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

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

示例  1:

输入: “abcabcbb”

输出: 3

解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

示例 2:

输入: “bbbbb”

输出: 1

解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。

示例 3:

输入: “pwwkew”

输出: 3

解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。

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

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


这题是比较典型的滑动窗口问题,定义一个左边界 left 和一个右边界 right,形成一个窗口,并且在这个窗口中保证不出现重复的字符串。

这需要用到一个新的变量 freqMap,用来记录窗口中的字母出现的频率数。在此基础上,先尝试取窗口的右边界再右边一个位置的值,也就是 str[right + 1],然后拿这个值去 freqMap 中查找:

  1. 这个值没有出现过,那就直接把 right ++,扩大窗口右边界。

  2. 如果这个值出现过,那么把 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

}

链表问题

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

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/swap-nodes-in-pairs

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


这题本意比较简单,1 -> 2 -> 3 -> 4 的情况下可以定义一个递归的辅助函数 helper,这个辅助函数对于节点和它的下一个节点进行交换,比如 helper(1) 处理 1 -> 2,并且把交换变成 2 -> 1 的尾节点 1next继续指向 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

}

深度优先遍历问题

二叉树的所有路径-257

给定一个二叉树,返回所有从根节点到叶子节点的路径。

说明:  叶子节点是指没有子节点的节点。

示例:

输入:

1

/   \

2     3

\

5

输出: [“1->2->5”, “1->3”]

解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/binary-tree-paths

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


用当前节点的值去拼接左右子树递归调用当前函数获得的所有路径。

也就是根节点拼上以左子树为根节点得到的路径,加上根节点拼上以右子树为根节点得到的所有路径。

直到叶子节点,仅仅返回包含当前节点的值的数组。

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

}

广度优先遍历(BFS)问题

在每个树行中找最大值-515

https://leetcode-cn.com/problems/find-largest-value-in-each-tree-row

您需要在二叉树的每一行中找到最大的值。

输入:

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

}

栈问题

有效的括号-20

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

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。

  • 左括号必须以正确的顺序闭合。

  • 注意空字符串可被认为是有效字符串。

示例 1:

输入: “()”

输出: true

示例 2:

输入: “()[]{}”

输出: true

示例 3:

输入: “(]”

输出: false

示例 4:

输入: “([)]”

输出: false

示例 5:

输入: “{[]}”

输出: true

https://leetcode-cn.com/problems/valid-parentheses


提前记录好左括号类型 (, {, [和右括号类型), }, ]的映射表,当遍历中遇到左括号的时候,就放入栈 stack 中(其实就是数组),当遇到右括号时,就把 stack 顶的元素 pop 出来,看一下是否是这个右括号所匹配的左括号(比如 ( 和 ) 是一对匹配的括号)。

当遍历结束后,栈中不应该剩下任何元素,返回成功,否则就是失败。

/**

* @param {string} s

* @return {boolean}

*/

let isValid = function (s) {

let sl = s.length

if (sl % 2 !== 0) return false

let leftToRight = {

“{”: “}”,

“[”: “]”,

“(”: “)”,

}

// 建立一个反向的 value -> key 映射表

let rightToLeft = createReversedMap(leftToRight)

// 用来匹配左右括号的栈

let stack = []

for (let i = 0; i < s.length; i++) {

let bracket = s[i]

// 左括号 放进栈中

if (leftToRight[bracket]) {

stack.push(bracket)

} else {

let needLeftBracket = rightToLeft[bracket]

// 左右括号都不是 直接失败

if (!needLeftBracket) {

return false

}

// 栈中取出最后一个括号 如果不是需要的那个左括号 就失败

let lastBracket = stack.pop()

if (needLeftBracket !== lastBracket) {

return false

}

}

}

if (stack.length) {

return false

}

return true

}

function createReversedMap(map) {

return Object.keys(map).reduce((prev, key) => {

const value = map[key]

prev[value] = key

return prev

}, {})

}

递归与回溯

直接看我写的这两篇文章即可,递归与回溯甚至是平常业务开发中最常见的算法场景之一了,所以我重点总结了两篇文章。

《前端电商 sku 的全排列算法很难吗?学会这个套路,彻底掌握排列组合。》[1]

前端「N 皇后」递归回溯经典问题图解[2]

动态规划

打家劫舍 - 198

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

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

输出: 4

解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。

偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入: [2,7,9,3,1]

输出: 12

解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。

偷窃到的最高金额 = 2 + 9 + 1 = 12 。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/house-robber 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


动态规划的一个很重要的过程就是找到「状态」和「状态转移方程」,在这个问题里,设 i是当前屋子的下标,状态就是 以 i 为起点偷窃的最大价值

在某一个房子面前,盗贼只有两种选择:偷或者不偷

  1. 偷的话,价值就是「当前房子的价值」+「下两个房子开始盗窃的最大价值」

  2. 不偷的话,价值就是「下一个房子开始盗窃的最大价值」

在这两个值中,选择最大值记录在 dp[i]中,就得到了以 i 为起点所能偷窃的最大价值。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

偷窃到的最高金额 = 2 + 9 + 1 = 12 。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/house-robber 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


动态规划的一个很重要的过程就是找到「状态」和「状态转移方程」,在这个问题里,设 i是当前屋子的下标,状态就是 以 i 为起点偷窃的最大价值

在某一个房子面前,盗贼只有两种选择:偷或者不偷

  1. 偷的话,价值就是「当前房子的价值」+「下两个房子开始盗窃的最大价值」

  2. 不偷的话,价值就是「下一个房子开始盗窃的最大价值」

在这两个值中,选择最大值记录在 dp[i]中,就得到了以 i 为起点所能偷窃的最大价值。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-lXUt7Z9y-1715833826188)]

[外链图片转存中…(img-C3YdJvdb-1715833826189)]

[外链图片转存中…(img-q37Sm4xF-1715833826189)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值