前端算法学习入门笔记 - JavaScript

TIP
LeetCode官网
算法题目前面的序号代表题号,[ ]为题目难易程度。题目作者都刷过一遍,有些解法是自己写的。
学习视频请戳 -> 链接
转载请标明出处!!!

目录

        • 20. 有效的括号 - [简单]
      • 队列
        • 933. 最近的请求次数 - [简单]
      • 链表
        • 237. 删除链表中的节点 - [简单]
        • 206. 反转链表 - [简单]
        • 2. 两数相加 - [中等]
        • 83. 删除排序链表中的重复元素 - [简单]
        • 141. 环形链表 - [简单]
      • 集合
        • 349. 两个数组的交集 - [简单]
      • 字典
        • 349. 两个数组的交集 - [简单]
        • 20. 有效的括号 - [简单]
        • 1. 两数之和 - [简单]
        • 3. 无重复字符的最长子串 - [中等]
        • 76. 最小覆盖子串 - [困难]
        • 深度/广度优先遍历
        • 二叉树的先中后序遍历(递归版)
        • 二叉树的先中后序遍历(非递归版)
        • 104. 二叉树的最大深度 - [简单]
        • 111. 二叉树的最小深度 - [简单]
        • 102. 二叉树的层序遍历 - [中等]
        • 94. 二叉树的中序遍历 - [简单]
        • 112. 路径总和 - [简单]
        • 遍历JSON的所有节点值
        • 渲染Antd的树组件
        • 深度/广度优先遍历
        • 65. 有效数字 - [困难]
        • 417. 太平洋大西洋水流问题 - [中等]
        • 133. 克隆图 - [中等]
        • JavaScript 实现最小堆类
        • 215. 数组中的第K个最大元素 - [中等]
        • 347. 前K个高频元素 - [中等]
        • 23. 合并K个升序链表 - [困难]
      • JS中的排序(数字越大,性能越好,性能3以上可用于实战)
        • 冒泡排序 - 性能1
        • 选择排序 - 性能1
        • 插入排序 - 性能2
        • 归并排序 - 性能3
        • 快速排序 - 性能3
      • JS中的搜索(数字越大,性能越好,性能3以上可用于实战)
        • 顺序搜索 - 性能1
        • 二分搜索 - 性能3
        • 21. 合并两个有序链表 - [简单]
        • 374. 猜数字大小 - [简单]
        • 69. x的平方根 - [简单]
        • 总结
      • 分治思想
        • 374. 猜数字大小 - [简单]
        • 226. 翻转二叉树 - [简单]
        • 100. 相同的树 - [简单]
        • 101. 对称二叉树 - [简单]
      • 动态规划
        • 70. 爬楼梯 - [简单]
        • 198. 打家劫舍 - [中等]
        • 213. 打家劫舍 II - [中等]
      • 贪心算法
        • 455. 分发饼干 - [简单]
        • 122. 买卖股票的最佳时机II - [中等]
      • 回溯算法
        • 46. 全排列 - [中等]
        • 78. 子集 - [中等]

  • 先进后出的数据场景
20. 有效的括号 - [简单]

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

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

示例 4:

输入:s = "([)]"
输出:false

示例 5:

输入:s = "{[]}"
输出:true

提示:

  • 1 <= s.length <= 10^4
  • s 仅由括号 '()[]{}' 组成
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if(s.length % 2 !== 0) return false
    // 转成数组
    const arr = s.split("");
    // 栈
    let stack = [];
    while(arr.length){
        const c = arr.shift()
        if(c === "(" || c === "{" || c === "["){
            stack.push(c)
        }else{
            const s = stack[stack.length - 1]
            if((c===")" && s === "(" )||( c==="}" && s === "{") ||( c==="]" && s === "[")){
                stack.pop()
            }else{
                // 要及时return
                return false
            }
        }
    }
    return stack.length === 0
};

队列

  • 先进先出的数据场景
933. 最近的请求次数 - [简单]

写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请你实现 RecentCounter 类:

RecentCounter() 初始化计数器,请求数为 0 。
int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。

保证 每次对 ping 的调用都使用比之前更大的 t 值。

示例 1:

输入:
[“RecentCounter”, “ping”, “ping”, “ping”, “ping”]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]

解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1); // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100); // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001); // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3

提示:

1 <= t <= 10^9
保证每次对 ping 调用所使用的 t 值都 严格递增
至多调用 ping 方法 10^4 次
var RecentCounter = function() {
    this.requests = []
};

/** 
 * @param {number} t
 * @return {number}
 */
RecentCounter.prototype.ping = function(t) {
    if(!t) return null
    this.requests.push(t)
    while(this.requests[0] < (t-3000)){
        this.requests.shift()
    }
    // 返回值是在该范围内的总数
    return this.requests.length
};

/**
 * Your RecentCounter object will be instantiated and called as such:
 * var obj = new RecentCounter()
 * var param_1 = obj.ping(t)
 */

链表

  • 多个元素组成的列表
  • 元素存储不连续,用next指针连在一起
    前端算法学习入门笔记 - JavaScript_第1张图片
  • 与数组的区别
    • 增删非首尾元素时往往需要移动元素
237. 删除链表中的节点 - [简单]

请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。

题目数据保证需要删除的节点 不是末尾节点 。

示例 1:

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9

示例 2:

输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9

提示:

链表中节点的数目范围是 [2, 1000]
-1000 <= Node.val <= 1000
链表中每个节点的值都是 唯一 的
需要删除的节点 node 是 链表中的节点 ,且 不是末尾节点

解题思路:将被删节点的值改为下个节点的值,删除下个节点, 相当于变相删除下个节点。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} node
 * @return {void} Do not return anything, modify node in-place instead.
 */
var deleteNode = function(node) {
    // node是当前要被删除的节点
    node.val = node.next.val;
    node.next = node.next.next;
};
206. 反转链表 - [简单]

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

示例一

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例二

输入:head = [1,2]
输出:[2,1]

