一、简介

  • 数据结构:计算机存储、组织数据的方式,就像锅碗瓢盆
  • 算法:一系列解决问题的清晰指令,就像食谱
  • 程序 = 数据结构 + 算法


数据结构**

  • 栈、队列、链表
  • 集合、字典
  • 树、堆、图

算法

  • 链表:遍历链表、删除链表节点
  • 树、图:深度/广度优先遍历
  • 数组:冒泡/选择/插入/归并/快速排序、顺序/二分搜索

算法设计思想

  • 分而治之
  • 动态规划
  • 贪心算法
  • 回溯算法

算法解题四步分析法

  1. 模拟:模拟题目的运行
  2. 规律:尝试总结出题目的一般规律和特点
  3. 匹配:找到符合这些特点的数据结构与算法
  4. 边界:考虑特殊情况

二、时间复杂度与空间复杂度

1、时间复杂度

  • 一个函数,用大O表示,比如O(1)、O(n)、O(logN)
  • 定性描述该算法的运行时间(执行次数)

2、空间复杂度

  • 一个函数,用大O表示,比如O(1)、O(n)、O(n ^ 2)
  • 算法在运行过程中临时占用存储空间大小的量度(占用的内存单元)

三、栈

一个后进先出的数据结构

1、使用场景例子

  • 十进制转二进制
  • 有效的括号(LeetCode-20)
  • js函数调用堆栈

2、常用操作方式

  • push、pop、stack[stack.length - 1]

四、队列

一个先进先出的数据结构

1、使用场景例子

  • 食堂排队打饭
  • JS异步中的任务队列
  • 计算最近请求次数(LeetCode-933)

事件循环与任务队列.png

2、常用操作方式

  • push、shift、queue[0]

五、链表

  • 多个元素组成的列表
  • 元素存储不连续,用 next 指针连在一起
  • 在 js 中使用 Object 实现链表

1、链表和数组的区别

  • 数组:增删非首尾元素时往往需要移动元素
  • 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可

2、链表创建方法及操作

  1. const a = { val: 'a' };
  2. const b = { val: 'b' };
  3. const c = { val: 'c' };
  4. const d = { val: 'd' };
  5. a.next = b;
  6. b.next = c;
  7. c.next = d;
  8. // 遍历链表
  9. let p = a;
  10. while (p) {
  11. console.log(p.val);
  12. p = p.next;
  13. }
  14. // 插入
  15. const e = { val: 'e' };
  16. c.next = e;
  17. e.next = d;
  18. // 删除
  19. c.next = d;

3、算法场景

  • 删除链表中的节点(LeetCode-237)
  • 反转链表(LeetCode-206)
  • 两数相加(LeetCode-2)
  • 删除排序链表中的重复元素(LeetCode-83)
  • 环形链表(LeetCode-141)

4、JS 中的链表 — 原型链

  • 原型链的本质是链表
  • 原型链上的节点是各种原型对象,比如 Function.prototype、Object.prototype
  • 原型链通过 __proto__ 属性链接各种原型对象

4.1 常见原型链

  • obj -> Object.prototype -> null
  • func -> Function.prototype -> Object.prototype -> null
  • arr -> Array.prototype -> Object.prototype -> null

4.2 原型链知识点

  • 如果 A 沿着原型链能找到 B.prototype,那么 A instanceof B 为 true
  1. // 手动实现 instanceof
  2. const instanceof = (A, B) => {
  3. let p = A
  4. while (p) {
  5. if (p === B.prototype) {
  6. return true
  7. }
  8. p = p.__proto__
  9. }
  10. return false
  11. }
  • 如果在 A 对象上没有找到 x 属性,那么会沿着原型链找 x 属性(如 toString)

5、JS 中的链表 — 使用链表指针获取 JSON 的节点值

  1. const json = {
  2. a: { b: { c: 1 } },
  3. d: { e: 2 },
  4. }
  5. const path = ['a', 'b', 'c']
  6. let p = json
  7. path.forEach(k => {
  8. p = p[k]
  9. })

