kakaDorothy's Algorithm Summary

176 阅读16分钟

一、复杂度计算

时间复杂度: 一个函数,用大O表示,比如O(1),O(n),O(logN)..., 用来定性描述该算法的运行时间。

空间复杂度:一个函数,用大O表示,比如O(1),O(n),O(n²)..., 用来定性描述该算法在运行时临时占用存储空间大小的度量。

image.png

二、栈

概念

是一个后进先出的数据结构。放进拿出对应的方法分别是push()、pop(), JavaScript中没有栈数据结构,但可以用Array实现栈的所有功能。

image.png

const stack = []
stack.push(1)
stack.push(2)

const item1 = stack.pop() // pop():移除一个元素并返回它
console.log(item1)

使用场景

需要后进先出的场景

如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈(js的执行上下文栈)...

十进制转二进制 image.png 判断字符串的括号是否有效

image.png 函数调用堆栈

image.png

题目

20. 有效的括号

利用栈的思想,后进先出:

  1. 1.新建一个栈(用数组模拟栈)
  2. 2.扫描字符串,遇到左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配直接判定为不合法返回false
  3. 3.最后检查栈空了就合法,否则不合法
  4. 如果字符串长度为奇数,直接返回false
var isValid = function(s) {
    if(s.length % 2 !== 0) {
        return false
    }
    let stack = []
    for (let i = 0; i <s.length; i++) {
        if(s[i] === '(' || s[i] === '[' || s[i] === '{') {
            stack.push(s[i]);
        } else {
            const top = stack[stack.length - 1]; // 获取栈顶元素
            // 因为括号种类不多,直接使用暴力枚举
            if(
                (top === '(' && s[i] === ')') ||
                (top === '[' && s[i] === ']') ||
                (top === '{' && s[i] === '}')){
                    stack.pop(stack.length - 1)
                } else {
                    return false
                }
        }
    } 
    return stack.length === 0  
}

时间复杂度:O(n) ,空间复杂度:O(n)

本题可利用字典来优化。

二、队列

概念

队列 是一个先进先出的数据结构。放进拿出对应的方法分别是push()、shift(), JavaScript中没有队列数据结构,但可以用Array实现队列的所有功能。

const queue = [];
queue.push(1);
queue.push(2);
const item1 = queue.shift();
const item2 = queue.shift();

使用场景

需要先进先出的场景

如:食堂排队打饭、JS异步中的任务队列、计算最近请求次数... JS异步中的任务队列

image.png

计算最近请求次数

题目

933. 最近的请求次数

队列思想:

  1. 1.有新请求就入队,3000ms前发出的请求出队
  2. 2.队列的长度就是最近请求次数
var RecentCounter = function() {
    this.q = [];
};

/** 
 * @param {number} t
 * @return {number}
 */
RecentCounter.prototype.ping = function(t) {
    this.q.push(t);
    while(this.q[0] < t - 3000){
        this.q.shift();
    }
    return this.q.length;
};

时间复杂度:O(n) ,空间复杂度:O(n)

三、链表

概念

链表是由多个元素组成的列表。元素存储不连续,用next指针连在一起。JavaScript中没有链表数据结构,但可以用Object模拟链表。

数组VS链表

数组:增删非首尾元素时,往往需要移动数组里面的元素。

链表:增删非首尾元素时,不需要移动元素,仅需要改动next指针。

image.png

const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while (p) {
    console.log(p.val);
    p = p.next;
}

// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;

// 删除
c.next = d;

题目

237. 删除链表中的节点

链表思想(js中使用object实现):无法直接获取被删除节点的上一个节点,那么将被删除的节点转移到下一个节点(即 交换值)。


var deleteNode = function(node) {
    node.val = node.next.val;
    node.next = node.next.next;
};

时间复杂度O(1), 空间复杂度O(1)

206. 反转链表

反转两个节点:将n+1的next指向n

反转多个节点:双指针遍历链表,重复上述操作。