示例三

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000
/**
 * 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) {
    if(!head) return head;
    // 头指针
    let prev = null;
    while(head){
        const next = head.next;
        head.next = prev
        prev = head;
        head = next;       
    }
    return prev
};

// 时间复杂度:O(n),空间复杂度O(1)
2. 两数相加 - [中等]

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

提示:

每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
// ---------- edited by ahhc -----------
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    // 新链表的当前节点
    let cur = new ListNode();
    // 进位
    let c = 0;
    let head = cur;
    // 遍历链表
    while(l1 || l2){
        let node = new ListNode();
        cur.next = node;
        cur = node; // 相当于 cur = cur.next
        const a = l1 ? l1.val : 0;
        const b = l2 ? l2.val : 0;
        const sum = a + b + c;
        cur.val = sum >= 10 ? sum - 10 : sum;
        c = sum >= 10 ? 1 : 0;
        l1 && (l1 = l1.next);
        l2 && (l2 = l2.next);
    }
    // 注意考虑加数溢出的情况
    if(c === 1){
        let node  = new ListNode(1);
        cur.next = node;
        cur = node;
    }
    return head.next;
};

// 时间复杂度:O(n), 空间复杂度:O(1)
83. 删除排序链表中的重复元素 - [简单]

给定一个已排序的链表的头 head ,删除所有重复的元素,使每个元素只出现一次,返回已排序的链表。

示例一

输入:head = [1,1,2]
输出:[1,2]

示例二

输入:head = [1,1,2,3,3]
输出:[1,2,3]

提示:

  • 链表中节点数目在范围 [0, 300] 内
  • -100 <= Node.val <= 100
  • 题目数据保证链表已经按升序 排列
/**
 * 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 deleteDuplicates = function(head) {
    // 遍历链表
    let p = head;
    while(p){  // 或者 while(p && p.next),就不用写?.符号
        if(p.val === p.next?.val){
            p.next = p.next.next
        }else{
            p = p.next;
        }
    }
    return head;
};
// 时间复杂度:O(n),空间复杂度:O(1)
141. 环形链表 - [简单]

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例一

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例二

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

链表中节点的数目范围是 [0, 104]
-105 <= Node.val <= 105
pos 为 -1 或者链表中的一个 有效索引 。
// ---------- edited by ahhc ---------
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    // 快慢指针方法,同一个起点
    let fast = slow = head;
    // 当快慢指针走到链表尾部,快慢指针仍未相遇时,即不存在闭环
    while(slow && fast && fast.next){
        slow = slow.next;
        fast = fast.next.next;
        if(fast === slow) return true
    }
    return false;
};
// 时间复杂度:O(n),空间复杂度:O(1)

集合

  • 一种无序唯一的数据结构。
  • ES6中有集合,名为Set。
  • 去重、判断某元素是否在集合中、求交集。
349. 两个数组的交集 - [简单]

给定两个数组 nums1nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

示例一

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例二

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    return [...new Set(nums1)].filter(a => nums2.includes(a))
};

// 时间复杂度:O(n^2),空间复杂度:O(1)

字典

  • 与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储。
  • ES6中有字典,名为Map。
  • 字典的常用操作:键值对的增删改查。
349. 两个数组的交集 - [简单]

给定两个数组 nums1nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

示例一

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例二

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 利用字典的唯一特性
    let myMap = new Map();
    nums1.forEach(n => {
        myMap.set(n, true)
    })
    let res = [];
    nums2.forEach(n => {
        if(myMap.get(n)){
            res.push(n);
            myMap.delete(n);
        }
    })
    return res;
};
20. 有效的括号 - [简单]

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

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

示例 4:

输入:s = "([)]"
输出:false

示例 5:

输入:s = "{[]}"
输出:true

提示:

  • 1 <= s.length <= 10^4
  • s 仅由括号 '()[]{}' 组成
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    // 长度为奇数则直接返回
    if(s.length % 2 !== 0) return false;
    const myMap = new Map();
    myMap.set("(",")");
    myMap.set("{","}");
    myMap.set("[","]");
    let res = [];
    // 注意:这里不能用forEach循环,return false无法阻止forEach循环
    for(let i = 0; i < s.length; i++){
        const ch = s[i];
        if(myMap.has(ch)){
            res.push(ch)
        }else{
            const n = res[res.length-1];
            if(myMap.get(n) === ch){
                res.pop()
            }else{
                // 记得return false!!!
                return false;
            }
        }
    }
    return res.length === 0;
};
// 时间复杂度:O(n),空间复杂度:O(1)
1. 两数之和 - [简单]

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    // 利用字典的特性
    const myMap = new Map();
    for(let i = 0; i < nums.length; i++){
        const n = nums[i];
        const n2 = target - n;
        if(myMap.has(n2)){
            return [myMap.get(n2), i];
        }else{
            myMap.set(n,i)
        }
    }
    return []
};


// --------- edited by ahhc --------- set的顺序也很重要,下面那么写就多了一些不必要的判断

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    // 利用字典的特性
    const myMap = new Map();
    for(let i = 0; i < nums.length; i++){
        let n = nums[i];
        if(!myMap.has(n)){
            myMap.set(n, i);
        }
        let minus = target - n;
        // 注意:加数和被加数不能是同一个数
        if(myMap.has(minus)){
            const index = myMap.get(minus);
            if(index !== i)
                return [index, i]
        }
    }
    return []
};

// 时间复杂度:O(n),空间复杂度:O(1)
3. 无重复字符的最长子串 - [中等]

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

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    // 滑动窗口+字典存储滑动窗口值保证不重复
    let l = 0;
    let res = 0;
    const map = new Map();
    for(let r = 0; r < s.length; r++){
        const ch = s[r];
        // 左指针不能往回走
        if(map.has(ch) && map.get(ch) >= l){
            // 移动左指针到重复字符的下一位
            l = map.get(ch) + 1;
        }
        res = Math.max(res, r - l + 1);
        map.set(ch, r);
    }
    return res;
};

// 时间复杂度: O(n),空间复杂度:O(m),m是字符串的个数
76. 最小覆盖子串 - [困难]

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

示例 2:

输入:s = "a", t = "a"
输出:"a"

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:

1 <= s.length, t.length <= 10^5
s 和 t 由英文字母组成

进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    // 统计字符串t的所有元素个数
    const need = new Map();
    for(let i of t){
        need.set(i, need.get(i) ? need.get(i) + 1 : 1);
    }
    // 保存结果字符串
    let res = '';
    // 左右指针开始循环
    let l = r = 0;
    // 需要的类型个数
    let needSize = need.size;
    // 先循环右指针
    while(r < s.length){
        const n = s[r];
        if(need.has(n)){
            need.set(n, need.get(n) - 1)
            if(need.get(n) === 0) needSize--;
        }
        while(needSize === 0){
            // 循环左指针,缩短字符串长度
            const newRes = s.substring(l, r+1); // 不包括end
            if(!res || newRes.length < res.length) res = newRes;
            const c = s[l];
            if(need.has(c)){
                need.set(c, need.get(c) + 1)
                if(need.get(c) === 1) needSize++;
            }
            l++;
        }
        r++;
    }
    return res;
};

// 时间复杂度:O(m+n),m是字符串s的长度,n是字符串t的长度
// 空间复杂度:O(n),n是字符串t的长度

  • 一种分层数据的抽象模型
  • 前端工作中常见的树包括:DOM树、级联选择、树形控件…
  • JS中没有树,但是可以用Object和Array构建树
  • 树的常用操作:深度/广度优先遍历、先中后序遍历
深度/广度优先遍历
  • 深度优先遍历:尽可能深的搜索树的分支。
    • 访问根节点。
    • 对根节点的children挨个进行深度优先遍历。
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};
const dfs = (root) => {
    console.log(root.val);
    root.children.forEach(dfs);  // 当children为空时会停止递归
    // 相当于
    // root.children.forEach(child => dfs(child))
}
  • 广度优先遍历:先访问离根节点最近的节点。
    • 新建一个队列,把根节点入队。
    • 把队头出队并访问。
    • 把队头的children挨个入队。
    • 重复第二、三步,直到队列为空。
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};
const bfs = (root) => {
    // 根节点先入队
    const q = [root];
    while(q.length > 0){
        // 队头出队
        const n = q.shift();
        console.log(n.val);
        // 队头的child分别入队
        n.child.forEach(child => {
            q.push(child)
        });
    }
};
二叉树的先中后序遍历(递归版)
  • 二叉树中每个节点最多只能有两个子节点。
  • 在JS中通常用Object来模拟二叉树。
  • 先序遍历
    • 访问根节点。
    • 对根节点的左子树进行先序遍历。
    • 对根节点的右子树进行先序遍历。
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};
const preorder = (root) => {
    if(!root) return;
    console.log(root.val)
    // 先访问左子树
    preorder(root.left);
    preorder(root.right);
}
preorder(bt); // 1245367
  • 中序遍历
    • 对根节点的左子树进行中序遍历。
    • 访问根节点。
    • 对根节点的右子树进行中序遍历。
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};
const inorder = (root) => {
    if(!root) return;
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
};
inorder(bt); // 4251637
  • 后序遍历
    • 对根节点的左子树进行后序遍历。
    • 对根节点的右子树进行后序遍历。
    • 访问根节点。
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};
const postorder = (root) => {
    if(!root) return;
    postorder(root.left);
    postorder(root.right);
    console.log(root.val);
};
postorder(bt); // 4526731
二叉树的先中后序遍历(非递归版)
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};

// 先序遍历
const preorder = (root) => {
    if(!root) return;
    // 利用栈实现
    const stack = [root];
    while(stack.length){
        // 访问根节点
        const n = stack.pop();
        console.log(n.val);
        // 注意:这里需要右子树先入栈,因为这里用的是pop方法
        n.right &&  stack.push(n.right);
        n.left && stack.push(n.left);
    }
}preorder(bt); // 1245367

// 中序遍历
const inorder = (root) => {
    if(!root) return;
    const stack = [];
    // 利用指针找到叶子节点
    let p = root;
    while(stack.length || p){
        while(p){
            stack.push(p);
            p = p.left;
        }
        const n = stack.pop();
        console.log(n.val);
        p = n.right;
    }
};
inorder(bt); // 4251637

// 后序遍历:更改顺序为根右左,利用先序遍历实现,再逆序输出
const postorder = (root) => {
    if(!root) {return};
    const outputStack = [];
    const stack = [root];
    while(stack.length){
        const n = stack.pop();
       	outputStack.push(n);
        // 后进先出
        n.left && stack.push(n.left);
        n.right && stack.push(n.right);
    }
    // 逆序输出
    while(outputStack.length){
        const n = outputStack.pop();
        console.log(n.val)
    }
}
postorder(bt); // 4526731
104. 二叉树的最大深度 - [简单]

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

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

示例:
给定二叉树 [3,9,20,null,null,15,7],

​ 3

/ \
9 20
/ \
15 7
返回它的最大深度 3 。

/**
 * 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 {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
     // 存储结果变量
     let res = 0;
     // 深度优先搜索, l表示层数
     const dfs = (root, l) => {
         if(!root) return;
         if(!root.left && !root.right){
             res = Math.max(res, l);
         }
         root.left && dfs(root.left, l+1);
         root.right && dfs(root.right, l + 1);
     }
     dfs(root, 1);
     return res;
};

// 时间复杂度:o(n), n是树的节点数;空间复杂度:最差-o(n),最好-o(log2n)
111. 二叉树的最小深度 - [简单]

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

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

示例 1:
前端算法学习入门笔记 - JavaScript_第2张图片

输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

提示:

树中节点数的范围在 [0, 10^5] 内
-1000 <= Node.val <= 1000

/**
 * 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 {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    if(!root) return 0;
    // 广度优先搜索
    let q = [[root, 1]];
    while(q.length){
        const [n,l] = q.shift();
        if(!n.left && !n.right){
            return l;
        }
        n.left && q.push([n.left, l+1]);
        n.right && q.push([n.right, l+1]);
    }
};

// 时间复杂度:o(n),n是树的节点数;空间复杂度:o(n),n是树的节点树
102. 二叉树的层序遍历 - [中等]

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:
前端算法学习入门笔记 - JavaScript_第3张图片

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:

输入:root = [1]
输出:[[1]]
示例 3:

输入:root = []
输出:[]

提示:

树中节点数目在范围 [0, 2000] 内
-1000 <= Node.val <= 1000

/**
 * 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 {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if(!root) return [];
    const q = [root]; // 记录层级
    // 存储结果数组
    const res = [];
    while(q.length){
        let len = q.length;
        res.push([]);
        while(len--){
            const n = q.shift();
            res[res.length-1].push(n.val);
            n.left && q.push(n.left);
            n.right && q.push(n.right)
        }
    }
    return res;
};
// 时间复杂度:o(n),n是树的节点数;空间复杂度:o(n)

// ---------- 逐层遍历 -------------
/**
 * 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 {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if(!root) return [];
    const q = [root];
    const res = [];
    // 逐层遍历
    while(q.length){
        let len = q.length;
        res.push([]);
        while(len--){
            const n = q.shift();
            res[res.length - 1].push(n.val);
            n.left && q.push(n.left);
            n.right && q.push(n.right);
        }
    }
    return res;
};
94. 二叉树的中序遍历 - [简单]

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

示例 1:
前端算法学习入门笔记 - JavaScript_第4张图片

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

提示:

树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
/**
 * 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 {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    if(!root) return [];
    const q = [];
    // 利用指针
    let p = root;
    // 存储结果数组
    const res = [];
    while(q.length || p){
        // 找到最左的叶子节点
        while(p){
            q.push(p);
            p = p.left;
        }
        const n = q.pop();
        res.push(n.val);
        p = n.right;        
    }
    return res;
};
// 时间复杂度:O(n),n是树的节点数;空间复杂度:O(n)
112. 路径总和 - [简单]

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点 是指没有子节点的节点。

示例 1:
前端算法学习入门笔记 - JavaScript_第5张图片

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

示例 2:
前端算法学习入门笔记 - JavaScript_第6张图片

输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。

示例 3:

输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。

提示:

树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
/**
 * 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 {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */
