my blog
    • 前端-刘发狮-18727782912
    • 算法笔记,JavaScript实现
    • 开发中遇到的cookie问题
    • 发布-订阅设计模式
    • 柯里化和组合函数
    • 从 HTTP 1.0 到 HTTP 3.0
    • 使用node将你的学习笔记一键转化为markdown
    • 谈一谈前端开发中的惰性思想
    • web项目的灰度发布/金丝雀发布
    • 关于我的组件库 rain-ui
    • 搭建react脚手架,以及脚手架工具
    • unit-test
    • webpack优化指南

    算法笔记,JavaScript实现

    持续更新中...

    链表#

    链表初识#

    链表:类似于数组,都是有序列表,线性结构。
    不同点在于,链表中,数据单位的名称叫做“结点”,而结点和结点的分布,相对于数据的连续来说在内存中可以是离散的。
    实现一个链表,通过嵌套对象的形式实现
    添加
    插入
    删除

    二叉树#

    二叉树#

    先序遍历二叉树 根节点->左子树->右子树
    中序遍历二叉树 左子树->根节点->右子树
    后序遍历二叉树 左子树->右子树->根节点

    数组的应用#

    Map的妙用#

    真题描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
    你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
    示例: 给定 nums = [2, 7, 11, 15], target = 9
    因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
    普通解法, 使用两层for循环,用nums[i]和nums[j]相加,若等于target, 则向result中push[i, j]
    从上面的解法来看,两层循环的时间复杂度是O(n^2),每次都要将第一次遍历的数与后面的所有数相加与target比较
    巧用map解法空间换时间,将求和问题变成球差问题
    上述的方法将nums中的元素作为key,元素的下标作为value,这样就可以求出target-nums[i]的值,如果map中有这个值,则说明有两个元素的和等于target
    使用es6的Map来做
    以上使用map的解法,无论是js的对象还是es6的Map,都可以实现相同的功能,然后时间复杂度都是O(n)
    留个疑问,如果你将代码运行一下就会发现使用es6的Map会比使用js对象所需的时间少个一半左右。why?
    扁平数据转树
    例:let arr = [
    { id: 1, name: "部门1", pid: 0 },
    { id: 2, name: "部门2", pid: 1 },
    { id: 3, name: "部门3", pid: 1 },
    { id: 4, name: "部门4", pid: 3 },
    { id: 5, name: "部门5", pid: 4 },
    { id: 6, name: "部门6", pid: 9 },
    ];
    普通递归
    使用Map的引用避免递归

    双指针法合并有序数组#

    真题描述:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
    说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
    示例: 输入:
    nums1 = [1,2,3,0,0,0], m = 3
    nums2 = [2,5,6], n = 3
    输出: [1,2,2,3,5,6]
    定义一个统计时间的函数
    在js中的另辟蹊径

    双指针法三数求和#

    真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
    注意:答案中不可以包含重复的三元组。
    示例: 给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
    定义一个统计时间的函数
    如果按照最传统的思路可能需要三层循环,时间复杂度为 O(n^3),肯定是不合适的。但是使用指针之后就会大大降低时间复杂度
    总结一下:遇到“有序”和“数组”这两个关键字,立刻把就要想到双指针法普通双指针走不通,立刻想对撞指针!

    字符串的应用#

    回文字符串#

    判断一个字符串是否是回文字符串;
    真题描述:给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
    示例 1: 输入: "aba"
    输出: True
    示例 2:
    输入: "abca"
    输出: True
    解释: 你可以删除c字符。
    注意: 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。

    字符串匹配问题#

    真题描述: 设计一个支持以下两种操作的数据结构:
    void addWord(word)
    bool search(word)
    search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z 。
    . 可以表示任何一个字母。
    示例: addWord("bad")
    addWord("dad")
    addWord("mad")
    search("pad") -> false
    search("bad") -> true
    search(".ad") -> true
    search("b..") -> true
    说明:
    你可以假设所有单词都是由小写字母 a-z 组成的。
    这个需要addWord和search两个功能,所以数据需要存在某个地方。
    分析:最简单的办法就是数组,将字符串存进数组,search的时候如果没有"."就直接用includes判断,如果有就new RegExp()
    这里为了降低查找时的复杂度,我们可以考虑以字符串的长度为 key,相同长度的字符串存在一个数组中,这样可以提高我们后续定位的效率。

    字符串与数字之间的转换问题#

    真题描述:请你来实现一个 atoi 函数,使其能将字符串转换成整数。
    首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
    当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
    该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
    注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
    在任何情况下,若函数不能进行有效的转换时,请返回 0。
    说明: 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31, 2^31 − 1]。如果数值超过这个范围,请返回 INT_MAX (2^31 − 1) 或 INT_MIN (−2^31) 。
    示例 1:
    输入: "42"
    输出: 42
    示例 2:
    输入: " -42"
    输出: -42
    解释: 第一个非空白字符为 '-', 它是一个负号。
    我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
    示例 3: 输入: "4193 with words"
    输出: 4193
    解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
    示例 4: 输入: "words and 987"
    输出: 0
    解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 因此无法执行有效的转换。
    示例 5:
    输入: "-91283472332"
    输出: -2147483648
    解释: 数字 "-91283472332" 超过 32 位有符号整数范围。因此返回 INT_MIN (−2^31) 。
    这题需要对正则比较了解
    首先,\s 这个符号,意味着空字符,它可以用来匹配回车、空格、换行等空白区域,这里,它用来被匹配空格。
    这个符号,跟在其它符号后面,意味着“前面这个符号可以出现0次或多次。\s,这里的意思就是空格出现0次或多次,都可被匹配到。
    接着 () 出现了。() 圈住的内容,就是我们要捕获起来额外存储的东西。
    []中的匹配符之间是“或”的关系,也就是说只要能匹配上其中一个就行了。
    这里[]中包括了-和+,-不必说匹配的是对应字符,这个+之所以加了一个斜杠符,是因为+本身是一个有特殊作用的正则匹配符,这里我们要让它回归+字符的本义,所以要用一个\来完成转义。
    [0-9]*结合咱们前面铺陈的知识,这个就不难理解了,它的意思是 0-9 之间的整数,能匹配到0个或多个就算匹配成功。
    最后的 .这个是任意字符的意思,.*用于字符串尾部匹配非数字的任意字符。我们看到.*是被排除捕获组之外的,所以说这个东西其实也不会被额外存储,它被“摘除”了。

    链表的应用#

    链表的合并#

    真题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。
    示例: 输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4

    链表的删除#

    真题描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
    示例 1:
    输入: 1->1->2
    输出: 1->2
    示例 2:
    输入: 1->1->2->3->3
    输出: 1->2->3
    删除问题的延伸——dummy 结点
    真题描述:给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。
    示例 1:
    输入: 1->2->3->3->4->4->5
    输出: 1->2->5
    示例 2:
    输入: 1->1->1->2->3
    输出: 2->3
    我们先来分析一下这道题和上道题有什么异同哈:相同的地方比较明显,都是删除重复元素。
    不同的地方在于,楼上我们删到没有重复元素就行了,可以留个“独苗”;但现在,题干要求我们只要一个元素发生了重复,就要把它彻底从链表中干掉,一个不留。
    时我们就可以用一个 dummy 结点解决这个问题。
    所谓 dummy 就是人为制造出来的第一个结点的前驱结点,这样链表中所有的结点都能确保有一个前驱结点,也就都能够用同样的逻辑来处理了。

    快慢指针#

    链表题目中,有一类会涉及到反复的遍历。
    往往会涉及相对复杂的链表操作,比如反转、指定位置的删除等等。
    快慢指针——删除链表的倒数第 N 个结点
    真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
    示例: 给定一个链表: 1->2->3->4->5, 和 n = 2.
    当删除了倒数第二个结点后,链表变为 1->2->3->5.
    说明: 给定的 n 保证是有效的。
    像上面说的,这题是属于指定位置的删除,这个时候需要两次遍历,如上题,删除倒数第n个就是删除正数length+1-n个
    这个时候就需要一次遍历拿到链表长度。
    不过这种超过一次的遍历必然需要引起我们的注意,我们应该主动去思考,“如果一次遍历来解决这个问题,我可以怎么做?”,这时候,就要请双指针法来帮忙了。

    多指针实现链表的反转#

    链表的反转
    真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
    示例:
    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL
    处理链表的本质,是处理链表结点之间的指针关系。
    遍历链表的时候让当前节点next指向上一个节点即可,需要注意next指向改变之后会导致丢失后续的遍历,需要一个next节点用来保存断开的后续节点
    局部反转一个链表
    真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
    说明: 1 ≤ m ≤ n ≤ 链表长度。
    示例:
    输入: 1->2->3->4->5->NULL, m = 2, n = 4
    输出: 1->4->3->2->5->NULL

    环形链表#

    真题描述:给定一个链表,判断链表中是否有环。
    示例 1:
    输入:[3,2,0,4] 输出:true
    解释:链表中存在一个环
    我们在遍历链表的时候添加一个flag,向后遍历的时候如果存在node.flag===true 就说明链表是环形
    环形链表衍生问题——定位环的起点
    这个问题和上面的其实基本一样,在第一次判断到flag为true的时候将当前节点返回即可
    这道题还有一个公认的比较经典的思路,就是用快慢指针来做:
    定义慢指针 slow,快指针 fast。两者齐头并进, slow 一次走一步、fast 一次 走两步。这样如果它们是在一个有环的链表里移动,一定有相遇的时刻。

    栈与队列#

    栈的应用#

    括号问题
    题目描述:给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
    有效字符串需满足: 左括号必须用相同类型的右括号闭合。
    左括号必须以正确的顺序闭合。
    注意空字符串可被认为是有效字符串。
    示例 1:
    输入: "()"
    输出: true
    示例 2:
    输入: "()[]{}"
    输出: true
    示例 3:
    输入: "(]"
    输出: false
    示例 4:
    输入: "([)]"
    输出: false
    示例 5:
    输入: "{[]}"
    输出: true
    括号问题在面试中出现频率非常高, 这类题目我们一般首选用栈来做。
    为什么可以用栈做?大家想想,括号成立意味着什么?意味着对称性。
    巧了,根据栈的后进先出原则,一组数据的入栈和出栈顺序刚好是对称的。比如说1、2、3、4、5、6按顺序入栈,其对应的出栈序列就是 6、5、4、3、2、1 对称关系一目了然。
    每日温度问题
    题目描述: 根据每日气温列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。
    例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
    提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
    思路:尝试去维持一个递减栈。
    最小栈问题
    题目描述:设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
    push(x) —— 将元素 x 推入栈中。
    pop() —— 删除栈顶的元素。
    top() —— 获取栈顶元素。
    getMin() —— 检索栈中的最小元素。
    示例:
    MinStack minStack = new MinStack();
    minStack.push(-2);
    minStack.push(0);
    minStack.push(-3);
    minStack.getMin(); --> 返回 -3.
    minStack.pop();
    minStack.top(); --> 返回 0.
    minStack.getMin(); --> 返回 -2.

    从栈到队列的世界#

    如何用栈实现一个队列?
    题目描述:使用栈实现队列的下列操作:
    push(x) -- 将一个元素放入队列的尾部。
    pop() -- 从队列首部移除元素。
    peek() -- 返回队列首部的元素。
    empty() -- 返回队列是否为空。
    示例: MyQueue queue = new MyQueue();
    queue.push(1);
    queue.push(2);
    queue.peek(); // 返回 1
    queue.pop(); // 返回 1
    queue.empty(); // 返回 false
    说明:
    你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
    你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
    假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。
    由题意我们无法使用push和shift来简单实现队列而是需要使用栈结构,在js中没有栈结构,我们可以使用list代替,使用push pop length 来实现队列
    我们知道队列是先进先出和栈后进先出是相反的操作,我们只需要使用两个栈,stack1和stack2,在实现队列出列之前将stack1出栈存放至stack2,stack2的pop操作就相当于队列的pop操作了 peek也是同理
    认识双端队列
    题目描述:给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
    示例: 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7]
    解释: 滑动窗口的位置
    [1 3 -1] -3 5 3 6 7
    1 [3 -1 -3] 5 3 6 7
    1 3 [-1 -3 5] 3 6 7
    1 3 -1 [-3 5 3] 6 7
    1 3 -1 -3 [5 3 6] 7
    1 3 -1 -3 5 [3 6 7]
    最大值分别对应:
    3 3 5 5 6 7
    提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
    通常可以使用双指针解决,但是使用双端队列可以降低时间复杂度
    O(kn)--->O(n) 使用双端队列实现一个单调递减的队列

    递归和回溯思想的应用#

    应用#

    题目描述:给定一个没有重复数字的序列,返回其所有可能的全排列。
    示例:
    输入: [1,2,3]
    输出: [
    [1,2,3],
    [1,3,2],
    [2,1,3],
    [2,3,1],
    [3,1,2],
    [3,2,1]
    ]
    题目描述:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
    说明:解集不能包含重复的子集。
    示例: 输入: nums = [1,2,3]
    输出:
    [
    [3],
    [1],
    [2],
    [1,2,3],
    [1,3],
    [2,3],
    [1,2],
    []
    ]
    题目描述:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
    示例: 输入: n = 4, k = 2
    输出:
    [
    [2,4],
    [3,4],
    [2,3],
    [1,2],
    [1,3],
    [1,4],
    ]
    什么时候用递归回溯思想
    看两个特征:
    题目中暗示了一个或多个解,并且要求我们详尽地列举出每一个解的内容时,一定要想到 DFS、想到递归回溯。
    题目经分析后,可以转化为树形逻辑模型求解。
    一个模型——树形逻辑模型;两个要点——递归式和递归边界。
    树形逻辑模型的构建,关键在于找“坑位”,一个坑位就对应树中的一层,每一层的处理逻辑往往是一样的,这个逻辑就是递归式的内容。至于递归边界,要么在题目中约束得非常清楚、要么默认为“坑位”数量的边界。
    function xxx(入参) {
    前期的变量定义、缓存等准备工作
    // 定义路径栈
    const path = []
    // 进入 dfs
    dfs(起点)
    // 定义 dfs
    dfs(递归参数) {
    if(到达了递归边界) {
    结合题意处理边界逻辑,往往和 path 内容有关
    return
    }
    // 注意这里也可能不是 for,视题意决定
    for(遍历坑位的可选值) {
    path.push(当前选中值)
    处理坑位本身的相关逻辑
    path.pop()
    }
    }
    }
    没有思路的时候对照着这个模板想一想说不定就有了呢

    二叉树真题归纳解读#

    二叉树遍历#

    先(前)序遍历迭代实现
    题目描述:给定一个二叉树,返回它的前序(先序)遍历序列。
    示例: 输入: [1,null,2,3]
    1
    \
    2
    /
    3
    输出: [1,2,3]
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?
    不使用递归 使用迭代算法
    前序遍历的规则是,先遍历根结点、然后遍历左孩子、最后遍历右孩子——这正是我们所期望的出栈序列。按道理,入栈序列和出栈序列相反,我们似乎应该按照 右->左->根 这样的顺序将结点入栈。不过需要注意的是,我们遍历的起点就是根结点,难道我们要假装没看到这个根结点、一鼓作气找到最右侧结点之后才开始进行入栈操作吗?答案当然是否定的,我们的出入栈顺序应该是这样的:
    1.将根结点入栈
    2.取出栈顶结点,将结点值 push 进结果数组
    3.若栈顶结点有右孩子,则将右孩子入栈
    4.若栈顶结点有左孩子,则将左孩子入栈
    异曲同工的后序遍历迭代实现
    思路清奇的中序遍历迭代实现
    层序遍历的衍生问题
    题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
    示例: 二叉树:[3,9,20,null,null,15,7],
    3
    /
    9 20
    /
    15 7
    返回其层次遍历结果:
    [
    [3],
    [9,20],
    [15,7]
    ]

    翻转二叉树#

    题目描述:翻转一棵二叉树。
    示例:
    输入:
    4
    /
    2 7
    / \ /
    1 3 6 9
    输出:
    4
    /
    7 2
    / \ /
    9 6 3 1

    二叉树搜索专题#

    二叉搜索树(Binary Search Tree)简称 BST,是二叉树的一种特殊形式。它有很多别名,比如排序二叉树、二叉查找树等等。
    什么是二叉搜索树?
    1.
    是一棵空树
    2.
    是一棵由根结点、左子树、右子树组成的树,同时左子树和右子树都是二叉搜索树,
    且左子树上所有结点的数据域都小于等于根结点的数据域,右子树上所有结点的数据域都大于等于根结点的数据域
    即 左孩子 <= 根结点 <= 右孩子
    关于二叉搜索树,大家需要掌握以下高频操作:
    3.
    查找数据域为某一特定值的结点
    4.
    插入新结点
    5.
    删除指定结点
    查找数据域为某一特定值的结点
    插入新结点
    删除指定结点
    结点删除后只需要继续保持二叉查找树的特性即可

    二叉搜索树真题#

    题目描述:给定一个二叉树,判断其是否是一个有效的二叉搜索树。
    假设一个二叉搜索树具有如下特征:
    节点的左子树只包含小于当前节点的数。
    节点的右子树只包含大于当前节点的数。
    所有左子树和右子树自身必须也是二叉搜索树。
    题目描述:将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
    本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
    示例: 给定有序数组: [-10,-3,0,5,9],
    一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
    修改于 2025-05-17 06:27:55
    上一页
    前端-刘发狮-18727782912
    下一页
    开发中遇到的cookie问题
    Built with