六、集合

  • 一种无序且唯一的数据结构
  • ES6 中有集合,名为 Set
  • 集合的常用操作:去重、判断某元素是否在集合中、求交集
  1. // 去重
  2. const arr = [1, 1, 2, 2]
  3. const arr2 = [...new Set(arr)]
  4. // 判断元素是否在集合中
  5. const set = new Set(arr)
  6. const has = set.has(1)
  7. // 求交集
  8. const set2 = new Set([2, 3])
  9. const set3 = new Set([...set].filter(v => set2.has(v)))

1、算法场景

  • 两个数组的交集(LeetCode-349)

2、ES6 的 Set

  • 使用 Set 对象:new、add、delete、has、size
  1. let mySet = new Set();
  2. mySet.add(1);
  3. mySet.add(5);
  4. mySet.add(5);
  5. mySet.add('some text');
  6. let o = { a: 1, b: 2 };
  7. mySet.add(o);
  8. mySet.add({ a: 1, b: 2 });
  9. const has = mySet.has(o);
  10. mySet.delete(5);
  11. console.log(mySet.size)
  • 迭代 Set:多种迭代方法、Set 与 Array 互转、求交集/差集
  1. // 三种迭代方法,其中第一、第二种等价
  2. for(let item of mySet.keys()) console.log(item)
  3. for(let item of mySet.values()) console.log(item)
  4. for(let [key, value] of mySet.entries()) console.log(key, value);
  5. mySet.forEach((value, key, current) => {})
  6. // Set 与 Array 互转
  7. const myArr = Array.from(mySet);
  8. const myArr2 = [...mySet]
  9. const mySet2 = new Set([1,2,3,4]);
  10. // 求交集/差集
  11. const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
  12. const difference = new Set([...mySet].filter(x => !mySet2.has(x)));

七、字典

  • 与集合类似,也是存储唯一值的数据结构,但它是以键值对的形式来存储
  • ES6 中有字典,名为 Map
  • 常用操作:键值对的增删改查
  1. const m = new Map();
  2. // 增
  3. m.set('a', 'aa');
  4. m.set('b', 'bb');
  5. // 删
  6. m.delete('b');
  7. // m.clear();
  8. // 改
  9. m.set('a', 'aaa');
  10. // 查
  11. m.get('a')
  12. // 遍历和与数组互转,方法同集合(set)

1、算法场景

  • 两个数组的交集(LeetCode-349)
  • 有效的括号(LeetCode-20)
  • 两数之和(LeetCode-1)
  • 无重复字符的最长子串(LeetCode-3)
  • 最小覆盖子串(LeetCode-76)

八、树

  • 一种分层数据的抽象模型
  • 前端工作中常见的树包括:DOM树、级联选择、树形控件
  • JS中没有树,但是可以用 Object 和 Array 构建树
  • 常用操作:深度/广度优先遍历、先中后序遍历

1、深度/广度优先遍历

  • 深度优先遍历:尽可能深的搜索树的分支
  • 广度优先遍历:先访问离根节点最近的节点

深度、广度优先遍历.png

1.1 深度优先遍历口诀

  1. 访问根节点
  2. 对根节点的 children 挨个进行深度优先遍历
  1. // dfs
  2. const tree = {
  3. val: 'a',
  4. children: [
  5. {
  6. val: 'b',
  7. children: [
  8. {
  9. val: 'd',
  10. children: [],
  11. },
  12. {
  13. val: 'e',
  14. children: [],
  15. }
  16. ],
  17. },
  18. {
  19. val: 'c',
  20. children: [
  21. {
  22. val: 'f',
  23. children: [],
  24. },
  25. {
  26. val: 'g',
  27. children: [],
  28. }
  29. ],
  30. }
  31. ],
  32. };
  33. const dfs = (root) => {
  34. console.log(root.val);
  35. root.children.forEach(dfs);
  36. };
  37. dfs(tree);