var hasPathSum = function(root, targetSum) {
    if(!root) return false;
    // 存放结果
    let flag = false;
    // 深度优先搜索
    const dfs = (root, target) => {
        target = target - root.val;
        if(target === 0 && !root.left && !root.right) flag = true;
        root.left && dfs(root.left, target);
        root.right && dfs(root.right, target);
    }

    dfs(root, targetSum);
    return flag;
};
// 时间复杂度:O(n),n是树的节点数;空间复杂度:最坏O(n),最好O(log2n)
遍历JSON的所有节点值
const json = {
  a: { b: { c: 1 } },
  d: [1, 2],
};

const dfs = (n, path) => {
  console.log(n, path);
  Object.keys(n).forEach(k=>{
    dfs(n[k], path.concat(k))
  })
}
dfs(json, [])
/* console.log
{ a: { b: { c: 1 } }, d: [ 1, 2 ] } []
{ b: { c: 1 } } [ 'a' ]
{ c: 1 } [ 'a', 'b' ]
1 [ 'a', 'b', 'c' ]
[ 1, 2 ] [ 'd' ]
1 [ 'd', '0' ]
2 [ 'd', '1' ]
*/
渲染Antd的树组件
// React, 类组件内
const json = [
    {
        title: '一',
        key: "1",
        children: [
            {
                title: '1.1',
        		key: "2",
                children: []
            }
        ]
    },
    {
        title: '二',
        key: "3",
        children: [
            {
                title: '2.1',
        		key: "4",
                children: []
            }
        ]
    }
]
dfs = (n) => {
    return (
    	
        	{n.children.map(this.dfs)}
        
    )
}
json.map(this.dfs)

  • 图是网络结构的抽象模型,是一组由边连接的节点。

  • 图可以表示任何二元关系,比如道路、航班。

  • JS中没有图,但是可以用Object和Array构建图。

  • 图的表示法:邻接矩阵、邻接表、关联矩阵…

    • 邻接矩阵
      • 前端算法学习入门笔记 - JavaScript_第7张图片
    • 邻接表
      • 前端算法学习入门笔记 - JavaScript_第8张图片
深度/广度优先遍历
  • 深度优先遍历:尽可能深地搜索图的分支。
    • 访问根节点。
    • 对根节点的没访问过的相邻节点挨个进行深度优先遍历。(防止陷入死循环)
    • 前端算法学习入门笔记 - JavaScript_第9张图片
const graph = {
    0: [1, 2],
    1: [2],
    2: [0, 3],
    3: [3]
};

// 深度优先遍历
const visited = new Set();
const dfs = (n) => {
    console.log(n);
    visited.add(n);
    graph[n].forEach(c => {
        if(!visited.has(c)){
            dfs(c);
        }
    });
};

dfs(2); // 起点是2, 2013
  • 广度优先遍历:先访问离根节点最近的节点。
    • 新建一个队列,把根节点入队。
    • 把队头出队并访问。
    • 把队头的没访问过的相邻节点入队。