步骤:

  1. 1.双指针一前一后遍历链表
  2. 2.反转双指针(改变next指针的指向
var reverseList = function(head) {
    let p1 = head,
        p2 = null
    while(p1) {
        const temp = p1.next
        p1.next = p2
        p2 = p1
        p1 = temp
    }
    return p2
};

时间复杂度O(n), 空间复杂度O(1)

2. 两数相加

遍历链表

  1. 1.新建一个空链表
  2. 2.遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加
var addTwoNumbers = function(l1, l2) {
    let l3 = new ListNode(0) // 题目中定义的链表结构, 新建空链表
    let p1 = l1, p2 = l2, p3 = l3, carry = 0
    while(p1 || p2) {
        let v1 = p1 ? p1.val : 0
        let v2 = p2 ? p2.val : 0
        let value = v1 + v2 + carry
        carry = Math.floor(value / 10)
        p3.next = new ListNode(value % 10)
        if(p1) p1 = p1.next
        if(p2) p2 = p2.next
        p3 = p3.next
    }
    if(carry) p3.next = new ListNode(carry)
    return l3.next
};

时间复杂度 O(n), 空间复杂度O(n)

83. 删除排序链表中的重复元素

双指针:

  1. 1.因为链表有序, 所以重复元素一定相邻
  2. 2.遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值(改变next指向
  3. 3.遍历结束后,返回原链表头部
var deleteDuplicates = function(head) {
    let p = head;
    while(p && p.next) {
        if(p.val === p.next.val) {
            p.next = p.next.next; // 如果p节点与它的下一个节点的值相等 就删除下一个节点(更改next指向便可以实现删除)
        }else{ // 思考为什么要放在else里面
            p = p.next; // 当遇见新的值时 才移动p指针
        }
    }
    return head;
};

时间复杂度O(n),空间复杂度O(1)

141. 环形链表

快慢指针思想:

  1. 1.用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有环,返回true
  2. 2.遍历结束,还没相逢就返回false p1 = p1.next; // 慢指针一次走一步 p2 = p2.next.next; // 快指针一次走两步 注意保证p2.next有值
var hasCycle = function(head) {
    if(!head) return false
    let p1 = head, p2 = head
    while(p1&&p2&&p2.next) {
        p1 = p1.next
        p2 = p2.next.next // 注意,不能形成null.next的情况,这样会出错,所以需要控制p2.next不为空
        if(p1 === p2) {
            return true
        }
    }
    return false
};

时间复杂度 O(n) ,空间复杂度O(1)

原型链

var foo = {},
    F = function() {}
Object.prototype.a = 'Aa'
Function.prototype.b = 'Bb'
console.log(foo.a)
console.log(foo.b)
console.log(F.a)
console.log(F.b)

// Aa
// undefined
// Aa
// Bb

使用链表指针获取JSON的节点值

const json = {
    a: { b: { c: 1 } },
    d: { e: 2 },
};

const path = ['a', 'b', 'c'];

let p = json;
path.forEach((k) => {
    p=p[k];
});

四、集合

概念

集合是一种无序且唯一的数据结构。ES6中有集合数据结构:Set,集合常用操作:去重、判断某元素是否在集合中、求交集等。

// 去重
const arr = [1, 1, 2, 2];
const arr2 = [...new Set(arr)];

// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);

// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));

题目

349. 两个数组的交集

使用集合的思想: 求交集,无序且唯一, 所以 , 可以先对某一个数组nums1进行去重处理,然后再遍历nums1,求交集筛选出num2也包含的值,都可使用Set数据结构

return [...new Set(nums1)].filter(item => new Set(nums2).has(item));

这种方法的时间复杂度和空间复杂度高,表现不优秀,其实没必要将nums2变成集合, 使用数组原生的方法includes判断可节省时间空间

var intersection = function(nums1, nums2) {
    //    return [...new Set(nums1)].filter(item => new Set(nums2).has(item));
        return [...new Set(nums1)].filter(item => nums2.includes(item));
    };

时间复杂度:O(n²) ,因为嵌套filter函数及includes函数

空间复杂度:O(n)

还可以使用Map字典来解决 ,更优

使用ES6的Set

Set对象常用方法:new、add()、delete()、has()、size 迭代Set:多种迭代方法、Set与Array互转、求交集/差集

let mySet = new Set();
// 添加
mySet.add(1);
mySet.add(5);
mySet.add(5); // 基本数据类型,重复则添加不进去
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });  // 引用数据类型,只是表面属性名/值一样,可以添加

// 判断是否含有某元素
const has = mySet.has(o);

// 删除某元素
mySet.delete(5);

// 迭代
// for...of直接遍历,.keys(), .values(), .entries(), forEach()方法都可以迭代
for(let [key, value] of mySet.entries()) console.log(key, value);

// 转为数组
const myArr = Array.from(mySet);
// 转为Set
const mySet2 = new Set([1,2,3,4]);

// 交集
const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
// 差集
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));

五、字典

概念

与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式存储。ES6中的字典为Map。常用操作为增删改查。

const m = new Map();

// 增
m.set('a', 'aa');
m.set('b', 'bb');

// 删
m.delete('b');
// m.clear();  // 清空

// 改
m.set('a', 'aaa');

// 查
m.get('a')

题目

349. 两个数组的交集

用字典建立一个映射关系,记录num1里面有的值,遍历num2,找出nums1里面也有的值。

  1. 1.新建一个字典,遍历nums1,填充字典。
  2. 2.遍历nums2,遇到字典里的值就选出,加入结果数组,并从字典中删除(防止重复添加)
var intersection = function(nums1, nums2) {
    const map = new Map()
    nums1.forEach(n => {
        map.set(n, true)
    })
    const res = []
    nums2.forEach(n => {
        if(map.has(n)) {
            res.push(n)
            map.delete(n)
        }
    })
    return res
};