1.2 广度优先遍历口诀

  1. 新建一个队列,把根节点入队
  2. 把队头出队并访问
  3. 把队头的 children 挨个入队
  4. 重复第二、三步,直到队列为空
  1. // bfs
  2. const tree = {
  3. val: 'a',
  4. children: [
  5. {
  6. val: 'b',
  7. children: [
  8. {
  9. val: 'd',
  10. children: [],
  11. },
  12. {
  13. val: 'e',
  14. children: [],
  15. }
  16. ],
  17. },
  18. {
  19. val: 'c',
  20. children: [
  21. {
  22. val: 'f',
  23. children: [],
  24. },
  25. {
  26. val: 'g',
  27. children: [],
  28. }
  29. ],
  30. }
  31. ],
  32. };
  33. const bfs = (root) => {
  34. const q = [root];
  35. while (q.length > 0) {
  36. const n = q.shift();
  37. console.log(n.val);
  38. n.children.forEach(child => {
  39. q.push(child);
  40. });
  41. }
  42. };
  43. bfs(tree);

2、二叉树先中后序遍历

  • 树中每个节点最多只能有两个子节点
  • 在 JS 中通常用 Object 来模拟二叉树
  1. const binaryTree = {
  2. val: 1,
  3. left: {
  4. val: 2,
  5. left: null,
  6. right: null
  7. },
  8. right: {
  9. val: 3,
  10. left: null,
  11. right: null
  12. }
  13. }

2.1 先序遍历算法口诀

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

先序遍历.png

  1. // preorder
  2. const bt = {
  3. val: 1,
  4. left: {
  5. val: 2,
  6. left: {
  7. val: 4,
  8. left: null,
  9. right: null,
  10. },
  11. right: {
  12. val: 5,
  13. left: null,
  14. right: null,
  15. },
  16. },
  17. right: {
  18. val: 3,
  19. left: {
  20. val: 6,
  21. left: null,
  22. right: null,
  23. },
  24. right: {
  25. val: 7,
  26. left: null,
  27. right: null,
  28. },
  29. },
  30. };
  31. // 递归版
  32. const preorder = (root) => {
  33. if (!root) { return; }
  34. console.log(root.val);
  35. preorder(root.left);
  36. preorder(root.right);
  37. };
  38. // 非递归版
  39. const preorder = (root) => {
  40. if (!root) { return; }
  41. const stack = [root]
  42. while (stack.length) {
  43. const n = stack.pop()
  44. console.log(n.val)
  45. if (n.right) stack.push(n.right)
  46. if (n.left) stack.push(n.left)
  47. }
  48. };
  49. preorder(bt);

2.2 中序遍历算法口诀

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

中序遍历.png

  1. // inorder
  2. // 递归版
  3. const inorder = (root) => {
  4. if (!root) { return; }
  5. inorder(root.left);
  6. console.log(root.val);
  7. inorder(root.right);
  8. };
  9. // 非递归版
  10. const inorder = (root) => {
  11. if (!root) { return; }
  12. const stack = []
  13. let p = root
  14. while (stack.length || p) {
  15. while (p) {
  16. stack.push(p)
  17. p = p.left
  18. }
  19. const n = stack.pop()
  20. console.log(n.val)
  21. p = n.right
  22. }
  23. };
  24. inorder(bt);

2.3 后序遍历算法口诀

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

后序遍历.png

  1. // postorder
  2. // 递归版
  3. const inorder = (root) => {
  4. if (!root) { return; }
  5. inorder(root.left);
  6. inorder(root.right);
  7. console.log(root.val);
  8. };
  9. // 非递归版
  10. // 逆序思考,类似先序遍历(根右左)
  11. const postorder = (root) => {
  12. if (!root) { return; }
  13. const stack = [root]
  14. const outputStack = []
  15. while (stack.length) {
  16. const n = stack.pop()
  17. outputStack.push(n)
  18. if (n.left) stack.push(n.left)
  19. if (n.right) stack.push(n.right)
  20. }
  21. while (outputStack.length) {
  22. const n = outputStack.pop()
  23. console.log(n.val)
  24. }
  25. };
  26. postorder(bt);

3、算法场景

  • 二叉树的最大深度(LeetCode-104)
  • 二叉树的最小深度(LeetCode-111)
  • 二叉树的层序遍历(LeetCode-102)
  • 二叉树的中序遍历(LeetCode-94)
  • 路径总和(LeetCode-112)

4、前端与树

  • 遍历 JSON 的所有节点值
  1. const json = {
  2. a: { b: { c: 1 } },
  3. d: [1, 2],
  4. };
  5. const dfs = (n, path) => {
  6. console.log(n, path);
  7. Object.keys(n).forEach(k => {
  8. dfs(n[k], path.concat(k));
  9. });
  10. };
  11. dfs(json, []);
  • 渲染 Antd 的树组件