const graph = {
    0: [1, 2],
    1: [2],
    2: [0, 3],
    3: [3]
};

const visited = new Set();
const q = [2];
while (q.length) {
    const n = q.shift();
    console.log(n);
    visited.add(n);
    graph[n].forEach(c => {
        if (!visited.has(c)) {
            q.push(c);
        }
    });
}
// console.log 2031

// ------ 这种方法更好,避免q里面还有元素没有被访问到但是已经存在在visited的情况 ------
const visited = new Set();
visited.add(2);
const q = [2];
while (q.length) {
    const n = q.shift();
    console.log(n);
    graph[n].forEach(c => {
        if (!visited.has(c)) {
            q.push(c);
            visited.add(n);
        }
    });
}
65. 有效数字 - [困难]

有效数字(按顺序)可以分成以下几个部分:

  1. 一个 小数 或者 整数
  2. (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个整数

小数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 下述格式之一:
    1. 至少一位数字,后面跟着一个点 ‘.’
    2. 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
    3. 一个点 ‘.’ ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

  1. (可选)一个符号字符(’+’ 或 ‘-’)
  2. 至少一位数字

部分有效数字列举如下:[“2”, “0089”, “-0.1”, “+3.14”, “4.”, “-.9”, “2e10”, “-90E3”, “3e+7”, “+6e-1”, “53.5e93”, “-123.456e789”]

部分无效数字列举如下:[“abc”, “1a”, “1e”, “e3”, “99e2.5”, “–6”, “-+3”, “95a54e53”]

给你一个字符串 s ,如果 s 是一个 有效数字 ,请返回 true 。

示例 1:

输入:s = “0”
输出:true

示例 2:

输入:s = “e”
输出:false

示例 3:

输入:s = “.”
输出:false

提示:

1 <= s.length <= 20
s 仅含英文字母(大写和小写),数字(0-9),加号 '+' ,减号 '-' ,或者点 '.' 。

前端算法学习入门笔记 - JavaScript_第10张图片

解题思路

  • 构建一个表示状态的图。
  • 遍历字符串,并沿着图走,如果到了某个节点无路可走就返回false。
  • 遍历结束,如走到状态3/5/6,就返回true,否则返回false。
/**
 * @param {string} s
 * @return {boolean}
 */
var isNumber = function(s) {
    // 构建图
    const graph = {
        // 状态0,blank其实没必要,直接s.trim()更好
        0: {
            "blank": 0,
            "digit": 6,
            "sign": 1,
            ".": 2
        },
        1: {
            "digit": 6,
            ".": 2
        },
        2: {
            "digit": 3
        },
        3: {
            "digit": 3,
            "e": 4
        },
        4: {
            "sign": 7,
            "digit": 5
        },
        5: {
            "digit": 5
        },
        6: {
            "digit": 6,
            ".": 3,
            "e": 4
        },
        7: {
            "digit": 5
        }
    };
    // 保存状态
    let state = 0;
    for(let c of s){
        if(c >= '0' && c <= "9"){
            c = "digit";
        }else if(c === "+" || c === "-"){
            c = "sign";
        }else if(c === " "){
            c = "blank";
        }else if(c === "E"){
            c = "e"
        }
        state = graph[state][c]; // 更新状态,无路可走时为undefined
        if(state === undefined) return false;
    }
    if(state === 3 || state === 5 || state === 6){
        return true;
    }
    return false;
};
// 时间复杂度:O(n),n是字符串的长度;空间复杂度:O(1)
417. 太平洋大西洋水流问题 - [中等]

有一个 m × n 的矩形岛屿,与 太平洋大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回 网格坐标 result 的 2D列表 ,其中 result[i] = [ri, ci] 表示雨水可以从单元格 (ri, ci) 流向 太平洋和大西洋

示例 1:

前端算法学习入门笔记 - JavaScript_第11张图片

输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]

示例 2:

输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]

提示:

  • m == heights.length
  • n == heights[r].length
  • 1 <= m, n <= 200
  • 0 <= heights[r][c] <= 10^5

解题思路

  • 把矩阵想象成图。
  • 从海岸线逆流而上遍历图,所到之处就是可以流到某个大洋的坐标。
  • 新建两个矩阵,分别记录能流到两个大洋的坐标。
  • 从海岸线,多管齐下,同时深度优先遍历图,过程中填充上述矩阵。
  • 遍历两个矩阵,找出能流到两个大洋的坐标。
/**
 * @param {number[][]} heights
 * @return {number[][]}
 */
var pacificAtlantic = function(heights) {
    // 特殊情况
    if(!heights || !heights[0]) return [];
    // 获取行数和列数
    const m = heights.length;
    const n = heights[0].length;
    // 构建存储两个海洋的矩阵
    const flow1 = Array.from({length: m}, () => new Array(n).fill(false));
    const flow2 = Array.from({length: m}, () => new Array(n).fill(false));
    // 图的深度优先搜索, r是行数,c是列数,flow是海洋类型
    const dfs = (r, c, flow) => {
        flow[r][c] = true;
        // 遍历当前节点的所有相邻节点
        [[r+1, c], [r-1, c], [r, c+1], [r, c-1]].forEach(([nr, nc]) => {
            // 满足继续深度搜索的条件
            if(
                // 当前节点在当前矩阵中
                nr >= 0 && nr < m && nc >= 0 && nc < n &&
                // 没有被访问过,防止死循环
                !flow[nr][nc] &&
                // 高度大于等于当前节点
                heights[nr][nc] >= heights[r][c]
            ){
                dfs(nr, nc, flow)
            }
        })
    }

    // 从海岸线开始遍历矩阵,多管齐下
    for(let r = 0; r < m; r++){
        dfs(r, 0, flow1);
        dfs(r, n-1, flow2);
    }
    for(let c = 0; c < n; c++){
        dfs(0, c, flow1);
        dfs(m-1, c, flow2);
    }

    // 遍历两个存储结果矩阵,找到同时能流进太平洋和大西洋的节点
    const res = [];
    for(let r = 0; r < m; r++){
        for(let c = 0; c < n; c++){
            if(flow1[r][c] && flow2[r][c]){
                res.push([r,c])
            }
        }
    }
    return res;
};
// 时间复杂度:O(mn);空间复杂度:O(mn);m是矩阵行数,n是矩阵列数
133. 克隆图 - [中等]

给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。

class Node {
    public int val;
    public List<Node> neighbors;
}

测试用例格式

简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。

邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。

给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。

示例 1:
前端算法学习入门笔记 - JavaScript_第12张图片

输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]
解释:
图中有 4 个节点。
节点 1 的值是 1,它有两个邻居:节点 2 和 4 。
节点 2 的值是 2,它有两个邻居:节点 1 和 3 。
节点 3 的值是 3,它有两个邻居:节点 2 和 4 。
节点 4 的值是 4,它有两个邻居:节点 1 和 3 。

示例 2:
前端算法学习入门笔记 - JavaScript_第13张图片

输入:adjList = [[]]
输出:[[]]
解释:输入包含一个空列表。该图仅仅只有一个值为 1 的节点,它没有任何邻居。

示例 3:

输入:adjList = []
输出:[]
解释:这个图是空的,它不含任何节点。

示例 4:
前端算法学习入门笔记 - JavaScript_第14张图片

输入:adjList = [[2],[1]]
输出:[[2],[1]]

提示:

  1. 节点数不超过 100 。
  2. 每个节点值 Node.val 都是唯一的,1 <= Node.val <= 100。
  3. 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。
  4. 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。
    图是连通图,你可以从给定节点访问到所有节点。

