本文将要介绍的内容如下:

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版

阅读小贴士:

阅读本文,请先掌握javascript基础知识。

推荐算法与数据结构入门书籍:《小灰算法》

推荐算法刷题网站:LeetCode


全文地图总览:

    数据结构与算法简介、时间复杂度、空间复杂度

    1. 数据结构:

  • 队列
  • 链表
  • 集合
  • 字典

    2. 算法:

  • 搜索排序
  • 分而治之
  • 动态规划
  • 贪心算法
  • 回溯算法

(*^_^*)与数据结构

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_02

和算法的邂逅(*^_^*)

1.数据结构、算法的自我介绍

数据结构:计算机存储、组织数据的方式

算法:一系列解决问题的清晰指令

程序 = 数据结构 + 算法

数据结构为算法提供服务,算法围绕数据结构操作。

2. 时间复杂度、空间复杂度

关于时间复杂度和空间复杂度的详细介绍和计算可以查阅:《小灰算法》书籍

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

空间复杂度:一个函数,用大O表示,如O(1)、O(n)等。用来表示算法在运行过程中临时占用存储空间大小的度量。

常见的时间复杂度如下图:

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_03


(*^_^*)打开”数据结构“的大门(*^_^*)

1. 栈:

一个后进先出的数据结构。

应用场景:需要后进先出的场景。如:10进制转2进制、判断字符串的括号是否有效、函数调用堆栈等……

JS中的函数调用堆栈:

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_04

注:javascript没有栈,但可以用Array实现栈的所有功能。

LeetCode上对应的题目:

20. 有效的括号

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_05

扩展:VSCode调试通过Node方式调试,而不用初始化为Node项目

[打断点-》F5-》进入了调试状态如下图]

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_06

2. 队列:

一个先进先出的数据结构。

JS同样没有队列,但是可以用Array实现队列的所有功能。

应用场景:需要先进先出的场景。如:JS异步任务中的任务队列、计算最近请求次数。

JS中主要考察的是JS的事件循环和任务队列(宏任务队列、微任务队列)。

可以了解下JS和DOM的事件模型。

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_07

LeetCode上对应的题目:

933. 最近的请求次数

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_08

3. 链表:

多个元素组成的列表。

但是元素的存储不连续,用next指针连在一起。

JS中还是没有链表,但是可以用Object模拟链表。

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;

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_09

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_10

注意数组和链表的区别:

数组:增删非首尾元素时往往需要移动元素

链表:增删非首尾元素,不需要移动元素,只需要更改next的指针即可

LeetCode上对应的题目:

237. 删除链表中的节点

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_11

206. 反转链表

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_12

2. 两数相加

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_13

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_14

141. 环形链表

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_15

扩展:前端与链表的结合点——原型链

原型链本质是链表。

原型链上的节点是各种原型对象,如Object.prototype

所谓的原型对象就是这些类的prototype属性值。

原型链通过__proto__属性连接各种原型对象。

如果A沿着原型链能找到B.prototype,那么A instanceof B为true.

如果A对象上没找到x属性,那么会沿着原型链向上攀升继续找x属性。

4. 集合:

一种无序且唯一的数据结构。

ES6中有集合:Set。

应用场景:去重、判断某元素是否在集合中、求交集等

LeetCode上对应的题目:

349. 两个数组的交集

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_16

5.  字典

存储唯一值的数据结构,但它以键值对的形式来存储

ES6中添加了字典Map

Map的操作时间复杂度都是O(1)

LeetCode上对应的题目:

349. 两个数组的交集

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_17

20.有效的括号

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_18

1. 两数之和

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_19

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_20

76.最小覆盖子串

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_21

6.  树

一种分层数据的抽象模型。

类似于生活中倒立 的树。

例如前端中的DOM树、级联选择(城市地区选择器等)、树形控件等

JS也是没有树的,但可以用Object和Array构建树