时间复杂度:O(m+n) ,空间复杂度:O(m)

20. 有效的括号

利用字典的思想:

  1. 1.新建一个字典
  2. 2.把括号存在字典里,左括号为key,右括号为value
var isValid = function(s) {
    if(s.length % 2 !== 0) return false
    let stack = []
    let map = new Map()
    map.set('(', ')')
    map.set('[', ']')
    map.set('{', '}')
    for (let i = 0; i<s.length; i++) {
        const c = s[i]
        if (map.has(c)) {
            stack.push(s[i])
        } else {
            const top = stack[stack.length - 1]
            if (map.get(t) === c) {
                stack.pop()
            } else {
                return false
            }
        }
    }
    return stack.length === 0
}

时间复杂度:O(n) ,空间复杂度:O(n)

1. 两数之和

把nums想象成相亲者,把target想象成匹配条件,用字典建立一个婚姻介绍所, 存储相亲者的数字和下标

步骤:

  1. 1、新建一个字典作为婚姻介绍所
  2. 2、nums里的值,逐个来介绍所找对象,没有合适的就先登记着,有合适的就牵手成功
var twoSum = function(nums, target) {
    const map = new Map();
    for (let i = 0; i<nums.length; i++) {
        const n = nums[i];
        const n2 = target - n;
        if(map.has(n2)) {
            return [map.get(n2), i] // 获取对象(key)的下标(value)以及自己的下标
        } else{
            map.set(n, i) // 否则就登记自己的信息 值(key)+下标(value)
        }
    }
};

时间复杂度O(n),空间复杂度O(n)

优化: 可以用时间换空间的方式 来优化空间复杂度, 思路是 利用二分查找。

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

思想: 先找出所有的不包含重复字符的子串,找出长度最大的那个字串 返回其长度即可。

步骤:

  1. 1、 用双指针维护一个滑动窗口,用来剪切子串
  2. 2、 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
  3. 3、过程中,记录所有窗口的长度,并返回最大值

注意控制下标是否包含在滑动窗口里的条件。

var lengthOfLongestSubstring = function(s) {
    let l = 0;
    let res = 0; // 用来记录最大窗口长度
    const map = new Map(); // 用来记录是否为重复字符
    for (let r = 0; r < s.length;r += 1) {
        if(map.has(s[r]) && map.get(s[r]) >= l) { // map.get(s[r]) >= l控制右指针在l的右边或等于l,否则就不移动左指针
            l = map.get(s[r]) + 1; // 移动下标
        }
        res = Math.max(res, r-l+1); // 历史的长度与当前ls长度差比较,取较大的一个
        map.set(s[r], r); // 记录右指针指向的字符和下标
    }
    return res
 };

时间复杂度O(n),空间复杂度O(m), m为字符中不重复字符的个数

76. 最小覆盖子串

思路: 先找出所有的包含T的子串,找出长度最小那个字串,返回即可

步骤:

  1. 1、用双指针维护一个滑动窗口,用来枚举所有子串
  2. 2、移动右指针,找到包含T的子串,移动左指针,尽量减少包含T的子串的长度(右指针用以寻找,左指针用以减少子串长度
 var minWindow = function(s, t) {
    let l = 0;
    let r = 0;
    const need = new Map();
    for(let c of t){ // 遍历t
        need.set(c, need.has(c)? need.get(c) + 1 : 1); // 每一个字符(key)及字符的个数(value),如‘ABBC’ ,‘A’=>1,'B'=>2,'C'=>1
    }
    let needType = need.size;// 需要的类型数量
    let res = ''; // 用来存放结果字符串
    while(r < s.length) {
        const d = s[r];
        if (need.has(d)) {
            need.set(d, need.get(d)-1); // 如果字典里有这样的字符,就将它需要的数量-1
            if (need.get(d) === 0) needType -= 1; // 如果字典里此字符的数量为0,则需要类型的数量就减少1
        }
        while (needType === 0){ // 如果已经包含了t,那么就使用l寻找最小的包含t的子串
            const newRes = s.substring(l, r + 1);
            if (!res || newRes.length < res.length) res = newRes;
            const c2 = s[l];
            if (need.has(c2)) {
                need.set(c2, need.get(c2) + 1); // 因为l需要左移,而如果要跳过的是我们需要的字符,则需要将其记录在字典里,将它的需求+1
                if (need.get(c2) === 1) needType += 1; // 如果needType不为0,则会跳出循环,使用r去寻找需求字符
            }
            l += 1;
        }
        r += 1;
    }
    return res;
};

时间复杂度O(m+n),空间复杂度O(m),m是t里面不同字符的个数

六、树

概念

是一种分层数据的抽象模型。前端工作中常见的树包括:Dom树、级联选择、树形控件等。

JS中没有树数据结构,可以使用Object和Array来构建树。

树的常用操作:深度/广度优先遍历、二叉树的先中后序遍历。

深度/广度优先遍历

image.png 深度优先遍历:1. 访问根节点 2. 对根节点的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);
};