解题思路

  • 深度或广度优先遍历所有节点。
  • 拷贝所有的节点,存储起来。
  • 将拷贝的节点,按照原图的连接方法进行连接。
/**
 * // Definition for a Node.
 * function Node(val, neighbors) {
 *    this.val = val === undefined ? 0 : val;
 *    this.neighbors = neighbors === undefined ? [] : neighbors;
 * };
 */

/**
 * @param {Node} node
 * @return {Node}
 */
 // ------ 深度优先遍历解法 ------
var cloneGraph = function(node) {
    if(!node) return;
    // 存储访问过的节点
    const visited = new Map();
    const dfs = (n) => {
        const newNode = new Node(n.val);
        visited.set(n, newNode);
        (n.neighbors || []).forEach(ne => {
            if(!visited.has(ne)){
                dfs(ne);
            }
            newNode.neighbors.push(visited.get(ne));
        })
    }
    dfs(node);
    return visited.get(node);
};
// 时间复杂度:O(n); 空间复杂度:O(n)

// -------------------------------------------------------------
/**
 * // Definition for a Node.
 * function Node(val, neighbors) {
 *    this.val = val === undefined ? 0 : val;
 *    this.neighbors = neighbors === undefined ? [] : neighbors;
 * };
 */

/**
 * @param {Node} node
 * @return {Node}
 */
 // ------ 广度优先遍历解法 ------
var cloneGraph = function(node) {
    if(!node) return;
    // 存储访问过的节点
    const visited = new Map();
    const q = [node];
    // 标记初始节点
    visited.set(node, new Node(node.val));
    while(q.length){
        const n = q.shift(); // 队列,先进先出
        (n.neighbors || []).forEach(ne => {
            if(!visited.has(ne)){
                q.push(ne);
                visited.set(ne, new Node(ne.val));
            }
            visited.get(n).neighbors.push(visited.get(ne));
        })
    }
    
    return visited.get(node);
};
// 时间复杂度:O(n); 空间复杂度:O(n)

  • 堆是一种特殊的完全二叉树。(完全二叉树:所有层的节点都被填满,如果缺少节点,只能缺少右边的若干节点)。
    前端算法学习入门笔记 - JavaScript_第15张图片
  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。
    前端算法学习入门笔记 - JavaScript_第16张图片

应用场景:

  • 堆能高效、快速地找出最大值和最小值,时间复杂度:O(1)。
  • 找出第K个最大(小)元素。
JavaScript 实现最小堆类
  • 插入
    • 将值插入堆的底部,即数组的尾部
    • 然后上移,将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
    • 大小为k的堆中插入元素的时间复杂度为O(logk)
  • 删除堆顶
    • 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
    • 然后下移,将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶
    • 大小为k的堆中删除堆顶的时间复杂度为O(logk)
  • 获取堆顶和堆的大小
    • 获取堆顶:返回数组的头部。
    • 获取堆的大小:返回数组的长度。
class MinHeap {
  constructor(){
    this.heap = [];
  }
  swap(i1,i2){
    const temp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = temp;
  }
  getParentIndex(i){
    return (i-1) >> 1;
  }
  getLeftIndex(i){
    return i * 2 + 1;
  }
  getRightIndex(i){
    return i * 2 + 2;
  }
  shiftUp(index){
    if(index === 0) return;
    // 上移操作
    const parentIndex = this.getParentIndex(index);
    if(this.heap[parentIndex] > this.heap[index]){
      this.swap(parentIndex, index);
      this.shiftUp(parentIndex);
    }
  }
  shiftDown(index){
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    if(this.heap[leftIndex] < this.heap[index]){
      this.swap(leftIndex, index);
      this.shiftDown(leftIndex);
    }
    if(this.heap[rightIndex] < this.heap[index]){
      this.swap(rightIndex, index);
      this.shiftDown(rightIndex);
    }
  }
  // 插入操作
  insert(value){
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
  }
  // 删除堆顶元素
  pop(){
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
  }
  peek(){
    return this.heap[0];
  }
  size(){
    return this.heap.length;
  }
}
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);
h.pop();
215. 数组中的第K个最大元素 - [中等]

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

提示:

1 <= k <= nums.length <= 104
-104 <= nums[i] <= 104

解题思路:

  • 构建一个最小堆,并依次把数组的值插入堆中。
  • 当堆的容量超过K,就删除堆顶。
  • 插入结束后,堆顶就是第K个最大元素。
class MinHeap {
  constructor(){
    this.heap = [];
  }
  swap(i1,i2){
    const temp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = temp;
  }
  getParentIndex(i){
    return (i-1) >> 1;
  }
  getLeftIndex(i){
    return i * 2 + 1;
  }
  getRightIndex(i){
    return i * 2 + 2;
  }
  shiftUp(index){
    if(index === 0) return;
    // 上移操作
    const parentIndex = this.getParentIndex(index);
    if(this.heap[parentIndex] > this.heap[index]){
      this.swap(parentIndex, index);
      this.shiftUp(parentIndex);
    }
  }
  shiftDown(index){
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    if(this.heap[leftIndex] < this.heap[index]){
      this.swap(leftIndex, index);
      this.shiftDown(leftIndex);
    }
    if(this.heap[rightIndex] < this.heap[index]){
      this.swap(rightIndex, index);
      this.shiftDown(rightIndex);
    }
  }
  // 插入操作
  insert(value){
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
  }
  // 删除堆顶元素
  pop(){
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
  }
  peek(){
    return this.heap[0];
  }
  size(){
    return this.heap.length;
  }
};

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const heap = new MinHeap();
    nums.forEach(i => {
        heap.insert(i);
        if(heap.size() > k){
            heap.pop();
        }
    })
    return heap.peek();
};
// 时间复杂度:O(nlogk);空间复杂度:O(k)
347. 前K个高频元素 - [中等]

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

1 <= nums.length <= 10^5
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    // 时间复杂度O(nlog(n))
    const map = new Map();
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1);
    });
    // 排序
    const arr = Array.from(map);
    arr.sort((a,b) => b[1] - a[1]);
    return arr.slice(0,k).map( n => n[0]);
};

// ------------ 方法二:最小堆 ---------

// 最小堆轮子
class MinHeap{
    constructor(){
        this.heap = [];
    }
    // 交换两个节点的值
    swap(i1,i2){
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    // 获取父节点
    getParentIndex(index){
        return (index-1) >> 1;
    }
    getLeftIndex(index){
        return index * 2 + 1;
    }
    getRightIndex(index){
        return index * 2 + 2;
    }
    shiftUp(index){
        if(index === 0) return; // 终止条件!!
        const parentIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value){
            this.swap(index,parentIndex);
            this.shiftUp(parentIndex);
        }
    }
    // 下移
    shiftDown(index){
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value){
            this.swap(leftIndex,index);
            this.shiftDown(leftIndex);
        }
        if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value){
            this.swap(rightIndex,index);
            this.shiftDown(rightIndex);
        }
    }
    insert(value){
        this.heap.push(value);
        // 上移
        this.shiftUp(this.heap.length-1);
    }
    // 删除堆顶节点
    pop(){
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
    peek(){
        return this.heap[0];
    }
    size(){
        return this.heap.length;
    }
}
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    // 时间复杂度O(nlogk)
    const map = new Map();
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1);
    });
    // 
    // 利用堆来降低时间复杂度
    const h = new MinHeap();
    map.forEach((value, key) => {
        h.insert({value,key});
        if(h.size() > k){
            h.pop();
        }
    })
    return h.heap.map(n => n.key);
};
// 空间复杂度:最坏 - O(n)
23. 合并K个升序链表 - [困难]

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

提示:

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4