九、图

  • 图是网络结构的抽象,是一组由边连接的节点
  • 图可以表示任何二元关系,比如道路、航班等
  • JS 中没有图,但是可以用 Object 和 Array 构建图
  • 图的表示法:邻接矩阵、邻接表、关联矩阵等
  • 常用操作:深度/广度优先遍历

邻接矩阵.png
邻接表.png

1、图的深度/广度优先遍历

  • 深度优先遍历:尽可能深的搜索图的分支
  • 广度优先遍历:先访问离根节点最近的节点

1.1 深度优先遍历口诀

  1. 访问根节点
  2. 对根节点的没访问过的相邻节点挨个进行深度优先遍历

图-深度优先遍历.png

  1. const graph = {
  2. 0: [1, 2],
  3. 1: [2],
  4. 2: [0, 3],
  5. 3: [3]
  6. };
  7. const visited = new Set()
  8. const dfs = (n) => {
  9. console.log(n)
  10. visited.add(n)
  11. graph[n].forEach(c => {
  12. if (!visited.has(c)) {
  13. dfs(c)
  14. }
  15. })
  16. }

1.2 广度优先遍历口诀

  1. 新增一个队列,把根节点入队
  2. 把队头出队并访问
  3. 把队头的没访问过的相邻节点入队
  4. 重复第二、三步直到队列为空

图-广度优先遍历.png

  1. const graph = {
  2. 0: [1, 2],
  3. 1: [2],
  4. 2: [0, 3],
  5. 3: [3]
  6. };
  7. const bfs = (root) => {
  8. const visited = new Set()
  9. visited.add(root)
  10. const q = [root]
  11. while(q.length) {
  12. const n = q.shift()
  13. console.log(n)
  14. graph[n].forEach(c => {
  15. if (!visited.has(c)) {
  16. q.push(c)
  17. visited.add(c)
  18. }
  19. })
  20. }
  21. }

2、算法场景

  • 有效数字(LeetCode-65)
  • 太平洋大西洋水流问题(LeetCode-417)
  • 克隆图(LeetCode-133)

十、堆

堆.png

  • 堆是一种特殊的完全二叉树(节点完全填满,只能剩余右侧的节点)
  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点

1、JS 中的堆

js中的堆.png

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

2、堆的应用

  • 堆能高效、快速地找出最大/最小值,时间复杂度是 O(1)
  • 找出第 k 个最大(小)元素

2.1 找出第 k 个最大(小)元素

第K个最大元素.png

2.2 最小堆类实现

  • 在类中,声明一个数组,用来装元素
  • 主要方法:插入、删除堆顶、获取堆顶、获取堆大小

插入
  1. 将值插入堆的底部,即数组的尾部
  2. 然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值

大小为 k 的堆中插入元素的时间复杂度为 O(logk)

删除堆顶
  1. 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
  2. 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶

大小为 k 的堆中删除堆顶的时间复杂度为 O(logk)