dfs(tree);

广度优先遍历:1.新建一个队列,把根节点入队 2. 把队头出队并访问 3. 把队头的children挨个入队 4. 重复第二、三步,直到队列为空

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);
        n.children.forEach(child => {
            q.push(child);
        });
    }
};

bfs(tree);

二叉树的先中后序遍历

二叉树中每个节点最多只能有两个子节点,在JS中通常用Object来模拟二叉树。

先序遍历:1. 访问根节点 2. 对根节点的左子树进行先序遍历 3. 对根节点的右子树进行先序遍历 (根 ->左 -> 右

// bt.js
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,
        },
    },
};

module.exports = bt;

// preorder.js
const bt = require('./bt');

const preorder = (root) => {
    if (!root) { return; }
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
};

// 非递归版:首先访问根节点 再分别往栈中放入左右子树
// const preorder = (root) => {
//     if (!root) { return; }
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         console.log(n.val);
//         if (n.right) stack.push(n.right);
//         if (n.left) stack.push(n.left);
//     }
// };

preorder(bt);

中序遍历:1. 对根节点的左子树进行中序遍历 2. 访问根节点 3. 对根节点的右子树进行中序遍历 (左 ->根 -> 右

const bt = require('./bt');

// 递归版
const inorder = (root) => {
    if (!root) { return; }
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
};

// 非递归版:首先把所有左子树都放入栈
// 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);

后序遍历:1. 对根节点的左子树进行后序遍历 2. 对根节点的右子树进行后序遍历 3. 访问根节点(左 -> 右 -> 根

const bt = require('./bt');

const postorder = (root) => {
    if (!root) { return; }
    postorder(root.left);
    postorder(root.right);
    console.log(root.val);
};

// 非递归版:改造先序遍历,利用栈的先进后出特性
// const postorder = (root) => {
//     if (!root) { return; }
//     const outputStack = [];
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         outputStack.push(n);
//         if (n.left) stack.push(n.left);
//         if (n.right) stack.push(n.right);
//     }
//     while(outputStack.length){
//         const n = outputStack.pop();
//         console.log(n.val);
//     }
// };

postorder(bt);

题目

104. 二叉树的最大深度

思路:

image.png

步骤:

  1. 1、 新建一个变量,记录最大深度
  2. 2、 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
  3. 3、遍历结束返回最大深度这个变量
var maxDepth = function(root) {
    let res = 0; //记录深度
    const dfs = (n, level) => {
        if (!n) return;
       //  console.log(n.val, level);
        if (!n.left && !n.right) {
            res = Math.max(res, level); // 在叶子节点处才刷新深度 减少不必要的刷新
        }
        dfs(n.left, level+1);
        dfs(n.right, level+1);
    };
    dfs(root, 1);
    return res;
};

时间复杂度O(n),空间复杂度O(logn)

111. 二叉树的最小深度

思路: 求最小深度,考虑使用广度优先遍历。在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级。

Step1:广度优先遍历整棵树,并记录每个节点的层级

Step2:遇到叶子节点,返回节点层级 ,停止遍历。

var minDepth = function(root) {
    // 遇到叶子节点便返回 后续不必再遍历
    if(!root) {return 0;}
    const queen = [[root,1]];
    while (queen.length){
        const [n, level] = queen.shift();
        if(!n.left && !n.right){
            return level;
        }
        if (n.left) queen.push([n.left, level+1]);
        if (n.right) queen.push([n.right, level+1]);
    }
};

时间复杂度O(n) , 空间复杂度O(n)

102. 二叉树的层序遍历

层序即广度优先遍历,区别是,层序遍历时需要记录当前节点所在的层级,方便将其添加到不同的数组中。(广度不知道所处层级)

Step1:广度优先遍历二叉树

Step2:遍历过程中,记录每个节点的层级,并将其添加到不同的数组中

// 方式一
var levelOrder = function(root) {
    if(!root) return [] 
    const q = [[root, 0]]
    let res = []
    while (q.length) {
        const [n, l] = q.shift()
        // console.log(n.val, l)
        res[l] ? res[l].push(n.val) : res.push([n.val])
        if(n.left) q.push([n.left, l + 1])
        if(n.right) q.push([n.right, l + 1])
    }
    return res
};

// 方式二
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)
            if (n.left) q.push(n.left);
            if (n.right) q.push(n.right);
        }
    }
    return res
};

O(n)/O(n)

94. 二叉树的中序遍历

// 递归方式
var inorderTraversal = function(root) {
    let res = []
    const inorder = (root) => {
        if (!root) { return; }
        inorder(root.left);
        res.push(root.val)
        inorder(root.right);
    };
     inorder(root)
     return res
 };
 
 // 迭代方式
  var inorderTraversal = function(root) {
    let res = []
    let stack = []
    let p = root
    while(stack.length || p) {
        while(p) {
            stack.push(p)
            p = p.left
        }
        const n = stack.pop()
        res.push(n.val)
        p = n.right
    }
    return res
 };