解题思路:

  • 新链表的下一个节点一定是k个链表头中的最小节点。
  • 考虑选择使用最小堆。
  • 构建一个最小堆,并依次把链表头插入堆中。
  • 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中。
  • 等堆元素全部弹出,合并工作就完成了。
class MinHeap {
  constructor(){
    this.heap = [];
  }
  swap(i1,i2){
    const temp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = temp;
  }
  getParentIndex(i){
    return (i-1) >> 1;
  }
  getLeftIndex(i){
    return i * 2 + 1;
  }
  getRightIndex(i){
    return i * 2 + 2;
  }
  shiftUp(index){
    if(index === 0) return;
    // 上移操作
    const parentIndex = this.getParentIndex(index);
    if(this.heap[parentIndex] && this.heap[parentIndex].val > this.heap[index].val){
      this.swap(parentIndex, index);
      this.shiftUp(parentIndex);
    }
  }
  shiftDown(index){
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    if(this.heap[leftIndex] && this.heap[leftIndex].val < this.heap[index].val){
      this.swap(leftIndex, index);
      this.shiftDown(leftIndex);
    }
    if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val){
      this.swap(rightIndex, index);
      this.shiftDown(rightIndex);
    }
  }
  // 插入操作
  insert(value){
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
  }
  // 删除堆顶元素
  pop(){
    if(this.size() === 1) return this.heap.shift();
    const top = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
    return top;
  }
  peek(){
    return this.heap[0];
  }
  size(){
    return this.heap.length;
  }
}
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    // 新建链表存储结果
    const res = new ListNode(0);
    let p = res;
    const h = new MinHeap();
    // 将排序链表中的所有头节点都插入到堆中
    lists.forEach(l => {
        if(l) h.insert(l);
    });
    while(h.size()){
        // 获得堆顶元素
        const n = h.pop();
        p.next = n;
        p = p.next;
        n.next && h.insert(n.next);
    }
    return res.next; // 注意,这里是res.next,res的第一个节点是空
};
// 时间复杂度:O(nlogk),空间复杂度:O(k)

JS中的排序(数字越大,性能越好,性能3以上可用于实战)

  • JS中的排序:数组的sort方法。
  • JS中的搜索:数组的indexOf方法。
冒泡排序 - 性能1
  • 比较所有相邻元素,如果第一个比第二个大,则交换它们。
  • 一轮下来,可以保证最后一个数是最大的。
  • 执行n-1轮,就可以完成排序。
Array.prototype.bubbleSort = function(){
    for(let i = 0; i < this.length - 1; i++){
        for(let j = 0; j < this.length - 1 - i; j++){
            if(this[j] > this[j+1]){
                const temp = this[j];
                this[j] = this[j+1];
                this[j+1] = temp;
            }
        }
    }
}
const arr = [5,4,3,2,1];
arr.bubbleSort();
console.log(arr); // [1,2,3,4,5]
// 时间复杂度:O(n^2);空间复杂度:O(1)
选择排序 - 性能1
  • 找到数组中的最小值,选中它并将其放置在第一位。
  • 接着找到第二小的值,选中它并将其放置在第二位。
  • 依次类推,执行n-1轮。
Array.prototype.selectionSort = function(){
  for(let i = 0; i < this.length - 1; i++){
      let indexMin = i;
      for(let j = i; j < this.length; j++){
          if(this[j] < this[indexMin]){
              indexMin = j;
          }
      }
      if(indexMin !== i){
          const temp = this[i];
      	  this[i] = this[indexMin];
      	  this[indexMin] = temp;
      }
  }
}
const arr = [3,4,2,1,5];
arr.selectionSort();
console.log(arr); // [1,2,3,4,5]
// 时间复杂度:O(n^2);空间复杂度:O(1)
插入排序 - 性能2

时间复杂度也是O(n^2),不过在排序小型数组时比前面两个算法要好。

  • 从第二个数开始往前比。
  • 比它大就往后排。
  • 以此类推进行到最后一个数。
Array.prototype.insertSort = function(){
    for(let i = 1; i < this.length; i++){
      let temp = this[i];
      let j = i;
      while(j > 0){
        if(this[j-1] > temp){
          this[j] = this[j-1];
        }else{
          break; // 这里要break,阻止后面的j--操作
        }
        j--;
      }
      this[j] = temp;
    }
}
const arr = [5,90,4,3,2,1];
arr.insertSort();
// 时间复杂度:O(n^2);空间复杂度:O(1)
console.log(arr); // [1,2,3,4,5,90]
归并排序 - 性能3
  • 分:把数组劈成两半,再递归地对子数组进行"分"操作,直到分成一个个单独的数。

  • 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组。

  • 新建一个空数组res,用于存放最终排序后的数组。

  • 比较两个有序数组的头部,较小者出队并推入res中。

  • 如果两个数组还有值,就重复第二步。

Array.prototype.mergeSort = function(){
  // 分, O(logn)
  const rec = (arr) => {
    if(arr.length === 1) {return arr;} // 递归终止条件
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid); // 不包括mid
    const right = arr.slice(mid, arr.length);
    const orderLeft = rec(left);
    const orderRight = rec(right);
    // 结果数组
    const res = [];
    while(orderLeft.length || orderRight.length){
      if(orderLeft.length && orderRight.length){
        res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
      }else{
        orderLeft.length && res.push(orderLeft.shift());
        orderRight.length && res.push(orderRight.shift());
      }
    }
    return res;
  } 
  const res = rec(this);
  // 拷贝一份
  res.forEach((n,i) => {
    this[i] = n;
  });
}
const arr = [5,90,4,66,3,2,1];
arr.mergeSort();
// 时间复杂度:O(nlogn);空间复杂度:O(n)
console.log(arr); // [1, 2, 3, 4, 5, 66, 90]
快速排序 - 性能3
  • 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面。
  • 递归:递归地对基准前后的子数组进行分区。
Array.prototype.quickSort = function(){
  const rec = (arr) => {
    if(arr.length <= 1) return arr; // 注意终止条件是小于等于1,因为arr可能是空数组
    const left = [];
    const right = [];
    const mid = arr[0];
    for(let i = 1; i < arr.length; i++){
      if(arr[i] < mid){
        left.push(arr[i]);
      }else{
        right.push(arr[i]);
      }
    }
    return [...rec(left), mid, ...rec(right)];
  }
  const res = rec(this);
  res.forEach((n,i) => {
    this[i] = n;
  })
}
const arr = [5,90,4,66,3,2,1];
arr.quickSort();
// 递归的时间复杂度是O(logN),分区操作的时间复杂度是O(n);时间复杂度:O(n*logN);空间复杂度:O(n)
console.log(arr);

另外一种解法:左右指针挖坑法

问题: 是否可以通过改变选取基准数来提高快排的效率?

左右指针挖坑法:

1.先从数列中取出一个数作为基准数(通常选第一个数)。 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。 3.再对左右区间重复第二步,直到各区间只有一个数。
前端算法学习入门笔记 - JavaScript_第17张图片
前端算法学习入门笔记 - JavaScript_第18张图片
前端算法学习入门笔记 - JavaScript_第19张图片
前端算法学习入门笔记 - JavaScript_第20张图片

// edited by Jesy_Hsu
/**
 * 左右指针挖坑法
 * @param nums 待排序数组
 * @param low 左指针
 * @param high 右指针
 */
function partition(nums, low, high) {
    // 基准数
    var pivot = nums[low];
    while (low < high) {
        while (low < high && nums[high] >= pivot) {
            high--;
        }
        // 互换位置,结束一轮
        nums[low] = nums[high];
        while (low < high && nums[low] <= pivot) {
            low++;
        }
        nums[high] = nums[low];
    }
    // 此时low===high
    nums[low] = pivot;
    return low;
}
/**
 * @param {number[]} nums
 * @return {number[]}
 */
