diff --git a/README.md b/README.md index cbc7626..4f48d65 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # JavaScript 数据结构与算法 -本仓库内容根据哔哩哔哩 [《coderwhy的JavaScript数据结构与算法》](https://www.bilibili.com/video/BV1x7411L7Q7?p=1) 视频整理的学习笔记,视频教程讲的特别好,配合本仓库的代码测试环境来练习,学习效果更佳,欢迎同学们 Star 和 Fork。 +本仓库内容根据哔哩哔哩 [《JavaScript 数据结构与算法》](https://www.bilibili.com/video/BV1x7411L7Q7?p=1) 视频整理的学习笔记,视频教程讲的特别好,配合本仓库的代码测试环境来练习,学习效果更佳,欢迎同学们 Star 和 Fork。 -推荐大家按照目录结构的顺序来学习,由浅入深,循序渐进,轻松搞定数据结构和算法。 +推荐大家按照目录顺序来学习,由浅入深,循序渐进,轻松搞定数据结构和算法。 代码部分均采用 ES6 编写,使用 webpack 和 babel 将 ES6 自动转换成 ES5。 -> 重点要掌握数据结构与算法的思想,编程语言只是一种实现工具。 +> 重点要掌握数据结构与算法的思想和原理,使用哪种编程语言区别不大。 + +**访问作者博客 [《JavaScript 数据结构与算法》](https://xpoet.cn/2020/07/JavaScript%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%93%E8%BE%91%EF%BC%89/) 可获取更好的阅读体验。** +**[传送门 🏃](https://xpoet.cn/2020/07/JavaScript%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%93%E8%BE%91%EF%BC%89/)** ## 文档目录 @@ -21,8 +24,9 @@ - [JavaScript 数据结构与算法(九)字典](assets/doc/09_JavaScript数据结构与算法(九)字典.md) - [JavaScript 数据结构与算法(十)哈希表](assets/doc/10_JavaScript数据结构与算法(十)哈希表.md) - [JavaScript 数据结构与算法(十一)树](assets/doc/11_JavaScript数据结构与算法(十一)树.md) -- [JavaScript 数据结构与算法(十二)二叉搜索树](assets/doc/12_JavaScript数据结构与算法(十二)二叉搜索树.md) -- [JavaScript 数据结构与算法(十三)图](assets/doc/13_JavaScript数据结构与算法(十三)图.md) +- [JavaScript 数据结构与算法(十二)二叉树](assets/doc/12_JavaScript数据结构与算法(十二)二叉树.md) +- [JavaScript 数据结构与算法(十三)二叉搜索树](assets/doc/13_JavaScript数据结构与算法(十三)二叉搜索树.md) +- [JavaScript 数据结构与算法(十四)图](assets/doc/14_JavaScript数据结构与算法(十四)图.md) ## 代码目录 @@ -50,6 +54,7 @@ npm run start ``` 开启**测试环境**的服务后,可在 `src/index.js` 选择要测试的代码,查看具体值输出。 +比如:我要测试**栈**,把 `// import './Stack'` 的注释去掉,要测试哪个就去掉哪个的注释。 ```js // 导入栈结构的封装及测试代码 @@ -77,7 +82,7 @@ npm run start // import './HashTable'; // 导入树结构的封装及测试代码 -import './Tree'; +// import './Tree'; // 导入图结构的封装及测试代码 // import './Graph'; diff --git "a/assets/doc/01_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\200\357\274\211\345\211\215\350\250\200.md" "b/assets/doc/01_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\200\357\274\211\345\211\215\350\250\200.md" index e09a2f2..8076840 100644 --- "a/assets/doc/01_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\200\357\274\211\345\211\215\350\250\200.md" +++ "b/assets/doc/01_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\200\357\274\211\345\211\215\350\250\200.md" @@ -56,7 +56,7 @@ ### 常见的数据结构 - 数组(Aarray) -- 栈(Queue) +- 栈(Stack) - 链表(Linked List) - 图(Graph) - 散列表(Hash) diff --git "a/assets/doc/02_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\214\357\274\211\346\225\260\347\273\204.md" "b/assets/doc/02_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\214\357\274\211\346\225\260\347\273\204.md" index 2cf0537..e8478e2 100644 --- "a/assets/doc/02_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\214\357\274\211\346\225\260\347\273\204.md" +++ "b/assets/doc/02_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\214\357\274\211\346\225\260\347\273\204.md" @@ -1,25 +1,46 @@ -## JavaScript 数据结构与算法(二)数组结构 +# JavaScript 数据结构与算法(二)数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构。 数组通常情况下用于存储一系列同一种数据类型的值。 但在 JavaScript 里,数组中可以保存不同类型的值。但我们还是要遵守最佳实践,别这么做(大多数语言都没这个能力)。 -### 创建和初始化数组 +## 创建和初始化数组 -- new Array() - `const daysOfWeek = new Array('Sunday', 'Monday', 'Tuesday', 'Wednesday','Thursday', 'Friday', 'Saturday');` +- `new Array()` -- [] - `const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];` + ```js + const daysOfWeek = new Array( + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ); + ``` + +- `[]` + ```js + const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + ``` -### 数组常见操作 +## 数组常见操作 -#### 添加元素 +### 添加元素 - 添加一个元素到数组的最后位置 `array.push(item)` - 在数组首位插入一个元素 `array.unshift(item)` -- 在指定索引位置插入元素 `array.splice(index.js, 0, item)` - splice() 第二个参数为 0 时,表示插入数据。 +- 在指定索引位置插入元素 `array.splice(index, 0, item)` + > splice() 第二个参数为 0 时,表示插入数据。 ```js let myArray = [1, 2, 3]; // 在 索引 0 的位置,插入 A @@ -27,7 +48,7 @@ console.log(myArray); //--> ['A', 1, 2, 3] ``` -#### 删除元素 +### 删除元素 - 删除数组最后的元素 `array.pop(item)` - 删除数组首位的元素 `array.shift(item)` @@ -35,21 +56,21 @@ 例如: ```js let myArray2 = [1, 2, 3, 4, 5]; - // 删除索引 3 位置起,2 个元素 - myArray2.splice(3, 2); + // 删除索引 4 位置起,2 个元素 + myArray2.splice(4, 2); console.log(myArray2); //--> [1, 2, 3] ``` ### 修改元素 -- 修改指定索引位置的元素 `array.splice(index.js, 1, item)` +- 修改指定索引位置的元素 `array.splice(index, 1, item)` ```js let myArray3 = [1, 2, 3, 4, 5, 6]; // 修改 索引 1 的位置的元素为 AA myArray2.splice(1, 1, "AA"); console.log(myArray3); //--> [1, "AA", 3, 4, 5, 6] ``` -- 修改指定索引位置的几个元素 `array.splice(index.js, number, item)` +- 修改指定索引位置的几个元素 `array.splice(index, number, item)` ```js let myArray4 = [1, 2, 3, 4, 5, 6, 7]; // 在 索引 2 的位置起,修改两个元素为 AA BB diff --git "a/assets/doc/03_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\211\357\274\211\346\240\210.md" "b/assets/doc/03_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\211\357\274\211\346\240\210.md" index 451c648..e48df7b 100644 --- "a/assets/doc/03_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\211\357\274\211\346\240\210.md" +++ "b/assets/doc/03_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\211\357\274\211\346\240\210.md" @@ -1,9 +1,11 @@ -## JavaScript 数据结构与算法(三)栈结构 +## JavaScript 数据结构与算法(三)栈 数组是一个线性结构,并且可以在数组的任意位置插入和删除元素。 但是有时候,我们为了实现某些功能,必须对这种任意性加以限制。 栈和队列就是比较常见的受限的线性结构。 +## 什么是栈 + 栈(stack)是一种运算受限的线性表: - `LIFO(last in first out)`表示就是后进入的元素,第一个弹出栈空间。类似于自动餐托盘,最后放上的托盘,往往先把拿出去使用。 @@ -12,11 +14,11 @@ - 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 如下图所示: -![stack](https://user-images.githubusercontent.com/24516169/88035463-caf63780-cb74-11ea-910d-e396a83659ea.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.71xt32okr3k0.png) 栈的特点:**先进后出,后进先出**。 -### 程序中的栈结构 +## 程序中的栈结构 - 函数调用栈:A(B(C(D()))): 即 A 函数中调用 B,B 调用 C,C 调用 D;在 A 执行的过程中会将 A 压入栈,随后 B 执行时 B 也被压入栈,函数 C 和 D 执行时也会被压入栈。所以当前栈的顺序为:A->B->C->D(栈顶);函数 D 执行完之后,会弹出栈被释放,弹出栈的顺序为 D->C->B->A; @@ -24,7 +26,7 @@ - 递归: 为什么没有停止条件的递归会造成栈溢出?比如函数 A 为递归函数,不断地调用自己(因为函数还没有执行完,不会把函数弹出栈),不停地把相同的函数 A 压入栈,最后造成栈溢出(Queue Overfloat)。 -### 练习 +## 练习 题目:有 6 个元素 6,5,4,3,2,1 按顺序进栈,问下列哪一个不是合法的出栈顺序? @@ -42,9 +44,9 @@ - C 答案:6543 进栈,3 出栈,4 出栈,之后应该 5 出栈而不是 6,所以错误。 - D 答案:65432 进栈,2 出栈,3 出栈,4 出栈,1 进栈出栈,5 出栈,6 出栈。符合入栈顺序。 -### 栈结构实现 +## 栈结构实现 -#### 栈常见的操作 +### 栈常见的操作 - `push()` 添加一个新元素到栈顶位置。 - `pop()` 移除栈顶的元素,同时返回被移除的元素。 @@ -53,12 +55,11 @@ - `size()` 返回栈里的元素个数。这个方法和数组的 `length` 属性类似。 - `toString()` 将栈结构的内容以字符串的形式返回。 -#### JavaScript 代码实现栈结构 +### JavaScript 代码实现栈结构 ```js // 栈结构的封装 class Map { - constructor() { this.items = []; } @@ -90,16 +91,16 @@ class Map { // toString() 返回以字符串形式的栈内元素数据 toString() { - let result = ''; + let result = ""; for (let item of this.items) { - result += item + ' '; + result += item + " "; } return result; } } ``` -#### 测试封装的栈结构 +### 测试封装的栈结构 ```js // push() 测试 @@ -124,7 +125,7 @@ console.log(stack.size()); //--> 2 console.log(stack.toString()); //--> 1 2 ``` -### 栈结构的简单应用 +## 栈结构的简单应用 利用栈结构的特点封装实现十进制转换为二进制的方法。 @@ -142,7 +143,7 @@ function dec2bin(dec) { dec = Math.floor(dec / 2); // 除数除以二,向下取整 } - let binaryString = ''; + let binaryString = ""; // 不断地从栈中取出元素(0 或 1),并拼接到一起。 while (!stack.isEmpty()) { binaryString += stack.pop(); @@ -152,7 +153,7 @@ function dec2bin(dec) { } ``` -#### 测试 +### 测试 ```js // dec2bin() 测试 diff --git "a/assets/doc/04_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\233\233\357\274\211\351\230\237\345\210\227.md" "b/assets/doc/04_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\233\233\357\274\211\351\230\237\345\210\227.md" index a0dd1fc..f25405e 100644 --- "a/assets/doc/04_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\233\233\357\274\211\351\230\237\345\210\227.md" +++ "b/assets/doc/04_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\233\233\357\274\211\351\230\237\345\210\227.md" @@ -1,6 +1,6 @@ -## JavaScript 数据结构与算法(四)队列结构 +# JavaScript 数据结构与算法(四)队列 -### 认识队列 +## 认识队列 队列(Queue)是一种运算受限的线性表,特点:先进先出。(FIFO:First In First Out) @@ -14,25 +14,25 @@ - 排队,比如在电影院,商场,甚至是厕所排队。 - 优先排队的人,优先处理。 (买票、结账、WC)。 -![queue](https://user-images.githubusercontent.com/24516169/88038526-e9f6c880-cb78-11ea-859d-1faaaebed3bf.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5mmiw2kdwbs0.png) -#### 队列图解 +### 队列图解 -![queue](https://user-images.githubusercontent.com/24516169/88038782-45c15180-cb79-11ea-8439-bdc7e240d10d.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.mq92bw3am0g.png) -#### 队列在程序中的应用 +### 队列在程序中的应用 - 打印队列:计算机打印多个文件的时候,需要排队打印。 - 线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待 CPU 处理。 -### 队列的实现 +## 队列的实现 队列的实现和栈一样,有两种方案: - 基于数组实现。 - 基于链表实现。 -#### 队列常见的操作 +### 队列常见的操作 - `enqueue(element)` 向队列尾部添加一个(或多个)新的项。 - `dequeue()` 移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。 @@ -41,11 +41,10 @@ - `size()` 返回队列包含的元素个数,与数组的 length 属性类似。 - `toString()` 将队列中的内容,转成字符串形式。 -#### 代码实现 +### 代码实现 ```js class Queue { - constructor() { this.items = []; } @@ -77,25 +76,25 @@ class Queue { // toString() 将队列中的元素以字符串形式返回 toString() { - let result = ''; + let result = ""; for (let item of this.items) { - result += item + ' '; + result += item + " "; } return result; } } ``` -#### 测试代码 +### 测试代码 ```js const queue = new Queue(); // enqueue() 测试 -queue.enqueue('a'); -queue.enqueue('b'); -queue.enqueue('c'); -queue.enqueue('d'); +queue.enqueue("a"); +queue.enqueue("b"); +queue.enqueue("c"); +queue.enqueue("d"); console.log(queue.items); //--> ["a", "b", "c", "d"] // dequeue() 测试 @@ -116,13 +115,13 @@ console.log(queue.size()); //--> 2 console.log(queue.toString()); //--> c d ``` -### 队列的应用 +## 队列的应用 使用队列实现小游戏:**击鼓传花**。 分析:传入一组数据集合和设定的数字 number,循环遍历数组内元素,遍历到的元素为指定数字 number 时将该元素删除,直至数组剩下一个元素。 -#### 代码实现 +### 代码实现 ```js // 利用队列结构的特点实现击鼓传花游戏求解方法的封装 @@ -161,11 +160,11 @@ function passGame(nameList, number) { } ``` -#### 测试代码 +### 测试代码 ```js // passGame() 测试 -const names = ['lily', 'lucy', 'tom', 'tony', 'jack']; +const names = ["lily", "lucy", "tom", "tony", "jack"]; const targetIndex = passGame(names, 4); -console.log('击鼓传花', names[targetIndex]); //--> lily +console.log("击鼓传花", names[targetIndex]); //--> lily ``` diff --git "a/assets/doc/05_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\224\357\274\211\344\274\230\345\205\210\351\230\237\345\210\227.md" "b/assets/doc/05_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\224\357\274\211\344\274\230\345\205\210\351\230\237\345\210\227.md" index 99066a8..f09f572 100644 --- "a/assets/doc/05_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\224\357\274\211\344\274\230\345\205\210\351\230\237\345\210\227.md" +++ "b/assets/doc/05_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\272\224\357\274\211\344\274\230\345\205\210\351\230\237\345\210\227.md" @@ -1,22 +1,22 @@ -## JavaScript 数据结构与算法(五)优先队列 +# JavaScript 数据结构与算法(五)优先队列 +## 场景 生活中类似**优先队列**的场景: - 优先排队的人,优先处理。 (买票、结账、WC)。 - 排队中,有紧急情况(特殊情况)的人可优先处理。 - -### 优先队列 +## 优先队列 优先级队列主要考虑的问题: - 每个元素不再只是一个数据,还包含优先级。 - 在添加元素过程中,根据优先级放入到正确位置。 -#### 优先队列的实现 +## 优先队列的实现 -##### 代码实现 +### 代码实现 ```js // 优先队列内部的元素类 @@ -29,7 +29,6 @@ class QueueElement { // 优先队列类(继承 Queue 类) export class PriorityQueue extends Queue { - constructor() { super(); } @@ -92,26 +91,26 @@ export class PriorityQueue extends Queue { // toString() 将队列中元素以字符串形式返回 // 重写 toString() toString() { - let result = ''; + let result = ""; for (let item of this.items) { - result += item.element + '-' + item.priority + ' '; + result += item.element + "-" + item.priority + " "; } return result; } } ``` -#### 测试代码 +### 测试代码 ```js const priorityQueue = new PriorityQueue(); // 入队 enqueue() 测试 -priorityQueue.enqueue('A', 10); -priorityQueue.enqueue('B', 15); -priorityQueue.enqueue('C', 11); -priorityQueue.enqueue('D', 20); -priorityQueue.enqueue('E', 18); +priorityQueue.enqueue("A", 10); +priorityQueue.enqueue("B", 15); +priorityQueue.enqueue("C", 11); +priorityQueue.enqueue("D", 20); +priorityQueue.enqueue("E", 18); console.log(priorityQueue.items); //--> output: // QueueElement {element: "A", priority: 10} @@ -139,5 +138,6 @@ console.log(priorityQueue.size()); //--> 3 console.log(priorityQueue.toString()); //--> B-15 E-18 D-20 ``` -### 补充:数组、栈和队列图解 -![array-stack-queue](https://user-images.githubusercontent.com/24516169/88051118-b02ebd80-cb8a-11ea-9acf-4329cbbff6fc.png) +## 数组、栈和队列图解 + +![数组、栈和队列图解](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.64kg5ej56vk0.png) diff --git "a/assets/doc/06_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\255\357\274\211\345\215\225\345\220\221\351\223\276\350\241\250.md" "b/assets/doc/06_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\255\357\274\211\345\215\225\345\220\221\351\223\276\350\241\250.md" index cb84650..546779b 100644 --- "a/assets/doc/06_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\255\357\274\211\345\215\225\345\220\221\351\223\276\350\241\250.md" +++ "b/assets/doc/06_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\255\357\274\211\345\215\225\345\220\221\351\223\276\350\241\250.md" @@ -1,12 +1,12 @@ -## JavaScript 数据结构与算法(六)单向链表 +# JavaScript 数据结构与算法(六)单向链表 -### 认识链表 +## 认识链表 -#### 链表和数组 +### 链表和数组 链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。 -##### 数组 +#### 数组 - 存储多个元素,数组(或列表)可能是最常用的数据结构。 @@ -18,7 +18,7 @@ 在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。 -##### 链表 +#### 链表 - 存储多个元素,另外一个选择就是使用链表。 @@ -42,27 +42,27 @@ 虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。 -### 单向链表 +## 单向链表 单向链表类似于火车,有一个火车头,火车头会连接一个节点,节点上有乘客,并且这个节点会连接下一个节点,以此类推。 - 链表的火车结构 - ![链表的火车结构](https://user-images.githubusercontent.com/24516169/88268829-a1b8e100-cd05-11ea-91d9-1c4322783a3d.png) + ![链表的火车结构](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.27xtn3c52zwg.png) - 链表的数据结构 head 属性指向链表的第一个节点。 - 链表中的最后一个节点指向 null。 - 当链表中一个节点也没有的时候,head 直接指向 null。 + 链表中的最后一个节点指向 `null`。 + 当链表中一个节点也没有的时候,head 直接指向 `null`。 - ![链表的数据结构](https://user-images.githubusercontent.com/24516169/88271130-50aaec00-cd09-11ea-8910-eaf4f4509c6d.png) + ![链表的数据结构](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.37j1by46a120.png) - 给火车加上数据后的结构 - ![给火车加上数据后的结构](https://user-images.githubusercontent.com/24516169/88268878-b09f9380-cd05-11ea-9fc7-f2e96fe8c764.png) + ![给火车加上数据后的结构](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.664djiie2t80.png) -#### 链表中的常见操作 +### 链表中的常见操作 - `append(element)` 向链表尾部添加一个新的项。 - `insert(position, element)` 向链表的特定位置插入一个新的项。 @@ -75,9 +75,9 @@ - `size()` 返回链表包含的元素个数,与数组的 length 属性类似。 - `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。 -#### 单向链表的封装 +### 单向链表的封装 -##### 创建单向链表类 +#### 创建单向链表类 先创建单向链表类 LinkedList,添加基本属性,再逐步实现单向链表的常用方法。 @@ -100,9 +100,9 @@ class LinkedList { } ``` -##### 实现 append() 方法 +#### 实现 append() 方法 -###### 代码实现 +##### 代码实现 ```js // append() 往链表尾部追加数据 @@ -137,17 +137,17 @@ append(data) { } ``` -###### 过程图解 +##### 过程图解 -- 首先让 currentNode 指向第一个节点。 +- 首先让 `currentNode` 指向第一个节点。 - ![](https://user-images.githubusercontent.com/24516169/88273783-4854b000-cd0d-11ea-8840-176f2e8cc219.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5iskrdf4nu40.png) -- 通过 while 循环使 currentNode 指向最后一个节点,最后通过 currentNode.next = newNode,让最后一个节点指向新节点 newNode。 +- 通过 `while` 循环使 `currentNode` 指向最后一个节点,最后通过 `currentNode.next = newNode`,让最后一个节点指向新节点 `newNode`。 - ![](https://user-images.githubusercontent.com/24516169/88273806-50aceb00-cd0d-11ea-95f7-c1583464e123.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4mw3bx5g80m0.png) -###### 代码测试 +##### 代码测试 ```js const linkedList = new LinkedList(); @@ -158,11 +158,11 @@ linkedList.append("C"); console.log(linkedList); ``` -![](https://user-images.githubusercontent.com/24516169/88274253-f9f3e100-cd0d-11ea-970d-e39e8e0f3caa.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.6kx4qbq8e5c.png) -##### 实现 toString() 方法 +#### 实现 toString() 方法 -###### 代码实现 +##### 代码实现 ```js toString() { @@ -179,16 +179,16 @@ toString() { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 toString 方法 console.log(linkedList.toString()); //--> AA BB CC ``` -##### 实现 insert() 方法 +#### 实现 insert() 方法 -###### 代码实现 +##### 代码实现 ```js // insert() 在指定位置(position)插入节点 @@ -235,7 +235,7 @@ insert(position, data) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 insert 方法 @@ -244,11 +244,11 @@ linkedList.insert(2, "456"); console.log(linkedList.toString()); //--> 123 AA 456 BB CC ``` -##### 实现 getData() 方法 +#### 实现 getData() 方法 获取指定位置(position)的 data。 -###### 代码实现 +##### 代码实现 ```js getData(position) { @@ -267,7 +267,7 @@ getData(position) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 getData 方法 @@ -275,11 +275,11 @@ console.log(linkedList.getData(0)); //--> 123 console.log(linkedList.getData(1)); //--> AA ``` -##### 实现 indexOf() 方法 +#### 实现 indexOf() 方法 indexOf(data) 返回指定 data 的 index,如果没有,返回 -1。 -###### 代码实现 +##### 代码实现 ```js indexOf(data) { @@ -299,7 +299,7 @@ indexOf(data) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 indexOf 方法 @@ -307,11 +307,11 @@ console.log(linkedList.indexOf("AA")); //--> 1 console.log(linkedList.indexOf("ABC")); //--> -1 ``` -##### 实现 update() 方法 +#### 实现 update() 方法 update(position, data) 修改指定位置节点的 data。 -###### 代码实现 +##### 代码实现 ```js update(position, data) { @@ -333,7 +333,7 @@ update(position, data) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 update 方法 @@ -343,11 +343,11 @@ linkedList.update(1, "54321"); console.log(linkedList.toString()); //--> 12345 54321 456 BB CC ``` -##### 实现 removeAt() 方法 +#### 实现 removeAt() 方法 removeAt(position) 删除指定位置的节点。 -###### 代码实现 +##### 代码实现 ```js removeAt(position) { @@ -383,7 +383,7 @@ removeAt(position) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 removeAt 方法 @@ -391,11 +391,11 @@ linkedList.removeAt(3); console.log(linkedList.toString()); //--> 12345 54321 456 CC ``` -##### 实现 remove() 方法 +#### 实现 remove() 方法 remove(data) 删除指定 data 所在的节点。 -###### 代码实现 +##### 代码实现 ```js remove(data) { @@ -403,7 +403,7 @@ remove(data) { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 remove 方法 @@ -411,11 +411,11 @@ linkedList.remove("CC"); console.log(linkedList.toString()); //--> 12345 54321 456 ``` -##### 实现 isEmpty() 方法 +#### 实现 isEmpty() 方法 isEmpty() 判断链表是否为空。 -###### 代码实现 +##### 代码实现 ```js isEmpty() { @@ -423,18 +423,18 @@ isEmpty() { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 isEmpty 方法 console.log(linkedList.isEmpty()); //--> false ``` -##### 实现 size() 方法 +#### 实现 size() 方法 size() 获取链表的长度。 -###### 代码实现 +##### 代码实现 ```js size() { @@ -442,14 +442,14 @@ size() { } ``` -###### 代码测试 +##### 代码测试 ```js // 测试 size 方法 console.log(linkedList.size()); //--> 3 ``` -##### 完整实现 +#### 完整实现 ```js class LinkedList { @@ -657,3 +657,4 @@ class LinkedList { } } ``` + diff --git "a/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" "b/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" index 4c83a68..17ac61a 100644 --- "a/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" +++ "b/assets/doc/07_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\270\203\357\274\211\345\217\214\345\220\221\351\223\276\350\241\250.md" @@ -1,14 +1,14 @@ -## JavaScript 数据结构与算法(七)双向链表 +# JavaScript 数据结构与算法(七)双向链表 -### 单向链表和双向链表 +## 单向链表和双向链表 -#### 单向链表 +### 单向链表 - 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)。 - 链表相连的过程是单向的,实现原理是上一个节点中有指向下一个节点的引用。 - 单向链表有一个比较明显的缺点:可以轻松到达下一个节点,但回到前一个节点很难,在实际开发中, 经常会遇到需要回到上一个节点的情况。 -#### 双向链表 +### 双向链表 - 既可以从头遍历到尾,也可以从尾遍历到头。 - 链表相连的过程是双向的。实现原理是一个节点既有向前连接的引用,也有一个向后连接的引用。 @@ -17,23 +17,17 @@ - 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些。 - 相对于单向链表,所占内存空间更大一些。 - 但是,相对于双向链表的便利性而言,这些缺点微不足道。 - - +## 双向链表结构 - - - -### 双向链表结构 - -![](https://user-images.githubusercontent.com/24516169/88497604-724ef080-cff3-11ea-969b-3496e3a64ca6.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3xy769h90v20.png) - 双向链表不仅有 head 指针指向第一个节点,而且有 tail 指针指向最后一个节点。 - 每一个节点由三部分组成:item 储存数据、prev 指向前一个节点、next 指向后一个节点。 - 双向链表的第一个节点的 prev 指向 null。 - 双向链表的最后一个节点的 next 指向 null。 -### 双向链表常见的操作 +## 双向链表常见的操作 - `append(element)` 向链表尾部追加一个新元素。 - `insert(position, element)` 向链表的指定位置插入一个新元素。 @@ -42,18 +36,18 @@ - `update(position, element)` 修改指定位置上的元素。 - `removeAt(position)` 从链表中的删除指定位置的元素。 - `remove(element)` 从链表删除指定的元素。 -- `isEmpty()` 如果链表中不包含任何元素,返回 trun,如果链表长度大于 0 则返回 false。 -- `size()` 返回链表包含的元素个数,与数组的 length 属性类似。 -- `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。 +- `isEmpty()` 如果链表中不包含任何元素,返回 `trun`,如果链表长度大于 0 则返回 `false`。 +- `size()` 返回链表包含的元素个数,与数组的 `length` 属性类似。 +- `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 `toString` 方法,让其只输出元素的值。 - `forwardString()` 返回正向遍历节点字符串形式。 - `backwordString()` 返回反向遍历的节点的字符串形式。 -### 双向链表的封装 +## 双向链表的封装 -#### 创建双向链表类 DoublyLinkedList +### 创建双向链表类 DoublyLinkedList -- DoublyNode 类继承单向链表的 Node 类,新添加 this.prev 属性,该属性用于指向上一个节点。 -- DoublyLinkedList 类继承 LinkedList 类,新添加 this.tail 属性,该属性指向末尾的节点。 +- DoublyNode 类继承单向链表的 Node 类,新添加 `this.prev` 属性,该属性用于指向上一个节点。 +- DoublyLinkedList 类继承 LinkedList 类,新添加 `this.tail` 属性,该属性指向末尾的节点。 ```js // 双向链表的节点类(继承单向链表的节点类) @@ -66,16 +60,14 @@ class DoublyNode extends Node { // 双向链表类继承单向链表类 class DoublyLinkedList extends LinkedList { - constructor() { super(); this.tail = null; } - } ``` -#### append(element) +### append(element) ```js // append(element) 往双向链表尾部追加一个新的元素 @@ -101,7 +93,7 @@ this.length++; } ``` -#### insert(position, element) +### insert(position, element) ```js // insert(position, data) 插入元素 @@ -157,7 +149,7 @@ insert(position, element) { } ``` -#### insert(position, element) +### insert(position, element) ```js // insert(position, data) 插入元素 @@ -213,7 +205,7 @@ insert(position, element) { } ``` -#### removeAt(position) +### removeAt(position) ```js // removeAt() 删除指定位置的节点 @@ -259,7 +251,7 @@ insert(position, element) { } ``` -#### update(position, data) +### update(position, data) ```js // update(position, data) 修改指定位置的节点 @@ -274,7 +266,7 @@ insert(position, element) { } ``` -#### forwardToString() +### forwardToString() ```js // forwardToString() 链表数据从前往后以字符串形式返回 @@ -292,7 +284,7 @@ insert(position, element) { } ``` -#### backwardString() +### backwardString() ```js // backwardString() 链表数据从后往前以字符串形式返回 @@ -310,15 +302,14 @@ insert(position, element) { } ``` -#### 其他方法的实现 +### 其他方法的实现 -其他方法通过继承单向链表来实现。 +双向链表的其他方法通过继承单向链表来实现。 -#### 完整实现 +### 完整实现 ```js class DoublyLinkedList extends LinkedList { - constructor() { super(); this.tail = null; @@ -328,7 +319,6 @@ class DoublyLinkedList extends LinkedList { // append(element) 往双向链表尾部追加一个新的元素 // 重写 append() append(element) { - // 1、创建双向链表节点 const newNode = new DoublyNode(element); @@ -357,7 +347,8 @@ class DoublyLinkedList extends LinkedList { const newNode = new DoublyNode(element); // 3、判断多种插入情况 - if (position === 0) { // 在第 0 个位置插入 + if (position === 0) { + // 在第 0 个位置插入 if (this.head === null) { this.head = newNode; @@ -368,13 +359,14 @@ class DoublyLinkedList extends LinkedList { this.head.perv = newNode; this.head = newNode; } - - } else if (position === this.length) { // 在最后一个位置插入 + } else if (position === this.length) { + // 在最后一个位置插入 this.tail.next = newNode; newNode.prev = this.tail; this.tail = newNode; - } else { // 在 0 ~ this.length 位置中间插入 + } else { + // 在 0 ~ this.length 位置中间插入 let targetIndex = 0; let currentNode = this.head; @@ -417,23 +409,26 @@ class DoublyLinkedList extends LinkedList { // 2、根据不同情况删除元素 let currentNode = this.head; - if (position === 0) { // 删除第一个节点的情况 + if (position === 0) { + // 删除第一个节点的情况 - if (this.length === 1) { // 链表内只有一个节点的情况 + if (this.length === 1) { + // 链表内只有一个节点的情况 this.head = null; this.tail = null; - } else { // 链表内有多个节点的情况 + } else { + // 链表内有多个节点的情况 this.head = this.head.next; this.head.prev = null; } - - } else if (position === this.length - 1) { // 删除最后一个节点的情况 + } else if (position === this.length - 1) { + // 删除最后一个节点的情况 currentNode = this.tail; this.tail.prev.next = null; this.tail = this.tail.prev; - - } else { // 删除 0 ~ this.length - 1 里面节点的情况 + } else { + // 删除 0 ~ this.length - 1 里面节点的情况 let targetIndex = 0; let previousNode = null; @@ -444,7 +439,6 @@ class DoublyLinkedList extends LinkedList { previousNode.next = currentNode.next; currentNode.next.perv = previousNode; - } this.length--; @@ -477,15 +471,14 @@ class DoublyLinkedList extends LinkedList { return super.size(); } - // forwardToString() 链表数据从前往后以字符串形式返回 forwardToString() { let currentNode = this.head; - let result = ''; + let result = ""; // 遍历所有的节点,拼接为字符串,直到节点为 null while (currentNode) { - result += currentNode.data + '--'; + result += currentNode.data + "--"; currentNode = currentNode.next; } @@ -495,11 +488,11 @@ class DoublyLinkedList extends LinkedList { // backwardString() 链表数据从后往前以字符串形式返回 backwardString() { let currentNode = this.tail; - let result = ''; + let result = ""; // 遍历所有的节点,拼接为字符串,直到节点为 null while (currentNode) { - result += currentNode.data + '--'; + result += currentNode.data + "--"; currentNode = currentNode.prev; } @@ -508,27 +501,27 @@ class DoublyLinkedList extends LinkedList { } ``` -#### 代码测试 +### 代码测试 ```js const doublyLinkedList = new DoublyLinkedList(); // append() 测试 -doublyLinkedList.append('ZZ'); -doublyLinkedList.append('XX'); -doublyLinkedList.append('CC'); +doublyLinkedList.append("ZZ"); +doublyLinkedList.append("XX"); +doublyLinkedList.append("CC"); console.log(doublyLinkedList); // insert() 测试 -doublyLinkedList.insert(0, '00'); -doublyLinkedList.insert(2, '22'); +doublyLinkedList.insert(0, "00"); +doublyLinkedList.insert(2, "22"); console.log(doublyLinkedList); // getData() 测试 console.log(doublyLinkedList.getData(1)); //--> ZZ // indexOf() 测试 -console.log(doublyLinkedList.indexOf('XX')); //--> 3 +console.log(doublyLinkedList.indexOf("XX")); //--> 3 console.log(doublyLinkedList); // removeAt() 测试 @@ -537,12 +530,12 @@ doublyLinkedList.removeAt(1); console.log(doublyLinkedList); // update() 测试 -doublyLinkedList.update(0, '111111'); +doublyLinkedList.update(0, "111111"); console.log(doublyLinkedList); // remove() 测试 -console.log(doublyLinkedList.remove('111111')); -console.log(doublyLinkedList.remove('22222')); +console.log(doublyLinkedList.remove("111111")); +console.log(doublyLinkedList.remove("22222")); console.log(doublyLinkedList); // forwardToString() 测试 diff --git "a/assets/doc/08_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\253\357\274\211\351\233\206\345\220\210.md" "b/assets/doc/08_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\253\357\274\211\351\233\206\345\220\210.md" index d35f8ba..b82cfb6 100644 --- "a/assets/doc/08_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\253\357\274\211\351\233\206\345\220\210.md" +++ "b/assets/doc/08_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\205\253\357\274\211\351\233\206\345\220\210.md" @@ -22,9 +22,9 @@ ES6 中的 `Set` 就是一个集合类,这里我们重新封装一个 `Set` - `add(value)` 向集合添加一个新的项。 - `remove(value)` 从集合移除一个值。 -- `has(value)` 如果值在集合中,返回 true,否则返回 false。 +- `has(value)` 如果值在集合中,返回 `true`,否则返回` false`。 - `clear()` 移除集合中的所有项。 -- `size()` 返回集合所包含元素的数量。与数组的 length 属性类似。 +- `size()` 返回集合所包含元素的数量。与数组的 `length` 属性类似。 - `values()` 返回一个包含集合中所有值的数组。 - 还有其他的方法,用的不多,这里不做封装。 @@ -111,7 +111,7 @@ console.log(set.values()); //--> [] - 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。 - 子集:验证一个给定集合是否是另一个集合的子集。 -![](https://user-images.githubusercontent.com/24516169/88532735-b1069a00-d037-11ea-9ece-e19b2b8a09e2.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4utjffapm1w0.png) #### 并集的实现 @@ -197,7 +197,6 @@ subset(otherSet) { ```js // 集合结构的封装 export default class Set { - constructor() { this.items = {}; } @@ -257,7 +256,6 @@ export default class Set { // intersection() 求两个集合的交集 intersection(otherSet) { - // 1、创建一个新集合 let intersectionSet = new Set(); @@ -273,7 +271,6 @@ export default class Set { // difference() 差集 difference(otherSet) { - // 1、创建一个新集合 let differenceSet = new Set(); @@ -289,7 +286,6 @@ export default class Set { // subset() 子集 subset(otherSet) { - // 从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,有不存在的返回 false // 遍历完所有的,返回 true for (let value of this.values()) { @@ -299,6 +295,5 @@ export default class Set { } return true; } - } ``` diff --git "a/assets/doc/09_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\271\235\357\274\211\345\255\227\345\205\270.md" "b/assets/doc/09_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\271\235\357\274\211\345\255\227\345\205\270.md" index 3876567..83a204d 100644 --- "a/assets/doc/09_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\271\235\357\274\211\345\255\227\345\205\270.md" +++ "b/assets/doc/09_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\344\271\235\357\274\211\345\255\227\345\205\270.md" @@ -6,14 +6,14 @@ - 字典存储的是**键值对**,主要特点是**一一对应**。 - 比如保存一个人的信息 - - 数组形式:`[19,‘Tom’,1.65]`,可通过下标值取出信息。 - - 字典形式:`{"age":19,"name":"Tom","height":165}`,可以通过 `key` 取出 `value`。 + - 数组形式:`[19,"Tom", 1.65]`,可通过下标值取出信息。 + - 字典形式:`{"age": 19, "name": "Tom", "height": 165}`,可以通过 `key` 取出 `value`。 - 此外,在字典中 key 是不能重复且无序的,而 Value 可以重复。 ### 字典和映射的关系 -- 有些编程语言中称这种映射关系为**字典**,如 Swift 中的 Dictonary,Python 中的 dict。 -- 有些编程语言中称这种映射关系为 **Map**,比如 Java 中的 HashMap 和 TreeMap 等。 +- 有些编程语言中称这种映射关系为**字典**,如 Swift 中的 `Dictonary`,Python 中的 `dict`。 +- 有些编程语言中称这种映射关系为 **Map**,比如 Java 中的 `HashMap` 和 `TreeMap` 等。 ### 字典常见的操作 diff --git "a/assets/doc/10_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\357\274\211\345\223\210\345\270\214\350\241\250.md" "b/assets/doc/10_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\357\274\211\345\223\210\345\270\214\350\241\250.md" index 87c6b0a..a9d4b65 100644 --- "a/assets/doc/10_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\357\274\211\345\223\210\345\270\214\350\241\250.md" +++ "b/assets/doc/10_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\357\274\211\345\223\210\345\270\214\350\241\250.md" @@ -6,7 +6,7 @@ 哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势: -- 哈希表可以提供非常快速的**插入-删除-查找**操作。 +- 哈希表可以提供非常快速的 **插入-删除-查找** 操作。 - 无论多少数据,插入和删除值都只需接近常量的时间,即 **O(1)** 的时间复杂度。实际上,只需要几个机器指令即可完成。 - 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。 - 哈希表相对于树来说编码要简单得多。 @@ -14,7 +14,7 @@ 哈希表同样存在不足之处: - 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。 -- 通常情况下,哈希表中的 key 是不允许重复的,不能放置相同的 key,用于保存不同的元素。 +- 通常情况下,哈希表中的 `key` 是不允许重复的,不能放置相同的 `key`,用于保存不同的元素。 哈希表是什么? @@ -79,7 +79,7 @@ 如下图所示,我们将每一个数字都对 10 进行取余操作,则余数的范围 0~9 作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。 -![](https://user-images.githubusercontent.com/24516169/88752895-8b8ba480-d18d-11ea-9c81-a827c70c9c87.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5irlba213e00.png) 这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。 @@ -89,7 +89,7 @@ 开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。 -![](https://user-images.githubusercontent.com/24516169/88753076-f2a95900-d18d-11ea-8b48-00e07ae50874.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.52qzixg5w4s0.png) 根据探测空白单元格位置方式的不同,可分为三种方法: @@ -124,7 +124,7 @@ - 比如插入 13 时就会发现,连续的单元 3~7 都不允许插入数据,并且在插入的过程中需要经历多次这种情况。二次探测法可以解决该问题。 -![](https://user-images.githubusercontent.com/24516169/88754562-99dbbf80-d191-11ea-97bf-3ce61a6e3c2d.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.47l36021j8m0.png) ##### 二次探测 @@ -183,19 +183,19 @@ 可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。 - ![](https://user-images.githubusercontent.com/24516169/88767432-3743ed00-d1ac-11ea-9cb3-f78f168a2565.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.2pcxv1f720o0.png) - 二次探测和再哈希化的性能 二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。 - ![](https://user-images.githubusercontent.com/24516169/88767440-3ad77400-d1ac-11ea-8ca2-7146e21a35a3.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.f06tizypf6g.png) - 链地址法的性能 可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如 Java 中的 HashMap 中使用的就是链地址法。 - ![](https://user-images.githubusercontent.com/24516169/88767455-3dd26480-d1ac-11ea-9393-b4687a5d87ca.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.45s3ntwwjia0.png) ### 哈希函数 @@ -210,7 +210,7 @@ 霍纳法则:在中国霍纳法则也叫做秦久韶算法,具体算法为: -![](https://user-images.githubusercontent.com/24516169/88768791-272d0d00-d1ae-11ea-8f64-450d82013781.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4kz61djvvau0.png) 求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求 n 次多项式 f(x)的值就转化为求 n 个一次多项式的值。 @@ -239,7 +239,7 @@ Java 中的 HashMap 采用的是链地址法,哈希化采用的是公式为: - `put(key, value)` 插入或修改操作。 - `get(key)` 获取哈希表中特定位置的元素。 - `remove(key)` 删除哈希表中特定位置的元素。 -- `isEmpty()` 如果哈希表中不包含任何元素,返回 trun,如果哈希表长度大于 0 则返回 false。 +- `isEmpty()` 如果哈希表中不包含任何元素,返回 `trun`,如果哈希表长度大于 0 则返回 `false`。 - `size()` 返回哈希表包含的元素个数。 - `resize(value)` 对哈希表进行扩容操作。 @@ -279,7 +279,7 @@ console.log(hashFn("abc")); //--> 6 封装的哈希表的数据结构模型: -![](https://user-images.githubusercontent.com/24516169/88771995-a6bcdb00-d1b2-11ea-95cf-e2989b65c76c.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.7h3eahcbrvs0.png) 首先创建哈希表类 HashTable,并添加必要的属性和上面实现的哈希函数,再进行其他方法的实现。 @@ -295,9 +295,9 @@ class HashTable { #### put(key,value) -哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个 `[key,value]` 时,如果原来不存在该 key,那么就是插入操作,如果原来已经存在该 key,那么就是修改操作。 +哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个 `[key, value]` 时,如果原来不存在该 key,那么就是插入操作,如果原来已经存在该 key,那么就是修改操作。 -![](https://user-images.githubusercontent.com/24516169/88772561-89d4d780-d1b3-11ea-9152-d9c1dc89f08b.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.2a81gwdznn8k.png) 实现思路: @@ -349,11 +349,11 @@ put(key, value) { 实现思路: -- 首先,根据 key 通过哈希函数获取它在 storage 中对应的索引值 index。 -- 然后,根据索引值获取对应的 bucket。 -- 接着,判断获取到的 bucket 是否为 null,如果为 null,直接返回 null。 -- 随后,线性遍历 bucket 中每一个 key 是否等于传入的 key。如果等于,直接返回对应的 value。 -- 最后,遍历完 bucket 后,仍然没有找到对应的 key,直接 return null 即可。 +- 首先,根据 key 通过哈希函数获取它在 `storage` 中对应的索引值 `index`。 +- 然后,根据索引值获取对应的 `bucket`。 +- 接着,判断获取到的 `bucket` 是否为 `null`,如果为 `null`,直接返回 `null`。 +- 随后,线性遍历 `bucket` 中每一个 `key` 是否等于传入的 `key`。如果等于,直接返回对应的 `value`。 +- 最后,遍历完 `bucket` 后,仍然没有找到对应的 `key`,直接 `return null` 即可。 代码实现 @@ -381,11 +381,11 @@ get(key) { 实现思路: -- 首先,根据 key 通过哈希函数获取它在 storage 中对应的索引值 index。 -- 然后,根据索引值获取对应的 bucket。 -- 接着,判断获取到的 bucket 是否为 null,如果为 null,直接返回 null。 -- 随后,线性查找 bucket,寻找对应的数据,并且删除。 -- 最后,依然没有找到,返回 null。 +- 首先,根据 key 通过哈希函数获取它在 `storage` 中对应的索引值 `index`。 +- 然后,根据索引值获取对应的 `bucket`。 +- 接着,判断获取到的 `bucket` 是否为 `null`,如果为 `null`,直接返回 `null`。 +- 随后,线性查找 `bucket`,寻找对应的数据,并且删除。 +- 最后,依然没有找到,返回 `null`。 ```js // remove(key) 删除指定 key 的数据 @@ -440,11 +440,11 @@ size() { - 前面我们在哈希表中使用的是长度为 7 的数组,由于使用的是链地址法,装填因子(loadFactor)可以大于 1,所以这个哈希表可以无限制地插入新数据。 -- 但是,随着数据量的增多,storage 中每一个 index 对应的 bucket 数组(链表)就会越来越长,这就会造成哈希表效率的降低。 +- 但是,随着数据量的增多,storage 中每一个 `index` 对应的 `bucket` 数组(链表)就会越来越长,这就会造成哈希表效率的降低。 什么情况下需要扩容? -- 常见的情况是 loadFactor > 0.75 的时候进行扩容。 +- 常见的情况是 `loadFactor > 0.75` 的时候进行扩容。 如何进行扩容? @@ -453,15 +453,15 @@ size() { 实现思路: -- 首先,定义一个变量,比如 oldStorage 指向原来的 storage。 -- 然后,创建一个新的容量更大的数组,让 this.storage 指向它。 -- 最后,将 oldStorage 中的每一个 bucket 中的每一个数据取出来依次添加到 this.storage 指向的新数组中。 +- 首先,定义一个变量,比如 oldStorage 指向原来的 `storage`。 +- 然后,创建一个新的容量更大的数组,让 `this.storage` 指向它。 +- 最后,将 oldStorage 中的每一个 bucket 中的每一个数据取出来依次添加到 `this.storage` 指向的新数组中。 -![](https://user-images.githubusercontent.com/24516169/88775673-91967b00-d1b7-11ea-8b12-feb7a656f70e.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.7xrayvjwh2w.png) ### resize() 的实现 -装填因子 = 哈希表中数据 / 哈希表长度,即 loadFactor = count / HashTable.length。 +装填因子 = 哈希表中数据 / 哈希表长度,即 `loadFactor = count / HashTable.length`。 resize 方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩。 @@ -489,7 +489,7 @@ resize(newLimit) { } ``` -- 通常情况下当装填因子 laodFactor > 0.75 时,对哈希表进行扩容。在哈希表中的添加方法(push 方法)中添加如下代码,判断是否需要调用扩容函数进行扩容。 +- 通常情况下当装填因子 `laodFactor > 0.75` 时,对哈希表进行扩容。在哈希表中的添加方法(push 方法)中添加如下代码,判断是否需要调用扩容函数进行扩容。 ```js // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 @@ -498,7 +498,7 @@ resize(newLimit) { } ``` -* 当装填因子 laodFactor < 0.25 时,对哈希表容量进行压缩。在哈希表中的删除方法(remove 方法)中添加如下代码,判断是否需要调用扩容函数进行压缩。 +* 当装填因子 `laodFactor < 0.25` 时,对哈希表容量进行压缩。在哈希表中的删除方法(remove 方法)中添加如下代码,判断是否需要调用扩容函数进行压缩。 ```js // 根据装填因子的大小,判断是否要进行哈希表压缩 @@ -548,9 +548,9 @@ resize(newLimit) { 实现思路: -2 倍扩容或压缩之后,通过循环调用 isPrime 判断得到的容量是否为质数,不是则+1,直到是为止。比如原长度:7,2 倍扩容后长度为 14,14 不是质数,14 + 1 = 15 不是质数,15 + 1 = 16 不是质数,16 + 1 = 17 是质数,停止循环,由此得到质数 17。 +2 倍扩容或压缩之后,通过循环调用 `isPrime` 判断得到的容量是否为质数,不是则+1,直到是为止。比如原长度:7,2 倍扩容后长度为 14,14 不是质数,`14 + 1 = 15` 不是质数,`15 + 1 = 16` 不是质数,`16 + 1 = 17` 是质数,停止循环,由此得到质数 17。 -- 第一步:首先需要为 HashTable 类添加判断质数的 isPrime 方法和获取质数的 getPrime 方法: +- 第一步:首先需要为 HashTable 类添加判断质数的 `isPrime` 方法和获取质数的 `getPrime` 方法: ```js // getPrime(number) 根据传入的 number 获取最临近的质数 @@ -562,9 +562,9 @@ resize(newLimit) { } ``` -- 修改添加元素的 put 方法和删除元素的 remove 方法中关于数组扩容的相关操作: +- 修改添加元素的 `put` 方法和删除元素的 `remove` 方法中关于数组扩容的相关操作: - 在 put 方法中添加如下代码: + 在 `put` 方法中添加如下代码: ```js // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 @@ -573,7 +573,7 @@ resize(newLimit) { } ``` - 在 remove 方法中添加如下代码: + 在 `remove` 方法中添加如下代码: ```js // 根据装填因子的大小,判断是否要进行哈希表压缩 @@ -586,11 +586,10 @@ resize(newLimit) { ```js class HashTable { - constructor() { this.storage = []; // 哈希表存储数据的变量 this.count = 0; // 当前存放的元素个数 - this.limit = 7; // 哈希表长度(初始设为质数 7) + this.limit = 7; // 哈希表长度(初始设为质数 7) // 装填因子(已有个数/总个数) this.loadFactor = 0.75; @@ -607,7 +606,6 @@ class HashTable { // put(key, value) 往哈希表里添加数据 put(key, value) { - // 1、根据 key 获取要映射到 storage 里面的 index(通过哈希函数获取) const index = hashFn(key, this.limit); @@ -616,14 +614,15 @@ class HashTable { // 3、判断是否存在 bucket if (bucket === undefined) { - bucket = []; // 不存在则创建 + bucket = []; // 不存在则创建 this.storage[index] = bucket; } // 4、判断是插入数据操作还是修改数据操作 for (let i = 0; i < bucket.length; i++) { let tuple = bucket[i]; // tuple 的格式:[key, value] - if (tuple[0] === key) { // 如果 key 相等,则修改数据 + if (tuple[0] === key) { + // 如果 key 相等,则修改数据 tuple[1] = value; return; // 修改完 tuple 里数据,return 终止,不再往下执行。 } @@ -637,12 +636,10 @@ class HashTable { if (this.count / this.limit > this.loadFactor) { this.resize(this.getPrime(this.limit * 2)); } - } // 根据 get(key) 获取 value get(key) { - const index = hashFn(key, this.limit); const bucket = this.storage[index]; @@ -660,7 +657,6 @@ class HashTable { // remove(key) 删除指定 key 的数据 remove(key) { - const index = hashFn(key, this.limit); const bucket = this.storage[index]; @@ -682,9 +678,7 @@ class HashTable { return tuple; } - } - } isEmpty() { @@ -697,7 +691,6 @@ class HashTable { // 重新调整哈希表大小,扩容或压缩 resize(newLimit) { - // 1、保存旧的 storage 数组内容 const oldStorage = this.storage; @@ -713,10 +706,8 @@ class HashTable { this.put(b[0], b[1]); } } - } } - - } ``` + diff --git "a/assets/doc/11_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\200\357\274\211\346\240\221.md" "b/assets/doc/11_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\200\357\274\211\346\240\221.md" index b50ff3a..3a772ec 100644 --- "a/assets/doc/11_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\200\357\274\211\346\240\221.md" +++ "b/assets/doc/11_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\200\357\274\211\346\240\221.md" @@ -6,7 +6,7 @@ #### 真实的树: -![](https://user-images.githubusercontent.com/24516169/89281453-b2117a00-d67c-11ea-946c-14d1c33af6ce.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.6pnzzxbinjs0.png) #### 树的特点: @@ -16,7 +16,7 @@ 现实生活中很多结构都是树的抽象,模拟的树结构相当于旋转 `180°` 的树。 -![](https://user-images.githubusercontent.com/24516169/89281620-ed13ad80-d67c-11ea-83d3-658cf8a90fa1.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4mrygbtnd6w0.png) #### 树结构对比于数组/链表/哈希表有哪些优势呢? @@ -47,11 +47,11 @@ - 对于任意一棵非空树(n > 0),它具备以下性质: - 数中有一个称为根(Root)的特殊节点,用 **r** 表示; - - 其余节点可分为 m(m > 0)个互不相交的有限集合 T~1~,T~2~,...,T~m~,其中每个集合本身又是一棵树,称为原来树的子树(SubTree)。 + - 其余节点可分为 m(m > 0)个互不相交的有限集合 T1,T2,...,Tm,其中每个集合本身又是一棵树,称为原来树的子树(SubTree)。 #### 树的常用术语: -![](https://user-images.githubusercontent.com/24516169/89282702-7ecfea80-d67e-11ea-9dbb-ff1ec3bd3707.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3t0ypfn5leo0.png) - 节点的度(Degree):节点的子树个数,比如节点 B 的度为 2; - 树的度:树的所有节点中最大的度数,如上图树的度为 2; @@ -67,7 +67,7 @@ ##### 最普通的表示方法: -![](https://user-images.githubusercontent.com/24516169/89384924-6ec51300-d731-11ea-9659-fc6d3d168499.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4v9sayu9zu60.png) 如图,树结构的组成方式类似于链表,都是由一个个节点连接构成。不过,根据每个父节点子节点数量的不同,每一个父节点需要的引用数量也不同。比如节点 A 需要 3 个引用,分别指向子节点 B,C,D;B 节点需要 2 个引用,分别指向子节点 E 和 F;K 节点由于没有子节点,所以不需要引用。 @@ -75,7 +75,7 @@ ##### 儿子-兄弟表示法: -![](https://user-images.githubusercontent.com/24516169/89385103-afbd2780-d731-11ea-8236-2f4db1a2a4cf.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3o34yy6h0420.png) 这种表示方法可以完整地记录每个节点的数据,比如: @@ -111,158 +111,10 @@ Node{ 以下为儿子-兄弟表示法组成的树结构: -![](https://user-images.githubusercontent.com/24516169/89385393-1e01ea00-d732-11ea-92d7-21d27ace4d25.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.6tucreh71ok0.png) 将其顺时针旋转 45° 之后: -![](https://user-images.githubusercontent.com/24516169/89385455-3a9e2200-d732-11ea-910e-396b4fe99b0b.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4blmsiyhevg0.png) 这样就成为了一棵二叉树,由此我们可以得出结论:任何树都可以通过二叉树进行模拟。但是这样父节点不是变了吗?其实,父节点的设置只是为了方便指向子节点,在代码实现中谁是父节点并没有关系,只要能正确找到对应节点即可。 - -### 二叉树 - -#### 二叉树的概念 - -如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树; - -#### 二叉树的组成 - -- 二叉树可以为空,也就是没有节点; -- 若二叉树不为空,则它由根节点和称为其左子树 TL 和右子树 TR 的两个不相交的二叉树组成; - -#### 二叉树的五种形态 - -![](https://user-images.githubusercontent.com/24516169/89387159-aed9c500-d734-11ea-8838-8a72d61070e5.png) - -上图分别表示:空的二叉树、只有一个节点的二叉树、只有左子树 TL 的二叉树、只有右子树 TR 的二叉树和有左右两个子树的二叉树。 - -#### 二叉树的特性 - -- 一个二叉树的第 i 层的最大节点树为:2^(i-1)^,i >= 1; -- 深度为 k 的二叉树的最大节点总数为:2^k^ - 1 ,k >= 1; -- 对任何非空二叉树,若 n~0~ 表示叶子节点的个数,n~2~表示度为 2 的非叶子节点个数,那么两者满足关系:n~0~ = n~2~ + 1;如下图所示:H,E,I,J,G 为叶子节点,总数为 5;A,B,C,F 为度为 2 的非叶子节点,总数为 4;满足 n~0~ = n~2~ + 1 的规律。 - -![](https://user-images.githubusercontent.com/24516169/89387356-f2ccca00-d734-11ea-9b04-ae65080c701c.png) - -#### 特殊的二叉树 - -##### 完美二叉树 - -完美二叉树(Perfect Binary Tree)也成为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有 2 个子节点,这就构成了完美二叉树。 - -![](https://user-images.githubusercontent.com/24516169/89387498-214aa500-d735-11ea-9766-255b3197e69d.png) - -##### 完全二叉树 - -完全二叉树(Complete Binary Tree): - -- 除了二叉树最后一层外,其他各层的节点数都达到了最大值; -- 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点; -- 完美二叉树是特殊的完全二叉树; - -![](https://user-images.githubusercontent.com/24516169/89387606-522ada00-d735-11ea-84af-18530ee0bd77.png) - -在上图中,由于 H 缺失了右子节点,所以它不是完全二叉树。 - -#### 二叉树的数据存储 - -常见的二叉树存储方式为数组和链表: - -##### 使用数组 - -- 完全二叉树:按从上到下,从左到右的方式存储数据。 - -![](https://user-images.githubusercontent.com/24516169/89388887-388a9200-d737-11ea-8608-321fb195fbc2.png) - -| 节点 | A | B | C | D | E | F | G | H | I | -| ---- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | -| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | - -使用数组存储时,取数据的时候也十分方便:左子节点的序号等于父节点序号 _ 2,右子节点的序号等于父节点序号 _ 2 + 1 。 - -- 非完全二叉树:非完全二叉树需要转换成完全二叉树才能按照上面的方案存储,这样会浪费很大的存储空间。 - -![](https://user-images.githubusercontent.com/24516169/89389454-0168b080-d738-11ea-881f-299c84436397.png) - -| 节点 | A | B | C | ^ | ^ | F | ^ | ^ | ^ | ^ | ^ | ^ | M | -| ---- | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | -| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | - -##### 使用链表 - -二叉树最常见的存储方式为链表:每一个节点封装成一个 Node,Node 中包含存储的数据、左节点的引用和右节点的引用。 - -![](https://user-images.githubusercontent.com/24516169/89389914-a08da800-d738-11ea-8597-2da99132edf3.png) - -### 二叉搜索树 - -二叉搜索树(BST,Binary Search Tree),也称为二叉排序树和二叉查找树。 - -二叉搜索树是一棵二叉树,可以为空。 - -如果不为空,则满足以下性质: - -- 条件 1:非空左子树的所有键值小于其根节点的键值。比如三中节点 6 的所有非空左子树的键值都小于 6; -- 条件 2:非空右子树的所有键值大于其根节点的键值;比如三中节点 6 的所有非空右子树的键值都大于 6; -- 条件 3:左、右子树本身也都是二叉搜索树; - -![](https://user-images.githubusercontent.com/24516169/89390195-f8c4aa00-d738-11ea-92c7-789e7bd7fd8b.png) - -如上图所示,树二和树三符合 3 个条件属于二叉树,树一不满足条件 3 所以不是二叉树。 - -总结:二叉搜索树的特点主要是较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中“搜索”的来源。 - -#### 二叉搜索树应用举例 - -下面是一个二叉搜索树: - -![](https://user-images.githubusercontent.com/24516169/89390510-67a20300-d739-11ea-8bb3-1f1cdf0e5aa9.png) - -若想在其中查找数据 10,只需要查找 4 次,查找效率非常高。 - -- 第 1 次:将 10 与根节点 9 进行比较,由于 10 > 9,所以 10 下一步与根节点 9 的右子节点 13 比较; -- 第 2 次:由于 10 < 13,所以 10 下一步与父节点 13 的左子节点 11 比较; -- 第 3 次:由于 10 < 11,所以 10 下一步与父节点 11 的左子节点 10 比较; -- 第 4 次:由于 10 = 10,最终查找到数据 10 。 - -![](https://user-images.githubusercontent.com/24516169/89390596-87d1c200-d739-11ea-9438-2061622f2e81.png) - -同样是 15 个数据,在排序好的数组中查询数据 10,需要查询 10 次: - -![](https://user-images.githubusercontent.com/24516169/89390662-a0da7300-d739-11ea-8707-296660ed838a.png) - -其实:如果是排序好的数组,可以通过二分查找:第一次找9,第二次找13,第三次找15...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。 - - -#### 二叉搜索树的封装 - -二叉搜索树有四个最基本的属性:指向节点的根(root),节点中的键(key)、左指针(right)、右指针(right)。 - -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.6hwur70i0tk0.png) - -所以,二叉搜索树中除了定义root属性外,还应定义一个节点内部类,里面包含每个节点中的left、right和key三个属性: - -```js -// 节点类 -class Node { - - constructor(key) { - this.key = key; - this.left = null; - this.right = null; - } - -} - -``` - -二叉搜索树的常见操作: - -- insert(key):向树中插入一个新的键; -- search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false; -- inOrderTraverse:通过中序遍历方式遍历所有节点; -- preOrderTraverse:通过先序遍历方式遍历所有节点; -- postOrderTraverse:通过后序遍历方式遍历所有节点; -- min:返回树中最小的值/键; -- max:返回树中最大的值/键; -- remove(key):从树中移除某个键; diff --git "a/assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\240\221.md" "b/assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 0000000..7544ff0 --- /dev/null +++ "b/assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,77 @@ +# JavaScript 数据结构与算法(十二)二叉树 + +## 二叉树 + +### 二叉树的概念 + +如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树; + +### 二叉树的组成 + +- 二叉树可以为空,也就是没有节点; +- 若二叉树不为空,则它由根节点和称为其左子树 TL 和右子树 TR 的两个不相交的二叉树组成; + +### 二叉树的五种形态 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.15ycsg4fqoio.png) + +上图分别表示:空的二叉树、只有一个节点的二叉树、只有左子树 TL 的二叉树、只有右子树 TR 的二叉树和有左右两个子树的二叉树。 + +### 二叉树的特性 + +- 一个二叉树的第 i 层的最大节点树为:2^(i-1)^,i >= 1; +- 深度为 k 的二叉树的最大节点总数为:2^k^ - 1 ,k >= 1; +- 对任何非空二叉树,若 n~0~ 表示叶子节点的个数,n~2~表示度为 2 的非叶子节点个数,那么两者满足关系:n~0~ = n~2~ + 1;如下图所示:H,E,I,J,G 为叶子节点,总数为 5;A,B,C,F 为度为 2 的非叶子节点,总数为 4;满足 n~0~ = n~2~ + 1 的规律。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.syjwffjltmo.png) + +### 特殊的二叉树 + +#### 完美二叉树 + +完美二叉树(Perfect Binary Tree)也成为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有 2 个子节点,这就构成了完美二叉树。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.223b2axrocf4.png) + +#### 完全二叉树 + +完全二叉树(Complete Binary Tree): + +- 除了二叉树最后一层外,其他各层的节点数都达到了最大值; +- 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点; +- 完美二叉树是特殊的完全二叉树; + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5y4rglrp8qk0.png) + +在上图中,由于 H 缺失了右子节点,所以它不是完全二叉树。 + +### 二叉树的数据存储 + +常见的二叉树存储方式为数组和链表: + +#### 使用数组 + +- 完全二叉树:按从上到下,从左到右的方式存储数据。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.29w4k62b51og.png) + +| 节点 | A | B | C | D | E | F | G | H | I | +| :--: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | + +使用数组存储时,取数据的时候也十分方便:左子节点的序号等于父节点序号 _ 2,右子节点的序号等于父节点序号 _ 2 + 1 。 + +- 非完全二叉树:非完全二叉树需要转换成完全二叉树才能按照上面的方案存储,这样会浪费很大的存储空间。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4jgiq6r2xee0.png) + +| 节点 | A | B | C | ^ | ^ | F | ^ | ^ | ^ | ^ | ^ | ^ | M | +| :--: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | + +#### 使用链表 + +二叉树最常见的存储方式为链表:每一个节点封装成一个 Node,Node 中包含存储的数据、左节点的引用和右节点的引用。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.2mlscfad5420.png) + diff --git "a/assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" similarity index 80% rename from "assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" rename to "assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" index 324415a..c3ed157 100644 --- "a/assets/doc/12_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\272\214\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" +++ "b/assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -1,8 +1,49 @@ -# 二叉搜索树的封装 +# JavaScript 数据结构与算法(十三)二叉搜索树 + +## 二叉搜索树 + +二叉搜索树(BST,Binary Search Tree),也称为二叉排序树和二叉查找树。 + +二叉搜索树是一棵二叉树,可以为空。 + +如果不为空,则满足以下性质: + +- 条件 1:非空左子树的所有键值小于其根节点的键值。比如三中节点 6 的所有非空左子树的键值都小于 6; +- 条件 2:非空右子树的所有键值大于其根节点的键值;比如三中节点 6 的所有非空右子树的键值都大于 6; +- 条件 3:左、右子树本身也都是二叉搜索树; + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.1lhxcdph4vpc.png) + +如上图所示,树二和树三符合 3 个条件属于二叉树,树一不满足条件 3 所以不是二叉树。 + +总结:二叉搜索树的特点主要是较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中“搜索”的来源。 + +## 二叉搜索树应用举例 + +下面是一个二叉搜索树: + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3l21fsg6qbc0.png) + +若想在其中查找数据 10,只需要查找 4 次,查找效率非常高。 + +- 第 1 次:将 10 与根节点 9 进行比较,由于 10 > 9,所以 10 下一步与根节点 9 的右子节点 13 比较; +- 第 2 次:由于 10 < 13,所以 10 下一步与父节点 13 的左子节点 11 比较; +- 第 3 次:由于 10 < 11,所以 10 下一步与父节点 11 的左子节点 10 比较; +- 第 4 次:由于 10 = 10,最终查找到数据 10 。 + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5x9xyvmbxy80.png) + +同样是 15 个数据,在排序好的数组中查询数据 10,需要查询 10 次: + +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.2gqz4t2jekw0.png) + +其实:如果是排序好的数组,可以通过二分查找:第一次找 9,第二次找 13,第三次找 15...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。 + +## 二叉搜索树的封装 二叉搜索树有四个最基本的属性:指向节点的根(root),节点中的键(key)、左指针(right)、右指针(right)。 -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.6hwur70i0tk0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5vdbb5to1n40.png) 所以,二叉搜索树中除了定义 root 属性外,还应定义一个节点内部类,里面包含每个节点中的 left、right 和 key 三个属性。 @@ -17,7 +58,7 @@ class Node { } ``` -## 二叉搜索树的常见操作: +### 二叉搜索树的常见操作: - `insert(key)` 向树中插入一个新的键。 - `search(key)` 在树中查找一个键,如果节点存在,则返回 true;如果不存在,则返回 `false`。 @@ -28,7 +69,7 @@ class Node { - `max` 返回树中最大的值/键。 - `remove(key)` 从树中移除某个键。 -### 插入数据 +#### 插入数据 实现思路: @@ -94,7 +135,7 @@ insertNode(root, node) { } ``` -### 遍历数据 +#### 遍历数据 这里所说的树的遍历不仅仅针对二叉搜索树,而是适用于所有的二叉树。由于树结构不是线性结构,所以遍历方式有多种选择,常见的三种二叉树遍历方式为: @@ -104,7 +145,7 @@ insertNode(root, node) { 还有层序遍历,使用较少。 -#### 先序遍历 +##### 先序遍历 先序遍历的过程为: @@ -112,7 +153,7 @@ insertNode(root, node) { 然后,遍历其左子树; 最后,遍历其右子树; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.1p2834z59wlc.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.69ooahvtbbo0.png) 如上图所示,二叉树的节点遍历顺序为:A -> B -> D -> H -> I -> E -> C -> F -> G。 @@ -134,7 +175,7 @@ preorderTraversalNode(node, result) { } ``` -#### 中序遍历 +##### 中序遍历 实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。 @@ -144,7 +185,7 @@ preorderTraversalNode(node, result) { 过程图解: -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.4xyrtyb08v80.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.762l4sxdr7o0.png) 输出节点的顺序应为:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 。 @@ -166,7 +207,7 @@ inorderTraversalNode(node, result) { } ``` -#### 后序遍历 +##### 后序遍历 实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。 @@ -176,7 +217,7 @@ inorderTraversalNode(node, result) { 过程图解: -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.67jks5h348g0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.5lqmen4jds80.png) 输出节点的顺序应为:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 。 @@ -198,17 +239,17 @@ postorderTraversalNode(node, result) { } ``` -#### 总结 +##### 总结 以遍历根(父)节点的顺序来区分三种遍历方式。比如:先序遍历先遍历根节点、中序遍历第二遍历根节点、后续遍历最后遍历根节点。 -### 查找数据 +#### 查找数据 -#### 查找最大值或最小值 +##### 查找最大值或最小值 在二叉搜索树中查找最值非常简单,最小值在二叉搜索树的最左边,最大值在二叉搜索树的最右边。只需要一直向左/右查找就能得到最值,如下图所示: -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.2o6ejkr6m4e0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3h3yfhyqgi00.png) 代码实现: @@ -234,7 +275,7 @@ max() { } ``` -#### 查找特定值 +##### 查找特定值 查找二叉搜索树当中的特定值效率也非常高。只需要从根节点开始将需要查找节点的 key 值与之比较,若 node.key < root 则向左查找,若 node.key > root 就向右查找,直到找到或查找到 null 为止。这里可以使用递归实现,也可以采用循环来实现。 @@ -278,7 +319,7 @@ search2(key) { } ``` -### 删除数据 +#### 删除数据 实现思路: @@ -318,7 +359,7 @@ while (currentNode.key !== key) { - 删除的是只有一个子节点的节点; - 删除的是有两个子节点的节点; -#### 删除的是叶子节点 +##### 删除的是叶子节点 删除的是叶子节点分两种情况: @@ -326,13 +367,13 @@ while (currentNode.key !== key) { 当该叶子节点为根节点时,如下图所示,此时 current == this.root,直接通过:this.root = null,删除根节点。 - ![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.18kl2ddg05kw.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.1j9353rx9b7k.png) - 叶子节点不为根节点 当该叶子节点不为根节点时也有两种情况,如下图所示 - ![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.3mb8g9a6diy0.png) + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3r99a6ocvug0.png) 若 current = 8,可以通过:parent.left = null,删除节点 8; @@ -355,7 +396,7 @@ while (currentNode.key !== key) { } ``` -#### 删除的是只有一个子节点的节点 +##### 删除的是只有一个子节点的节点 有六种情况: @@ -367,7 +408,7 @@ while (currentNode.key !== key) { - 情况 3:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.left,删除节点 9; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.4zc06mlt2e00.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.27lwqd0qfnpc.png) 当 current 存在右子节点时(current.left = null): @@ -377,7 +418,7 @@ while (currentNode.key !== key) { - 情况 6:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.right,删除节点 9; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.23k6katp0f5s.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3edzg72fx7y0.png) 代码实现: @@ -412,7 +453,7 @@ while (currentNode.key !== key) { } ``` -#### 删除的是有两个子节点的节点 +##### 删除的是有两个子节点的节点 这种情况十分复杂,首先依据以下二叉搜索树,讨论这样的问题: @@ -425,7 +466,7 @@ while (currentNode.key !== key) { - 方式 1:从节点 9 的左子树中选择一合适的节点替代节点 9,可知节点 8 符合要求; - 方式 2:从节点 9 的右子树中选择一合适的节点替代节点 9,可知节点 10 符合要求; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.1by38b4350v4.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.73rduwimfvo0.png) **删除节点 7** @@ -434,7 +475,7 @@ while (currentNode.key !== key) { - 方式 1:从节点 7 的左子树中选择一合适的节点替代节点 7,可知节点 5 符合要求; - 方式 2:从节点 7 的右子树中选择一合适的节点替代节点 7,可知节点 8 符合要求; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.58f3ovxac100.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.2h9hjd5bhwo0.png) **删除节点 15** @@ -443,7 +484,7 @@ while (currentNode.key !== key) { - 方式 1:从节点 15 的左子树中选择一合适的节点替代节点 15,可知节点 14 符合要求; - 方式 2:从节点 15 的右子树中选择一合适的节点替代节点 15,可知节点 18 符合要求; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.2a3phv4h6ow0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4f5tzwdvueq0.png) 相信你已经发现其中的规律了! @@ -454,14 +495,14 @@ while (currentNode.key !== key) { - current 左子树中比 current 小一点点的节点,即 current 左子树中的最大值; - current 右子树中比 current 大一点点的节点,即 current 右子树中的最小值; -##### 前驱&后继 +###### 前驱&后继 在二叉搜索树中,这两个特殊的节点有特殊的名字: - 比 current 小一点点的节点,称为 current 节点的前驱。比如下图中的节点 5 就是节点 7 的前驱; - 比 current 大一点点的节点,称为 current 节点的后继。比如下图中的节点 8 就是节点 7 的后继; -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.47qp7idxveq0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.71vg0g9z7io0.png) 查找需要被删除的节点 current 的后继时,需要在 current 的右子树中查找最小值,即在 current 的右子树中一直向左遍历查找; @@ -516,7 +557,7 @@ getSuccessor(delNode) { } ``` -#### 完整实现 +##### 完整实现 ```js // 删除节点 @@ -635,15 +676,15 @@ getSuccessor(delNode) { } ``` -# 平衡树 +## 平衡树 二叉搜索树的缺陷:当插入的数据是有序的数据,就会造成二叉搜索树的深度过大。比如原二叉搜索树由 11 7 15 组成,如下图所示: -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.6xy1t7bijis0.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.1nkd51rg5dz4.png) 当插入一组有序数据:6 5 4 3 2 就会变成深度过大的搜索二叉树,会严重影响二叉搜索树的性能。 -![](https://cdn.jsdelivr.net/gh/XPoet/image-hosting/JavaScript数据结构与算法/image.5xvlspn12p80.png) +![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.154bnlgtd5z4.png) 非平衡树 @@ -663,3 +704,4 @@ getSuccessor(delNode) { - AVL 树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于 AVL 树是平衡树,所以它的时间复杂度也是 O(log n)。但是它的整体效率不如红黑树,开发中比较少用。 - 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是 O(log n)。进行插入/删除等操作时,性能优于 AVL 树,所以平衡树的应用基本都是红黑树。 + diff --git "a/assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\345\233\276.md" "b/assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\345\233\276.md" deleted file mode 100644 index 0fee793..0000000 --- "a/assets/doc/13_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\344\270\211\357\274\211\345\233\276.md" +++ /dev/null @@ -1,378 +0,0 @@ -# JavaScript 数据结构与算法(十三)图 - - -## 一、图概念 - - -在计算机程序设计中, 图也是一种非常常见的数据结构,图论其实是一个非常大的话题,在数学上起源于哥尼斯堡七桥问题。 - -### 什么是图? - -* 图是一种与树有些相似的数据结构. - * 实际上, 在数学的概念上, 树是图的一种. - * 我们知道树可以用来模拟很多现实的数据结构, 比如: 家谱/公司组织架构等等 -* 那么图长什么样子呢? 或者什么样的数据使用图来模拟更合适呢? - * 人与人之间的关系网 - ![人与人](../image/1102036-8379ebe9f7e5c5d8.webp) - * 互联网中的网络关系 - ![互联网](../image/1102036-0c9fb552c956e0d5.webp) - * 广州地铁图 - ![地铁](../image/metro.gif) -* 那么, 什么是图呢? - * 我们会发现, 上面的结点(其实图中叫顶点Vertex)之间的关系, 是不能使用树来表示(几叉树都不可以) - * 这个时候, 我们就可以使用图来模拟它们. -* 图通常有什么特点呢? - * 一组顶点:通常用 V (Vertex) 表示顶点的集合 - * 一组边:通常用 E (Edge) 表示边的集合 - * 边是顶点和顶点之间的连线 - * 边可以是有向的, 也可以是无向的.(比如A --- B, 通常表示无向. A --> B, 通常表示有向) - -### 图的术语 - -* 关于术语 - * 我们在学习树的时候, 树有很多的其他术语, 了解这些术语有助于我们更深层次的理解图. - * 但是图的术语其实非常多, 如果你找一本专门讲图的各个方面的书籍, 会发现只是术语就可以占据一个章节. - * 这里, 这里介绍几个比较常见的术语, 某些术语后面用到的时候, 再了解. 没有用到的, 不做赘述. - * 下面这是个抽象出来的图 - ![抽象图](../image/1102036-7e22c0e47e42f69a.webp) -* 顶点: - * 顶点刚才我们已经介绍过了, 表示图中的一个结点. - * 比如地铁站中某个站/多个村庄中的某个村庄/互联网中的某台主机/人际关系中的人. -* 边: - * 边表示顶点和顶点之间的连线. - * 比如地铁站中两个站点之间的直接连线, 就是一个边. - * 注意: 这里的边不要叫做路径, 路径有其他的概念, 后面会区分. -* 相邻顶点 - * 由一条边连接在一起的顶点称为相邻顶点. - * 比如0 - 1是相邻的, 0 - 3是相邻的. 0 - 2是不相邻的 -* 度: - * 一个顶点的度是相邻顶点的数量. - * 比如0顶点和其他两个顶点相连, 0顶点的度是2 - * 比如1顶点和其他四个顶点相连, 1顶点的度是4 -* 路径: - * 路径是顶点v1, v2..., vn的一个连续序列, 比如上图中0 1 5 9就是一条路径. - * 简单路径: 简单路径要求不包含重复的顶点. 比如 0 1 5 9是一条简单路径. - * 回路: 第一个顶点和最后一个顶点相同的路径称为回路. 比如 0 1 5 6 3 0 -* 无向图: - * 上面的图就是一张无向图, 因为所有的边都没有方向. - * 比如 0 - 1之间有变, 那么说明这条边可以保证 0 -> 1, 也可以保证 1 -> 0. -* 有向图: - * 有向图表示的图中的边是有方向的. - * 比如 0 -> 1, 不能保证一定可以 1 -> 0, 要根据方向来定. - -**无权图和带权图** - -* 无权图: - * 我们上面的图就是一张无权图(边没有携带权重) - * 我们上面的图中的边是没有任何意义的, 不能收 0 - 1的边, 比4 - 9的边更远或者用的时间更长. -* 带权图: - * 带权图表示边有一定的权重. - * 这里的权重可以是任意你希望表示的数据: 比如距离或者花费的时间或者票价. - * 我们来看一张有向和带权的图 - ![带权图](../image/1102036-6511eda63dc574f6.webp) - -### 现实建模 - -* 对交通流量建模 - * 顶点可以表示街道的十字路口, 边可以表示街道. - * 加权的边可以表示限速或者车道的数量或者街道的距离. - * 建模人员可以用这个系统来判定最佳路线以及最可能堵车的街道. -* 对飞机航线建模 - * 航空公司可以用图来为其飞行系统建模. - * 将每个机场看成顶点, 将经过两个顶点的每条航线看作一条边. - * 加权的边可以表示从一个机场到另一个机场的航班成本, 或两个机场间的距离. - * 建模人员可以利用这个系统有效的判断从一个城市到另一个城市的最小航行成 - 本. -​ - -## 二、图的表示 - - -我们知道一个图包含很多顶点, 另外包含顶点和顶点之间的连线(边), 这两个都是非常重要的图信息, 因此都需要在程序中体现出来. - -### 顶点表示 - -* 顶点的表示相对简单. - * 上面的顶点, 我们抽象成了1 2 3 4, 也可以抽象成A B C D. 在后面的案例 中, 我们 使用A B C D. - * 那么这些A B C D我们可以使用一个数组来存储起来(存储所有的顶点) - * 当然, A, B, C, D有可能还表示其他含义的数据(比如村庄的名字), 这个时 候, 可以另外创建一个数组, 用于存储对应的其他数据. -* 边的表示略微复杂 - * 因为边是两个顶点之间的关系, 所以表示起来会稍微麻烦一些. - * 下面是变常见的表示方式. - -### 邻接矩阵 - -* 概述 - * 邻接矩阵让每个节点和一个整数向关联, 该整数作为数组的下标值. - * 我们用一个二维数组来表示顶点之间的连接. - * 演示 - ![邻接矩阵](../image/1102036-9a6cd682c1cf8d29.webp) -* 图片解析: - * 在二维数组中, 0表示没有连线, 1表示有连线. - * 通过二维数组, 我们可以很快的找到一个顶点和哪些顶点有连线.(比如A顶点, 只需要 遍历第一行即可) - * 另外, A - A, B - B(也就是顶点到自己的连线), 通常使用0表示. -* 邻接矩阵的问题: - * 如果是一个无向图, 邻接矩阵展示出来的二维数组, 其实是一个对称图. - * 也就是A -> D是1的时候, 对称的位置 D -> 1一定也是1. - * 那么这种情况下会造成空间的浪费,解决办法需自己去研究下。 - * 邻接矩阵还有一个比较严重的问题就是如果图是一个稀疏图 - * 那么矩阵中将存在大量的0, 这意味着我们浪费了计算机存储空间来表示根本不存在的边. - * 而且即使只有一个边, 我们也必须遍历一行来找出这个边, 也浪费很多时间. - -### 邻接表 - -* 概述 - * 邻接表由图中每个顶点以及和顶点相邻的顶点列表组成. - * 这个列表有很多中方式来存储: 数组/链表/字典(哈希表)都可以. - * 演示 - ![邻接矩阵](../image/1102036-be0b2812d32fe1e8.webp) -* 图片解析: - * 其实图片比较容易理解. - * 比如我们要表示和A顶点有关联的顶点(边), A和B/C/D有边, 那么我们可以通过A找到 对应的数组/链表/字典, 再取出其中的内容就可以啦. -* 邻接表的问题: - * 邻接表计算"出度"是比较简单的(出度: 指向别人的数量, 入度: 指向自己的数量) - * 邻接表如果需要计算有向图的"入度", 那么是一件非常麻烦的事情. - * 它必须构造一个"“逆邻接表", 才能有效的计算"入度". 而临街矩阵会非常简单. - - -## 三、图的封装 - - -### 创建图类 - -* 先来创建Graph类,定义了两个属性: - * vertexes: 用于存储所有的顶点, 使用一个数组来保存. - * adjList: adj是adjoin的缩写, 邻接的意思. adjList用于存储所有的边, 这里采用邻接表的形式. -```js -class Graph { - constructor() { - this.vertexes = [] // 存储顶点 - this.adjList = new Dictionay() //存储边信息 - } -} -``` - -### 添加方法 - -* 添加顶点: 可以向图中添加一些顶点. - * 将添加的顶点放入到数组中. - * 另外, 给该顶点创建一个数组[], 该数组用于存储顶点连接的所有的边.(回顾邻接表的实现方式) -```js -// 添加顶点 -addVertex(val) { - // 添加点 - this.vertexes.push(val) - // 添加点的关系 采用邻接矩阵法 结构用Map - this.adjList.set(val, []) -} -``` -* 添加边: 可以指定顶点和顶点之间的边. - * 添加边需要传入两个顶点, 因为边是两个顶点之间的边, 边不可能单独存在. - * 根据顶点v取出对应的数组, 将w加入到它的数组中. - * 根据顶点w取出对应的数组, 将v加入到它的数组中. - * 因为这里实现的是无向图, 所以边是可以双向的. -```js -// 添加边 -addEdge(val1, val2) { - // 添加边需要传入两个顶点, 因为边是两个顶点之间的边, 边不可能单独存在. - // 这里实现的是无向图, 所以这里不考虑方向问题 - this.adjList.get(val1).push(val2) - this.adjList.get(val2).push(val1) -} -``` -* toString方法:为了能够正确的显示图的结果 - * 就是拿出二维数组的每一项 -```js -// 输出图结构 -toString() { - let res = '' - for (let i = 0; i < this.vertexes.length; i++) { - res += this.vertexes[i] + "->" - let adj = this.adjList.get(this.vertexes[i]) - for (let j = 0; j < adj.length; j++) { - res += adj[j] + "" - } - res += "\n" - } - return res -} -``` -### 测试代码 -```js -// 测试代码 -let graph = new Graph() - -// 添加顶点 -let myVertexes = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] -for (let i = 0; i < myVertexes.length; i++) { - graph.addVertex(myVertexes[i]) -} - -// 添加边 -graph.addEdge('A', 'B'); -graph.addEdge('A', 'C'); -graph.addEdge('A', 'D'); -graph.addEdge('C', 'D'); -graph.addEdge('C', 'G'); -graph.addEdge('D', 'G'); -graph.addEdge('D', 'H'); -graph.addEdge('B', 'E'); -graph.addEdge('B', 'F'); -graph.addEdge('E', 'I'); -``` - - -## 四、图的遍历 - - -和其他数据结构一样, 需要通过某种算法来遍历图结构中每一个数据.这样可以保证, 在我们需要时, 通过这种算法来访问某个顶点的数据以及它对应的边. - -### 遍历的方式 - -* 图的遍历思想 - * 图的遍历算法的思想在于必须访问每个第一次访问的节点, 并且追踪有哪些顶点还没有被访问到. -* 有两种算法可以对图进行遍历 - * 广度优先搜索(Breadth-First Search, 简称BFS) - * 深度优先搜索(Depth-First Search, 简称DFS) - * 两种遍历算法, 都需要明确指定第一个被访问的顶点. -* 遍历的注意点: - * 完全探索一个顶点要求我们便查看该顶点的每一条边. - * 对于每一条所连接的没有被访问过的顶点, 将其标注为被发现的, 并将其加进待访问顶点列表中. - * 为了保证算法的效率: 每个顶点至多访问两次. -* 两种算法的思想: - * BFS: 基于队列, 入队列的顶点先被探索. - * DFS: 基于栈, 通过将顶点存入栈中, 顶点是沿着路径被探索的, 存在新的相邻顶点就去访问. -* 为了记录顶点是否被访问过, 我们使用三种颜色来反应它们的状态:(或者两种颜色也可以) - * 白色: 表示该顶点还没有被访问. - * 灰色: 表示该顶点被访问过, 但并未被探索过. - * 黑色: 表示该顶点被访问过且被完全探索过. - * 初始化颜色代码 -```js -// 初始化顶点的颜色 -_initializeColor() { - // 白色: 表示该顶点还没有被访问. - // 灰色: 表示该顶点被访问过, 但并未被探索过. - // 黑色: 表示该顶点被访问过且被完全探索过. - let colors = [] - for (let i = 0; i < this.vertexes.length; i++) { - colors[this.vertexes[i]] = "white" - } - return colors -} -``` - -### 广度优先搜索(BFS) - -* 广度优先搜索算法的思路: - * 广度优先算法会从指定的第一个顶点开始遍历图, 先访问其所有的相邻点, 就像一次访问图的一层. - * 换句话说, 就是先宽后深的访问顶点 -* 图解BFS - ![图解BFS](../image/1102036-97199fd04e5c6515.webp) -* 广度优先搜索的实现: - 1. 创建一个队列Q. - 2. 将v标注为被发现的(灰色), 并将v将入队列Q - 3. 如果Q非空, 执行下面的步骤: - * 将v从Q中取出队列. - * 将v标注为被发现的灰色. - * 将v所有的未被访问过的邻接点(白色), 加入到队列中. - * 将v标志为黑色. -* 广度优先搜索的代码: -```js -// 广度优先搜索 -bfs(handle) { - // 1.初始化颜色 - let color = this._initializeColor() - // 2. 创建队列 - let queue = new Queue - // 3. 将传入的顶点放入队列 - queue.enqueue(this.vertexes[0]) - // 4.依赖队列操作数据 队列不为空时一直持续 - while (!queue.isEmpty()) { - // 4.1 拿到队头 - let qVal = queue.dequeue() - // 4.2 拿到队头所关联(相连)的点并设置为访问中状态(灰色) - let qAdj = this.adjList.get(qVal) - color[qVal] = "gray" - // 4.3 将队头关联的点添加到队尾 - // 这一步是完成bfs的关键,依赖队列的先进先出的特点。 - for (let i = 0; i < qAdj.length; i++) { - let a = qAdj[i] - if (color[a] === "white") { - color[a] = "gray" - queue.enqueue(a) - } - } - // 4.5设置访问完的点为黑色。 - color[qVal] = "black" - if (handle) [ - handle(qVal) - ] - } -} -``` -* 测试代码 -```js -// 调用广度优先算法 -let result = "" -graph.bfs(graph.vertexes[0], function (v) { - result += v + " " -}) -console.log(result) // A B C D E F G H I -``` -### 深度优先搜索(DFS) - -* 深度优先搜索的思路: - * 深度优先搜索算法将会从第一个指定的顶点开始遍历图, 沿着路径知道这条路径最后被访问了. - * 接着原路回退并探索吓一条路径. -* 图解DFS: -![图解BFS](../image/1102036-1c873ab8dad0805e.webp) -* 深度优先搜索算法的实现: - * 广度优先搜索算法我们使用的是队列, 这里可以使用栈完成, 也可以使用递归. - * 方便代码书写, 我们还是使用递归(递归本质上就是函数栈的调用) -* 深度优先搜索算法的代码: -```js -// 深度优先搜索 -dfs(handle) { - // 1.初始化颜色 - let color = this._initializeColor() - // 2. 遍历所有顶点,开始访问 - for (let i = 0; i < this.vertexes.length; i++) { - if (color[this.vertexes[i]] === "white") { - this._dfsVisit(this.vertexes[i], color, handle) - } - } -} -// dfs的递归方法 这里直接使用函数的调用栈 -_dfsVisit(val, color, handle) { - // 1. 将颜色设置为访问中 - color[val] = "gray" - // 2. 执行相应的回调 - if (handle) { - handle(val) - } - // 3. 拿与该点相邻的点,对每个点操作 - let adj = this.adjList.get(val) - for (let i = 0; i < adj.length; i++) { - let w = adj[i] - // 如果相邻点未未访问状态,开始访问。 - if (color[w] === "white") { - this._dfsVisit(w, color, handle) - } - } - // 4. 处理完后设置为访问过点。 - color[val] = "black" -} -``` -* 测试代码 -```js -// 调用深度优先算法 -result = "" -graph.dfs(function (v) { - result += v + " " -}) -// 输出深度优先 -console.log(result) //A B E I F C D G H - -``` -* 递归的代码较难理解一些, 这副图来帮助理解过程: -![图解递归](../image/1102036-a1c010fe114de198.webp) \ No newline at end of file diff --git "a/assets/doc/14_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\345\233\233\357\274\211\345\233\276.md" "b/assets/doc/14_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\345\233\233\357\274\211\345\233\276.md" new file mode 100644 index 0000000..032ff4a --- /dev/null +++ "b/assets/doc/14_JavaScript\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225\357\274\210\345\215\201\345\233\233\357\274\211\345\233\276.md" @@ -0,0 +1,441 @@ +# JavaScript 数据结构与算法(十四)图 + +## 图的概念 + +在计算机程序设计中,图也是一种非常常见的数据结构,图论其实是一个非常大的话题,在数学上起源于哥尼斯堡七桥问题。 + +### 什么是图? + +- 图是一种与树有些相似的数据结构。 + + - 实际上,在数学的概念上,树是图的一种。 + - 我们知道树可以用来模拟很多现实的数据结构,比如:家谱/公司组织架构等等。 + +- 那么图长什么样子呢?或者什么样的数据使用图来模拟更合适呢? + + - 人与人之间的关系网 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4cdhxz0ereu0.png) + + - 互联网中的网络关系 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.4ljxyy69a1s0.png) + + - 广州地铁图 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.46k2cvwmthw0.png) + +- 那么,什么是图呢? + + - 我们会发现,上面的结点(其实图中叫顶点 Vertex)之间的关系,是不能使用树来表示(几叉树都不可以)。 + - 这个时候,我们就可以使用**图**来模拟它们。 + +- 图通常有什么特点呢? + - 一组顶点:通常用 V (Vertex) 表示顶点的集合 + - 一组边:通常用 E (Edge) 表示边的集合 + - 边是顶点和顶点之间的连线 + - 边可以是有向的,也可以是无向的。(比如 A --- B,通常表示无向。 A --> B,通常表示有向) + +### 图的术语 + +#### 术语 + +- 我们在学习树的时候,树有很多的其他术语,了解这些术语有助于我们更深层次的理解图。 +- 但是图的术语其实非常多,如果你找一本专门讲图的各个方面的书籍,会发现只是术语就可以占据一个章节。 +- 这里,这里介绍几个比较常见的术语,某些术语后面用到的时候,再了解,没有用到的,不做赘述。 +- 下面这是个抽象出来的图 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.rr153grdbc0.png) + +- 顶点 + + - 顶点刚才我们已经介绍过了,表示图中的一个结点。 + - 比如地铁站中某个站/多个村庄中的某个村庄/互联网中的某台主机/人际关系中的人。 + +- 边 + + - 边表示顶点和顶点之间的连线。 + - 比如地铁站中两个站点之间的直接连线, 就是一个边。 + - 注意:这里的边不要叫做路径,路径有其他的概念,后面会区分。 + +- 相邻顶点 + + - 由一条边连接在一起的顶点称为相邻顶点。 + - 比如 `0 - 1` 是相邻的,`0 - 3` 是相邻的。`0 - 2` 是不相邻的。 + +- 度 + + - 一个顶点的度是相邻顶点的数量 + - 比如 0 顶点和其他两个顶点相连,0 顶点的度是 2 + - 比如 1 顶点和其他四个顶点相连,1 顶点的度是 4 + +- 路径 + + - 路径是顶点 `v1`,`v2`...,`vn` 的一个连续序列, 比如上图中 `0 1 5 9` 就是一条路径。 + - 简单路径: 简单路径要求不包含重复的顶点. 比如 `0 1 5 9` 是一条简单路径。 + - 回路:第一个顶点和最后一个顶点相同的路径称为回路。比如 `0 1 5 6 3 0`。 + +- 无向图 + + - 上面的图就是一张无向图,因为所有的边都没有方向。 + - 比如 `0 - 1` 之间有变,那么说明这条边可以保证 `0 -> 1`,也可以保证 `1 -> 0`。 + +- 有向图 + + - 有向图表示的图中的边是有方向的。 + - 比如 `0 -> 1`,不能保证一定可以 `1 -> 0`,要根据方向来定。 + +#### 无权图和带权图 + +- 无权图 + + - 我们上面的图就是一张无权图(边没有携带权重) + - 我们上面的图中的边是没有任何意义的,不能收 `0 - 1` 的边,比 `4 - 9` 的边更远或者用的时间更长。 + +- 带权图 + - 带权图表示边有一定的权重 + - 这里的权重可以是任意你希望表示的数据:比如距离或者花费的时间或者票价。 + - 我们来看一张有向和带权的图 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3q0nj5eq5p80.png) + +### 现实建模 + +- 对交通流量建模 + + - 顶点可以表示街道的十字路口,边可以表示街道.。 + - 加权的边可以表示限速或者车道的数量或者街道的距离。 + - 建模人员可以用这个系统来判定最佳路线以及最可能堵车的街道。 + +- 对飞机航线建模 + + - 航空公司可以用图来为其飞行系统建模。 + - 将每个机场看成顶点,将经过两个顶点的每条航线看作一条边。 + - 加权的边可以表示从一个机场到另一个机场的航班成本,或两个机场间的距离。 + - 建模人员可以利用这个系统有效的判断从一个城市到另一个城市的最小航行成本。 + ​ + +## 二、图的表示 + +我们知道一个图包含很多顶点,另外包含顶点和顶点之间的连线(边),这两个都是非常重要的图信息,因此都需要在程序中体现出来。 + +### 顶点表示 + +- 顶点的表示相对简单 + + - 上面的顶点,我们抽象成了 1 2 3 4,也可以抽象成 A B C D。在后面的案例中,我们使用 A B C D。 + - 那么这些 A B C D 我们可以使用一个数组来存储起来(存储所有的顶点)。 + - 当然,A B C D 有可能还表示其他含义的数据(比如村庄的名字),这个时候,可以另外创建一个数组,用于存储对应的其他数据。 + +- 边的表示略微复杂 + - 因为边是两个顶点之间的关系,所以表示起来会稍微麻烦一些。 + - 下面是变常见的表示方式。 + +### 邻接矩阵 + +- 概述 + + - 邻接矩阵让每个节点和一个整数向关联, 该整数作为数组的下标值. + - 我们用一个二维数组来表示顶点之间的连接. + - 演示 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.66y7l5b21nw0.png) + +- 图片解析 + + - 在二维数组中,0 表示没有连线,1 表示有连线。 + - 通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。(比如 A 顶点, 只需要 遍历第一行即可) + - 另外,A - A,B - B(也就是顶点到自己的连线),通常使用 0 表示。 + +- 邻接矩阵的问题 + + - 如果是一个无向图,邻接矩阵展示出来的二维数组,其实是一个对称图。 + + - 也就是 A -> D 是 1 的时候,对称的位置 D -> 1 一定也是 1。 + - 那么这种情况下会造成空间的浪费,解决办法需自己去研究下。 + + - 邻接矩阵还有一个比较严重的问题就是如果图是一个稀疏图 + - 那么矩阵中将存在大量的 0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。 + - 而且即使只有一个边,我们也必须遍历一行来找出这个边,也浪费很多时间。 + +### 邻接表 + +- 概述 + + - 邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。 + - 这个列表有很多中方式来存储:数组/链表/字典(哈希表)都可以。 + - 演示 + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3mu1hv8a84u0.png) + +- 图片解析 + + - 其实图片比较容易理解 + - 比如我们要表示和 A 顶点有关联的顶点(边),A 和 B/C/D 有边,那么我们可以通过 A 找到 对应的数组/链表/字典,再取出其中的内容就可以啦。 + +- 邻接表的问题 + - 邻接表计算“出度”是比较简单的(出度:指向别人的数量, 入度: 指向自己的数量) + - 邻接表如果需要计算有向图的“入度”,那么是一件非常麻烦的事情。 + - 它必须构造一个“逆邻接表”,才能有效的计算“入度”。而临街矩阵会非常简单。 + +## 三、图的封装 + +### 创建图类 + +- 先来创建 Graph 类,定义了两个属性: + - `vertexes` 用于存储所有的顶点,使用一个数组来保存。 + - `adjList` adj 是 adjoin 的缩写,邻接的意思。adjList 用于存储所有的边,这里采用邻接表的形式。 + +```js +class Graph { + constructor() { + this.vertexes = []; // 存储顶点 + this.adjList = new Dictionay(); //存储边信息 + } +} +``` + +### 添加方法 + +- 添加顶点:可以向图中添加一些顶点。 + - 将添加的顶点放入到数组中。 + - 另外,给该顶点创建一个数组`[]`,该数组用于存储顶点连接的所有的边.(回顾邻接表的实现方式) + +```js +// 添加顶点 +addVertex(val) { + // 添加点 + this.vertexes.push(val) + // 添加点的关系 采用邻接矩阵法 结构用Map + this.adjList.set(val, []) +} +``` + +- 添加边:可以指定顶点和顶点之间的边。 + - 添加边需要传入两个顶点,因为边是两个顶点之间的边,边不可能单独存在。 + - 根据顶点 v 取出对应的数组,将 w 加入到它的数组中。 + - 根据顶点 w 取出对应的数组,将 v 加入到它的数组中。 + - 因为这里实现的是无向图,所以边是可以双向的。 + +```js +// 添加边 +addEdge(val1, val2) { + // 添加边需要传入两个顶点, 因为边是两个顶点之间的边, 边不可能单独存在. + // 这里实现的是无向图, 所以这里不考虑方向问题 + this.adjList.get(val1).push(val2) + this.adjList.get(val2).push(val1) +} +``` + +toString 方法:为了能够正确的显示图的结果,就是拿出二维数组的每一项。 + +```js +// 输出图结构 +toString() { + let res = '' + for (let i = 0; i < this.vertexes.length; i++) { + res += this.vertexes[i] + "->" + let adj = this.adjList.get(this.vertexes[i]) + for (let j = 0; j < adj.length; j++) { + res += adj[j] + "" + } + res += "\n" + } + return res +} +``` + +### 测试代码 + +```js +// 测试代码 +let graph = new Graph(); + +// 添加顶点 +let myVertexes = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]; +for (let i = 0; i < myVertexes.length; i++) { + graph.addVertex(myVertexes[i]); +} + +// 添加边 +graph.addEdge("A", "B"); +graph.addEdge("A", "C"); +graph.addEdge("A", "D"); +graph.addEdge("C", "D"); +graph.addEdge("C", "G"); +graph.addEdge("D", "G"); +graph.addEdge("D", "H"); +graph.addEdge("B", "E"); +graph.addEdge("B", "F"); +graph.addEdge("E", "I"); +``` + +## 四、图的遍历 + +和其他数据结构一样,需要通过某种算法来遍历图结构中每一个数据。这样可以保证,在我们需要时,通过这种算法来访问某个顶点的数据以及它对应的边。 + +### 遍历的方式 + +- 图的遍历思想 + 图的遍历算法的思想在于必须访问每个第一次访问的节点,并且追踪有哪些顶点还没有被访问到。 + +- 有两种算法可以对图进行遍历 + + - 广度优先搜索(Breadth-First Search, 简称 BFS) + - 深度优先搜索(Depth-First Search, 简称 DFS) + - 两种遍历算法,都需要明确指定第一个被访问的顶点。 + +- 遍历的注意点 + + - 完全探索一个顶点要求我们便查看该顶点的每一条边。 + - 对于每一条所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。 + - 为了保证算法的效率:每个顶点至多访问两次。 + +- 两种算法的思想 + + - BFS 基于队列,入队列的顶点先被探索。 + - DFS 基于栈,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。 + +- 为了记录顶点是否被访问过,我们使用三种颜色来反应它们的状态。(或者两种颜色也可以) + - **白色**表示该顶点还没有被访问. + - **灰色**表示该顶点被访问过, 但并未被探索过. + - **黑色**表示该顶点被访问过且被完全探索过. + - 初始化颜色代码 + +```js +// 初始化顶点的颜色 +_initializeColor() { + // 白色: 表示该顶点还没有被访问. + // 灰色: 表示该顶点被访问过, 但并未被探索过. + // 黑色: 表示该顶点被访问过且被完全探索过. + let colors = [] + for (let i = 0; i < this.vertexes.length; i++) { + colors[this.vertexes[i]] = "white" + } + return colors +} +``` + +### 广度优先搜索(BFS) + +- 广度优先搜索算法的思路 + 广度优先算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深的访问顶点。 + +- 图解 BFS + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.3vz7fx7tgvs0.png) + +- 广度优先搜索的实现 + + 1. 创建一个队列 Q + 2. 将 v 标注为被发现的(灰色), 并将 v 将入队列 Q + 3. 如果 Q 非空, 执行下面的步骤: + - 将 v 从 Q 中取出队列 + - 将 v 标注为被发现的灰色 + - 将 v 所有的未被访问过的邻接点(白色),加入到队列中 + - 将 v 标志为黑色 + +- 广度优先搜索的代码 + + ```js + // 广度优先搜索 + bfs(handle) { + // 1.初始化颜色 + let color = this._initializeColor() + // 2. 创建队列 + let queue = new Queue + // 3. 将传入的顶点放入队列 + queue.enqueue(this.vertexes[0]) + // 4.依赖队列操作数据 队列不为空时一直持续 + while (!queue.isEmpty()) { + // 4.1 拿到队头 + let qVal = queue.dequeue() + // 4.2 拿到队头所关联(相连)的点并设置为访问中状态(灰色) + let qAdj = this.adjList.get(qVal) + color[qVal] = "gray" + // 4.3 将队头关联的点添加到队尾 + // 这一步是完成bfs的关键,依赖队列的先进先出的特点。 + for (let i = 0; i < qAdj.length; i++) { + let a = qAdj[i] + if (color[a] === "white") { + color[a] = "gray" + queue.enqueue(a) + } + } + // 4.5设置访问完的点为黑色。 + color[qVal] = "black" + if (handle) [ + handle(qVal) + ] + } + } + ``` + +- 测试代码 + + ```js + // 调用广度优先算法 + let result = ""; + graph.bfs(graph.vertexes[0], function (v) { + result += v + " "; + }); + console.log(result); // A B C D E F G H I + ``` + +### 深度优先搜索(DFS) + +深度优先搜索的思路: + +- 深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径知道这条路径最后被访问了。 +- 接着原路回退并探索吓一条路径。 +- 图解 DFS + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.1bjimck65r8g.png) + +深度优先搜索算法的实现: + +- 广度优先搜索算法我们使用的是队列,这里可以使用栈完成,也可以使用递归。 +- 方便代码书写,我们还是使用递归(递归本质上就是函数栈的调用) +- 深度优先搜索算法的代码: + + ```js + // 深度优先搜索 + dfs(handle) { + // 1.初始化颜色 + let color = this._initializeColor() + // 2. 遍历所有顶点,开始访问 + for (let i = 0; i < this.vertexes.length; i++) { + if (color[this.vertexes[i]] === "white") { + this._dfsVisit(this.vertexes[i], color, handle) + } + } + } + // dfs的递归方法 这里直接使用函数的调用栈 + _dfsVisit(val, color, handle) { + // 1. 将颜色设置为访问中 + color[val] = "gray" + // 2. 执行相应的回调 + if (handle) { + handle(val) + } + // 3. 拿与该点相邻的点,对每个点操作 + let adj = this.adjList.get(val) + for (let i = 0; i < adj.length; i++) { + let w = adj[i] + // 如果相邻点未未访问状态,开始访问。 + if (color[w] === "white") { + this._dfsVisit(w, color, handle) + } + } + // 4. 处理完后设置为访问过点。 + color[val] = "black" + } + ``` + +- 测试代码 + + ```js + // 调用深度优先算法 + result = ""; + graph.dfs(function (v) { + result += v + " "; + }); + // 输出深度优先 + console.log(result); //A B E I F C D G H + ``` + +- 递归的代码较难理解一些,这副图来帮助理解过程: + ![image](https://cdn.jsdelivr.net/gh/XPoet/image-hosting@master/JavaScript-数据结构与算法/image.6z6nkgmevxo0.png) + + diff --git a/assets/image/1102036-0c9fb552c956e0d5.webp b/assets/image/1102036-0c9fb552c956e0d5.webp deleted file mode 100644 index 91be9db..0000000 Binary files a/assets/image/1102036-0c9fb552c956e0d5.webp and /dev/null differ diff --git a/assets/image/1102036-1c873ab8dad0805e.webp b/assets/image/1102036-1c873ab8dad0805e.webp deleted file mode 100644 index 91a04b9..0000000 Binary files a/assets/image/1102036-1c873ab8dad0805e.webp and /dev/null differ diff --git a/assets/image/1102036-6511eda63dc574f6.webp b/assets/image/1102036-6511eda63dc574f6.webp deleted file mode 100644 index 3cfb10f..0000000 Binary files a/assets/image/1102036-6511eda63dc574f6.webp and /dev/null differ diff --git a/assets/image/1102036-7e22c0e47e42f69a.webp b/assets/image/1102036-7e22c0e47e42f69a.webp deleted file mode 100644 index bc022d0..0000000 Binary files a/assets/image/1102036-7e22c0e47e42f69a.webp and /dev/null differ diff --git a/assets/image/1102036-8379ebe9f7e5c5d8.webp b/assets/image/1102036-8379ebe9f7e5c5d8.webp deleted file mode 100644 index ebf1508..0000000 Binary files a/assets/image/1102036-8379ebe9f7e5c5d8.webp and /dev/null differ diff --git a/assets/image/1102036-97199fd04e5c6515.webp b/assets/image/1102036-97199fd04e5c6515.webp deleted file mode 100644 index 8f865fe..0000000 Binary files a/assets/image/1102036-97199fd04e5c6515.webp and /dev/null differ diff --git a/assets/image/1102036-9a6cd682c1cf8d29.webp b/assets/image/1102036-9a6cd682c1cf8d29.webp deleted file mode 100644 index 5f1192e..0000000 Binary files a/assets/image/1102036-9a6cd682c1cf8d29.webp and /dev/null differ diff --git a/assets/image/1102036-a1c010fe114de198.webp b/assets/image/1102036-a1c010fe114de198.webp deleted file mode 100644 index 6d35ee6..0000000 Binary files a/assets/image/1102036-a1c010fe114de198.webp and /dev/null differ diff --git a/assets/image/1102036-be0b2812d32fe1e8.webp b/assets/image/1102036-be0b2812d32fe1e8.webp deleted file mode 100644 index 3b93291..0000000 Binary files a/assets/image/1102036-be0b2812d32fe1e8.webp and /dev/null differ diff --git a/assets/image/metro.gif b/assets/image/metro.gif deleted file mode 100644 index bf50c91..0000000 Binary files a/assets/image/metro.gif and /dev/null differ