时间复杂度O(n), 空间复杂度O(n)

112. 路径总和

思路:

image.png

 var hasPathSum = function(root, targetSum) {
    if (!root) return false
    let res = 0
    const dfs = (root, targetSum) => {
        if (!root) return
        if(!root.left && !root.right) {
            if(targetSum === root.val) {
                res = 1
            }
        }
        dfs(root.left, targetSum - root.val)
        dfs(root.right, targetSum - root.val)
    }
    dfs(root, targetSum)
    return res
};

时间复杂度:O(n), 空间复杂度最坏O(n) ,平均O(logn) ,使用递归会调用函数调用堆栈,产生空间消耗

七、图

概念

是网络结构的抽象模型,是一组由连接的节点。 图可以表示任何二元关系,比如:道路、航班...

JS中没有图,可以利用Object和Array来构建图。图的表示法:邻接矩阵、邻接表、关联矩阵...

邻接矩阵: image.png

邻接表:

image.png

图的常用操作:深度优先遍历、广度优先遍历

深度优先遍历

image.png

// graph.js
const graph = {
    0: [1, 2],
    1: [2],
    2: [0, 3],
    3: [3]
};

module.exports = graph;

//bfs.js
const graph = require('./graph');

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);

广度优先遍历

image.png

const graph = require('./graph');

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(c);
        }
    });
}

题目

65. 有效数字

image.png

步骤: image.png

var isNumber = function(s) {
    // 邻接表,表示状态转换图
    const graph = {
        0: {'blank': 0, 'sign': 1, '.': 2, 'digit':6},
        1: {'digit': 6, '.': 2},
        2: {'digit': 3},
        3: {'digit': 3, 'e': 4, 'E': 4},
        4: {'digit': 5, 'sign': 7},
        5: {'digit': 5},
        6: {'digit': 6, '.': 3, 'e': 4, 'E': 4},
        7: {'digit': 5}
    }

    let state = 0; // 记录状态
    for(c of s.trim()) { // 去掉字符串前后空格
        if(c >= '0' && c <= '9') {
            c = 'digit'
        } else if (c === ' ') {
            c = 'blank'
        } else if (c === '+' || c=== '-') {
            c = 'sign'
        }
        state = graph[state][c]
        if (state === undefined){
            return false
        }
    }
    if (state === 3 || state === 5 || state === 6) {
        return true
    }
    return false
};

O(n)/O(1)

417. 太平洋大西洋水流问题

思路: 把矩阵想象成图,从海岸线逆流而上遍历图,所到之处就是可以留到某个大洋的坐标。

image.png

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))  // 构建矩阵并填充false
    const flow2 = Array.from({length:m}, () => new Array(n).fill(false))

    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 += 1){
        dfs(r, 0, flow1)
        dfs(r, n-1, flow2)
    }
    for (let c=0; c<=n; c += 1) {
        dfs(0, c, flow1)
        dfs(m-1, c, flow2)
    }

    // 收集能流到两个大洋的坐标
    const res = []
    for (let r = 0; r< m; r += 1) {
        for (let c=0; c<n; c += 1) {
            if (flow1[r][c] && flow2[r][c]) {
                res.push([r,c])
            }
        }
    }
    return res
};

时间复杂度O(m×n) ,空间复杂度O(m×n)

133. 克隆图

思路: 拷贝所有节点;拷贝所有的边。

image.png

// 深度优先遍历
var cloneGraph = function(node) {
    if (!node) return
    const visited = new Map()
    const dfs = (n) => {
        // console.log(n.val)
        const nCopy = new Node(n.val) // 克隆节点
        visited.set(n, nCopy);  // 对原始节点和拷贝节点做映射
        (n.neighbors || []).forEach(ne => {
            if(!visited.has(ne)) {
                dfs(ne)
            }
            nCopy.neighbors.push(visited.get(ne))  // 拷贝边
        })
    }
    dfs(node)
    return visited.get(node)
};


// 广度优先遍历
var cloneGraph = function(node) {
    if (!node) return
    const visited = new Map()
    visited.set(node, new Node(node.val)) // 拷贝起始节点
    const q = [node]
    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)

八、堆

概念

是一种特殊的完全二叉树,所有的节点都大于等于(最大堆)或者小于等于(最小堆)它的子节点。

image.png

JS中使用数组表示堆。左侧子节点的位置是2 * index + 1;右侧子节点的位置是2 * index + 2;父节点位置是 (index - 1) / 2

image.png

堆的应用: 堆能高效、快速地找出最大值和最小值,时间复杂度:O(1); 找出第k个最大(小)元素。

image.png

JS实现:最小堆类

插入操作image.png

删除堆顶image.png