var quickSort = function (nums, low, high) {
    // 分治思想,划分左右区间
    if (low < high) {
        // 找寻基准数的正确索引
        var index = partition(nums, low, high);

        // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
        quickSort(nums, low, index - 1);
        quickSort(nums, index + 1, high);
    }
    return nums;
};
/**
 * @param {number[]} nums
 * @return {number[]}
 */
var sortArray = function(nums) {
    var low = 0;
    var high = nums.length - 1;
    return quickSort(nums, low, high);
};

复杂度分析

时间复杂度: 基于随机选取主元的快速排序时间复杂度为期望 O(nlogn),其中 n 为数组的长度。

空间复杂度: O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为logn,空间复杂度为 O(logn)。

JS中的搜索(数字越大,性能越好,性能3以上可用于实战)

顺序搜索 - 性能1
  • 遍历数组。
  • 找到跟目标值相等的元素,就返回它的下标。
  • 遍历结束后,如果没有搜索到目标值,就返回-1。
Array.prototype.sequentialSearch = function(target){
  for(let i = 0; i < this.length; i++){
    if(this[i] === target){
      return i
    }
  }
  return -1;
}
const arr = [1,2,3,4,5];
const res = arr.sequentialSearch(3);
console.log(res);
// 时间复杂度:O(n)
二分搜索 - 性能3

前提: 数组有序

  • 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。
  • 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。
Array.prototype.binarySearch = function(target){
  // 前提:数组有序
  let low = 0;
  let high = this.length - 1;
  while(low <= high){
    const mid = Math.floor((low + high) / 2);
    const element = this[mid];
    if(element < target){
      low = mid + 1;
    }else if(element > target){
      high = mid - 1;
    }else{
      return mid;
    }
  }
  return -1;
}
const arr = [1,2,3,4,5];
const res = arr.binarySearch(3);
console.log(res);
// 时间复杂度:O(logN)
21. 合并两个有序链表 - [简单]

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:
前端算法学习入门笔记 - JavaScript_第21张图片

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

提示:

两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列

解题思路:

  • 新建一个新链表,作为返回结果。
  • 用指针遍历两个有序链表,并比较两个链表的当前节点,较小者先接入新链表,并将指针后移一步。
  • 链表遍历结束,返回新链表。
/**
 * 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) {
    let res = new ListNode(0);
    let p = res;
    let p1 = list1;
    let p2 = list2;
    while(p1 && p2){
        if(p1.val < p2.val){
            p.next = p1;
            p1 = p1.next;
        }else{
            p.next= p2;
            p2 = p2.next;
        }
        p = p.next;
    }
    p1 && (p.next = p1);
    p2 && (p.next = p2);
    return res.next;
};
// 时间复杂度:O(M+N);空间复杂度:O(1)
374. 猜数字大小 - [简单]

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

  • -1:我选出的数字比你猜的数字小 pick < num
  • 1:我选出的数字比你猜的数字大 pick > num
  • 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

示例 1:

输入:n = 10, pick = 6
输出:6

示例 2:

输入:n = 1, pick = 1
输出:1

示例 3:

输入:n = 2, pick = 1
输出:1

示例 4:

输入:n = 2, pick = 2
输出:2

提示:

  • 1 <= n <= 2^31 - 1
  • 1 <= pick <= n
/** 
 * Forward declaration of guess API.
 * @param {number} num   your guess
 * @return 	            -1 if num is lower than the guess number
 *			             1 if num is higher than the guess number
 *                       otherwise return 0
 * var guess = function(num) {}
 */

/**
 * @param {number} n
 * @return {number}
 */
var guessNumber = function(n) {
    // 二分搜索
    // 条件:一定会找到
    let low = 1;
    let high = n;
    while(low <= high){
        const mid = Math.floor((low + high) / 2);
        const res = guess(mid);
        if(res === 0){
            return mid;
        }else if(res === 1){
            low = mid + 1;
        }else{
            high = mid - 1;
        }
    }
};
// 时间复杂度:O(logn);空间复杂度:O(1)
69. x的平方根 - [简单]

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

示例 1:

输入:x = 4
输出:2

示例 2:

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

提示:

0 <= x <= 2^31 - 1

解题思路

由于 x 平方根的整数部分 ans 是满足 k2≤x 的最大 k 值,因此我们可以对 k 进行二分查找,从而得到答案。

二分查找的下界为 0,上界可以粗略地设定为 x。在二分查找的每一步中,我们只需要比较中间元素 mid 的平方与 x 的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案 ans 后,也就不需要再去尝试 ans+1了。

/**
 * @param {number} x
 * @return {number}
 */
var mySqrt = function(x) {
    // 二分搜索
    let ans = -1;
    let low = 0;
    let high = x;
    while(low <= high){
        let mid = Math.floor((low + high) / 2);
        const temp = mid * mid ;
        if(temp <= x){
            ans = mid;
            low = mid + 1;
        }else{
            high = mid - 1;
        }
    }
    return ans;
};
// 时间复杂度:O(logx);空间复杂度:O(1)
总结

Chrome浏览器的Array.prototype.sort()方法采用的是数组长度小于11时使用插入排序,其他使用快速排序算法,插入排序是稳定的,快速排序不稳定,JS引擎是V8。

Firefox浏览器的Array.prototype.sort()方法采用的是归并排序算法,归并排序是稳定的,JS引擎是SpiderMonkey。

Safari浏览器的Array.prototype.sort()方法采用的是桶排序归并排序算法,桶排序稳定性不确定,归并排序是稳定的,内核是Nitro。

Edge和IE浏览器的Array.prototype.sort()方法采用的是不稳定的快速排序算法,内核是Chakra。

分治思想

  • 分而治之是算法设计中的一种方法。
  • 它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
  • 需要返回值时则return,不需要时不return。
374. 猜数字大小 - [简单]

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

  • -1:我选出的数字比你猜的数字小 pick < num
  • 1:我选出的数字比你猜的数字大 pick > num
  • 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

示例 1:

输入:n = 10, pick = 6
输出:6

示例 2:

输入:n = 1, pick = 1
输出:1

示例 3:

输入:n = 2, pick = 1
输出:1

示例 4:

输入:n = 2, pick = 2
输出:2

提示:

  • 1 <= n <= 2^31 - 1
  • 1 <= pick <= n

解题思路

  • 分:计算中间元素,分割数组。
  • 解:递归地在较大或者较小子数组进行二分搜索。
  • 合:不需要此步,因为在子数组中搜到就返回了。
/** 
 * Forward declaration of guess API.
 * @param {number} num   your guess
 * @return 	            -1 if num is lower than the guess number
 *			             1 if num is higher than the guess number
 *                       otherwise return 0
 * var guess = function(num) {}
 */

/**
 * @param {number} n
 * @return {number}
 */
var guessNumber = function(n) {
    // 分而治之的思想
    const rec = (low, high) => {
        // 递归终止条件
        if(low > high) { return; }
        const mid = Math.floor((low + high) / 2);
        const res = guess(mid);
        if(res === 0){
            return mid;
        }else if(res === 1){
            return rec(mid + 1, high);
        }else{
            return rec(low, mid - 1);
        }
    }
    return rec(1,n);
};
// 时间复杂度:O(logn),空间复杂度:O(logn);
226. 翻转二叉树 - [简单]

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:
前端算法学习入门笔记 - JavaScript_第22张图片

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:
前端算法学习入门笔记 - JavaScript_第23张图片

输入:root = [2,1,3]
输出:[2,3,1]

示例 3:

输入:root = []
输出:[]

提示:

树中节点数目范围在 [0, 100] 内
-100 <= Node.val <= 100