获取堆顶和堆的大小
  • 获取堆顶:返回数组的头部
  • 获取堆的大小:返回数组的长度 ```javascript 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 // return Math.floor((i - 1) / 2) }

    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]) {

    1. this.swap(parentIndex, index)
    2. this.shiftUp(parentIndex)

    } }

    shiftDown(index) { const leftIndex = this.getLeftIndex(index) const rightIndex = this.getRightIndex(index)

    if (this.heap[leftIndex] < this.heap[index]) {

    1. this.swap(leftIndex, index)
    2. this.shiftDown(leftIndex)

    } if (this.heap[rightIndex] < this.heap[index]) {

    1. this.swap(rightIndex, index)
    2. 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) }

    // 获取堆顶 top() { return this.heap[0] }

    // 获取堆大小 size() { return this.heap.length } }

// 示例 const h = new MinHeap() h.insert(3) h.insert(2) h.insert(1) h.pop()

  1. <a name="GlNGh"></a>
  2. ### 3、算法场景
  3. - 数组中的第 K 个最大元素(LeetCode-215)
  4. - 前 K 个高频元素(LeetCode-347)
  5. - 合并 K 个排序链表(LeetCode-23)
  6. <a name="uVEvR"></a>
  7. ## 十一、搜索排序
  8. <a name="WWed6"></a>
  9. ### 1、排序
  10. 把某个乱序的数组变成升序或者降序的数组,JS 中的排序如 sort 方法,常见排序算法如下:
  11. <a name="swz6I"></a>
  12. #### 1.1 冒泡排序
  13. **思路:**
  14. 1. 比较所有相邻元素,如果第一个比第二个大,则交换它们
  15. 2. 一轮下来可以保证最后一个数是最大的
  16. 3. 执行 n - 1 轮,就可以完成排序
  17. **<br />**代码实现:**
  18. ```javascript
  19. // 时间复杂度 O(n^2)
  20. // 升序
  21. Array.prototype.bubbleSort = function () {
  22. for (let i = 0; i < this.length - 1; i += 1) {
  23. for (let j = 0; j < this.length - 1 - i; j += 1) {
  24. if (this[j] > this[j + 1]) {
  25. const temp = this[j]
  26. this[j] = this[j + 1]
  27. this[j + 1] = temp
  28. }
  29. }
  30. }
  31. }
  32. const arr = [5, 4, 3, 2, 1]
  33. arr.bubbleSort()
  34. console.log(arr)

1.2 选择排序

思路:

  1. 找到数组中的最小值,选中它并将其放置在第一位
  2. 接着找到第二小的值,选中它并将其放置在第二位
  3. 以此类推,执行 n - 1 轮


代码实现:**

  1. // 时间复杂度 O(n^2)
  2. // 升序
  3. Array.prototype.selectionSort = function () {
  4. for (let i = 0; i < this.length - 1; i += 1) {
  5. let minIndex = i
  6. for (let j = i + 1; j < this.length; j += 1) {
  7. if (this[minIndex] > this[j]) {
  8. minIndex = j
  9. }
  10. }
  11. if (i !== minIndex) {
  12. const temp = this[i]
  13. this[i] = this[minIndex]
  14. this[minIndex] = temp
  15. }
  16. }
  17. }
  18. const arr = [5, 4, 3, 2, 1]
  19. arr.selectionSort()
  20. console.log(arr)

1.3 插入排序

思路:

  1. 从第二个数开始,将其“提取”往前比
  2. 比它大就往后移一格,否则将其插入
  3. 以此类推进行到最后一个数


代码实现:**

  1. // 时间复杂度 O(n^2)
  2. // 升序
  3. Array.prototype.insertSort = function () {
  4. for (let i = 1; i < this.length; i += 1) {
  5. const temp = this[i]
  6. for (let j = i - 1; j >= 0; j -= 1) {
  7. if (this[j] > temp) {
  8. this[j + 1] = this[j]
  9. this[j] = temp
  10. } else {
  11. break
  12. }
  13. }
  14. }
  15. }
  16. const arr = [5, 4, 3, 2, 1]
  17. arr.insertSort()
  18. console.log(arr)

1.4 归并排序

思路:

  1. 分:把数组劈成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数
  2. 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组

合并两个有序数组:

  1. 新建一个空数组 res,用于存放最终排序后的数组
  2. 比较两个有序数组的头部,较小者出队并推入 res 中
  3. 如果两个数组还有值,就重复第二步


代码实现:**

  1. // 时间复杂度 O(nlogn)
  2. // 升序
  3. Array.prototype.mergeSort = function () {
  4. const rec = (arr) => {
  5. if (arr.length === 1) return arr
  6. const mid = Math.floor(arr.length / 2)
  7. const left = arr.slice(0, mid)
  8. const right = arr.slice(mid, arr.length)
  9. const orderLeft = rec(left)
  10. const orderRight = rec(right)
  11. const res = []
  12. while (orderLeft.length || orderRight.length) {
  13. if (orderLeft.length && orderRight.length) {
  14. res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
  15. } else if (orderLeft.length) {
  16. res.push(orderLeft.shift())
  17. } else if (orderRight.length) {
  18. res.push(orderRight.shift())
  19. }
  20. }
  21. return res
  22. }
  23. const res = rec(this)
  24. res.forEach((v, i) => {
  25. this[i] = v
  26. })
  27. }
  28. const arr = [5, 4, 3, 2, 1]
  29. arr.mergeSort()
  30. console.log(arr)