class MinHeap {
    constructor() {
        this.heap = []
    }
    getParentIndex(i) { // 获取父节点的位置
        // return Math.floor((i-1)/2)
        return (i-1) >> 1 // 位运算,二进制往右移一位
    }
    getLeftIndex(i) { //获取左侧子节点的位置
        return i*2 + 1
    }
    getRightIndex(i) { //获取右侧子节点的位置
        return i*2 + 2
    }
    swap (p, c) {
        const temp = this.heap[p]
        this.heap[p] = this.heap[c]
        this.heap[c] = temp
    }
    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.shifDown(0)
    }
}

获取堆顶和堆的大小

image.png

    peek() {
        return this.heap[0]
    }
    size() {
        return this.heap.length
    }

题目

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

思路: 看到“第k个最大元素”,考虑选择使用最小堆

image.png

// 方法一:利用sort()
var findKthLargest = function(nums, k) {
    function func(a,b) {
        return a-b
    }
    var arr = nums.sort(func).reverse()
    // console.log(arr)
    return arr[k-1]
};

// 方法二:最小堆实现
var findKthLargest = function(nums, k) {
    const h = new MinHeap()
    nums.forEach(n => {
        h.insert(n)
        if(h.size() > k) {
            h.pop()
        }
    })
    return h.peak()
};

O(n * logk) / O(k)

347. 前 K 个高频元素

// 方法一: 使用map
var topKFrequent = function(nums, k) {
    const map = new Map()
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1)
    })
    const list = Array.from(map).sort((a, b) => b[1] - a[1]) // 对频率降序排列
    return list.slice(0,k).map(n => n[0])
};
// 时间复杂度O(nlogn)
// 方法二:最小堆
class MinHeap {
    constructor() {
        this.heap = []
    }
    getParentIndex(i) { // 获取父节点的位置
        // return Math.floor((i-1)/2)
        return (i-1) >> 1 // 位运算,二进制往右移一位
    }
    getLeftIndex(i) { //获取左侧子节点的位置
        return i*2 + 1
    }
    getRightIndex(i) { //获取右侧子节点的位置
        return i*2 + 2
    }
    swap (p, c) {
        const temp = this.heap[p]
        this.heap[p] = this.heap[c]
        this.heap[c] = temp
    }
    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(parentIndex, index)
            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
    }
}

var topKFrequent = function(nums, k) {
    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(a => a.key)
};
// 时间复杂度O(nlogk) , 空间复杂度O(n)

23. 合并K个升序链表

思路: 新链表的下一个节点一定是k个链表头中的最小节点; 考虑选择使用最小堆

image.png