树常见的遍历方式是:深度/广度优先遍历、先序遍历、中序遍历、后序遍历

树的深度与广度优先遍历:

6.1 深度优先遍历:尽可能深的搜索树的分支

  • 访问根节点
  • 对根节点的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((child) => {dfs(child)});};dfs(tree);

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_22

6.2 广度优先遍历:先访问离根节点最近的节点

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的children挨个入队
  • 重复上面的第2、3步,直到队列为空
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 => {            // 队头的children挨个入队            q.push(child);        })    }};bfs(tree);

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_23

6.3 二叉树

树 分为二叉树和多叉树等。

二叉树中每个节点最多只能有2个子节点。

JS中常用Object来模拟二叉树(val代表当前节点的值):

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_24

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_25

其实,先序、中序、后序不难理解。明白”序“是相对于根节点来说的进行理解即可。且这些遍历可以用递归实现,也可以用非递归实现。

6.3.1 二叉树的先序遍历:

  • 访问根节点
  • 对根节点的左子树进行先序遍历
  • 对根节点的右子树进行先序遍历
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)

非递归版:

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); // 访问根节点的值        if(n.right) stack.push(n.right);        if(n.left) stack.push(n.left);    }};preorder(bt)

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_26

6.3.2 二叉树的中序遍历:

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历
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);

非递归版:

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_27

6.3.3 二叉树的后序遍历:

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点
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);

非递归版:

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_28

LeetCode上对应的题目:

104. 二叉树的最大深度

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_29

111.二叉树的最小深度

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_30

102.二叉树的层序遍历

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_31

94.二叉树的中序遍历

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_32

112.路径总和

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_33

7.  图

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

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

一条边只能连接2个节点,所以表示的是2元关系。

JS中还是没有图,依然用Object和Array构建图。
图的表示法:邻接矩阵、邻接表、关联矩阵等等

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_34

7.1 图的表示法1——邻接矩阵

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_35

有如上图左边那个”图“,A连接到B,B连接到C,C连接到E……

JS中可以用二维数组来表示这个图,如A能连接到B,那么

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_36

箭头所指处第一行代表A这个节点,第2列代表B这个节点。

依此类推,如果我们想表示某个节点能连接到另外一个节点,那么就设置2个节点的交叉位置为1. 这就是常说的邻接矩阵。

7.2 图的表示法2——邻接表

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_37