解题思路

  • 先翻转左右子树,再将子树换个位置。
  • 分:获取左右子树。
  • 解:递归地翻转左右子树。
  • 合:将翻转后的左右子树换个位置放在根节点上。
/**
 * 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 {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    // 用递归
    const rec = function(head){
        if(!head?.left && !head?.right) return head;
        // 获取左右子树
        const leftNode = head.left;
        const rightNode = head.right;
        head.left = rec(rightNode);
        head.right = rec(leftNode);
        return head;
    }
    return rec(root);
};
// 时间复杂度:O(n),n为节点数;空间复杂度:O(h),h是树的高度

// ================= Method 2 ======================
/**
 * 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 {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    if(!root) {return null};
    return{
        val: root.val,
        left: invertTree(root.right),
        right: invertTree(root.left)
    }
};
100. 相同的树 - [简单]

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入:p = [1,2,3], q = [1,2,3]
输出:true

示例 2:

输入:p = [1,2], q = [1,null,2]
输出:false

示例 3:

输入:p = [1,2,1], q = [1,1,2]
输出:false

提示:

两棵树上的节点数目都在范围 [0, 100] 内
-10^4 <= Node.val <= 10^4
/**
 * 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 {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function(p, q) {
    if(!p && !q) return true;
    if(p && q && p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right)){
        return true;
    }
    return false;
};
// 时间复杂度:O(n);空间复杂度:O(h),h为树的高度。
101. 对称二叉树 - [简单]

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:
前端算法学习入门笔记 - JavaScript_第24张图片

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:
前端算法学习入门笔记 - JavaScript_第25张图片

输入:root = [1,2,2,null,3,null,3]
输出:false

提示:

树中节点数目在范围 [1, 1000] 内
-100 <= Node.val <= 100
/**
 * 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 {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    const rec = (left, right) => {
        if(!left && !right) return true;
        if(left && right && left.val === right.val && rec(left.left,right.right) && rec(left.right, right.left)){
            return true
        }
        return false;
    }
    return rec(root.left, root.right);
};
// 时间复杂度:O(n);空间复杂度:O(h),h为树的高度。

动态规划

  • 动态规划是算法设计中的一种方法。
  • 它将一个问题分解为相互重叠的子问题,通过反复求解子问题来解决原来的问题。
70. 爬楼梯 - [简单]

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

解题思路

  • 爬到第n阶可以在第n-1阶爬1个台阶,或者在第n-2阶爬2个台阶。
  • 定义子问题:F(n) = F(n-1) + F(n-2)。
  • 反复执行:从2循环到n,执行上述公式。
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    // 动态规划的初始值很重要
    if(n < 2) return 1;
    let dp0 = 1; // 初始值
    let dp1 = 1;
    for(let i = 2; i <= n; i++){
        let temp = dp0;
        dp0 = dp1;
        dp1 = dp1 + temp;
    }
    return dp1;
};
// 时间复杂度:O(n);空间复杂度:O(1)
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 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

解题思路

  • f(k) = 从前k个房屋中能偷窃到的最大数额。
  • Ak = 第k个房屋的钱数。
  • f(k) = max(f(k-2) + Ak, f(k-1))。
/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    if(nums.length === 1) return nums[0];
    // F(n) = Math.max(F(n-1), n+F(n-2))
    // 初始值是2
    let dp0 = nums[0];
    let dp1 = Math.max(nums[0],nums[1]);
    for(let i = 2; i < nums.length; i++){
        let temp = dp1;
        dp1 = Math.max(dp0+nums[i], temp);
        dp0 = temp;
    }
    return dp1;
};
// 时间复杂度:O(n);空间复杂度:O(1)
213. 打家劫舍 II - [中等]

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

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

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000
// 分成两个打家劫舍的问题
/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    const length = nums.length;
    if(length === 1) return nums[0];
    const rec = (arr, l, r) => {
        let dp0 = 0;
        let dp1 = arr[l];
        for(let i = l+1; i < r; i++){
            const temp = dp1;
            dp1 = Math.max(dp0 + arr[i], dp1);
            dp0 = temp
        }
        return dp1;
    }
    return Math.max(rec(nums, 0, length - 1), rec(nums, 1, length));
};
// 时间复杂度:O(n),空间复杂度:O(1)

贪心算法

  • 贪心算法是算法设计中的一种方法。
  • 期盼通过每个阶段的局部最优选择,从而达到全局的最优。
  • 结果并不一定是最优
455. 分发饼干 - [简单]

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:

输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

提示:

  • 1 <= g.length <= 3 * 10^4
  • 0 <= s.length <= 3 * 10^4
  • 1 <= g[i], s[j] <= 2^31 - 1

解题思路

  • 局部最优:既能满足孩子,还消耗最少。
  • 先将"较小的饼干"分给"胃口最小"的孩子。
  • 对饼干数组和胃口数组升序排序。
  • 遍历饼干数组,找到能满足第一个孩子的饼干。
  • 然后继续遍历饼干数组,找到满足第二、三、…、n个孩子的饼干。
/**
 * @param {number[]} g
 * @param {number[]} s
 * @return {number}
 */
var findContentChildren = function(g, s) {
    // 每个孩子最多只能给一块饼干
    // 先将饼干数组和胃口数组排序
    g.sort((a,b) => a-b); // 升序
    s.sort((a,b) => a-b);
    // 遍历饼干数组
    let i = 0;
    s.forEach(n => {
        if(n >= g[i]){
            i++;
        }
    });
    return i;
};
// 时间复杂度:O(nlogn),空间复杂度:O(1)
122. 买卖股票的最佳时机II - [中等]

给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。

在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。
返回 你能获得的 最大 利润 。

示例 1:

输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:

输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 10^4
  • 0 <= prices[i] <= 10^4

解题思路

  • 局部最优:见好就收,见差就不动,不做任何长远打算。
  • 新建一个变量,用来统计总利润。
  • 遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不交易。
  • 遍历结束后,返回所有利润之和。
/**
 * @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;
};
// 时间复杂度:O(n),空间复杂度:O(1)

回溯算法

  • 回溯算法是算法设计中的一种方法。

  • 回溯算法是一种渐进式寻找并构建问题解决方式的策略。

  • 回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。

  • 适用场景:

    • 有很多路,有死路,也有出路。
    • 通常需要递归来模拟所有的路。
46. 全排列 - [中等]

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

解题思路

  • 用递归模拟所有情况。
  • 遇到包含重复元素的情况,就回溯。
  • 收集所有到达递归终点的情况,并返回。
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    // 回溯算法
    const res = [];
    const backtrace = (path) => {
        // 递归终止条件:找到了一个全排列的结果
        if(path.length === nums.length){
            res.push(path)
            return;
        }
        nums.forEach(n => {
            if(path.includes(n)){ return; }
            backtrace(path.concat(n));
        })
    }
    backtrace([]);
    return res;
};
// 时间复杂度:O(n!),空间复杂度:O(n)
78. 子集 - [中等]

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

解题思路

  • 用递归模拟出所有情况。
  • 保证接的数字都是后面的数字。
  • 收集所有到达递归终点的情况,并返回。
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    // 回溯算法
    const res = []; 
    const backtrace = (path,l,start) => {
        if(path.length === l){
            res.push(path);
            return;
        }
        for(let i = start; i < nums.length; i++){
            backtrace(path.concat(nums[i]), l, i + 1);
        }
    }
    for(let i = 0; i <= nums.length; i++){
        backtrace([], i, 0) // 包含了空集
    }
    return res;
};
// 时间复杂度:O(2^N),因为每个元素都有两种可能(存在或不存在)
// 空间复杂度:O(N)

你可能感兴趣的:(前端,javascript,算法)