class MinHeap {
    constructor() {
        this.heap = []
    }
    getParentIndex(i) { // 获取父节点的位置
        // return Math.floor((i-1)/2)
        return (i-1) >> 1 // 位运算,二进制往右移一位
    }
    getLeftIndex(i) { //获取左侧子节点的位置
        return i*2 + 1
    }
    getRightIndex(i) { //获取右侧子节点的位置
        return i*2 + 2
    }
    swap (p, c) {
        const temp = this.heap[p]
        this.heap[p] = this.heap[c]
        this.heap[c] = temp
    }
    shiftUp(index) { // 上移操作
        if(index == 0) return
        const parentIndex = this.getParentIndex(index)
        if(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
    }
}
var mergeKLists = function(lists) {
    const res = new ListNode(0)
    let p = res
    const h = new MinHeap()
    lists.forEach(l => {
        if(l) h.insert(l)
    })
    // console.log(h)
    while(h.size()) {
        const n = h.pop()
        p.next = n
        p = p.next
        if (n.next) h.insert(n.next)
    }
    return res.next
};

时间复杂度O(nlogk) ,空间复杂度O(k)

九、排序与搜索

概念

排序:把某个乱序的数组变成升序或者降序的数组。 JS中的排序:数组的sort方法。

排序算法包括:冒泡排序、选择排序、插入排序、归并排序、快速排序等。

搜索: 找出数组中某个元素的下标。 JS中的搜索:数组的indexOf方法。

搜索算法包括:顺序搜索、二分搜索...

JS实现:冒泡排序

思路: image.png

Array.prototype.bubbleSort = function() {
    for(let i = 0; i< this.length - 1; i += 1) {
        for(let j = 0; j< this.length - 1 - i; j += 1) {
            if(this[j] > this[j+1]) {
                const temp = this[j]
                this[j] = this[j + 1]
                this[j + 1] = temp
            }
        }
    }
    // console.log(this)
}
const arr = [5,4,3,2,1]
arr.bubbleSort()

两个嵌套循环,时间复杂度O(n²)

JS实现:选择排序

思路: image.png

Array.prototype.selectSort = function() {
    for(let i = 0; i< this.length - 1; i += 1) {
        let indexMin = i
        for(let j = i; j< this.length; j += 1) {
            if(this[j] < this[indexMin]) {
                indexMin = j   
            }
        }
        if (indexMin !== i) {
            const temp = this[i]
            this[i] = this[indexMin]
            this[indexMin] = temp
        }
    }
    // console.log(this)
}
const arr = [5,4,3,2,1]
arr.selectSort()

两个嵌套循环,时间复杂度O(n²)

JS实现:插入排序

思路: 从第二个数开始往前比; 比它大就往后排; 以此类推进行到最后一个数。

Array.prototype.insertSort = function() {
    for(let i = 0; i < this.length; i += 1) {
        const temp = this[i]
        let j = i
        while(j > 0) {
            if (this[j - 1] > temp) {
                this[j] = this[j-1]
            } else {
                break
            }
            j -= 1
        }
        this[j] = temp
    }
    // console.log(this)
}
const arr = [5,4,3,2,1]
arr.insertSort()

两个嵌套循环,时间复杂度O(n²)

JS实现:归并排序

思路: image.png

image.png

Array.prototype.mergeSort = function() {
    const rec = (arr) => {
        if (arr.length === 1) return arr
        // "分"
        const mid = Math.floor(arr.length / 2)
        const left = arr.slice(0, 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 if(orderLeft.length) {
                res.push(orderLeft.shift())
            } else if(orderRight.length) {
                res.push(orderRight.shift())
            }
        }
        return res
    }
    const res = rec(this)
    // 拷贝回原数组
    res.forEach((n, i) => { this[i] = n})
}
const arr = [5,4,3,2,1]
arr.mergeSort()

时间复杂度O(nlogn)

JS实现:快速排序

思路: image.png

Array.prototype.quickSort = function () {
    const rec = (arr) => {
        if (arr.length === 1) return arr
        const left = []
        const right = []
        const mid = arr[0]
        for (let i = 1; i < arr.length; i += 1) {
            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
    })
    // console.log(this)
}
const arr = [2, 4, 5, 3, 1]
arr.quickSort()

时间复杂度O(nlogn)

JS实现:顺序搜索

思路: image.png

Array.prototype.sequentialSearch = function (item) {
    for (let i = 0; i < this.length; i += 1) {
        if (this[i] === item) {
            return i
        }
    }
    return -1
}
const res = [1, 2, 3, 4, 5].sequentialSearch(3)

遍历数组是一个循环操作,时间复杂度O(n)

JS实现:二分搜索

思路: image.png

Array.prototype.binarySearch = function (item) {
    let low = 0
    let high = this.length - 1
    while (low <= high) {
        const mid = Math.floor((low + high) / 2)
        const element = this[mid]
        if(element<item) {
            low = mid +1
        } else if(element>item) {
            high = mid-1
        } else {
            return mid
        }
    }
    return -1
}
const res = [1, 2, 3, 4, 5].binarySearch(3)

在数组无序的情况下,需要对数组进行排序。

每一次比较都使搜索范围缩小一半,时间复杂度O(logn),效率高于顺序搜搜。

题目

21. 合并两个有序链表

解题思路: 与归并排序中的合并两个有序数组很相似,将数组替换成链表即可。

步骤: image.png

var mergeTwoLists = function (list1, list2) {
    const res = new ListNode(0)
    let p = res,
        p1 = list1,
        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
    }
    if (p1) {
        p.next = p1
    }
    if (p2) {
        p.next = p2
    }
    return res.next
};

时间复杂度O(n), 空间复杂度O(1)

374. 猜数字大小

解题思路:利用二分搜索;调用guess函数,来判断中间元素是否是目标值。

步骤: image.png

var guessNumber = function (n) {
    let low = 0
    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) {
            high = mid - 1
        } else if (res === 1) {
            low = mid + 1
        }
    }
};

时间复杂度O(logn), 空间复杂度O(1)

十、分而治之

概念

分而治之是算法设计中的一种方法,并不是具体的数据结构或某个算法,它是一种思想。 它将一个问题成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。

场景一: 归并排序

image.png

场景二: 快速排序

image.png

题目

374. 猜数字大小

步骤: image.png

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(1, mid - 1)
        } else if (res === 1) {
            return rec(mid + 1, high)
        }
    }
    return rec(1, n)
};

时间复杂度O(logn), 空间复杂度O(logn)

226. 翻转二叉树

思路:先翻转左右子树,再将子树换个位置;符合“分、解、合”特性,考虑分而治之。

步骤: image.png

var invertTree = function(root) {
    if(!root) return null
    return {
        val: root.val,
        left: invertTree(root.right),
        right: invertTree(root.left)
    }
};

时间复杂度O(n), 空间复杂度O(n)

100. 相同的树

image.png

var isSameTree = function(p, q) {
    if(!p && !q) return true
    if((!p && q) || (p && !q)) return false
    return p.val === q.val ? (isSameTree(p.left, q.left) && isSameTree(p.right, q.right)) : false
};

时间复杂度O(n), 空间复杂度O(n)

101. 对称二叉树