可以构建一个对象,对象的Key就是各个节点,value就是各个节点可连接到的节点。如上图的 B: ['c', 'D’] 代表B可以连接到C和D。当然邻接表不只是这种表现形式,但只要能表示连接关系即可。

7.3 图的深度优先遍历

尽可能深的搜索图的分支。

  • 访问根节点
  • 对根节点的没访问过的相邻节点挨个进行深度优先遍历。【如果访问已访问过的节点就会陷入死循环】

所以正确做法是下图中的右图。

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_38

// 图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 => {  //graph[n]是指相邻节点        if(!visited.has(c)) {            dfs(c);        }    })};dfs(2);

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_39

7.4 图的广度优先遍历

先访问离根节点最近的节点。

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的没访问过的相邻节点入队
  • 重复第2、3步知道队列为空

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_40

// 图const graph = {    0: [1, 2],    1: [2],    2: [0, 3],    3: [3]}// 图的广度优先遍历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);        }    })}

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_41

LeetCode上对应的题目:

65.有效数字

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_42

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_43

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_44

133.克隆图

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_45

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_46

8.  堆

一种特殊的完全二叉树。

JS通常使用数组表示堆,

堆能快速找出最大值和最小值,其时间复杂度是O(1)。

堆还能找出第K个最大(小)元素

  • 左侧的子节点的位置:2* index + 1
  • 右侧的子节点的位置:2*index +2
  • 父节点位置是(index-1)/2

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_47

其所有节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_链表_48

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_49

堆找出第K个最大元素:

  • 构建一个最小堆,并将元素依次插入堆中
  • 当堆的容量超过K时,就删除堆顶
  • 插入结束后,堆顶就是第K个最大元素
// 最小堆类:// 1. 类中声明一个数组,来装元素// 2. 主要方法:插入、删除堆顶、获取堆顶、获取堆大小class MinHeap {     constructor() {        this.heap = [];    }    // 插入:    // 1. 将值插入堆的底部,即数组的尾部    // 2. 上移,将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值    // 3. 大小为k的堆来说,插入元素的时间复杂度为O(logk)    insert(value) {        this.heap.push(value);        // 上移        this.shiftUp(this.heap.length - 1);    }    // 删除堆顶:    // 1. 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆的结构)    // 2. 下移,将新的堆顶和它的子节点进行操作,直到子节点大于等于这个新堆顶    // 3. 大小为k的堆中,删除堆顶的时间复杂度为O(logk)    pop() {        this.heap[0] = this.heap.pop();        this.shiftDown(0);    }    // 获取堆顶和堆的大小    // 1. 获取堆顶:返回数组的头部    // 2. 获取堆的大小:返回数组的长度    peek() {        return this.heap[0];    }    size() {        return this.heap.length;    }    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);        }    }    // 拿到该节点的左侧子节点    getLeftIndex(i) {        return i * 2 + 1;    }    // 拿到该节点的右侧子节点    getRightIndex(i) {        return i * 2 + 2;    }    // 拿到该节点的父节点    getParentIndex(i) {        // return Math.floor((i-1)/2);        return (i - 1) >> 1;    }    // 交换    swap(i1, i2) {        const temp = this.heap[i1];        this.heap[i1] = this.heap[i2];        this.heap[i2] = 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);        }    }}const h = new MinHeap();h.insert(3);h.insert(2);h.insert(1);// result: 1 3 2h.pop();//result : 2, 3

LeetCode对应题目:

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

// 最小堆类:// 1. 类中声明一个数组,来装元素// 2. 主要方法:插入、删除堆顶、获取堆顶、获取堆大小class MinHeap {     constructor() {        this.heap = [];    }    // 插入:    // 1. 将值插入堆的底部,即数组的尾部    // 2. 上移,将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值    // 3. 大小为k的堆来说,插入元素的时间复杂度为O(logk)    insert(value) {        this.heap.push(value);        // 上移        this.shiftUp(this.heap.length - 1);    }    // 删除堆顶:    // 1. 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆的结构)    // 2. 下移,将新的堆顶和它的子节点进行操作,直到子节点大于等于这个新堆顶    // 3. 大小为k的堆中,删除堆顶的时间复杂度为O(logk)    pop() {        this.heap[0] = this.heap.pop();        this.shiftDown(0);    }    // 获取堆顶和堆的大小    // 1. 获取堆顶:返回数组的头部    // 2. 获取堆的大小:返回数组的长度    peek() {        return this.heap[0];    }    size() {        return this.heap.length;    }    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);        }    }    // 拿到该节点的左侧子节点    getLeftIndex(i) {        return i * 2 + 1;    }    // 拿到该节点的右侧子节点    getRightIndex(i) {        return i * 2 + 2;    }    // 拿到该节点的父节点    getParentIndex(i) {        // return Math.floor((i-1)/2);        return (i - 1) >> 1;    }    // 交换    swap(i1, i2) {        const temp = this.heap[i1];        this.heap[i1] = this.heap[i2];        this.heap[i2] = 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);        }    }}/** * @param {number[]} nums * @param {number} k * @return {number} */var findKthLargest = function(nums, k) {    // 构建最小堆    const h = new MinHeap();    nums.forEach(n => {        // 插入元素        h.insert(n);        // 裁员        if(h.size() > k ) {            h.pop();// 删除堆顶        }    });    return h.peek();};

347.前K个高频元素

/** * @param {number[]} nums * @param {number} k * @return {number[]} */var topKFrequent = function(nums, k) {    // 解法1:    const map = new Map();    nums.forEach(n=> {        map.set(n, map.has(n)?map.get(n)+1 : 1);    });    // console.log(map); // 统计元素频率    // console.log(Array.from(map));// 对频率进行排序    const list = Array.from(map).sort((a,b) => b[1] - a[1]);    // console.log(list)    return list.slice(0, k).map(n => n[0]);};
// 最小堆类:// 1. 类中声明一个数组,来装元素// 2. 主要方法:插入、删除堆顶、获取堆顶、获取堆大小class MinHeap {     constructor() {        this.heap = [];    }    // 插入:    // 1. 将值插入堆的底部,即数组的尾部    // 2. 上移,将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值    // 3. 大小为k的堆来说,插入元素的时间复杂度为O(logk)    insert(value) {        this.heap.push(value);        // 上移        this.shiftUp(this.heap.length - 1);    }    // 删除堆顶:    // 1. 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆的结构)    // 2. 下移,将新的堆顶和它的子节点进行操作,直到子节点大于等于这个新堆顶    // 3. 大小为k的堆中,删除堆顶的时间复杂度为O(logk)    pop() {        this.heap[0] = this.heap.pop();        this.shiftDown(0);    }    // 获取堆顶和堆的大小    // 1. 获取堆顶:返回数组的头部    // 2. 获取堆的大小:返回数组的长度    peek() {        return this.heap[0];    }    size() {        return this.heap.length;    }    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);        }    }    // 拿到该节点的左侧子节点    getLeftIndex(i) {        return i * 2 + 1;    }    // 拿到该节点的右侧子节点    getRightIndex(i) {        return i * 2 + 2;    }    // 拿到该节点的父节点    getParentIndex(i) {        // return Math.floor((i-1)/2);        return (i - 1) >> 1;    }    // 交换    swap(i1, i2) {        const temp = this.heap[i1];        this.heap[i1] = this.heap[i2];        this.heap[i2] = 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);        }    }}/** * @param {number[]} nums * @param {number} k * @return {number[]} */var topKFrequent = function(nums, k) {    // sort()的时间复杂度是O(nlogn)    const map = new Map();    nums.forEach(n=> {        map.set(n, map.has(n)?map.get(n)+1 : 1);    });    // console.log(map); // 统计元素频率    // console.log(Array.from(map));// 对频率进行排序    // const list = Array.from(map).sort((a,b) => b[1] - a[1]);    // console.log(list)    // return list.slice(0, k).map(n => n[0]);        // 为了优化时间复杂度就没必要全排列,用堆即可。    // 构建前K个高频元素都在堆里面    const h = new MinHeap();    map.forEach((value, key) => {        h.insert({value, key});// 插入的是js对象        if(h.size() > k) {            h.pop();        }    });    return h.heap.map(a => a.key);};// 时间复杂度是O(nlogk)

23.合并K个排序链表

/** * 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;        if(n.next) h.insert(n.next);    }    return res.next;};// 最小堆类:// 1. 类中声明一个数组,来装元素// 2. 主要方法:插入、删除堆顶、获取堆顶、获取堆大小class MinHeap {     constructor() {        this.heap = [];    }    // 插入:    // 1. 将值插入堆的底部,即数组的尾部    // 2. 上移,将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值    // 3. 大小为k的堆来说,插入元素的时间复杂度为O(logk)    insert(value) {        this.heap.push(value);        // 上移        this.shiftUp(this.heap.length - 1);    }    // 删除堆顶:    // 1. 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆的结构)    // 2. 下移,将新的堆顶和它的子节点进行操作,直到子节点大于等于这个新堆顶    // 3. 大小为k的堆中,删除堆顶的时间复杂度为O(logk)    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;    }    // 获取堆顶和堆的大小    // 1. 获取堆顶:返回数组的头部    // 2. 获取堆的大小:返回数组的长度    peek() {        return this.heap[0];    }    size() {        return this.heap.length;    }    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);        }    }    // 拿到该节点的左侧子节点    getLeftIndex(i) {        return i * 2 + 1;    }    // 拿到该节点的右侧子节点    getRightIndex(i) {        return i * 2 + 2;    }    // 拿到该节点的父节点    getParentIndex(i) {        // return Math.floor((i-1)/2);        return (i - 1) >> 1;    }    // 交换    swap(i1, i2) {        const temp = this.heap[i1];        this.heap[i1] = this.heap[i2];        this.heap[i2] = temp;    }    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);        }    }}

(*^_^*)"算法"的成神之路(*^_^*)

1.  搜索排序,你知多少?

【提示: 可能有些算法过程太抽象难以理解,那么推荐算法可视化网站,可视化的结果结合https://visualgo.net/zh 这个网站和代码调试更加深刻的理解算法】

这里为了排版方便,将不会录制gif图展示,有疑问的可自行去网站查看可视化过程。

排序:把某个乱序的数组变成升序或降序的数组。

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

JS中,排序:数组的sort(),搜索:数组的indexOf()、includes()等

一个好的开发者不应该仅仅停留在API的使用阶段。

知其所以然,才能成为大牛!

接下来我将从0开始介绍排序和搜索算法有哪些,怎么实现。

1.1 常用的排序算法:

1.1.1 冒泡排序

冒泡排序是排序算法中最简单的一个,但性能较差。工作中几乎应该用不到的。

其思路:

  • 比较所有相邻元素,如果第一个比第二个大,则交换他们
  • 一轮下来,可以保证最后一个数是最大的
  • 执行n-1轮,就可以完成排序

思考1下,传统的冒泡排序的相邻比较的时候是不是全部都要比。既然每次最大的都会到最右边,那么最右边的区域是不是会有序的了,还需要参与后面的比较吗?肯定不需要啊,提高了性能

时间复杂度:O(n^2) 因为2个嵌套循环

Array.prototype.bubbleSort = function() {    for(let i=0;i<this.length-1;i++) { // 控制回合数        for(let j=0;j<this.length-1 - i;j++) {// 控制列数            // console.log(this[j], this[j+1])// 获取相邻元素            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)

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_50

1.1.2 选择排序

性能也不太好。但和冒泡排序一样简单。

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

注意,这里也优化了以下,不是每次都从0开始,应该每次放在前从而有序了。所以直接跳过位置对调。

时间复杂度:O(n^2) 因为2个嵌套循环

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 = [5,4,3,2,1]arr.selectionSort();console.log(arr)

1.1.3 插入排序

排序小型数组的时候,插入排序比选择排序、冒泡排序的性能都要好。

  • 从第二个数开始往前比
  • 比它大的就往后排
  • 以此类推进行到最后一个数

时间复杂度:O(n^2)因为2个嵌套循环

Array.prototype.insertionSort = function() {    for(let i = 1; i <this.length; i++){        // 第2个数往前比        const temp = this[i];        let j = i; // 第二个数的下标        while(j > 0) {            if(this[j - 1] > temp) {// 前面的数比后面的大,后移                this[j] = this[j-1];            }else {                break;            }            j--;        }        this[j] = temp;    }};const arr = [5,4,3,2,1]arr.insertionSort();console.log(arr)

1.1.4 归并排序:

在Code中常用这个。

  • 分:把数组分为2半,再递归的对子数组进行“分”操作,直到分成一个个单独的数
  • 合:把2个数合并为有序数组,再对有序数组进行合并,直到全部子数组都合并为一个完整的数组。
  • 合并2个有序数组:新建一个空数组res,用于存放最终排序后的数组。比较2个有序数组的头部,较小者出队并推入res中。如果2个数组还有值,重复比较2个有序数组的头部,较小者出队并推入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);        const right = arr.slice(mid, arr.length);        const orderLeft = rec(left);        const orderRight = rec(right);        // 合并:循环时间复杂度O(n)        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 result = rec(this);    result.forEach((n, i )=> this[i] = n);};const arr = [5,4,3,2,1]arr.mergeSort();console.log(arr)//总体时间复杂度:O(nlogN)

1.1.4 快速排序

性能比冒泡排序、选择排序、插入排序的性能更好。

Chrome曾用快速排序作为sort()的排序方法

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

时间复杂度:因为递归的时间复杂度O(logN),分区的时间复杂度O(n),所以整体时间复杂度:O(n*logN)

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 ++) {            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 = [2,4,5,3,1]arr.quickSort();console.log(arr)

1.2 常用的搜索算法:

1.2.1 顺序搜索:

最基本的搜索算法,也最低效。

  • 遍历数组
  • 如果找到和目标值相等的元素,就返回该它的下标
  • 遍历结束后, 如果没搜索到目标值就返回-1

时间复杂度:O(n)。因为循环

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

1.2.2 二分搜索/折半搜索:

前提:数组是有序的。

比顺序搜索的效率高得多

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

时间复杂度:O(logN) 因为每次比较都使搜索范围缩小一半。

Array.prototype.binarySearch= function(target) {    this.sort(); // 先排序,如果原数组不是有序的    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 res = [1,2,3,4,5].binarySearch(4); // 下标是3console.log(res)

LeetCode对应题目:

21. 合并2个有序链表

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_51

374.猜数字大小

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_52

2.  君王何以治天下——分而治之

分而治之是算法设计中的一种方法、思想。

  • 将1个问题分为多个和原问题相似的小问题,递归解决小问题,然后将结果合并以解决原来的问题

2.1 归并排序

就是利用分而治之来设计的:

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

2.2 快速排序:

也是利用分而治之来设计的:

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

LeetCode对应题目:

374. 猜数字大小

/**  * 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) {    // 解法2:分而治之    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(1, mid-1);        }    };    return rec(1, n);    // 二分搜索    // let low = 1;    // let high = n;    // while(low <= high) {    //     const mid = Math.floor((low + high) / 2);    //     const res = guess(mid);    //     console.log('mid', mid);    //     console.log('res', res);    //     if(res === 0) {    //         return mid;    //     }else if(res === 1){    //         low = mid + 1;    //     } else{    //         high = mid - 1;    //     }    // }};

226. 翻转二叉树

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_53

100. 相同的树

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_54

101. 对称二叉树

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_55

3.  你对你的职业规划是什么——动态规划

动态规划也是算法设计中的一种方法、思想。

  • 将一个问题分解为相互重叠的子问题,通过反复求解子问题来解决原来的问题

例如,解决斐波那契数列问题:

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_56

动态规划和分而治之的区别是子问题是否是相互独立的。LeetCode对应题目:70.爬楼梯


数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_二叉树_57

198. 打家劫舍

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_58

4.  你贪心了吗?——贪心算法

也是算法设计中的一种方法、思想。

  • 期盼通过每个阶段的局部最优选择,达到全局的最优
  • 结果不一定是最优的

贪心算法有时候能得到最优解,有时候又得不到:

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_59

LeetCode对应题目:

455. 分发饼干

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构与算法分析c++版pdf_60

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

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_61

5.  回溯算法

它也是算法设计中的一种方法。

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

适合回溯算法的问题:

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

例如经典的全排列问题。

数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_62

对于全排列问题:


  • 用递归模拟出所有的情况
  • 遇到包含重复元素的情况,就回溯
  • 收集所有到达递归终点的情况,并返回

LeetCode对应题目:46.全排列:


数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构c++版_63

78.子集:


数据结构与算法分析c++版pdf 数据结构与算法c++版 pdf_数据结构_64