1.5 快速排序

思路:

  1. 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面
  2. 递归:递归地对基准前后的子数组进行分区


代码实现:**

  1. // 时间复杂度 O(nlogn)
  2. // 升序
  3. Array.prototype.quickSort = function () {
  4. const rec = (arr) => {
  5. if (arr.length <= 1) return arr
  6. const left = []
  7. const right = []
  8. const mid = arr[0]
  9. for (let i = 1; i < arr.length; i += 1) {
  10. if (arr[i] < mid) {
  11. left.push(arr[i])
  12. } else {
  13. right.push(arr[i])
  14. }
  15. }
  16. return [...rec(left), mid, ...rec(right)]
  17. }
  18. const res = rec(this)
  19. res.forEach((v, i) => {
  20. this[i] = v
  21. })
  22. }
  23. const arr = [5, 4, 3, 2, 1]
  24. arr.quickSort()
  25. console.log(arr)

2、搜索

找出数组中某个元素的下标,JS 中的搜索如 indexOf、findIndex等方法,常见搜索算法如下:

2.1 顺序搜索

思路:

  1. 遍历数组
  2. 找到跟目标值相等的元素,就返回它的下标
  3. 遍历结束后,如果没找到,就返回 -1


代码实现:**

  1. // 时间复杂度 O(n)
  2. Array.prototype.sequentialSearch = function (target) {
  3. for(let i = 0; i < this.length; i += 1) {
  4. if (this[i] === target) return i
  5. }
  6. return -1
  7. }
  8. const arr = [5, 4, 3, 2, 1]
  9. const index = arr.sequentialSearch(2)
  10. console.log(index)

2.2 二分搜索

必须有序
思路:

  1. 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束
  2. 如果目标值大于或小于中间元素,则在大于或小于中间元素的那一半数组中搜索


代码实现:**

  1. // 时间复杂度 O(logn)
  2. Array.prototype.binarySearch = function (target) {
  3. let begin = 0
  4. let end = this.length - 1
  5. while (begin <= end) {
  6. const mid = Math.floor((end + begin) / 2)
  7. if (this[mid] === target) {
  8. return mid
  9. } else if (this[mid] < target) {
  10. begin = mid + 1
  11. } else if (this[mid] > target) {
  12. end = mid - 1
  13. }
  14. }
  15. return -1
  16. }
  17. const arr = [1, 2, 3, 4, 5]
  18. const index = arr.binarySearch(2)
  19. console.log(index)

3、算法场景

  • 合并两个有序链表(LeetCode-21)
  • 猜数字大小(LeetCode-374)

十二、分而治之

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

1、场景一:归并排序

  • 分:把数组从中间一分为二
  • 解:递归地对两个子数组进行归并排序
  • 合:合并有序子数组

2、场景二:快速排序

  • 分:选基准,按基准把数组分成两个子数组
  • 解:递归地对两个子数组进行快速排序
  • 合:对两个子数组进行合并

3、算法场景

  • 猜数字大小(LeetCode-374)
  • 翻转二叉树(LeetCode-226)
  • 相同的树(LeetCode-100)
  • 对称二叉树(LeetCode-101)

十三、动态规划

  • 动态规划是算法设计中的一种方法,是一种思想
  • 它将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题

何为相互重叠呢?看一个典型的例子,斐波那契数列
斐波那契数列.png

1、动态规划 vs 分而治之

  • 相同点:动态规划和分而治之都是将一个问题分解为子问题
  • 不同点:动态规划是相互重叠的子问题,子问题之间有关联关系;分而治之的子问题彼此相互独立

2、算法场景

  • 爬楼梯(LeetCode-70)
  • 打家劫舍(LeetCode-198)

十四、贪心算法

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

1、算法场景

  • 分饼干(LeetCode-455)
  • 买卖股票的最佳时机(LeetCode-122)

十五、回溯算法

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


1、适用场景

  • 有很多路
  • 这些路里,有死路,也有出路
  • 通常需要递归来模拟所有的路

全排列为例:
全排列.png

2、算法场景

  • 全排列(LeetCode-46)
  • 子集(LeetCode-78)