步骤: image.png

var isSymmetric = function(root) {
    if(!root) return true
    const rec = (l, r) => {
        if(!l && !r) return true
        if(l && r && l.val === r.val &&
            rec(l.left, r.right) &&
            rec(l.right, r.left)){
                return true
            }
        return false
    }
    return rec(root.left, root.right)
};

时间复杂度O(n), 空间复杂度O(n)

十一、动态规划

概念

动态规划是算法设计中的一种方法,它将一个问题分解为相互重叠的子问题(分而治之的子问题是相互独立的),通过反复求解子问题,来解决原来的问题。

典型例子:斐波那契数列问题 image.png

题目

70. 爬楼梯

思路: image.png

var climbStairs = function (n) {
    if (n < 2) return 1
    const dp = [1,1]
    for (let i = 2; i <= n; i += 1) {
        dp[i] = dp[i-1] + dp[i-2]
    }
    return dp[n]
};

时间复杂度O(n), 空间复杂度O(n),可定义两个变量代替dp数组,优化空间复杂度为O(1)

198. 打家劫舍

思路: image.png

步骤: image.png

var rob = function (nums) {
    if(!nums.length) return 0
    const dp = [0, nums[0]] // 定义dp数组,记录每一个房间能打劫的最大金额
    for (let i = 2; i <= nums.length; i += 1) {
        dp[i] = Math.max(dp[i-2]+nums[i-1], dp[i-1]) // 注意nums的下标
    }
    return dp[nums.length]
};

时间复杂度O(n), 空间复杂度O(n),可定义两个变量代替dp数组,优化空间复杂度为O(1)

213. 打家劫舍 II

var rob = function (nums) {
    if(!nums.length) return 0
    if(nums.length === 1) return nums[0]
    const dp0 = [0, nums[0]]
    const dp1 = [0, nums[1]]
    for (let i = 2; i < nums.length; i += 1) {
        dp0[i] = Math.max(dp0[i-2]+nums[i-1], dp0[i-1]) 
    }
    for (let i = 2; i < nums.length; i += 1) {
        dp1[i] = Math.max(dp1[i-2]+nums[i], dp1[i-1]) 
    }
    return Math.max(dp0[nums.length - 1], dp1[nums.length - 1])
};

十二、贪心算法

概念

贪心算法是算法设计中的一种方法(算法设计思想),它期盼通过每个阶段按的局部最优选择,从而达到全局的最优。结果并不一定是最优的。

题目

455. 分发饼干

思路: 局部最优: 既能满足孩子,还能消耗最少;先将“较小的饼干”分给“胃口”最小的孩子。

步骤: image.png

var findContentChildren = function(g, s) {
    const sortFunc = (a,b) => {
        return a-b
    }
    g.sort(sortFunc) // 按照自定义的方式排序
    s.sort(sortFunc)
    let i = 0
    for(let n = 0; n < s.length; n++) {
        if(s[n] >= g[i]) {
            i += 1
        }
    }
    return i
};

时间复杂度O(nlogn), 空间复杂度O(1)

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

思路: image.png

步骤: image.png

var maxProfit = function(prices) {
    let res = 0
    for(let n = 1; n < prices.length; n++) {
        if(prices[n] > prices[n-1]) {
            res += prices[n] - prices[n-1]
        }
    }
    return res
};

时间复杂度O(n), 空间复杂度O(1)

十三、回溯算法

概念

回溯算法也是算法设计中的一种方法,它是一种渐进式寻找并构建问题解决方式的策略,它会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到解决问题。

什么问题适合用回溯算法解决: 有很多路,这些路里,有死路也有出路,通常需要递归来模拟所有的出路。

如全排列问题: image.png

题目

46. 全排列

思路:要求1、所有排列情况 2、没有重复元素;有出路有死路;考虑使用回溯算法。

步骤: image.png 到达终点的情况即path长度为数组长度(图示中为3)

var permute = function(nums) {
    const res = []
    const backtrack = (path) => {
        if(path.length === nums.length) {
            res.push(path)
            return
        }
        nums.forEach(element => {
            if(path.includes(element)) return  // 死路回溯
            backtrack(path.concat(element))  // 加入路径
        });
    }
    backtrack([])
    return res
};

时间复杂度O(n!), 空间复杂度O(n)

78. 子集

步骤: image.png 达到终点的情况:从0遍历到数组长度,每一种情况下都进行一遍回溯

var subsets = function (nums) {
    const res = []
    const backtrack = (path, l, start) => {
        if (path.length === l) {
            res.push(path)
            return
        }
        for (let i = start; i < nums.length; i++) {
            backtrack(path.concat(nums[i]), l, i + 1) // 加入路径
        }
        // if(path.includes(element)) return  // 死路回溯
    }
    for (let i = 0; i <= nums.length; i += 1) {
        backtrack([], i, 0) // 将路径、长度、起始下标传进去
    }
    return res
};

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