├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── package.json └── src ├── .vitepress └── config.mts └── docs ├── algorithm ├── classic.md ├── idea.md ├── search.md └── sort.md ├── data-structure ├── Array.md ├── BinarySearchTree.md ├── BinaryTree.md ├── DoubleLinkedList.md ├── Graph.md ├── HashTable.md ├── LinkedList.md ├── Map.md ├── PriorityQueue.md ├── Queue.md ├── Set.md ├── Stack.md └── Tree.md ├── foreword.md ├── index.md └── public ├── CNAME ├── data-structure ├── BinarySearchTree │ ├── index.html │ ├── index.js │ └── tree.js ├── DoublyLinkedList │ ├── doublyLinkedList.js │ ├── index.html │ └── index.js ├── HashTable │ ├── hashTable.js │ ├── index.html │ └── index.js ├── LinkedList │ ├── index.html │ ├── index.js │ └── linkedList.js ├── Map │ ├── index.html │ ├── index.js │ └── map.js ├── PriorityQueue │ ├── index.html │ ├── index.js │ └── priorityQueue.js ├── Queue │ ├── index.html │ ├── index.js │ ├── passGame.js │ └── queue.js ├── Set │ ├── index.html │ ├── index.js │ └── set.js └── Stack │ ├── dec2bin.js │ ├── index.html │ ├── index.js │ └── stack.js └── images ├── algorithm └── img-01.png ├── data-structure ├── img-01.png ├── img-02.png ├── img-03.png ├── img-04.png ├── img-05.png ├── img-06.png ├── img-07.png ├── img-08.png ├── img-09.png ├── img-10.png ├── img-11.png ├── img-12.png ├── img-13.png ├── img-14.png ├── img-15.png ├── img-16.png ├── img-17.png ├── img-18.png ├── img-19.png ├── img-20.png ├── img-21.png ├── img-22.png ├── img-23.png ├── img-24.png ├── img-25.png ├── img-26.png ├── img-27.png ├── img-28.png ├── img-29.png ├── img-30.png ├── img-31.png ├── img-32.png ├── img-33.png ├── img-34.png ├── img-35.png ├── img-36.png ├── img-37.png ├── img-38.png ├── img-39.png ├── img-40.png ├── img-41.png ├── img-42.png ├── img-43.png ├── img-44.png ├── img-45.png ├── img-46.png ├── img-47.png ├── img-48.png ├── img-49.png ├── img-50.png ├── img-51.png ├── img-52.png ├── img-53.png ├── img-54.png ├── img-55.png ├── img-56.png ├── img-57.png ├── img-58.png ├── img-59.png ├── img-60.png ├── img-61.png ├── img-62.png ├── img-63.png ├── img-64.png ├── img-65.png └── img-66.png └── logo.png /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup Node.js v18.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: "18.x" 21 | 22 | - name: Install 23 | run: npm install 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | publish_dir: ./dist 32 | publish_branch: gh-pages 33 | github_token: ${{ secrets.GH_PAGES_DEPLOY }} 34 | user_name: ${{ secrets.MY_USER_NAME }} 35 | user_email: ${{ secrets.MY_USER_EMAIL }} 36 | commit_message: deploy docs site 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /src/.vitepress/dist 5 | /src/.vitepress/cache 6 | dist 7 | dist-ssr 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | node_modules 13 | yarn.lock 14 | package-lock.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | # !.vscode/settings.json 28 | # !.vscode/tasks.json 29 | # !.vscode/launch.json 30 | # !.vscode/extensions.json 31 | 32 | # misc 33 | /.sass-cache 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | npm-debug.log 38 | testem.log 39 | /typings 40 | 41 | # e2e 42 | /e2e/src/*.js 43 | /e2e/src/*.map 44 | /cypress/screenshots 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | 50 | # Other 51 | *.local 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 数据结构与算法 2 | 3 | 本仓库的[文档](https://data-structure-and-algorithm.xpoet.cn)根据哔哩哔哩[《JavaScript 数据结构与算法》](https://www.bilibili.com/video/BV1x7411L7Q7)视频内容整理而成的学习笔记,视频教程讲得特别好,配合本仓库的文档和测试代码,学习效果更佳。 4 | 5 | 推荐大家按照目录结构的顺序来学习,由浅入深,循序渐进,轻松搞定数据结构和算法。 6 | 7 | ## 在线预览 8 | 9 | **https://data-structure-and-algorithm.xpoet.cn** 10 | 11 | ## 学习交流 12 | 13 | 作者组建了学习氛围特别好的前端技术交流群,欢迎同学们一起来交流吐槽。由于群人数较多,需要添加作者才能邀请进群。 14 | 15 | > 添加微信时请备注来意,以便可以及时处理。 16 | 17 | ![image](https://xpoet.cn/images/fp/fp-slogan.webp) 18 | 19 | ## 数据结构 20 | 21 | - [数组](https://data-structure-and-algorithm.xpoet.cn/data-structure/Array.html) 22 | - [栈](https://data-structure-and-algorithm.xpoet.cn/data-structure/Stack.html) 23 | - [队列](https://data-structure-and-algorithm.xpoet.cn/data-structure/Queue.html) 24 | - [优先队列](https://data-structure-and-algorithm.xpoet.cn/data-structure/PriorityQueue.html) 25 | - [单向链表](https://data-structure-and-algorithm.xpoet.cn/data-structure/LinkedList.html) 26 | - [双向链表](https://data-structure-and-algorithm.xpoet.cn/data-structure/DoubleLinkedList.html) 27 | - [集合](https://data-structure-and-algorithm.xpoet.cn/data-structure/Set.html) 28 | - [字典](https://data-structure-and-algorithm.xpoet.cn/data-structure/Map.html) 29 | - [哈希表](https://data-structure-and-algorithm.xpoet.cn/data-structure/HashTable.html) 30 | - [树](https://data-structure-and-algorithm.xpoet.cn/data-structure/Tree.html) 31 | - [二叉树](https://data-structure-and-algorithm.xpoet.cn/data-structure/BinaryTree.html) 32 | - [二叉搜索树](https://data-structure-and-algorithm.xpoet.cn/data-structure/BinarySearchTree.html) 33 | - [图](https://data-structure-and-algorithm.xpoet.cn/data-structure/Graph.html) 34 | 35 | ## 算法 36 | 37 | - [排序算法](https://data-structure-and-algorithm.xpoet.cn/algorithm/sort.html) 38 | - [搜索算法](https://data-structure-and-algorithm.xpoet.cn/algorithm/search.html) 39 | - [算法设计思想](https://data-structure-and-algorithm.xpoet.cn/algorithm/idea.html) 40 | - [经典算法题](https://data-structure-and-algorithm.xpoet.cn/algorithm/classic.html) 41 | 42 | ## 测试代码 43 | 44 | 数据结构的测试代码存放在 [src/docs/public/ data-structure](https://github.com/XPoet/js-data-structure-and-algorithm/tree/master/src/docs/public/data-structure) 目录。 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-structure-and-algorithm", 3 | "version": "0.0.1", 4 | "description": "JavaScript 数据结构与算法", 5 | "author": "XPoet ", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "vitepress dev src --open", 9 | "preview": "vitepress preview src", 10 | "build": "vitepress build src" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/XPoet/js-data-structure-and-algorithm.git" 15 | }, 16 | "keywords": [ 17 | "javascript", 18 | "data-structure", 19 | "algorithm" 20 | ], 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/XPoet/js-data-structure-and-algorithm/issues" 24 | }, 25 | "homepage": "https://github.com/XPoet/js-data-structure-and-algorithm#readme", 26 | "devDependencies": { 27 | "vitepress": "^1.2.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "数据结构与算法", 6 | 7 | description: "JavaScript 数据结构与算法", 8 | 9 | lang: 'zh-CN', 10 | 11 | lastUpdated: true, 12 | 13 | srcDir: 'docs', 14 | 15 | outDir:'../dist', 16 | 17 | head: [ 18 | ['link', { rel: 'icon', href: '/images/logo.png' }], 19 | ], 20 | 21 | markdown: { 22 | lineNumbers: true, // 代码行号 23 | image: { 24 | lazyLoading: true // 图片懒加载 25 | } 26 | }, 27 | 28 | // https://vitepress.dev/reference/default-theme-config 29 | themeConfig: { 30 | logo: '/images/logo.png', 31 | 32 | outline: { 33 | level: [2, 4] 34 | }, 35 | 36 | search: { 37 | provider: 'local' 38 | }, 39 | 40 | nav: [ 41 | { text: '前言', link: '/foreword', activeMatch: '/foreword' }, 42 | { text: '数据结构', link: '/data-structure/Array', activeMatch: '/data-structure/' }, 43 | { text: '算法', link: '/algorithm/sort', activeMatch: '/algorithm/' } 44 | ], 45 | 46 | sidebar: { 47 | // 数据结构 48 | '/data-structure/': { 49 | items: [ 50 | { text: '数组', link: '/data-structure/Array' }, 51 | { text: '栈', link: '/data-structure/Stack' }, 52 | { text: '队列', link: '/data-structure/Queue' }, 53 | { text: '优先队列', link: '/data-structure/PriorityQueue' }, 54 | { text: '单向链表', link: '/data-structure/LinkedList' }, 55 | { text: '双向链表', link: '/data-structure/DoubleLinkedList' }, 56 | { text: '集合', link: '/data-structure/Set' }, 57 | { text: '字典', link: '/data-structure/Map' }, 58 | { text: '哈希表', link: '/data-structure/HashTable' }, 59 | { text: '树', link: '/data-structure/Tree' }, 60 | { text: '二叉树', link: '/data-structure/BinaryTree' }, 61 | { text: '二叉搜索树', link: '/data-structure/BinarySearchTree' }, 62 | { text: '图', link: '/data-structure/Graph' }, 63 | ] 64 | }, 65 | 66 | // 算法 67 | '/algorithm/': { 68 | text: '算法', 69 | collapsed: false, 70 | items: [ 71 | { text: '排序算法', link: '/algorithm/sort' }, 72 | { text: '搜索算法', link: '/algorithm/search' }, 73 | { text: '算法设计思想', link: '/algorithm/idea' }, 74 | { text: '经典算法真题', link: '/algorithm/classic' }, 75 | ] 76 | }, 77 | }, 78 | 79 | socialLinks: [ 80 | { icon: 'github', link: 'https://github.com/XPoet/js-data-structure-and-algorithm' } 81 | ], 82 | 83 | footer: { 84 | message: 'Released under the AGPL-3.0 License', 85 | copyright: 'Copyright © 2019-present XPoet' 86 | }, 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /src/docs/algorithm/classic.md: -------------------------------------------------------------------------------- 1 | # 经典算法真题 2 | 3 | ## 数组扁平化 4 | 5 | - 通过递归来实现,当元素为数组时递归调用,兼容性好。 6 | 7 | ```js 8 | function flattenArray(array) { 9 | // 检查传入的参数是否为数组 10 | if (!Array.isArray(array)) return 11 | 12 | // 定义结果数组,用于存储扁平化后的元素 13 | let result = [] 14 | 15 | // 使用 reduce 方法遍历传入的数组 16 | result = array.reduce(function (pre, item) { 17 | // 判断当前元素是否为数组 18 | if (Array.isArray(item)) { 19 | // 如果是数组,则递归调用 flattenArray 函数,结果与前一个结果合并 20 | return pre.concat(flattenArray(item)) 21 | } 22 | // 如果不是数组,则直接将元素添加到结果数组中 23 | return pre.concat(item) 24 | }, []) // 初始值为空数组 25 | 26 | // 返回扁平化后的结果数组 27 | return result 28 | } 29 | 30 | // 测试代码,输出扁平化后的数组 31 | console.log(flattenArray([1, 2, [3, 4, [5, 6]]])) // [1, 2, 3, 4, 5, 6] 32 | ``` 33 | 34 | - 利用 `toString()` 方法,缺点是改变了元素的类型,只适合于数组中元素都是整数的情况。 35 | 36 | ```js 37 | function flattenArray(array) { 38 | return array 39 | .toString() 40 | .split(',') 41 | .map(function (item) { 42 | return +item 43 | }) 44 | } 45 | 46 | console.log(flattenArray([1, 2, [3, 4, [5, 6]]])) // [1, 2, 3, 4, 5, 6] 47 | ``` 48 | 49 | ## 数组去重 50 | 51 | ```js 52 | // ES5 53 | function unique(array) { 54 | // 检查传入的参数是否为数组,或者数组长度是否小于等于 1,如果是则直接返回 55 | if (!Array.isArray(array) || array.length <= 1) return array 56 | 57 | // 定义结果数组,用于存储唯一的元素 58 | var result = [] 59 | 60 | // 遍历传入的数组 61 | array.forEach(function (item) { 62 | // 检查结果数组中是否已经包含当前元素 63 | if (result.indexOf(item) === -1) { 64 | // 如果不包含,则将当前元素添加到结果数组中 65 | result.push(item) 66 | } 67 | }) 68 | 69 | // 返回去重后的结果数组 70 | return result 71 | } 72 | 73 | // 测试,输出去重后的数组 74 | console.log(unique([1, 2, 2, 3, 4, 4, 5])) // [1, 2, 3, 4, 5] 75 | 76 | 77 | 78 | // ES6+ 79 | function unique(array) { 80 | // 检查传入的参数是否为数组,或者数组长度是否小于等于 1,如果是则直接返回原数组 81 | if (!Array.isArray(array) || array.length <= 1) return array 82 | 83 | // 使用 Set 数据结构去重,然后使用扩展运算符将 Set 转换回数组 84 | return [...new Set(array)] 85 | } 86 | 87 | // 测试,输出去重后的数组 88 | console.log(unique([1, 1, 1, 2, 3, 3, 4, 5])) // [1, 2, 3, 4, 5] 89 | 90 | ``` 91 | 92 | ## 求数组的最大值和最小值 93 | 94 | ```js 95 | // 定义一个包含若干数字的数组 96 | const array = [6, 4, 1, 8, 2, 11, 23] 97 | 98 | // 使用 Math.max 方法找到数组中的最大值 99 | // apply 方法将数组展开为 Math.max 方法的参数 100 | console.log(Math.max.apply(null, array)) // 23 101 | 102 | // 使用 Math.min 方法找到数组中的最小值 103 | // apply 方法将数组展开为 Math.min 方法的参数 104 | console.log(Math.min.apply(null, array)) // 1 105 | 106 | ``` 107 | 108 | ## 求两个数的最大公约数 109 | 110 | 基本思想是采用**辗转相除**的方法,用大的数去除以小的那个数,然后再用小的数去除以的得到的余数,一直这样递归下去,直到余数为 0 时,最后的被除数就是两个数的最大公约数。 111 | 112 | ```js 113 | // 定义一个函数来计算两个数的最大公约数(GCD) 114 | function getMaxCommonDivisor(a, b) { 115 | // 使用递归方式实现欧几里得算法 116 | // 如果 b 为 0,则返回 a,此时 a 就是最大公约数 117 | if (b === 0) return a 118 | 119 | // 否则递归调用函数,将 b 和 a 对 b 取余后的结果作为新的参数 120 | return getMaxCommonDivisor(b, a % b) 121 | } 122 | 123 | // 测试代码,输出 12 和 8 的最大公约数 124 | console.log(getMaxCommonDivisor(12, 8)) // 4 125 | 126 | // 测试代码,输出 12 和 16 的最大公约数 127 | console.log(getMaxCommonDivisor(12, 16)) // 4 128 | ``` 129 | 130 | ## 求两个数的最小公倍数 131 | 132 | 在 JavaScript 中,可以通过分解质因数并计算两数的最大公约数(GCD)的方式来求两个数的最小公倍数(LCM)。最小公倍数可以通过以下公式计算: 133 | 134 | ``` 135 | LCM(a, b) = |a * b| / GCD(a, b) 136 | ``` 137 | 138 | 其中 `|a * b|` 表示 `a` 和 `b` 的乘积的绝对值,`GCD(a, b)` 表示 `a` 和 `b` 的最大公约数。 139 | 140 | 以下是使用欧几里得算法计算最大公约数进而求最小公倍数的 JavaScript 实现: 141 | 142 | ```javascript 143 | // 计算两个数的最大公约数(GCD) 144 | function gcd(a, b) { 145 | // 使用递归方式实现欧几里得算法 146 | if (b === 0) { 147 | // 如果 b 为 0,则返回 a,此时 a 就是最大公约数 148 | return a 149 | } 150 | // 否则递归调用函数,将 b 和 a 对 b 取余后的结果作为新的参数 151 | return gcd(b, a % b) 152 | } 153 | 154 | // 计算两个数的最小公倍数(LCM) 155 | function lcm(a, b) { 156 | // 最小公倍数公式:|a * b| / gcd(a, b) 157 | // 使用绝对值函数 Math.abs 确保结果为正数 158 | return Math.abs(a * b) / gcd(a, b) 159 | } 160 | 161 | // 测试代码,输出 4 和 6 的最小公倍数 162 | console.log(lcm(4, 6)) // 12 163 | ``` 164 | 165 | ## 实现 IndexOf 方法 166 | 167 | 详情查看 → [搜索算法](./search) 168 | 169 | ## 判断一个字符串是否为回文字符串 170 | 171 | 回文字符串是一个正读和反读都一样的字符串。 172 | 173 | ```js 174 | function isPalindrome(str) { 175 | // 移除所有非字母和非数字字符,并将字符串转换为小写 176 | const cleanedStr = str.replace(/[^A-Za-z0-9]/g, '').toLowerCase() 177 | 178 | // 获取清理后的字符串的反转版 179 | const reversedStr = cleanedStr.split('').reverse().join('') 180 | 181 | // 比较清理后的字符串和它的反转版 182 | return cleanedStr === reversedStr 183 | } 184 | 185 | // 示例用法 186 | console.log(isPalindrome('A man, a plan, a canal: Panama')) // 输出 true 187 | console.log(isPalindrome('race a car')) // 输出 false 188 | ``` 189 | 190 | ## 累加函数 191 | 192 | ```js 193 | function sum(...args) { 194 | // 初始化结果变量为 0 195 | let result = 0 196 | 197 | // 使用 reduce 函数将传入的初始参数数组求和,并赋值给 result 198 | result = args.reduce(function (pre, item) { 199 | return pre + item 200 | }, 0) 201 | 202 | // 定义一个内部函数 add,用于继续累加新的参数 203 | const add = function (...args) { 204 | // 将新的参数数组求和并加到当前 result 的值上 205 | result = args.reduce(function (pre, item) { 206 | return pre + item 207 | }, result) 208 | 209 | // 返回 add 函数自身,以便实现链式调用 210 | return add 211 | } 212 | 213 | // 重写 add 函数的 valueOf 方法,使其在被转换为原始值时返回 result 的值 214 | add.valueOf = function () { 215 | console.log(result) 216 | } 217 | 218 | // 返回 add 函数,以便外部可以继续累加 219 | return add 220 | } 221 | 222 | // 使用示例 223 | sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).valueOf() // 55 224 | sum(1, 2, 3)(2).valueOf() // 8 225 | sum(1, 2, 3, 4, 5)(2, 3, 4).valueOf() // 24 226 | ``` 227 | 228 | ## 查找一篇英文文章中出现频率最高的单词 229 | 230 | ```js 231 | function findMostWord(text) { 232 | // 将文本转换为小写,以便忽略大小写差异 233 | text = text.toLowerCase() 234 | 235 | // 使用正则表达式去除文本中的标点符号,并按空格分割成单词数组 236 | const words = text.replace(/[.,!?;:()"]/g, '').split(/\s+/) 237 | 238 | // 创建一个对象来存储每个单词的出现频率 239 | const wordCount = {} 240 | 241 | // 遍历单词数组,统计每个单词的出现次数 242 | words.forEach((word) => { 243 | if (wordCount[word]) { 244 | wordCount[word]++ 245 | } else { 246 | wordCount[word] = 1 247 | } 248 | }) 249 | 250 | // 初始化两个变量,用于存储频率最高的单词及其出现次数 251 | let maxCount = 0 252 | let mostFrequentWord = '' 253 | 254 | // 遍历 wordCount 对象,找出出现次数最多的单词 255 | for (const word in wordCount) { 256 | if (wordCount[word] > maxCount) { 257 | maxCount = wordCount[word] 258 | mostFrequentWord = word 259 | } 260 | } 261 | 262 | // 返回频率最高的单词及其出现次数 263 | return { word: mostFrequentWord, count: maxCount } 264 | } 265 | 266 | // 示例使用 267 | console.log(findMostWord('This is a test. This test is only a test.')) // { word: 'test', count: 3 } 268 | ``` 269 | 270 | ## 斐波那契数列 271 | 272 | 大家都知道斐波那契数列,现在要求输入一个整数 n(n <= 39),请你输出斐波那契数列的第 n 项。 273 | 274 | 思路: 275 | 276 | 斐波那契数列的规律是,第一项为 0,第二项为 1,第三项以后的值都等于前面两项的和,因此可以通过循环的方式,不断通过叠加来实现第 n 项值的构建。 277 | 278 | 通过循环而不是递归的方式来实现,时间复杂度降为了 O(n),空间复杂度为 O(1)。 279 | 280 | 代码实现: 281 | 282 | ```js 283 | function fibonacci(n) { 284 | // 检查输入是否有效 285 | if (n <= 0) { 286 | return 'Invalid input. The number should be greater than 0.' 287 | } 288 | 289 | // 斐波那契数列的第一个数 290 | if (n === 1) { 291 | return 0 292 | } 293 | 294 | // 斐波那契数列的第二个数 295 | if (n === 2) { 296 | return 1 297 | } 298 | 299 | // 初始化前两个斐波那契数 300 | let prevPrev = 0 301 | let prev = 1 302 | 303 | // 通过迭代计算斐波那契数列的第 n 个数 304 | for (let i = 3; i <= n; i++) { 305 | const current = prevPrev + prev // 当前斐波那契数是前两个数之和 306 | prevPrev = prev // 更新前前一个数 307 | prev = current // 更新前一个数 308 | } 309 | 310 | // 返回第 n 个斐波那契数 311 | return prev 312 | } 313 | 314 | // 示例使用 315 | console.log(fibonacci(1)) // 输出:0 316 | console.log(fibonacci(2)) // 输出:1 317 | console.log(fibonacci(3)) // 输出:1 318 | console.log(fibonacci(10)) // 输出:34 319 | console.log(fibonacci(20)) // 输出:4181 320 | console.log(fibonacci(-5)) // 输出:'Invalid input. The number should be greater than 0.' 321 | ``` 322 | 323 | ## 跳台阶 324 | 325 | 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 326 | 327 | 思路: 328 | 329 | 跳台阶的问题是一个动态规划的问题,由于一次只能够跳 1 级或者 2 级,因此跳上 n 级台阶一共有两种方案,一种是从 n-1 跳上,一种是从 n-2 级跳上,因此 `f(n) = f(n-1) + f(n-2)`。 330 | 331 | 和斐波那契数列类似,不过初始两项的值变为了 1 和 2,后面每项的值等于前面两项的和。 332 | 333 | 代码实现: 334 | 335 | ```js 336 | function jumpStairs(n) { 337 | if (n <= 0) { 338 | return 0 // 如果台阶数小于或等于 0,返回 0 339 | } 340 | if (n === 1) { 341 | return 1 // 只有一级台阶时,只有 1 种跳法 342 | } 343 | if (n === 2) { 344 | return 2 // 有两级台阶时,有 2 种跳法(一步一级或一步两级) 345 | } 346 | 347 | // 初始化前两项,dp 数组的大小需要是 n+1 以便存储所有结果 348 | const dp = [0, 1, 2] 349 | 350 | // 从第 3 级台阶开始计算,直到第 n 级台阶 351 | for (let i = 3; i <= n; i++) { 352 | dp[i] = dp[i - 1] + dp[i - 2] // 动态规划公式:f(n) = f(n-1) + f(n-2) 353 | } 354 | 355 | return dp[n] // 返回第 n 级台阶的跳法数 356 | } 357 | 358 | // 示例使用 359 | console.log(jumpStairs(0)) // 输出:0 360 | console.log(jumpStairs(1)) // 输出:1 361 | console.log(jumpStairs(2)) // 输出:2 362 | console.log(jumpStairs(3)) // 输出:3 363 | console.log(jumpStairs(4)) // 输出:3 364 | console.log(jumpStairs(10)) // 输出:89 365 | ``` 366 | 367 | ## 最小的 K 个数 368 | 369 | 输入 n 个整数,找出其中最小的 K 个数。 370 | 371 | 例如:输入 `[4, 5, 1, 6, 2, 7, 3, 8]` 这 8 个数字,则最小的 4 个数字是 `[1, 2, 3, 4]`。 372 | 373 | 思路: 374 | 375 | - 第一种思路是首先将数组排序,排序后再取最小的 k 个数。这一种方法的时间复杂度取决于我们选择的排序算法的时间复杂度,最好的情况下为 O(n log n)。 376 | 377 | ```js 378 | function findKSmallestNumbers(nums, k) { 379 | nums.sort((a, b) => a - b) 380 | return nums.slice(0, k) 381 | } 382 | 383 | const nums = [4, 5, 1, 6, 2, 7, 3, 8] 384 | const k = 4 385 | console.log(findKSmallestNumbers(nums, k)) // 输出:[1, 2, 3, 4] 386 | ``` 387 | 388 | - 第二种思路是由于我们只需要获得最小的 k 个数,这 k 个数不一定是按序排序的。因此我们可以使用快速排序中的 partition 函数来实现。每一次选择一个枢纽值,将数组分为比枢纽值大和比枢纽值小的两个部分,判断枢纽值的位置,如果该枢纽值的位置为 k-1 的话,那么枢纽值和它前面的所有数字就是最小的 k 个数。如果枢纽值的位置小于 k-1 的话,假设枢纽值的位置为 n-1,那么我们已经找到了前 n 小的数字了,我们就还需要到后半部分去寻找后半部分 k-n 小的值,进行划分。当该枢纽值的位置比 k-1 大时,说明最小的 k 个值还在左半部分,我们需要继续对左半部分进行划分。这一种方法的平均时间复杂度为 O(n)。 389 | 390 | ```js 391 | function quickSelect(nums, k) { 392 | function partition(nums, left, right, pivotIndex) { 393 | const pivotValue = nums[pivotIndex] 394 | ;[nums[pivotIndex], nums[right]] = [nums[right], nums[pivotIndex]] 395 | let storeIndex = left 396 | for (let i = left; i < right; i++) { 397 | if (nums[i] < pivotValue) { 398 | ;[nums[storeIndex], nums[i]] = [nums[i], nums[storeIndex]] 399 | storeIndex++ 400 | } 401 | } 402 | ;[nums[right], nums[storeIndex]] = [nums[storeIndex], nums[right]] 403 | return storeIndex 404 | } 405 | 406 | function select(nums, left, right, k) { 407 | if (left === right) return nums[left] 408 | const pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left 409 | const newPivotIndex = partition(nums, left, right, pivotIndex) 410 | if (newPivotIndex === k - 1) { 411 | return nums[newPivotIndex] 412 | } 413 | if (newPivotIndex < k - 1) { 414 | return select(nums, newPivotIndex + 1, right, k) 415 | } 416 | return select(nums, left, newPivotIndex - 1, k) 417 | } 418 | 419 | const sortedNums = [...nums] 420 | const kthSmallest = select(sortedNums, 0, nums.length - 1, k - 1) 421 | 422 | // 获取 k 个最小数 423 | const kSmallestNums = [] 424 | for (let i = 0; i < nums.length; i++) { 425 | if (nums[i] <= kthSmallest) { 426 | kSmallestNums.push(nums[i]) 427 | if (kSmallestNums.length === k) break 428 | } 429 | } 430 | return kSmallestNums 431 | } 432 | 433 | const nums = [4, 5, 1, 6, 2, 7, 3, 8] 434 | const k = 4 435 | console.log(quickSelect(nums, k)) // 输出:[1, 2, 3, 4] 436 | ``` 437 | 438 | - 第三种方法是维护一个容量为 k 的最大堆。对数组进行遍历时,如果堆的容量还没有达到 k,则直接将元素加入到堆中,这就相当于我们假设前 k 个数就是最小的 k 个数。对 k 以后的元素遍历时,我们将该元素与堆的最大值进行比较,如果比最大值小,那么我们则将最大值与其交换,然后调整堆。如果大于等于堆的最大值,则继续向后遍历,直到数组遍历完成。这一种方法的平均时间复杂度为 O(nlogk)。 439 | 440 | ```js 441 | function findKSmallestNumbersWithMaxHeap(nums, k) { 442 | nums.sort((a, b) => b - a) // 先将数组逆序,方便模拟最大堆 443 | const result = [] 444 | for (let i = 0; i < nums.length && result.length < k; i++) { 445 | if (result.length === 0 || nums[i] <= result[0]) { 446 | result.unshift(nums[i]) // 添加到堆顶 447 | result.sort((a, b) => b - a) // 重新调整堆顶元素为最大值 448 | } 449 | } 450 | return result 451 | } 452 | 453 | const nums = [4, 5, 1, 6, 2, 7, 3, 8] 454 | const k = 4 455 | console.log(findKSmallestNumbersWithMaxHeap(nums, k)) // 输出:[1, 2, 3, 4] 456 | ``` 457 | 458 | -------------------------------------------------------------------------------- /src/docs/algorithm/search.md: -------------------------------------------------------------------------------- 1 | # 搜索算法 2 | 3 | 搜索算法简单来说就是用于找出数组中某个元素的下标。 4 | 5 | JavaScript 语言中的自带的搜索:数组的 `indexOf` 方法。 6 | 7 | ## 顺序搜索 8 | 9 | 顺序搜索(Sequential Search)算法是一种简单的搜索算法,它按顺序检查列表中的每个元素,直到找到目标元素或遍历完整个列表。 10 | 11 | 代码实现: 12 | 13 | ```js 14 | function sequentialSearch(array, target) { 15 | // 遍历数组中的每个元素 16 | for (let i = 0; i < array.length; i++) { 17 | // 如果当前元素等于目标元素,则返回当前元素的索引 18 | if (array[i] === target) { 19 | return i 20 | } 21 | } 22 | // 如果未找到目标元素,则返回 -1 23 | return -1 24 | } 25 | 26 | // 测试 27 | console.log(sequentialSearch([1, 2, 3, 4, 5], 0)) // -1 28 | console.log(sequentialSearch([1, 2, 3, 4, 5], 3)) // 2 29 | ``` 30 | 31 | 顺序搜索的时间复杂度为 O(n),其中 n 是列表的长度。 32 | 33 | ## 二分搜索 34 | 35 | 二分搜索(Binary Search)是一种高效的搜索算法,适用于有序数组。该算法通过重复将搜索范围缩小为一半来找到目标值。 36 | 37 | ```js 38 | function binarySearch(arr, target) { 39 | let low = 0 // 搜索范围的最低索引 40 | let high = arr.length - 1 // 搜索范围的最高索引 41 | 42 | while (low <= high) { 43 | const mid = Math.floor((low + high) / 2) // 中间索引 44 | 45 | if (arr[mid] === target) { 46 | return mid // 找到目标元素,返回索引 47 | } 48 | if (arr[mid] < target) { 49 | low = mid + 1 // 目标元素在右半部分,调整搜索范围的最低索引 50 | } else { 51 | high = mid - 1 // 目标元素在左半部分,调整搜索范围的最高索引 52 | } 53 | } 54 | 55 | return -1 // 目标元素未找到 56 | } 57 | 58 | // 测试 59 | console.log(binarySearch([1, 2, 3, 4, 5], 0)) // -1 60 | console.log(binarySearch([1, 2, 3, 4, 5], 3)) // 2 61 | ``` 62 | 63 | 二分搜索的时间复杂度为 O(log n),其中 n 是数组的长度。 64 | -------------------------------------------------------------------------------- /src/docs/algorithm/sort.md: -------------------------------------------------------------------------------- 1 | # 排序算法 2 | 3 | 简单来说,排序算法用于将一组乱序的元素按照升序或降序的顺序重新排列。排序算法的性能通常通过其时间复杂度、空间复杂度、稳定性等指标来衡量。 4 | 5 | JavaScript 语言中的自带的排序:数组的 `sort` 方法。 6 | 7 | ## 冒泡排序 8 | 9 | 冒泡排序(Bubble Sort)是一种简单的比较排序算法。它重复地遍历待排序数组,每次比较相邻的两个元素,如果顺序相反则进行交换。这样,每一轮遍历都会将最大(或最小)的元素“冒泡”到顶端,直到整个数组都排序完成,最终达到完全有序。 10 | 11 | 步骤: 12 | 13 | 1. **遍历数组**:从头到尾遍历数组,比较相邻的两个元素。 14 | 2. **比较相邻元素**:每次比较相邻的两个元素,如果它们的顺序不正确(比如,前一个元素大于后一个元素),则交换它们。 15 | 3. **重复遍历**:重复上述步骤,直到没有任何一对元素需要交换,即数组已经完全排序。 16 | 17 | 代码实现: 18 | 19 | ```js 20 | function bubbleSort(array) { 21 | // 检查输入是否为数组且长度大于 1,若不满足条件,则直接返回原数组 22 | if (!Array.isArray(array) || array.length <= 1) return array 23 | 24 | // 初始化最后一个未排序元素的索引 25 | let lastIndex = array.length - 1 26 | 27 | // 当还有未排序的元素时,执行排序过程 28 | while (lastIndex > 0) { 29 | // 初始化交换标志为 true,若本轮未发生交换,则排序完成 30 | let flag = true 31 | // 记录最后一次交换元素的位置,初始设置为未排序部分的末尾 32 | const k = lastIndex 33 | 34 | // 遍历未排序部分的元素 35 | for (let j = 0; j < k; j++) { 36 | // 若当前元素大于其后面的元素,则交换它们的位置 37 | if (array[j] > array[j + 1]) { 38 | flag = false // 发生了交换,将标志设置为 false 39 | lastIndex = j // 记录最后一次交换的位置 40 | ;[array[j], array[j + 1]] = [array[j + 1], array[j]] // 交换元素 41 | } 42 | } 43 | 44 | // 若本轮未发生交换,则数组已经有序,直接退出循环 45 | if (flag) break 46 | } 47 | 48 | // 返回排序后的数组 49 | return array 50 | } 51 | 52 | // 测试 53 | console.log(bubbleSort([6,1,5,4,2,3])) // [1, 2, 3, 4, 5, 6] 54 | ``` 55 | 56 | 冒泡排序有几种可以优化的空间: 57 | 58 | - **优化遍历范围**:在每一轮排序中,可以观察到最后一次交换发生的位置之后的元素已经是有序的,因此可以将下一轮排序的范围限定在上一轮最后一次交换的位置之前。这样可以减少不必要的比较和交换操作。 59 | 60 | - **添加标志位**:如果在一轮排序过程中没有发生任何元素的交换,说明数组已经是有序的,可以提前结束排序过程。 61 | 62 | - **针对部分有序数组的优化**:如果数组在初始状态下已经接近有序,可以记录下每轮排序中最后一次交换的位置,然后下一轮排序时只需要遍历到该位置即可,这样可以大大减少排序的比较次数。 63 | 64 | - **鸡尾酒排序(双向冒泡排序)**:在一次排序过程中,既从左到右比较交换,又从右到左比较交换,可以在某些特定情况下提升效率。 65 | 66 | 时间复杂度: 67 | 68 | - **最优时间复杂度**:O(n) 69 | 当输入数据已经是有序时,冒泡排序可以通过设置一个标志变量来检测是否发生了交换操作,如果在某一趟排序中没有交换操作发生,说明数组已经有序,因此可以提前结束排序过程。此时,最优时间复杂度为 O(n)。 70 | 71 | - **最坏时间复杂度**:O(n^2) 72 | 在最坏情况下,输入数据是逆序的,此时需要进行 n-1 趟排序,每一趟排序中需要进行的比较次数逐渐减少,总比较次数为 n(n-1)/2,因此最坏时间复杂度为 O(n^2)。 73 | 74 | - **平均时间复杂度**:O(n^2) 75 | 在一般情况下,冒泡排序的比较和交换操作的次数与输入数据的初始排列状态有关,但总体而言其时间复杂度仍为 O(n^2)。 76 | 77 | 空间复杂度: 78 | 79 | 冒泡排序是一种**原地排序算法**,它在排序过程中只需要常数级的额外空间,即只使用了少量的辅助变量,因此其空间复杂度为 O(1)。 80 | 81 | 稳定性: 82 | 83 | 冒泡排序是一种**稳定排序算法**。在排序过程中,如果两个相等的元素相互比较,它们不会交换位置,因此相等元素的相对位置不会改变。 84 | 85 | 冒泡排序由于其简单易懂的特性,常用于教学和小规模数据集的排序,但由于其较低的效率,通常不适合大规模数据集的排序任务。 86 | 87 | ## 选择排序 88 | 89 | 选择排序(Selection Sort)是一种简单的比较排序算法。它的基本思想是在未排序数组中找到最小(或最大)的元素,然后将其放置到数组的起始位置,接着在剩余的未排序部分中继续寻找最小(或最大)的元素,依次类推,直到所有元素都排序完成。 90 | 91 | 步骤: 92 | 93 | 1. **初始状态:** 将整个序列看作两部分,一部分是未排序的,一部分是已排序的(初始时已排序部分为空)。 94 | 95 | 2. **遍历未排序部分:** 遍历未排序部分,找到最小(或最大)的元素。 96 | 97 | 3. **交换元素:** 将找到的最小(或最大)元素与未排序部分的第一个元素交换位置,使得找到的最小元素成为已排序部分的最后一个元素。 98 | 99 | 4. **扩大已排序部分:** 将已排序部分的长度增加 1,未排序部分的长度减少 1。 100 | 101 | 5. **重复:** 重复以上步骤,直到所有元素都已经排序完毕。 102 | 103 | 这个过程类似于每次从一堆未排序的卡片中选出最小(或最大)的卡片,然后放到已排序的卡片堆中。选择排序的特点是每次遍历都只进行一次交换操作,因此相对于其他排序算法,它的交换次数较少。 104 | 105 | 代码实现: 106 | 107 | ```js 108 | function selectionSort(array) { 109 | // 获取数组长度 110 | const { length } = array 111 | 112 | // 如果不是数组或者数组长度小于等于 1,直接返回,不需要排序 113 | if (!Array.isArray(array) || length <= 1) return array 114 | 115 | // 外层循环,遍历整个数组,每次找到当前未排序部分的最小元素并放到已排序部分的末尾 116 | for (let i = 0; i < length - 1; i++) { 117 | let minIndex = i // 设置当前循环最小元素索引 118 | 119 | // 内层循环,从当前元素的下一个位置开始遍历,找到未排序部分的最小元素 120 | for (let j = i + 1; j < length; j++) { 121 | // 如果当前元素比最小元素索引小,则更新最小元素索引 122 | if (array[minIndex] > array[j]) { 123 | minIndex = j 124 | } 125 | } 126 | 127 | // 交换最小元素到当前位置 128 | swap(array, i, minIndex) 129 | } 130 | 131 | return array 132 | } 133 | 134 | // 交换数组中两个元素的位置 135 | function swap(array, left, right) { 136 | const temp = array[left] 137 | array[left] = array[right] 138 | array[right] = temp 139 | } 140 | 141 | // 测试 142 | console.log(selectionSort([6, 1, 5, 4, 2, 3])) // [1, 2, 3, 4, 5, 6] 143 | ``` 144 | 145 | 时间复杂度: 146 | 147 | - **最优时间复杂度**:O(n^2) 148 | 无论输入数据的初始排列状态如何,选择排序总是需要进行 n(n-1)/2 次比较,因此最优时间复杂度为 O(n^2)。 149 | 150 | - **最坏时间复杂度**:O(n^2) 151 | 同样地,在最坏情况下,选择排序仍需要进行 n(n-1)/2 次比较,所以最坏时间复杂度为 O(n^2)。 152 | 153 | - **平均时间复杂度**:O(n^2) 154 | 由于选择排序每一趟排序所需的比较次数固定,因此其平均时间复杂度也为 O(n^2)。 155 | 156 | 空间复杂度: 157 | 158 | 选择排序是一种**原地排序算法**,只需要常数级的辅助空间(通常是用于交换元素的临时变量),因此其空间复杂度为 O(1)。 159 | 160 | 稳定性: 161 | 162 | 选择排序通常**不是稳定排序**。在选择排序过程中,每次从未排序部分选择最小(或最大)元素并将其与未排序部分的第一个元素交换时,如果相等元素存在,原有的相对顺序可能会被打破。例如: 163 | 164 | - 初始数组:[3, 2, 2, 1] 165 | - 第一次选择:选择最小元素 1,与第一个元素 3 交换,结果:[1, 2, 2, 3] 166 | - 第二次选择:选择最小元素 2,与第二个元素 2 交换,结果:[1, 2, 2, 3] 167 | 168 | 虽然这个例子没有改变相同元素的相对顺序,但在某些情况下,如处理:[2, 3, 1, 2],第二个“2”会被提前,与第一个“2”交换,导致顺序改变。 169 | 170 | 选择排序由于其简单性和恒定的空间复杂度,适用于对内存空间要求较高但对时间效率要求不高的场景。然而,由于其 O(n^2) 的时间复杂度,选择排序在处理大规模数据集时效率较低,通常不作为首选的排序算法。 171 | 172 | ## 插入排序 173 | 174 | 插入排序(Insertion Sort)是一种简单的比较排序算法。它的基本思想是将待排序数组分成**已排序**和**未排序**两部分,初始时已排序部分只有一个元素(即数组的第一个元素),然后从未排序部分依次取出元素,将其插入到已排序部分的正确位置,直到所有元素都被插入完成。 175 | 176 | > 插入排序类似扑克牌思想,想象在打扑克牌,拿起来第一张,放哪里无所谓,再拿起来一张,比第一张小,放左边,继续拿,可能是中间数,就插在中间,依次把牌拿完。 177 | 178 | 步骤: 179 | 180 | 1. **初始已排序部分**:初始时,将待排序数组的第一个元素视为已排序部分,其余元素视为未排序部分。 181 | 2. **遍历未排序部分**:从第二个元素开始,依次遍历未排序部分的元素。 182 | 3. **插入到已排序部分**:对于每个未排序部分的元素,将其与已排序部分的元素逐个比较,找到正确的插入位置。 183 | 4. **重复插入**:将元素插入到已排序部分的正确位置后,已排序部分的长度增加 1,未排序部分的长度减少 1,继续重复上述步骤,直到所有元素都被插入完成。 184 | 185 | 代码实现: 186 | 187 | ```js 188 | function insertSort(array) { 189 | const { length } = array 190 | 191 | // 如果不是数组或者数组长度小于等于 1,直接返回,不需要排序 192 | if (!Array.isArray(array) || length <= 1) return array 193 | 194 | // 循环从 1 开始,0 位置为默认的已排序的序列 195 | for (let i = 1; i < length; i++) { 196 | const temp = array[i] // 保存当前需要排序的元素 197 | let j = i 198 | 199 | // 在当前已排序序列中比较,如果比需要排序的元素大,就依次往后移动位置 200 | while (j - 1 >= 0 && array[j - 1] > temp) { 201 | array[j] = array[j - 1] 202 | j-- 203 | } 204 | 205 | // 将找到的位置插入元素 206 | array[j] = temp 207 | } 208 | 209 | return array 210 | } 211 | 212 | insertSort([6,1,5,4,2,3]) // [1, 2, 3, 4, 5, 6] 213 | ``` 214 | 215 | 时间复杂度: 216 | 217 | - **最优时间复杂度**:O(n) 218 | 当输入数据已经有序时,插入排序每次只需要比较一次即可确定元素的位置,无需进行交换操作。此时,最优时间复杂度为 O(n)。 219 | 220 | - **最坏时间复杂度**:O(n^2) 221 | 在最坏情况下,输入数据是逆序的。此时,插入排序需要进行大量的比较和移动操作,每次插入元素时都需要将其与已经排序的部分进行比较并移动其他元素。因此最坏时间复杂度为 O(n^2)。 222 | 223 | - **平均时间复杂度**:O(n^2) 224 | 在一般情况下,插入排序的比较和移动操作次数与输入数据的初始排列状态有关,但总体而言,其平均时间复杂度为 O(n^2)。 225 | 226 | 空间复杂度: 227 | 228 | 插入排序是一种**原地排序算法**,它在排序过程中只需要常数级的额外空间(用于存储待插入的元素的临时变量),因此其空间复杂度为 O(1)。 229 | 230 | 稳定性: 231 | 232 | 插入排序是一种**稳定排序算法**。在插入过程中,如果待插入的元素与已排序部分的某个元素相等,插入排序会将待插入的元素放在相等元素的后面,从而保持相等元素的相对顺序不变。 233 | 234 | 插入排序由于其简单性和对小规模数据集的高效性,常用于对小型数组进行排序或在其他更复杂的排序算法(如快速排序、归并排序)的过程中处理小数据块。然而,由于其 O(n^2) 的时间复杂度,插入排序在处理大规模数据集时效率较低。 235 | 236 | ## 希尔排序 237 | 238 | 希尔排序(Shell Sort)是一种改进的插入排序算法,也被称为“缩小增量排序”。它的基本思想是通过定义一个间隔序列(称为增量序列),将待排序数组分成若干个子序列,对每个子序列进行插入排序。随着排序的进行,增量序列逐渐缩小,直到增量为 1,最后对整个数组进行插入排序。 239 | 240 | 步骤: 241 | 242 | 1. **选择增量序列**:定义一个增量序列,确定每个增量值(间隔),通常以递减的方式选择。 243 | 2. **分组排序**:将待排序数组根据当前增量值分成若干个子序列,对每个子序列进行插入排序。 244 | 3. **逐步缩小增量**:重复上述步骤,逐步缩小增量值,直到增量为 1。 245 | 4. **最终排序**:当增量为 1 时,对整个数组进行一次插入排序,完成排序过程。 246 | 247 | 代码实现: 248 | 249 | ```js 250 | function hillSort(array) { 251 | const { length } = array 252 | 253 | // 如果输入不是数组或者数组长度小于等于 1,直接返回原数组,不需要排序 254 | if (!Array.isArray(array) || length <= 1) return array 255 | 256 | // 第一层循环:确定增量的大小,每次增量的大小减半 257 | for (let gap = parseInt(length >> 1); gap >= 1; gap = parseInt(gap >> 1)) { 258 | // 对每个分组使用插入排序,相当于将插入排序的 1 换成了 gap 259 | for (let i = gap; i < length; i++) { 260 | const temp = array[i] // 保存当前元素 261 | let j = i 262 | 263 | // 第二层循环:对当前分组进行插入排序 264 | // 如果 j - gap >= 0 并且前一个元素大于当前元素,则进行交换 265 | while (j - gap >= 0 && array[j - gap] > temp) { 266 | array[j] = array[j - gap] // 将前一个元素后移 267 | j -= gap // 继续比较下一个分组内的元素 268 | } 269 | array[j] = temp // 插入元素到正确的位置 270 | } 271 | } 272 | 273 | return array // 返回排序后的数组 274 | } 275 | 276 | hillSort([6,1,5,4,2,3]) // [1, 2, 3, 4, 5, 6] 277 | ``` 278 | 279 | 时间复杂度: 280 | 281 | 希尔排序的时间复杂度较为复杂,与所选的增量序列(gap sequence)有很大关系。常见的增量序列有希尔增量序列、Hibbard 增量序列、Sedgewick 增量序列等。以下是几种常见增量序列的时间复杂度分析: 282 | 283 | - **希尔增量序列**(gap = n/2, n/4, ..., 1):最坏时间复杂度:O(n^2) 284 | 285 | - **Hibbard 增量序列**(gap = 2^k - 1):最坏时间复杂度:O(n^(3/2)) 286 | 287 | - **Sedgewick 增量序列**(一种较为复杂的序列):最坏时间复杂度:O(n^(4/3)) 288 | 289 | - **更优的增量序列**:有些优化过的增量序列可以达到 O(n log^2 n) 的最坏时间复杂度。 290 | 291 | 由于增量序列的选择对希尔排序的时间复杂度有很大的影响,所以具体的时间复杂度因实现而异,但通常在 O(n^2) 和 O(n log^2 n) 之间。 292 | 293 | 空间复杂度: 294 | 295 | 希尔排序是一种**原地排序算法**,其空间复杂度为 O(1),只需要常数级的额外空间。 296 | 297 | 稳定性: 298 | 299 | 希尔排序**不是稳定排序**。在排序过程中,元素可能会跨越多个位置进行交换,因此相同元素的相对顺序可能会被打乱。 300 | 301 | 希尔排序由于其高效性和相对简单的实现,在实际应用中有一定的优势,特别是在数据规模较大时。它通过对插入排序的改进,大大减少了数据移动的次数,从而提高了排序的效率。 302 | 303 | ## 归并排序 304 | 305 | 归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)的高效排序算法。其基本思想是将数组分成更小的子数组,分别对这些子数组进行排序,然后再将它们合并起来,以得到一个有序的数组。 306 | 307 | 步骤: 308 | 309 | 1. **分割(Divide)**:将数组从中间分成两个子数组(递归地分割直到子数组的长度为 1)。 310 | 2. **排序(Conquer)**:对每个子数组进行排序。因为子数组的长度为 1,所以它们是有序的。 311 | 3. **合并(Combine)**:将两个有序的子数组合并成一个有序的数组。重复这个过程,直到所有的子数组被合并成一个完整的有序数组。 312 | 313 | 314 | 代码实现: 315 | 316 | ```js 317 | function mergeSort(array) { 318 | const { length } = array 319 | 320 | // 如果不是数组或者数组长度小于等于 0,直接返回,不需要排序 321 | if (!Array.isArray(array) || length <= 1) return array 322 | 323 | const mid = parseInt(length >> 1) // 找到中间索引值 324 | const left = array.slice(0, mid) // 截取左半部分 325 | const right = array.slice(mid, length) // 截取右半部分 326 | 327 | return merge(mergeSort(left), mergeSort(right)) // 递归分解后,进行排序合并 328 | } 329 | 330 | function merge(leftArray, rightArray) { 331 | const result = [] 332 | const leftLength = leftArray.length 333 | const rightLength = rightArray.length 334 | let il = 0 335 | let ir = 0 336 | 337 | // 左右两个数组的元素依次比较,将较小的元素加入结果数组中,直到其中一个数组的元素全部加入完则停止 338 | while (il < leftLength && ir < rightLength) { 339 | if (leftArray[il] < rightArray[ir]) { 340 | result.push(leftArray[il++]) 341 | } else { 342 | result.push(rightArray[ir++]) 343 | } 344 | } 345 | 346 | // 如果是左边数组还有剩余,则把剩余的元素全部加入到结果数组中。 347 | while (il < leftLength) { 348 | result.push(leftArray[il++]) 349 | } 350 | 351 | // 如果是右边数组还有剩余,则把剩余的元素全部加入到结果数组中。 352 | while (ir < rightLength) { 353 | result.push(rightArray[ir++]) 354 | } 355 | 356 | return result 357 | } 358 | 359 | mergeSort([6,1,5,4,2,3]) // [1, 2, 3, 4, 5, 6] 360 | ``` 361 | 362 | 基本过程: 363 | 364 | 1. **分割**:将待排序数组分成两半。 365 | 2. **递归排序**:对每一半分别进行递归排序。 366 | 3. **合并**:合并两个有序子数组以形成一个有序的整体。 367 | 368 | 时间复杂度: 369 | 370 | 1. **最优时间复杂度**:O(n log n) 371 | 2. **最坏时间复杂度**:O(n log n) 372 | 3. **平均时间复杂度**:O(n log n) 373 | 374 | 归并排序在最优、最坏和平均情况下的时间复杂度都是 O(n log n),因为它始终将数组分成两半,然后对每一半进行排序,再合并结果。 375 | 376 | 空间复杂度: 377 | 378 | 归并排序的空间复杂度为 O(n),这是因为归并排序在合并过程中需要一个额外的数组来暂存数据。对于递归实现,还需要考虑递归调用的栈空间,但总的额外空间仍然是 O(n)。 379 | 380 | 稳定性: 381 | 382 | 归并排序是一种**稳定排序算法**。在合并两个有序子数组的过程中,如果两个元素相等,先将前一个数组的元素放入结果数组中,从而保持相等元素的相对顺序不变。 383 | 384 | 归并排序由于其稳定性和 O(n log n) 的时间复杂度,常用于处理大规模数据集,尤其是在需要稳定排序的情况下。虽然归并排序的空间复杂度较高,但其分治策略使其在许多应用中表现出色。 385 | 386 | ## 快速排序 387 | 388 | 快速排序(Quick Sort)是一种高效的排序算法,基于分治法(Divide and Conquer)。它通过选择一个"基准"(pivot)元素,并将数组分成两部分,其中一部分的所有元素都小于基准,另一部分的所有元素都大于基准。然后递归地对这两部分进行排序。 389 | 390 | 步骤: 391 | 392 | 1. **选择基准**:从数组中选择一个元素作为基准(pivot)。 393 | 2. **分区**:重新排列数组,使得所有小于基准的元素在基准的左边,所有大于基准的元素在基准的右边(相等的元素可以放在任一侧)。此时基准元素处于其正确的位置。 394 | 3. **递归排序**:递归地对基准左边的子数组和右边的子数组进行排序。 395 | 396 | 代码实现: 397 | 398 | ```js 399 | function quickSort(array) { 400 | // 如果不是数组或者数组长度小于等于 0,直接返回,不需要排序 401 | if (!Array.isArray(array) || array.length <= 1) return array 402 | 403 | // 选择基准 404 | const pivot = array[array.length - 1] 405 | // 使用两个数组 left 和 right 来存储小于和大于基准的元素 406 | const left = [] 407 | const right = [] 408 | 409 | // 分区过程 410 | for (let i = 0; i < array.length - 1; i++) { 411 | if (array[i] < pivot) { 412 | left.push(array[i]) 413 | } else { 414 | right.push(array[i]) 415 | } 416 | } 417 | 418 | // 递归地排序左右子数组并合并 419 | return [...quickSort(left), pivot, ...quickSort(right)] 420 | } 421 | 422 | quickSort([6, 1, 5, 4, 2, 3]) // [1, 2, 3, 4, 5, 6] 423 | ``` 424 | 425 | 时间复杂度: 426 | 427 | - **最优时间复杂度**:O(n log n) 428 | 当每次划分的子数组都比较均匀时,递归树的高度为 log n,每层的操作复杂度为 O(n),所以最优时间复杂度为 O(n log n)。 429 | 430 | - **最坏时间复杂度**:O(n^2) 431 | 在最坏情况下,每次划分的子数组高度不均匀,例如每次选择的基准(pivot)是最大或最小元素,这会导致递归树退化为链表形式,时间复杂度为 O(n^2)。 432 | 433 | - **平均时间复杂度**:O(n log n) 434 | 在实际应用中,快速排序的平均性能通常很好,期望时间复杂度为 O(n log n),因为随机选择基准或使用“三数取中”等方法可以有效避免最坏情况。 435 | 436 | 空间复杂度: 437 | 438 | 快速排序的空间复杂度主要取决于递归调用栈的深度: 439 | 440 | - **平均空间复杂度**:O(log n) 441 | 在理想情况下,递归调用栈的深度为 log n,因此空间复杂度为 O(log n)。 442 | 443 | - **最坏空间复杂度**:O(n) 444 | 在最坏情况下,递归调用栈的深度为 n,因此空间复杂度为 O(n)。 445 | 446 | 稳定性: 447 | 448 | 快速排序**不是稳定排序**。在排序过程中,元素的相对顺序可能会被改变,因为基准元素的交换可能会使得相等的元素顺序颠倒。 449 | 450 | 快速排序因其高效性和较好的平均性能,广泛应用于各种排序任务。通过随机选择基准或“三数取中”等方法,可以有效地改善其性能,避免最坏情况的发生。 451 | 452 | ## 堆排序 453 | 454 | 堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。堆排序可以分为两个阶段:构建初始堆和在堆上进行排序操作。 455 | 456 | 步骤: 457 | 458 | 1. **构建最大堆**:将无序数组构建成一个最大堆(max heap),最大堆是一个完全二叉树,其中每个节点的值都大于或等于其子节点的值。 459 | 2. **排序**:交换堆顶元素(最大值)和堆的最后一个元素,并将堆的大小减少 1。然后对堆的根节点进行调整,使其重新成为最大堆。 460 | 重复上述步骤,直到堆中剩余元素只剩一个,即完成排序。 461 | 462 | ```js 463 | function heapSort(array) { 464 | // 如果不是数组或者数组长度小于等于 1,直接返回,不需要排序 465 | if (!Array.isArray(array) || array.length <= 1) return array 466 | 467 | const n = array.length 468 | 469 | // 构建最大堆 470 | for (let i = Math.floor(n / 2) - 1; i >= 0; i--) { 471 | heapify(array, n, i) 472 | } 473 | 474 | // 逐一从堆中取出元素,并对剩余元素重新堆化 475 | for (let i = n - 1; i > 0; i--) { 476 | // 将堆顶(最大值)和堆的最后一个元素交换 477 | ;[array[0], array[i]] = [array[i], array[0]] 478 | 479 | // 对堆的剩余部分重新堆化 480 | heapify(array, i, 0) 481 | } 482 | 483 | return array 484 | } 485 | 486 | // 堆化函数,维护堆的性质 487 | function heapify(array, n, i) { 488 | let largest = i // 假设当前节点是最大值 489 | const left = 2 * i + 1 // 左子节点 490 | const right = 2 * i + 2 // 右子节点 491 | 492 | // 如果左子节点大于当前节点,则更新最大值 493 | if (left < n && array[left] > array[largest]) { 494 | largest = left 495 | } 496 | 497 | // 如果右子节点大于当前节点,则更新最大值 498 | if (right < n && array[right] > array[largest]) { 499 | largest = right 500 | } 501 | 502 | // 如果最大值不是当前节点,则交换并继续堆化 503 | if (largest !== i) { 504 | ;[array[i], array[largest]] = [array[largest], array[i]] 505 | heapify(array, n, largest) 506 | } 507 | } 508 | 509 | heapSort([6, 1, 5, 4, 2, 3]) // [1, 2, 3, 4, 5, 6] 510 | ``` 511 | 512 | 堆排序(Heap Sort)是一种高效的排序算法,利用了堆数据结构的特性。以下是堆排序的时间复杂度、空间复杂度及其稳定性的详细分析: 513 | 514 | 时间复杂度: 515 | 516 | - **最优时间复杂度**:O(n log n) 517 | 在最优情况下,堆排序的时间复杂度为 O(n log n),因为构建最大堆和进行堆排序的时间复杂度都是 O(n log n)。 518 | 519 | - **最坏时间复杂度**:O(n log n) 520 | 在最坏情况下,堆排序的时间复杂度也是 O(n log n)。无论输入数据的顺序如何,都需要将数据构建成最大堆,然后进行排序。 521 | 522 | - **平均时间复杂度**:O(n log n) 523 | 524 | 空间复杂度: 525 | 526 | 堆排序是一种**原地排序算法**,它只需要常数级别的额外空间来存储堆的数据结构,因此其空间复杂度为 O(1)。 527 | 528 | 稳定性: 529 | 530 | 堆排序**不是稳定排序算法**。在堆排序中,可能会破坏相同元素的相对顺序,因此它不是稳定的排序算法。 531 | 532 | 堆排序由于其高效性和原地排序的特性,常用于需要稳定且较高性能的排序任务。虽然堆排序的实现相对复杂,但它的时间复杂度稳定在 O(n log n),在实践中具有较好的性能表现。 533 | 534 | ## 基数排序 535 | 536 | 基数排序(Radix Sort)是一种非比较性的排序算法,它根据关键字的每个位的值来排序。基数排序适用于元素都是整数的数组,其中每个元素都有相同的位数或范围。基本思想是将待排序的元素按照位数进行分组,然后按照每一位的顺序依次排序。 537 | 538 | 步骤: 539 | 540 | 1. **按照最低有效位进行排序**:从最低位(个位)开始,将元素按照该位的值进行分组(例如 0 到 9),并按照顺序重新排列。 541 | 542 | 2. **依次对更高位进行排序**:对每一位重复上述排序过程,直到按照最高位排序完成。 543 | 544 | 3. **合并分组**:每次按照位数排序后,将所有分组合并为一个数组,形成新的待排序数组。 545 | 546 | 4. **重复步骤 1~3**,直到所有位都被处理完毕。 547 | 548 | 示例: 549 | 550 | 假设我们有一个无序数组 `[170, 45, 75, 90, 802, 24, 2, 66]`,使用基数排序对其进行排序: 551 | 552 | 1. **按照个位进行排序**: 553 | 将数字按照个位的值进行分组:`[170, 90, 802, 2], [24], [45, 75], [66]`,并按照顺序重新排列:`[170, 90, 802, 2, 24, 45, 75, 66]`。 554 | 555 | 2. **按照十位进行排序**: 556 | 将数字按照十位的值进行分组:`[802, 2], [24], [45, 66], [75], [170, 90]`,并按照顺序重新排列:`[802, 2, 24, 45, 66, 75, 170, 90]`。 557 | 558 | 3. **按照百位进行排序**(如果有的话,本例中没有)。 559 | 560 | 4. 排序完成,得到有序数组 `[2, 24, 45, 66, 75, 90, 170, 802]`。 561 | 562 | 代码实现: 563 | 564 | ```js 565 | // 获取数字的指定位数上的数字 566 | function getDigit(num, place) { 567 | return Math.floor(Math.abs(num) / 10 ** place) % 10 568 | } 569 | 570 | // 获取数字的位数(最大位数) 571 | function digitCount(num) { 572 | if (num === 0) return 1 573 | return Math.floor(Math.log10(Math.abs(num))) + 1 574 | } 575 | 576 | // 获取数组中最大数字的位数 577 | function mostDigits(nums) { 578 | let maxDigits = 0 579 | for (let i = 0; i < nums.length; i++) { 580 | maxDigits = Math.max(maxDigits, digitCount(nums[i])) 581 | } 582 | return maxDigits 583 | } 584 | 585 | function radixSort(nums) { 586 | const maxDigitCount = mostDigits(nums) 587 | 588 | for (let k = 0; k < maxDigitCount; k++) { 589 | // 创建 10 个桶(0 到 9) 590 | const digitBuckets = Array.from({ length: 10 }, () => []) 591 | 592 | // 将数字放入相应的桶中 593 | for (let i = 0; i < nums.length; i++) { 594 | const digit = getDigit(nums[i], k) 595 | digitBuckets[digit].push(nums[i]) 596 | } 597 | 598 | // 合并所有桶中的数字成为新的待排序数组 599 | nums = [].concat(...digitBuckets) 600 | } 601 | 602 | return nums 603 | } 604 | 605 | radixSort([6, 1, 5, 4, 2, 3]) // [1, 2, 3, 4, 5, 6] 606 | ``` 607 | 608 | 时间复杂度: 609 | 610 | - **最优时间复杂度**:O(n * k) 611 | 最优情况下,每个关键字的位数相同,基数排序的时间复杂度为 O(n * k),其中 n 是元素个数,k 是关键字的位数。 612 | 613 | - **最坏时间复杂度**:O(n * k) 614 | 最坏情况下,基数排序的时间复杂度仍然为 O(n * k)。 615 | 616 | - **平均时间复杂度**:O(n * k) 617 | 基数排序的平均时间复杂度也为 O(n * k),其中 k 通常为常数。 618 | 619 | 基数排序的时间复杂度主要取决于关键字的位数和元素个数,与元素的大小范围无关。 620 | 621 | 空间复杂度: 622 | 623 | 基数排序的空间复杂度取决于辅助存储空间的使用,通常需要一个额外的数组来存储中间结果。因此,其空间复杂度为 O(n + k),其中 n 是元素个数,k 是关键字的范围(通常是 10)。 624 | 625 | 稳定性: 626 | 627 | 基数排序是一种**稳定排序算法**。在基数排序过程中,相同位数的元素根据其原始顺序进行排序,不会改变相等元素的相对位置,因此是稳定的。 628 | 629 | 基数排序适用于处理整数或字符串等具有固定位数的元素集合。它的时间复杂度相对较低,并且是稳定排序算法,因此在一些特定的排序场景中具有一定的优势。 630 | 631 | ## 计数排序 632 | 633 | 计数排序(Counting Sort)是一种非比较性的排序算法,适用于待排序元素都属于一个有限范围的整数。计数排序的基本思想是通过统计待排序数组中每个元素出现的次数,然后根据统计信息将元素放置到正确的位置上。 634 | 635 | 步骤: 636 | 637 | 1. **统计元素出现次数**:遍历待排序数组,统计每个元素出现的次数,存储在一个辅助数组中。 638 | 2. **累加统计次数**:对统计数组进行累加,使得每个位置存储的值表示小于等于该值的元素的个数。 639 | 3. **根据统计信息排序**:遍历待排序数组,根据统计数组中的信息,将元素放置到正确的位置上。 640 | 641 | 示例: 642 | 643 | 假设我们有一个无序数组 `[4, 2, 2, 8, 3, 3, 1]`,使用计数排序对其进行排序: 644 | 645 | 1. **统计元素出现次数**:统计数组中每个元素的出现次数:`[1:1, 2:2, 3:2, 4:1, 8:1]`。 646 | 647 | 2. **累加统计次数**:将统计数组中的值进行累加:`[1:1, 2:3, 3:5, 4:6, 8:7]`,表示小于等于每个元素的个数。 648 | 649 | 3. **根据统计信息排序**:根据累加统计次数,将待排序数组中的元素放置到正确的位置上,得到有序数组 `[1, 2, 2, 3, 3, 4, 8]`。 650 | 651 | 代码实现: 652 | 653 | ```js 654 | function countingSort(array) { 655 | // 找到待排序数组中的最大值和最小值 656 | let min = array[0] 657 | let max = array[0] 658 | for (let i = 1; i < array.length; i++) { 659 | if (array[i] < min) min = array[i] 660 | if (array[i] > max) max = array[i] 661 | } 662 | 663 | // 创建统计数组,长度为 max - min + 1 664 | const countArray = new Array(max - min + 1).fill(0) 665 | 666 | // 统计每个元素出现的次数 667 | for (let i = 0; i < array.length; i++) { 668 | countArray[array[i] - min]++ 669 | } 670 | 671 | // 根据统计信息对元素进行排序 672 | let sortedIndex = 0 673 | for (let i = 0; i < countArray.length; i++) { 674 | while (countArray[i] > 0) { 675 | array[sortedIndex] = i + min 676 | sortedIndex++ 677 | countArray[i]-- 678 | } 679 | } 680 | 681 | return array 682 | } 683 | 684 | countingSort([6, 1, 5, 4, 2, 3]) // [1, 2, 3, 4, 5, 6] 685 | ``` 686 | 687 | 时间复杂度: 688 | 689 | - **最优时间复杂度**:O(n + k) 690 | 最优情况下,计数排序的时间复杂度为 O(n + k),其中 n 是元素个数,k 是元素的范围。 691 | 692 | - **最坏时间复杂度**:O(n + k) 693 | 最坏情况下,计数排序的时间复杂度仍然为 O(n + k)。 694 | 695 | - **平均时间复杂度**:O(n + k) 696 | 计数排序的平均时间复杂度也为 O(n + k)。 697 | 698 | 计数排序的时间复杂度主要取决于元素的范围,而与元素的个数无关。 699 | 700 | 空间复杂度: 701 | 702 | 计数排序的空间复杂度取决于额外的计数数组和输出数组。因此,其空间复杂度为 O(n + k),其中 n 是元素个数,k 是元素的范围。 703 | 704 | 稳定性: 705 | 706 | 计数排序是一种**稳定排序算法**。在计数排序中,相同元素的相对顺序不会改变,因此是稳定的。 707 | 708 | 计数排序适用于对一定范围内的整数进行排序,并且适合于元素范围不是很大的情况下。由于其时间复杂度和空间复杂度均为线性,因此在一些特定的排序场景中具有较好的性能表现。 -------------------------------------------------------------------------------- /src/docs/data-structure/Array.md: -------------------------------------------------------------------------------- 1 | # 数组 2 | 3 | 数组是最简单的内存数据结构,几乎所有的编程语言都原生支持数组类型。 4 | 5 | 大多数强类型的编程语言里面数组通常情况下都用于存储一系列同一种数据类型的值,但在 JavaScript 里,数组中可以保存不同类型的值。 6 | 7 | ## 创建和初始化数组 8 | 9 | - 使用 `new Array()` 创建数组 10 | 11 | ```js 12 | const daysOfWeek = new Array( 13 | "Sunday", 14 | "Monday", 15 | "Tuesday", 16 | "Wednesday", 17 | "Thursday", 18 | "Friday", 19 | "Saturday" 20 | ); 21 | // ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] 22 | ``` 23 | 24 | - 使用 `[]` 创建数组 25 | ```js 26 | const daysOfWeek = [ 27 | "Sunday", 28 | "Monday", 29 | "Tuesday", 30 | "Wednesday", 31 | "Thursday", 32 | "Friday", 33 | "Saturday", 34 | ]; 35 | // ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] 36 | ``` 37 | 38 | ## 数组常见操作 39 | 40 | ### 添加元素 41 | 42 | - 添加一个元素到数组的最后位置 `array.push(item)` 43 | - 在数组首位插入一个元素 `array.unshift(item)` 44 | - 在指定索引位置插入元素 `array.splice(index, 0, item)` 45 | > splice() 第二个参数为 0 时,表示插入数据。 46 | ```js 47 | const myArray = [1, 2, 3]; 48 | // 在索引 0 的位置,插入 A 49 | myArray.splice(0, 0, "A"); 50 | console.log(myArray); //--> ['A', 1, 2, 3] 51 | ``` 52 | 53 | ### 删除元素 54 | 55 | - 删除数组最后的元素 `array.pop()` 56 | - 删除数组首位的元素 `array.shift()` 57 | - 删除指定索引位置的元素 `array.splice(start, deleteCount)` 58 | ```js 59 | const myArray2 = [1, 2, 3, 4, 5]; 60 | // 删除索引 3 位置起的 2 个元素 61 | myArray2.splice(3, 2); 62 | console.log(myArray2); //--> [1, 2, 3] 63 | ``` 64 | 65 | ### 修改元素 66 | 67 | - 修改指定索引位置的元素 `array.splice(index, 1, item)` 68 | ```js 69 | const myArray3 = [1, 2, 3, 4, 5, 6]; 70 | // 修改索引 1 的位置的元素为 AA 71 | myArray3.splice(1, 1, "AA"); 72 | console.log(myArray3); //--> [1, "AA", 3, 4, 5, 6] 73 | ``` 74 | 75 | - 修改指定索引位置的几个元素 `array.splice(index, number, item)` 76 | ```js 77 | const myArray4 = [1, 2, 3, 4, 5, 6, 7]; 78 | // 在索引 2 的位置起,修改两个元素为 AA BB 79 | myArray4.splice(2, 2, "AA", "BB"); 80 | console.log(myArray4); //--> [1, 2, "AA", "BB", 5, 6, 7] 81 | ``` 82 | -------------------------------------------------------------------------------- /src/docs/data-structure/BinarySearchTree.md: -------------------------------------------------------------------------------- 1 | # 二叉搜索树 2 | 3 | 二叉搜索树(BST,Binary Search Tree),也称为二叉排序树和二叉查找树。 4 | 5 | 二叉搜索树是一棵二叉树,可以为空。 6 | 7 | 如果不为空,则满足以下性质: 8 | 9 | - 条件 1:非空左子树的所有键值小于其根节点的键值。比如树三中节点 6 的所有非空左子树的键值都小于 6; 10 | - 条件 2:非空右子树的所有键值大于其根节点的键值;比如树三中节点 6 的所有非空右子树的键值都大于 6; 11 | - 条件 3:左、右子树本身也都是二叉搜索树; 12 | 13 | ![img](../public/images/data-structure/img-37.png) 14 | 15 | 如上图所示,树二和树三符合 3 个条件属于二叉树,树一不满足条件 3 所以不是二叉树。 16 | 17 | 总结:二叉搜索树的特点主要是**较小的值总是保存在左节点上**,**相对较大的值总是保存在右节点上**。这种特点使得二叉搜索树的查询效率非常高,这也就是二叉搜索树中“搜索”的来源。 18 | 19 | ## 二叉搜索树应用举例 20 | 21 | 下面是一个二叉搜索树: 22 | 23 | ![img](../public/images/data-structure/img-38.png) 24 | 25 | 若想在其中查找数据 10,只需要查找 4 次,查找效率非常高。 26 | 27 | - 第 1 次:将 10 与根节点 9 进行比较,由于 10 > 9,所以 10 下一步与根节点 9 的右子节点 13 比较; 28 | - 第 2 次:由于 10 < 13,所以 10 下一步与父节点 13 的左子节点 11 比较; 29 | - 第 3 次:由于 10 < 11,所以 10 下一步与父节点 11 的左子节点 10 比较; 30 | - 第 4 次:由于 10 = 10,最终查找到数据 10。 31 | 32 | ![img](../public/images/data-structure/img-39.png) 33 | 34 | 同样是 15 个数据,在排序好的数组中查询数据 10,需要查询 10 次: 35 | 36 | ![img](../public/images/data-structure/img-40.png) 37 | 38 | 其实:如果是排序好的数组,可以通过二分查找:第一次找 9,第二次找 13,第三次找 15...。我们发现如果把每次二分的数据拿出来以树的形式表示的话就是二叉搜索树。这就是数组二分法查找效率之所以高的原因。 39 | 40 | ## 二叉搜索树的封装 41 | 42 | 二叉搜索树有四个最基本的属性:指向节点的根(root),节点中的键(key)、左指针(right)、右指针(right)。 43 | 44 | ![img](../public/images/data-structure/img-41.png) 45 | 46 | 所以,二叉搜索树中除了定义 root 属性外,还应定义一个节点内部类,里面包含每个节点中的 left、right 和 key 三个属性。 47 | 48 | ```js 49 | // 节点类 50 | class Node { 51 | constructor(key) { 52 | this.key = key; 53 | this.left = null; 54 | this.right = null; 55 | } 56 | } 57 | ``` 58 | 59 | ## 二叉搜索树的常见操作 60 | 61 | - `insert(key)` 向树中插入一个新的键。 62 | - `search(key)` 在树中查找一个键,如果节点存在,则返回 true;如果不存在,则返回 `false`。 63 | - `preOrderTraverse` 通过先序遍历方式遍历所有节点。 64 | - `inOrderTraverse` 通过中序遍历方式遍历所有节点。 65 | - `postOrderTraverse` 通过后序遍历方式遍历所有节点。 66 | - `min` 返回树中最小的值/键。 67 | - `max` 返回树中最大的值/键。 68 | - `remove(key)` 从树中移除某个键。 69 | 70 | ### 插入数据 71 | 72 | 实现思路: 73 | 74 | - 首先根据传入的 key 创建节点对象。 75 | - 然后判断根节点是否存在,不存在时通过:this.root = newNode,直接把新节点作为二叉搜索树的根节点。 76 | - 若存在根节点则重新定义一个内部方法 `insertNode()` 用于查找插入点。 77 | 78 | `insert(key)` 代码实现 79 | 80 | ```js 81 | // insert(key) 插入数据 82 | insert(key) { 83 | const newNode = new Node(key); 84 | 85 | if (this.root === null) { 86 | this.root = newNode; 87 | } else { 88 | this.insertNode(this.root, newNode); 89 | } 90 | 91 | } 92 | ``` 93 | 94 | `insertNode()` 的实现思路: 95 | 96 | 根据比较传入的两个节点,一直查找新节点适合插入的位置,直到成功插入新节点为止。 97 | 98 | - 当 newNode.key < node.key 向左查找: 99 | 100 | - 情况 1:当 node 无左子节点时,直接插入: 101 | 102 | - 情况 2:当 node 有左子节点时,递归调用 insertNode(),直到遇到无左子节点成功插入 newNode 后,不再符合该情况,也就不再调用 insertNode(),递归停止。 103 | 104 | - 当 newNode.key >= node.key 向右查找,与向左查找类似: 105 | 106 | - 情况 1:当 node 无右子节点时,直接插入: 107 | 108 | - 情况 2:当 node 有右子节点时,依然递归调用 insertNode(),直到遇到传入 insertNode 方法 的 node 无右子节点成功插入 newNode 为止。 109 | 110 | insertNode(root, node) 代码实现 111 | 112 | ```js 113 | insertNode(root, node) { 114 | 115 | if (node.key < root.key) { // 往左边查找插入 116 | 117 | if (root.left === null) { 118 | root.left = node; 119 | } else { 120 | this.insertNode(root.left, node); 121 | } 122 | 123 | } else { // 往右边查找插入 124 | 125 | if (root.right === null) { 126 | root.right = node; 127 | } else { 128 | this.insertNode(root.right, node); 129 | } 130 | 131 | } 132 | 133 | } 134 | ``` 135 | 136 | ### 遍历数据 137 | 138 | 这里所说的树的遍历不仅仅针对二叉搜索树,而是适用于所有的二叉树。由于树结构不是线性结构,所以遍历方式有多种选择,常见的三种二叉树遍历方式为: 139 | 140 | - 先序遍历 141 | - 中序遍历 142 | - 后序遍历 143 | 144 | 还有层序遍历,使用较少。 145 | 146 | #### 先序遍历 147 | 148 | 先序遍历的过程为: 149 | 150 | 首先,遍历根节点; 151 | 然后,遍历其左子树; 152 | 最后,遍历其右子树; 153 | 154 | ![img](../public/images/data-structure/img-42.png) 155 | 156 | 如上图所示,二叉树的节点遍历顺序为:A -> B -> D -> H -> I -> E -> C -> F -> G。 157 | 158 | 代码实现: 159 | 160 | ```js 161 | // 先序遍历(根左右 DLR) 162 | preorderTraversal() { 163 | const result = []; 164 | this.preorderTraversalNode(this.root, result); 165 | return result; 166 | } 167 | 168 | preorderTraversalNode(node, result) { 169 | if (node === null) return result; 170 | result.push(node.key); 171 | this.preorderTraversalNode(node.left, result); 172 | this.preorderTraversalNode(node.right, result); 173 | } 174 | ``` 175 | 176 | #### 中序遍历 177 | 178 | 实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。 179 | 180 | 首先,遍历其左子树; 181 | 然后,遍历根(父)节点; 182 | 最后,遍历其右子树; 183 | 184 | 过程图解: 185 | 186 | ![img](../public/images/data-structure/img-43.png) 187 | 188 | 输出节点的顺序应为:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25。 189 | 190 | 代码实现: 191 | 192 | ```js 193 | // 中序遍历(左根右 LDR) 194 | inorderTraversal() { 195 | const result = []; 196 | this.inorderTraversalNode(this.root, result); 197 | return result; 198 | } 199 | 200 | inorderTraversalNode(node, result) { 201 | if (node === null) return result; 202 | this.inorderTraversalNode(node.left, result); 203 | result.push(node.key); 204 | this.inorderTraversalNode(node.right, result); 205 | } 206 | ``` 207 | 208 | #### 后序遍历 209 | 210 | 实现思路:与先序遍历原理相同,只不过是遍历的顺序不一样了。 211 | 212 | 首先,遍历其左子树; 213 | 然后,遍历其右子树; 214 | 最后,遍历根(父)节点; 215 | 216 | 过程图解: 217 | 218 | ![img](../public/images/data-structure/img-44.png) 219 | 220 | 输出节点的顺序应为:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11。 221 | 222 | 代码实现: 223 | 224 | ```js 225 | // 后序遍历(左右根 LRD) 226 | postorderTraversal() { 227 | const result = []; 228 | this.postorderTraversalNode(this.root, result); 229 | return result; 230 | } 231 | 232 | postorderTraversalNode(node, result) { 233 | if (node === null) return result; 234 | this.postorderTraversalNode(node.left, result); 235 | this.postorderTraversalNode(node.right, result); 236 | result.push(node.key); 237 | } 238 | ``` 239 | 240 | #### 总结 241 | 242 | 以遍历根(父)节点的顺序来区分三种遍历方式。 243 | - 先序遍历先遍历根节点(根左右) 244 | - 中序遍历第二遍历根节点(左根右) 245 | - 后续遍历最后遍历根节点。(左右根) 246 | 247 | ### 查找数据 248 | 249 | #### 查找最大值或最小值 250 | 251 | 在二叉搜索树中查找最值非常简单,最小值在二叉搜索树的最左边,最大值在二叉搜索树的最右边。只需要一直向左/右查找就能得到最值,如下图所示: 252 | 253 | ![img](../public/images/data-structure/img-45.png) 254 | 255 | 代码实现: 256 | 257 | ```js 258 | // min() 获取二叉搜索树最小值 259 | min() { 260 | if (!this.root) return null; 261 | let node = this.root; 262 | while (node.left !== null) { 263 | node = node.left; 264 | } 265 | return node.key; 266 | } 267 | 268 | // max() 获取二叉搜索树最大值 269 | max() { 270 | if (!this.root) return null; 271 | let node = this.root; 272 | while (node.right !== null) { 273 | node = node.right; 274 | } 275 | return node.key; 276 | } 277 | ``` 278 | 279 | #### 查找特定值 280 | 281 | 查找二叉搜索树当中的特定值效率也非常高。只需要从根节点开始将需要查找节点的 key 值与之比较,若 node.key < root 则向左查找,若 node.key > root 就向右查找,直到找到或查找到 null 为止。这里可以使用递归实现,也可以采用循环来实现。 282 | 283 | 代码实现: 284 | 285 | ```js 286 | // search(key) 查找二叉搜索树中是否有相同的 key,存在返回 true,否则返回 false 287 | search(key) { 288 | return this.searchNode(this.root, key); 289 | } 290 | 291 | // 通过递归实现 292 | searchNode(node, key) { 293 | if (node === null) return false; 294 | if (key < node.key) { 295 | return this.searchNode(node.left, key); 296 | } else if (key > node.key) { 297 | return this.searchNode(node.right, key); 298 | } else { 299 | return true; 300 | } 301 | } 302 | 303 | // 通过 while 循环实现 304 | search2(key) { 305 | 306 | let node = this.root; 307 | 308 | while (node !== null) { 309 | if (key < node.key) { 310 | node = node.left; 311 | } else if (key > node.key) { 312 | node = node.right; 313 | } else { 314 | return true; 315 | } 316 | } 317 | 318 | return false; 319 | 320 | } 321 | ``` 322 | 323 | ### 删除数据 324 | 325 | 实现思路: 326 | 327 | 第一步:先找到需要删除的节点,若没找到,则不需要删除; 328 | 329 | 首先定义变量 current 用于保存需要删除的节点、变量 parent 用于保存它的父节点、变量 isLeftChild 保存 current 是否为 parent 的左节点,这样方便之后删除节点时改变相关节点的指向。 330 | 331 | ```js 332 | let currentNode = this.root; 333 | let parentNode = null; 334 | let isLeftChild = true; 335 | 336 | // 循环查找到要删除的节点 currentNode,以及它的 parentNode、isLeftChild 337 | while (currentNode.key !== key) { 338 | parentNode = currentNode; 339 | 340 | // 小于,往左查找 341 | if (key < currentNode.key) { 342 | isLeftChild = true; 343 | currentNode = currentNode.left; 344 | } else { 345 | // 否则往右查找 346 | isLeftChild = false; 347 | currentNode = currentNode.right; 348 | } 349 | 350 | // 找到最后都没找到相等的节点,返回 false 351 | if (currentNode === null) { 352 | return false; 353 | } 354 | } 355 | ``` 356 | 357 | 第二步:删除找到的指定节点,后分 3 种情况: 358 | 359 | - 删除的是叶子节点; 360 | - 删除的是只有一个子节点的节点; 361 | - 删除的是有两个子节点的节点; 362 | 363 | #### 删除的是叶子节点 364 | 365 | 删除的是叶子节点分两种情况: 366 | 367 | - 叶子节点也是根节点 368 | 369 | 当该叶子节点为根节点时,如下图所示,此时 current == this.root,直接通过:this.root = null,删除根节点。 370 | 371 | ![img](../public/images/data-structure/img-46.png) 372 | 373 | - 叶子节点不为根节点 374 | 375 | 当该叶子节点不为根节点时也有两种情况,如下图所示 376 | 377 | ![img](../public/images/data-structure/img-47.png) 378 | 379 | 若 current = 8,可以通过:parent.left = null,删除节点 8; 380 | 381 | 若 current = 10,可以通过:parent.right = null,删除节点 10; 382 | 383 | 代码实现: 384 | 385 | ```js 386 | // 1、删除的是叶子节点的情况 387 | if (currentNode.left === null && currentNode.right === null) { 388 | if (currentNode === this.root) { 389 | this.root = null; 390 | } else if (isLeftChild) { 391 | parentNode.left = null; 392 | } else { 393 | parentNode.right = null; 394 | } 395 | 396 | // 2、删除的是只有一个子节点的节点 397 | } 398 | ``` 399 | 400 | #### 删除的是只有一个子节点的节点 401 | 402 | 有六种情况: 403 | 404 | 当 current 存在左子节点时(current.right == null): 405 | 406 | - 情况 1:current 为根节点(current == this.root),如节点 11,此时通过:this.root = current.left,删除根节点 11; 407 | 408 | - 情况 2:current 为父节点 parent 的左子节点(isLeftChild == true),如节点 5,此时通过:parent.left = current.left,删除节点 5; 409 | 410 | - 情况 3:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.left,删除节点 9; 411 | 412 | ![img](../public/images/data-structure/img-48.png) 413 | 414 | 当 current 存在右子节点时(current.left = null): 415 | 416 | - 情况 4:current 为根节点(current == this.root),如节点 11,此时通过:this.root = current.right,删除根节点 11。 417 | 418 | - 情况 5:current 为父节点 parent 的左子节点(isLeftChild == true),如节点 5,此时通过:parent.left = current.right,删除节点 5; 419 | 420 | - 情况 6:current 为父节点 parent 的右子节点(isLeftChild == false),如节点 9,此时通过:parent.right = current.right,删除节点 9; 421 | 422 | ![img](../public/images/data-structure/img-49.png) 423 | 424 | 代码实现: 425 | 426 | ```js 427 | // 2、删除的是只有一个子节点的节点 428 | } else if (currentNode.right === null) { // currentNode 只存在左节点 429 | //-- 2.1、currentNode 只存在<左节点>的情况 430 | //---- 2.1.1、currentNode 等于 root 431 | //---- 2.1.2、parentNode.left 等于 currentNode 432 | //---- 2.1.3、parentNode.right 等于 currentNode 433 | 434 | if (currentNode === this.root) { 435 | this.root = currentNode.left; 436 | } else if (isLeftChild) { 437 | parentNode.left = currentNode.left; 438 | } else { 439 | parentNode.right = currentNode.left; 440 | } 441 | 442 | } else if (currentNode.left === null) { // currentNode 只存在右节点 443 | //-- 2.2、currentNode 只存在<右节点>的情况 444 | //---- 2.1.1 currentNode 等于 root 445 | //---- 2.1.1 parentNode.left 等于 currentNode 446 | //---- 2.1.1 parentNode.right 等于 currentNode 447 | 448 | if (currentNode === this.root) { 449 | this.root = currentNode.right; 450 | } else if (isLeftChild) { 451 | parentNode.left = currentNode.right; 452 | } else { 453 | parentNode.right = currentNode.right; 454 | } 455 | ``` 456 | 457 | #### 删除的是有两个子节点的节点 458 | 459 | 这种情况十分复杂,首先依据以下二叉搜索树,讨论这样的问题: 460 | 461 | ![img](../public/images/data-structure/img-50.png) 462 | 463 | **删除节点 9** 464 | 465 | 在保证删除节点 9 后原二叉树仍为二叉搜索树的前提下,有两种方式: 466 | 467 | - 方式 1:从节点 9 的左子树中选择一合适的节点替代节点 9,可知节点 8 符合要求; 468 | - 方式 2:从节点 9 的右子树中选择一合适的节点替代节点 9,可知节点 10 符合要求; 469 | 470 | ![img](../public/images/data-structure/img-51.png) 471 | 472 | **删除节点 7** 473 | 474 | 在保证删除节点 7 后原二叉树仍为二叉搜索树的前提下,也有两种方式: 475 | 476 | - 方式 1:从节点 7 的左子树中选择一合适的节点替代节点 7,可知节点 5 符合要求; 477 | - 方式 2:从节点 7 的右子树中选择一合适的节点替代节点 7,可知节点 8 符合要求; 478 | 479 | ![img](../public/images/data-structure/img-52.png) 480 | 481 | **删除节点 15** 482 | 483 | 在保证删除节点 15 后原树二叉树仍为二叉搜索树的前提下,同样有两种方式: 484 | 485 | - 方式 1:从节点 15 的左子树中选择一合适的节点替代节点 15,可知节点 14 符合要求; 486 | - 方式 2:从节点 15 的右子树中选择一合适的节点替代节点 15,可知节点 18 符合要求; 487 | 488 | ![img](../public/images/data-structure/img-53.png) 489 | 490 | 相信你已经发现其中的规律了! 491 | 492 | 规律总结:如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从要删除节点下面的子节点中找到一个合适的节点,来替换当前的节点。 493 | 494 | 若用 current 表示需要删除的节点,则合适的节点指的是: 495 | 496 | - current 左子树中比 current 小一点点的节点,即 current 左子树中的最大值; 497 | - current 右子树中比 current 大一点点的节点,即 current 右子树中的最小值; 498 | 499 | ##### 前驱&后继 500 | 501 | 在二叉搜索树中,这两个特殊的节点有特殊的名字: 502 | 503 | - 比 current 小一点点的节点,称为 current 节点的前驱。比如下图中的节点 5 就是节点 7 的前驱; 504 | - 比 current 大一点点的节点,称为 current 节点的后继。比如下图中的节点 8 就是节点 7 的后继; 505 | 506 | ![img](../public/images/data-structure/img-54.png) 507 | 508 | 查找需要被删除的节点 current 的后继时,需要在 current 的右子树中查找最小值,即在 current 的右子树中一直向左遍历查找; 509 | 510 | 查找前驱时,则需要在 current 的左子树中查找最大值,即在 current 的左子树中一直向右遍历查找。 511 | 512 | 下面只讨论查找 current 后继的情况,查找前驱的原理相同,这里暂不讨论。 513 | 514 | 代码实现: 515 | 516 | ```js 517 | // 3、删除的是有两个子节点的节点 518 | } else { 519 | 520 | // 1、找到后续节点 521 | let successor = this.getSuccessor(currentNode); 522 | 523 | // 2、判断是否为根节点 524 | if (currentNode === this.root) { 525 | this.root = successor; 526 | } else if (isLeftChild) { 527 | parentNode.left = successor; 528 | } else { 529 | parentNode.right = successor; 530 | } 531 | 532 | // 3、将后续的左节点改为被删除的左节点 533 | successor.left = currentNode.left; 534 | } 535 | } 536 | 537 | // 获取后续节点,即从要删除的节点的右边开始查找最小的值 538 | getSuccessor(delNode) { 539 | 540 | // 定义变量,保存要找到的后续 541 | let successor = delNode; 542 | let current = delNode.right; 543 | let successorParent = delNode; 544 | 545 | // 循环查找 current 的右子树节点 546 | while (current !== null) { 547 | successorParent = successor; 548 | successor = current; 549 | current = current.left; 550 | } 551 | 552 | // 判断寻找到的后续节点是否直接就是要删除节点的 right 553 | if (successor !== delNode.right) { 554 | successorParent.left = successor.right; 555 | successor.right = delNode.right; 556 | } 557 | return successor; 558 | } 559 | ``` 560 | 561 | #### 完整实现 562 | 563 | ```js 564 | // 删除节点 565 | remove(key) { 566 | 567 | let currentNode = this.root; 568 | let parentNode = null; 569 | let isLeftChild = true; 570 | 571 | // 循环查找到要删除的节点 currentNode,以及它的 parentNode、isLeftChild 572 | while (currentNode.key !== key) { 573 | 574 | parentNode = currentNode; 575 | 576 | // 小于,往左查找 577 | if (key < currentNode.key) { 578 | isLeftChild = true; 579 | currentNode = currentNode.left; 580 | 581 | } else { // 否则往右查找 582 | isLeftChild = false; 583 | currentNode = currentNode.right; 584 | } 585 | 586 | // 找到最后都没找到相等的节点,返回 false 587 | if (currentNode === null) { 588 | return false; 589 | } 590 | 591 | } 592 | 593 | 594 | // 1、删除的是叶子节点的情况 595 | if (currentNode.left === null && currentNode.right === null) { 596 | 597 | if (currentNode === this.root) { 598 | this.root = null; 599 | } else if (isLeftChild) { 600 | parentNode.left = null; 601 | } else { 602 | parentNode.right = null; 603 | } 604 | 605 | 606 | // 2、删除的是只有一个子节点的节点 607 | } else if (currentNode.right === null) { // currentNode 只存在左节点 608 | //-- 2.1、currentNode 只存在<左节点>的情况 609 | //---- 2.1.1、currentNode 等于 root 610 | //---- 2.1.2、parentNode.left 等于 currentNode 611 | //---- 2.1.3、parentNode.right 等于 currentNode 612 | 613 | if (currentNode === this.root) { 614 | this.root = currentNode.left; 615 | } else if (isLeftChild) { 616 | parentNode.left = currentNode.left; 617 | } else { 618 | parentNode.right = currentNode.left; 619 | } 620 | 621 | } else if (currentNode.left === null) { // currentNode 只存在右节点 622 | //-- 2.2、currentNode 只存在<右节点>的情况 623 | //---- 2.1.1 currentNode 等于 root 624 | //---- 2.1.1 parentNode.left 等于 currentNode 625 | //---- 2.1.1 parentNode.right 等于 currentNode 626 | 627 | if (currentNode === this.root) { 628 | this.root = currentNode.right; 629 | } else if (isLeftChild) { 630 | parentNode.left = currentNode.right; 631 | } else { 632 | parentNode.right = currentNode.right; 633 | } 634 | 635 | 636 | // 3、删除的是有两个子节点的节点 637 | } else { 638 | 639 | // 1、找到后续节点 640 | let successor = this.getSuccessor(currentNode); 641 | 642 | // 2、判断是否为根节点 643 | if (currentNode === this.root) { 644 | this.root = successor; 645 | } else if (isLeftChild) { 646 | parentNode.left = successor; 647 | } else { 648 | parentNode.right = successor; 649 | } 650 | 651 | // 3、将后续的左节点改为被删除的左节点 652 | successor.left = currentNode.left; 653 | } 654 | } 655 | 656 | // 获取后续节点,即从要删除的节点的右边开始查找最小的值 657 | getSuccessor(delNode) { 658 | 659 | // 定义变量,保存要找到的后续 660 | let successor = delNode; 661 | let current = delNode.right; 662 | let successorParent = delNode; 663 | 664 | // 循环查找 current 的右子树节点 665 | while (current !== null) { 666 | successorParent = successor; 667 | successor = current; 668 | current = current.left; 669 | } 670 | 671 | // 判断寻找到的后续节点是否直接就是要删除节点的 right 672 | if (successor !== delNode.right) { 673 | successorParent.left = successor.right; 674 | successor.right = delNode.right; 675 | } 676 | return successor; 677 | } 678 | ``` 679 | 680 | # 平衡树 681 | 682 | 二叉搜索树的缺陷:当插入的数据是有序的数据,就会造成二叉搜索树的深度过大。比如原二叉搜索树由 11 7 15 组成,如下图所示: 683 | 684 | ![img](../public/images/data-structure/img-55.png) 685 | 686 | 当插入一组有序数据:6 5 4 3 2 就会变成深度过大的搜索二叉树,会严重影响二叉搜索树的性能。 687 | 688 | ![img](../public/images/data-structure/img-56.png) 689 | 690 | 非平衡树 691 | 692 | - 比较好的二叉搜索树,它的数据应该是左右均匀分布的。 693 | - 但是插入连续数据后,二叉搜索树中的数据分布就变得不均匀了,我们称这种树为非平衡树。 694 | - 对于一棵平衡二叉树来说,插入/查找等操作的效率是 O(log n)。 695 | - 而对于一棵非平衡二叉树来说,相当于编写了一个链表,查找效率变成了 O(n)。 696 | 697 | 树的平衡性 698 | 699 | 为了能以较快的时间 O(log n) 来操作一棵树,我们需要保证树总是平衡的: 700 | 701 | - 起码大部分是平衡的,此时的时间复杂度也是接近 O(log n) 的; 702 | - 这就要求树中每个节点左边的子孙节点的个数,应该尽可能地等于右边的子孙节点的个数; 703 | 704 | 常见的平衡树 705 | 706 | - AVL 树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于 AVL 树是平衡树,所以它的时间复杂度也是 O(log n)。但是它的整体效率不如红黑树,开发中比较少用。 707 | - 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是 O(log n)。进行插入/删除等操作时,性能优于 AVL 树,所以平衡树的应用基本都是红黑树。 708 | 709 | -------------------------------------------------------------------------------- /src/docs/data-structure/BinaryTree.md: -------------------------------------------------------------------------------- 1 | # 二叉树 2 | 3 | ## 二叉树的概念 4 | 5 | 如果树中的每一个节点最多只能有两个子节点,这样的树就称为二叉树。 6 | 7 | ## 二叉树的组成 8 | 9 | - 二叉树可以为空,也就是没有节点; 10 | - 若二叉树不为空,则它由根节点和称为其左子树 TL 和右子树 TR 的两个不相交的二叉树组成; 11 | 12 | ## 二叉树的五种形态 13 | 14 | ![img](../public/images/data-structure/img-30.png) 15 | 16 | 上图分别表示: 17 | - a 空的二叉树 18 | - b 只有一个节点的二叉树 19 | - c 只有左子树 TL 的二叉树 20 | - d 只有右子树 TR 的二叉树 21 | - e 有左右两个子树的二叉树。 22 | 23 | ## 二叉树的特性 24 | 25 | - 一个二叉树的第 i 层的最大节点树为:2^(i-1)^,i >= 1; 26 | - 深度为 k 的二叉树的最大节点总数为:2^k^ - 1,k >= 1; 27 | - 对任何非空二叉树,若 n~0~ 表示叶子节点的个数,n~2~表示度为 2 的非叶子节点个数,那么两者满足关系:n~0~ = n~2~ + 1; 28 | 如下图所示:H,E,I,J,G 为叶子节点,总数为 5;A,B,C,F 为度为 2 的非叶子节点,总数为 4;满足 n~0~ = n~2~ + 1 的规律。 29 | 30 | ![img](../public/images/data-structure/img-31.png) 31 | 32 | ## 特殊的二叉树 33 | 34 | ### 完美二叉树 35 | 36 | 完美二叉树(Perfect Binary Tree)也成为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有 2 个子节点,这就构成了完美二叉树。 37 | 38 | ![img](../public/images/data-structure/img-32.png) 39 | 40 | ### 完全二叉树 41 | 42 | 完全二叉树(Complete Binary Tree): 43 | 44 | - 除了二叉树最后一层外,其他各层的节点数都达到了最大值; 45 | - 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点; 46 | - 完美二叉树是特殊的完全二叉树; 47 | 48 | ![img](../public/images/data-structure/img-33.png) 49 | 50 | 在上图中,由于 H 缺失了右子节点,所以它不是完全二叉树。 51 | 52 | ## 二叉树的数据存储 53 | 54 | 常见的二叉树存储方式为数组和链表: 55 | 56 | ### 使用数组 57 | 58 | - 完全二叉树:按从上到下,从左到右的方式存储数据。 59 | 60 | ![img](../public/images/data-structure/img-34.png) 61 | 62 | | 节点 | A | B | C | D | E | F | G | H | I | 63 | | :--: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | 64 | | 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 65 | 66 | 使用数组存储时,取数据的时候也十分方便:左子节点的序号等于父节点序号 _ 2,右子节点的序号等于父节点序号 _ 2 + 1。 67 | 68 | - 非完全二叉树:非完全二叉树需要转换成完全二叉树才能按照上面的方案存储,这样会浪费很大的存储空间。 69 | 70 | ![img](../public/images/data-structure/img-35.png) 71 | 72 | | 节点 | A | B | C | ^ | ^ | F | ^ | ^ | ^ | ^ | ^ | ^ | M | 73 | | :--: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | 74 | | 序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 75 | 76 | ### 使用链表 77 | 78 | 二叉树最常见的存储方式为链表:每一个节点封装成一个 Node,Node 中包含存储的数据、左节点的引用和右节点的引用。 79 | 80 | ![img](../public/images/data-structure/img-36.png) 81 | 82 | -------------------------------------------------------------------------------- /src/docs/data-structure/DoubleLinkedList.md: -------------------------------------------------------------------------------- 1 | # 双向链表 2 | 3 | ## 单向链表和双向链表 4 | 5 | ### 单向链表 6 | 7 | - 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)。 8 | - 链表相连的过程是单向的,实现原理是上一个节点中有指向下一个节点的引用。 9 | - 单向链表有一个比较明显的缺点:可以轻松到达下一个节点,但回到前一个节点很难,在实际开发中,经常会遇到需要回到上一个节点的情况。 10 | 11 | ### 双向链表 12 | 13 | - 既可以从头遍历到尾,也可以从尾遍历到头。 14 | - 链表相连的过程是双向的。实现原理是一个节点既有向前连接的引用,也有一个向后连接的引用。 15 | - 双向链表可以有效的解决单向链表存在的问题。 16 | - 双向链表缺点: 17 | - 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些。 18 | - 相对于单向链表,所占内存空间更大一些。 19 | - 但是,相对于双向链表的便利性而言,这些缺点微不足道。 20 | 21 | ## 双向链表结构 22 | 23 | ![img](../public/images/data-structure/img-11.png) 24 | 25 | - 双向链表不仅有 head 指针指向第一个节点,而且有 tail 指针指向最后一个节点。 26 | - 每一个节点由三部分组成:item 储存数据、prev 指向前一个节点、next 指向后一个节点。 27 | - 双向链表的第一个节点的 prev 指向 null。 28 | - 双向链表的最后一个节点的 next 指向 null。 29 | 30 | ## 双向链表常见的操作 31 | 32 | - `append(element)` 向链表尾部追加一个新元素。 33 | - `insert(position, element)` 向链表的指定位置插入一个新元素。 34 | - `getElement(position)` 获取指定位置的元素。 35 | - `indexOf(element)` 返回元素在链表中的索引。如果链表中没有该元素就返回 -1。 36 | - `update(position, element)` 修改指定位置上的元素。 37 | - `removeAt(position)` 从链表中的删除指定位置的元素。 38 | - `remove(element)` 从链表删除指定的元素。 39 | - `isEmpty()` 如果链表中不包含任何元素,返回 `trun`,如果链表长度大于 0 则返回 `false`。 40 | - `size()` 返回链表包含的元素个数,与数组的 `length` 属性类似。 41 | - `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 `toString` 方法,让其只输出元素的值。 42 | - `forwardString()` 返回正向遍历节点字符串形式。 43 | - `backwordString()` 返回反向遍历的节点的字符串形式。 44 | 45 | ## 双向链表的封装 46 | 47 | ### 创建双向链表类 DoublyLinkedList 48 | 49 | - DoublyNode 类继承单向链表的 Node 类,新添加 `this.prev` 属性,该属性用于指向上一个节点。 50 | - DoublyLinkedList 类继承 LinkedList 类,新添加 `this.tail` 属性,该属性指向末尾的节点。 51 | 52 | ```js 53 | // 双向链表的节点类(继承单向链表的节点类) 54 | class DoublyNode extends Node { 55 | constructor(element) { 56 | super(element); 57 | this.prev = null; 58 | } 59 | } 60 | 61 | // 双向链表类继承单向链表类 62 | class DoublyLinkedList extends LinkedList { 63 | constructor() { 64 | super(); 65 | this.tail = null; 66 | } 67 | } 68 | ``` 69 | 70 | ### append(element) 71 | 72 | ```js 73 | // append(element) 往双向链表尾部追加一个新的元素 74 | // 重写 append() 75 | append(element) { 76 | 77 | // 1、创建双向链表节点 78 | const newNode = new DoublyNode(element); 79 | 80 | // 2、追加元素 81 | if (this.head === null) { 82 | this.head = newNode; 83 | this.tail = newNode; 84 | } else { 85 | // !!跟单向链表不同,不用通过循环找到最后一个节点 86 | // 巧妙之处 87 | this.tail.next = newNode; 88 | newNode.prev = this.tail; 89 | this.tail = newNode; 90 | } 91 | 92 | this.length++; 93 | } 94 | ``` 95 | 96 | ### insert(position, element) 97 | 98 | ```js 99 | // insert(position, data) 插入元素 100 | // 重写 insert() 101 | insert(position, element) { 102 | // 1、position 越界判断 103 | if (position < 0 || position > this.length) return false; 104 | 105 | // 2、创建新的双向链表节点 106 | const newNode = new DoublyNode(element); 107 | 108 | // 3、判断多种插入情况 109 | if (position === 0) { // 在第 0 个位置插入 110 | 111 | if (this.head === null) { 112 | this.head = newNode; 113 | this.tail = newNode; 114 | } else { 115 | //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// 116 | newNode.next = this.head; 117 | this.head.perv = newNode; 118 | this.head = newNode; 119 | } 120 | 121 | } else if (position === this.length) { // 在最后一个位置插入 122 | 123 | this.tail.next = newNode; 124 | newNode.prev = this.tail; 125 | this.tail = newNode; 126 | } else { // 在 0 ~ this.length 位置中间插入 127 | 128 | let targetIndex = 0; 129 | let currentNode = this.head; 130 | let previousNode = null; 131 | 132 | // 找到要插入位置的节点 133 | while (targetIndex++ < position) { 134 | previousNode = currentNode; 135 | currentNode = currentNode.next; 136 | } 137 | 138 | // 交换节点信息 139 | previousNode.next = newNode; 140 | newNode.prev = previousNode; 141 | 142 | newNode.next = currentNode; 143 | currentNode.prev = newNode; 144 | } 145 | 146 | this.length++; 147 | 148 | return true; 149 | } 150 | ``` 151 | 152 | ### insert(position, element) 153 | 154 | ```js 155 | // insert(position, data) 插入元素 156 | // 重写 insert() 157 | insert(position, element) { 158 | // 1、position 越界判断 159 | if (position < 0 || position > this.length) return false; 160 | 161 | // 2、创建新的双向链表节点 162 | const newNode = new DoublyNode(element); 163 | 164 | // 3、判断多种插入情况 165 | if (position === 0) { // 在第 0 个位置插入 166 | 167 | if (this.head === null) { 168 | this.head = newNode; 169 | this.tail = newNode; 170 | } else { 171 | //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// 172 | newNode.next = this.head; 173 | this.head.perv = newNode; 174 | this.head = newNode; 175 | } 176 | 177 | } else if (position === this.length) { // 在最后一个位置插入 178 | 179 | this.tail.next = newNode; 180 | newNode.prev = this.tail; 181 | this.tail = newNode; 182 | } else { // 在 0 ~ this.length 位置中间插入 183 | 184 | let targetIndex = 0; 185 | let currentNode = this.head; 186 | let previousNode = null; 187 | 188 | // 找到要插入位置的节点 189 | while (targetIndex++ < position) { 190 | previousNode = currentNode; 191 | currentNode = currentNode.next; 192 | } 193 | 194 | // 交换节点信息 195 | previousNode.next = newNode; 196 | newNode.prev = previousNode; 197 | 198 | newNode.next = currentNode; 199 | currentNode.prev = newNode; 200 | } 201 | 202 | this.length++; 203 | 204 | return true; 205 | } 206 | ``` 207 | 208 | ### removeAt(position) 209 | 210 | ```js 211 | // removeAt() 删除指定位置的节点 212 | // 重写 removeAt() 213 | removeAt(position) { 214 | // 1、position 越界判断 215 | if (position < 0 || position > this.length - 1) return null; 216 | 217 | // 2、根据不同情况删除元素 218 | let currentNode = this.head; 219 | if (position === 0) { // 删除第一个节点的情况 220 | 221 | if (this.length === 1) { // 链表内只有一个节点的情况 222 | this.head = null; 223 | this.tail = null; 224 | } else { // 链表内有多个节点的情况 225 | this.head = this.head.next; 226 | this.head.prev = null; 227 | } 228 | 229 | } else if (position === this.length - 1) { // 删除最后一个节点的情况 230 | 231 | currentNode = this.tail; 232 | this.tail.prev.next = null; 233 | this.tail = this.tail.prev; 234 | 235 | } else { // 删除 0 ~ this.length - 1 里面节点的情况 236 | 237 | let targetIndex = 0; 238 | let previousNode = null; 239 | while (targetIndex++ < position) { 240 | previousNode = currentNode; 241 | currentNode = currentNode.next; 242 | } 243 | 244 | previousNode.next = currentNode.next; 245 | currentNode.next.perv = previousNode; 246 | 247 | } 248 | 249 | this.length--; 250 | return currentNode.data; 251 | } 252 | ``` 253 | 254 | ### update(position, data) 255 | 256 | ```js 257 | // update(position, data) 修改指定位置的节点 258 | // 重写 update() 259 | update(position, data) { 260 | // 1、删除 position 位置的节点 261 | const result = this.removeAt(position); 262 | 263 | // 2、在 position 位置插入元素 264 | this.insert(position, data); 265 | return result; 266 | } 267 | ``` 268 | 269 | ### forwardToString() 270 | 271 | ```js 272 | // forwardToString() 链表数据从前往后以字符串形式返回 273 | forwardToString() { 274 | let currentNode = this.head; 275 | let result = ''; 276 | 277 | // 遍历所有的节点,拼接为字符串,直到节点为 null 278 | while (currentNode) { 279 | result += currentNode.data + '--'; 280 | currentNode = currentNode.next; 281 | } 282 | 283 | return result; 284 | } 285 | ``` 286 | 287 | ### backwardString() 288 | 289 | ```js 290 | // backwardString() 链表数据从后往前以字符串形式返回 291 | backwardString() { 292 | let currentNode = this.tail; 293 | let result = ''; 294 | 295 | // 遍历所有的节点,拼接为字符串,直到节点为 null 296 | while (currentNode) { 297 | result += currentNode.data + '--'; 298 | currentNode = currentNode.prev; 299 | } 300 | 301 | return result; 302 | } 303 | ``` 304 | 305 | ### 其他方法的实现 306 | 307 | 双向链表的其他方法通过继承单向链表来实现。 308 | 309 | ### 完整实现 310 | 311 | ```js 312 | class DoublyLinkedList extends LinkedList { 313 | constructor() { 314 | super(); 315 | this.tail = null; 316 | } 317 | 318 | // ------------ 链表的常见操作 ------------ // 319 | // append(element) 往双向链表尾部追加一个新的元素 320 | // 重写 append() 321 | append(element) { 322 | // 1、创建双向链表节点 323 | const newNode = new DoublyNode(element); 324 | 325 | // 2、追加元素 326 | if (this.head === null) { 327 | this.head = newNode; 328 | this.tail = newNode; 329 | } else { 330 | // !!跟单向链表不同,不用通过循环找到最后一个节点 331 | // 巧妙之处 332 | this.tail.next = newNode; 333 | newNode.prev = this.tail; 334 | this.tail = newNode; 335 | } 336 | 337 | this.length++; 338 | } 339 | 340 | // insert(position, data) 插入元素 341 | // 重写 insert() 342 | insert(position, element) { 343 | // 1、position 越界判断 344 | if (position < 0 || position > this.length) return false; 345 | 346 | // 2、创建新的双向链表节点 347 | const newNode = new DoublyNode(element); 348 | 349 | // 3、判断多种插入情况 350 | if (position === 0) { 351 | // 在第 0 个位置插入 352 | 353 | if (this.head === null) { 354 | this.head = newNode; 355 | this.tail = newNode; 356 | } else { 357 | //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// 358 | newNode.next = this.head; 359 | this.head.perv = newNode; 360 | this.head = newNode; 361 | } 362 | } else if (position === this.length) { 363 | // 在最后一个位置插入 364 | 365 | this.tail.next = newNode; 366 | newNode.prev = this.tail; 367 | this.tail = newNode; 368 | } else { 369 | // 在 0 ~ this.length 位置中间插入 370 | 371 | let targetIndex = 0; 372 | let currentNode = this.head; 373 | let previousNode = null; 374 | 375 | // 找到要插入位置的节点 376 | while (targetIndex++ < position) { 377 | previousNode = currentNode; 378 | currentNode = currentNode.next; 379 | } 380 | 381 | // 交换节点信息 382 | previousNode.next = newNode; 383 | newNode.prev = previousNode; 384 | 385 | newNode.next = currentNode; 386 | currentNode.prev = newNode; 387 | } 388 | 389 | this.length++; 390 | 391 | return true; 392 | } 393 | 394 | // getData() 继承单向链表 395 | getData(position) { 396 | return super.getData(position); 397 | } 398 | 399 | // indexOf() 继承单向链表 400 | indexOf(data) { 401 | return super.indexOf(data); 402 | } 403 | 404 | // removeAt() 删除指定位置的节点 405 | // 重写 removeAt() 406 | removeAt(position) { 407 | // 1、position 越界判断 408 | if (position < 0 || position > this.length - 1) return null; 409 | 410 | // 2、根据不同情况删除元素 411 | let currentNode = this.head; 412 | if (position === 0) { 413 | // 删除第一个节点的情况 414 | 415 | if (this.length === 1) { 416 | // 链表内只有一个节点的情况 417 | this.head = null; 418 | this.tail = null; 419 | } else { 420 | // 链表内有多个节点的情况 421 | this.head = this.head.next; 422 | this.head.prev = null; 423 | } 424 | } else if (position === this.length - 1) { 425 | // 删除最后一个节点的情况 426 | 427 | currentNode = this.tail; 428 | this.tail.prev.next = null; 429 | this.tail = this.tail.prev; 430 | } else { 431 | // 删除 0 ~ this.length - 1 里面节点的情况 432 | 433 | let targetIndex = 0; 434 | let previousNode = null; 435 | while (targetIndex++ < position) { 436 | previousNode = currentNode; 437 | currentNode = currentNode.next; 438 | } 439 | 440 | previousNode.next = currentNode.next; 441 | currentNode.next.perv = previousNode; 442 | } 443 | 444 | this.length--; 445 | return currentNode.data; 446 | } 447 | 448 | // update(position, data) 修改指定位置的节点 449 | // 重写 update() 450 | update(position, data) { 451 | // 1、删除 position 位置的节点 452 | const result = this.removeAt(position); 453 | 454 | // 2、在 position 位置插入元素 455 | this.insert(position, data); 456 | return result; 457 | } 458 | 459 | // remove(data) 删除指定 data 所在的节点(继承单向链表) 460 | remove(data) { 461 | return super.remove(data); 462 | } 463 | 464 | // isEmpty() 判断链表是否为空 465 | isEmpty() { 466 | return super.isEmpty(); 467 | } 468 | 469 | // size() 获取链表的长度 470 | size() { 471 | return super.size(); 472 | } 473 | 474 | // forwardToString() 链表数据从前往后以字符串形式返回 475 | forwardToString() { 476 | let currentNode = this.head; 477 | let result = ""; 478 | 479 | // 遍历所有的节点,拼接为字符串,直到节点为 null 480 | while (currentNode) { 481 | result += currentNode.data + "--"; 482 | currentNode = currentNode.next; 483 | } 484 | 485 | return result; 486 | } 487 | 488 | // backwardString() 链表数据从后往前以字符串形式返回 489 | backwardString() { 490 | let currentNode = this.tail; 491 | let result = ""; 492 | 493 | // 遍历所有的节点,拼接为字符串,直到节点为 null 494 | while (currentNode) { 495 | result += currentNode.data + "--"; 496 | currentNode = currentNode.prev; 497 | } 498 | 499 | return result; 500 | } 501 | } 502 | ``` 503 | 504 | ### 代码测试 505 | 506 | ```js 507 | const doublyLinkedList = new DoublyLinkedList(); 508 | 509 | // append() 测试 510 | doublyLinkedList.append("ZZ"); 511 | doublyLinkedList.append("XX"); 512 | doublyLinkedList.append("CC"); 513 | console.log(doublyLinkedList); 514 | 515 | // insert() 测试 516 | doublyLinkedList.insert(0, "00"); 517 | doublyLinkedList.insert(2, "22"); 518 | console.log(doublyLinkedList); 519 | 520 | // getData() 测试 521 | console.log(doublyLinkedList.getData(1)); //--> ZZ 522 | 523 | // indexOf() 测试 524 | console.log(doublyLinkedList.indexOf("XX")); //--> 3 525 | console.log(doublyLinkedList); 526 | 527 | // removeAt() 测试 528 | doublyLinkedList.removeAt(0); 529 | doublyLinkedList.removeAt(1); 530 | console.log(doublyLinkedList); 531 | 532 | // update() 测试 533 | doublyLinkedList.update(0, "111111"); 534 | console.log(doublyLinkedList); 535 | 536 | // remove() 测试 537 | console.log(doublyLinkedList.remove("111111")); 538 | console.log(doublyLinkedList.remove("22222")); 539 | console.log(doublyLinkedList); 540 | 541 | // forwardToString() 测试 542 | console.log(doublyLinkedList.forwardToString()); 543 | 544 | // backwardString() 测试 545 | console.log(doublyLinkedList.backwardString()); 546 | ``` 547 | -------------------------------------------------------------------------------- /src/docs/data-structure/Graph.md: -------------------------------------------------------------------------------- 1 | # 图 2 | 3 | ## 图的概念 4 | 5 | 在计算机程序设计中,图也是一种非常常见的数据结构。图论是一个非常大的话题,在数学上起源于哥尼斯堡七桥问题。 6 | 7 | ### 什么是图? 8 | 9 | - 图是一种与树有些相似的数据结构。 10 | 11 | - 实际上,在数学的概念上,树是图的一种。 12 | - 我们知道树可以用来模拟很多现实的数据结构,比如:家谱/公司的组织架构等等。 13 | 14 | - 图长什么样子呢?或者什么样的数据使用图来模拟更合适呢? 15 | 16 | - 人与人之间的关系网 17 | ![img](../public/images/data-structure/img-57.png) 18 | 19 | - 互联网中的网络关系 20 | ![img](../public/images/data-structure/img-58.png) 21 | 22 | - 广州地铁图 23 | ![img](../public/images/data-structure/img-59.png) 24 | 25 | - 什么是图呢? 26 | 27 | - 我们会发现,上面的结点(其实图中叫顶点 Vertex)之间的关系,是不能使用树来表示(几叉树都不可以)。 28 | - 这个时候,我们就可以使用**图**来模拟它们。 29 | 30 | - 图通常有什么特点呢? 31 | - 一组顶点:通常用 V (Vertex) 表示顶点的集合 32 | - 一组边:通常用 E (Edge) 表示边的集合 33 | - 边是顶点和顶点之间的连线 34 | - 边可以是有向的,也可以是无向的。(比如 A --- B,通常表示无向。A --> B,通常表示有向) 35 | 36 | ### 图的术语 37 | 38 | 我们在学习树的时候,树有很多的其他术语,了解这些术语有助于我们更深层次的理解图。 39 | 40 | 图的术语也非常多,如果你找一本专门讲图的各个方面的书籍,会发现只是术语就可以占据一个章节。 41 | 42 | 这里,这里介绍几个比较常见的术语,某些术语后面用到的时候,再了解,没有用到的,不做赘述。 43 | 44 | 通过下图来了解图的术语: 45 | 46 | ![img](../public/images/data-structure/img-60.png) 47 | 48 | - 顶点 49 | 50 | - 顶点刚才我们已经介绍过了,表示图中的一个结点。 51 | - 比如地铁站中某个站/多个村庄中的某个村庄/互联网中的某台主机/人际关系中的人。 52 | 53 | - 边 54 | 55 | - 边表示顶点和顶点之间的连线。 56 | - 比如地铁站中两个站点之间的直接连线,就是一个边。 57 | - 注意:这里的边不要叫做路径,路径有其他的概念,后面会区分。 58 | 59 | - 相邻顶点 60 | 61 | - 由一条边连接在一起的顶点称为相邻顶点。 62 | - 比如 `0 - 1` 是相邻的,`0 - 3` 是相邻的。`0 - 2` 是不相邻的。 63 | 64 | - 度 65 | 66 | - 一个顶点的度是相邻顶点的数量 67 | - 比如 0 顶点和其他两个顶点相连,0 顶点的度是 2 68 | - 比如 1 顶点和其他四个顶点相连,1 顶点的度是 4 69 | 70 | - 路径 71 | 72 | - 路径是顶点 `v1`,`v2`...,`vn` 的一个连续序列,比如上图中 `0 1 5 9` 就是一条路径。 73 | - 简单路径:简单路径要求不包含重复的顶点。比如 `0 1 5 9` 是一条简单路径。 74 | - 回路:第一个顶点和最后一个顶点相同的路径称为回路。比如 `0 1 5 6 3 0`。 75 | 76 | - 无向图 77 | 78 | - 上面的图就是一张无向图,因为所有的边都没有方向。 79 | - 比如 `0 - 1` 之间有边,那么说明这条边可以保证 `0 -> 1`,也可以保证 `1 -> 0`。 80 | 81 | - 有向图 82 | 83 | - 有向图表示的图中的边是有方向的。 84 | - 比如 `0 -> 1`,不能保证一定可以 `1 -> 0`,要根据方向来定。 85 | 86 | - 无权图 87 | 88 | - 如上图就是一张无权图,边没有携带权重。 89 | - 上图中的边是没有任何意义的,不能说 `0 - 1` 的边,比 `4 - 9` 的边更远或者用的时间更长。 90 | 91 | - 带权图 92 | - 带权图表示边有一定的权重 93 | - 这里的权重可以是任意你希望表示的数据:比如距离或者花费的时间或者票价。 94 | - 我们来看一张有向和带权的图: 95 | ![img](../public/images/data-structure/img-61.png) 96 | 97 | ### 现实建模 98 | 99 | - 对交通流量建模 100 | 101 | - 顶点可以表示街道的十字路口,边可以表示街道.。 102 | - 加权的边可以表示限速或者车道的数量或者街道的距离。 103 | - 建模人员可以用这个系统来判定最佳路线以及最可能堵车的街道。 104 | 105 | - 对飞机航线建模 106 | 107 | - 航空公司可以用图来为其飞行系统建模。 108 | - 将每个机场看成顶点,将经过两个顶点的每条航线看作一条边。 109 | - 加权的边可以表示从一个机场到另一个机场的航班成本,或两个机场间的距离。 110 | - 建模人员可以利用这个系统有效的判断从一个城市到另一个城市的最小航行成本。 111 | ​ 112 | 113 | ## 图的表示 114 | 115 | 一个图包含很多顶点,另外包含顶点和顶点之间的连线(边),这两个都是非常重要的图信息,因此都需要在程序中体现出来。 116 | 117 | ### 顶点表示 118 | 119 | - 顶点的表示相对简单 120 | 121 | - 上面的顶点,我们抽象成了 1 2 3 4,也可以抽象成 A B C D。在后面的案例中,我们使用 A B C D。 122 | - 那么这些 A B C D 我们可以使用一个数组来存储起来 (存储所有的顶点)。 123 | - 当然,A B C D 有可能还表示其他含义的数据 (比如村庄的名字),这个时候,可以另外创建一个数组,用于存储对应的其他数据。 124 | 125 | - 边的表示略微复杂,因为边是两个顶点之间的关系,所以表示起来会稍微麻烦一些。 126 | 127 | ### 邻接矩阵 128 | 129 | - 概述 130 | 131 | - 邻接矩阵让每个节点和一个整数向关联,该整数作为数组的下标值。 132 | - 我们用一个二维数组来表示顶点之间的连接。 133 | 134 | ![img](../public/images/data-structure/img-62.png) 135 | 136 | - 图片解析 137 | 138 | - 在二维数组中,0 表示没有连线,1 表示有连线。 139 | - 通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。(比如 A 顶点,只需要 遍历第一行即可) 140 | - 另外,A - A,B - B(也就是顶点到自己的连线),通常使用 0 表示。 141 | 142 | - 邻接矩阵的问题 143 | 144 | - 如果是一个无向图,邻接矩阵展示出来的二维数组,其实是一个对称图。 145 | 也就是 A -> D 是 1 的时候,对称的位置 D -> 1 一定也是 1。 146 | 那么这种情况下会造成空间的浪费,解决办法需自己去研究下。 147 | 148 | - 如果图是一个稀疏图 149 | 那么矩阵中将存在大量的 0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。 150 | 而且即使只有一个边,我们也必须遍历一行来找出这个边,也浪费很多时间。 151 | 152 | ### 邻接表 153 | 154 | - 概述 155 | 156 | - 邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。 157 | - 这个列表有很多中方式来存储:数组/链表/字典 (哈希表) 都可以。 158 | 159 | ![img](../public/images/data-structure/img-63.png) 160 | 161 | - 图片解析 162 | 163 | 比如我们要表示和 A 顶点有关联的顶点(边),A 和 B/C/D 有边,那么我们可以通过 A 找到 对应的数组/链表/字典,再取出其中的内容。 164 | 165 | - 邻接表的问题 166 | - 邻接表计算“出度”是比较简单的(出度:指向别人的数量,入度:指向自己的数量) 167 | - 邻接表如果需要计算有向图的“入度”,那么是一件非常麻烦的事情。 168 | - 它必须构造一个“逆邻接表”,才能有效的计算“入度”。而临街矩阵会非常简单。 169 | 170 | ## 图的封装 171 | 172 | ### 创建图类 173 | 174 | 先来创建 Graph 类,定义了两个属性: 175 | - `vertexes` 用于存储所有的顶点,使用一个数组来保存。 176 | - `adjList` adj 是 adjoin 的缩写,邻接的意思。adjList 用于存储所有的边,这里采用邻接表的形式。 177 | 178 | ```js 179 | class Graph { 180 | constructor() { 181 | this.vertexes = []; // 存储顶点 182 | this.adjList = new Dictionay(); //存储边信息 183 | } 184 | } 185 | ``` 186 | 187 | ### 方法 188 | 189 | #### 添加顶点 190 | 191 | 向图中添加一些顶点。 192 | - 将添加的顶点放入到数组中。 193 | - 另外,给该顶点创建一个数组 `[]`,该数组用于存储顶点连接的所有的边。(回顾邻接表的实现方式) 194 | 195 | ```js 196 | // 添加顶点 197 | addVertex(val) { 198 | // 添加点 199 | this.vertexes.push(val) 200 | // 添加点的关系 采用邻接矩阵法 结构用 Map 201 | this.adjList.set(val, []) 202 | } 203 | ``` 204 | 205 | #### 添加边 206 | 207 | 可以指定顶点和顶点之间的边。 208 | 209 | - 添加边需要传入两个顶点,因为边是两个顶点之间的边,边不可能单独存在。 210 | - 根据顶点 v 取出对应的数组,将 w 加入到它的数组中。 211 | - 根据顶点 w 取出对应的数组,将 v 加入到它的数组中。 212 | - 因为这里实现的是无向图,所以边是可以双向的。 213 | 214 | ```js 215 | // 添加边 216 | addEdge(val1, val2) { 217 | // 添加边需要传入两个顶点,因为边是两个顶点之间的边,边不可能单独存在。 218 | // 这里实现的是无向图,所以这里不考虑方向问题 219 | this.adjList.get(val1).push(val2) 220 | this.adjList.get(val2).push(val1) 221 | } 222 | ``` 223 | 224 | #### toString 方法 225 | 226 | 为了能够正确的显示图的结果,就是拿出二维数组的每一项。 227 | 228 | ```js 229 | // 输出图结构 230 | toString() { 231 | let res = '' 232 | for (let i = 0; i < this.vertexes.length; i++) { 233 | res += this.vertexes[i] + "->" 234 | let adj = this.adjList.get(this.vertexes[i]) 235 | for (let j = 0; j < adj.length; j++) { 236 | res += adj[j] + "" 237 | } 238 | res += "\n" 239 | } 240 | return res 241 | } 242 | ``` 243 | 244 | ### 测试代码 245 | 246 | ```js 247 | // 测试代码 248 | let graph = new Graph(); 249 | 250 | // 添加顶点 251 | let myVertexes = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]; 252 | for (let i = 0; i < myVertexes.length; i++) { 253 | graph.addVertex(myVertexes[i]); 254 | } 255 | 256 | // 添加边 257 | graph.addEdge("A", "B"); 258 | graph.addEdge("A", "C"); 259 | graph.addEdge("A", "D"); 260 | graph.addEdge("C", "D"); 261 | graph.addEdge("C", "G"); 262 | graph.addEdge("D", "G"); 263 | graph.addEdge("D", "H"); 264 | graph.addEdge("B", "E"); 265 | graph.addEdge("B", "F"); 266 | graph.addEdge("E", "I"); 267 | ``` 268 | 269 | ## 图的遍历 270 | 271 | 和其他数据结构一样,需要通过某种算法来遍历图结构中每一个数据。这样可以保证,在我们需要时,通过这种算法来访问某个顶点的数据以及它对应的边。 272 | 273 | ### 遍历的方式 274 | 275 | 图的遍历算法的思想在于必须访问每个第一次访问的节点,并且追踪有哪些顶点还没有被访问到。 276 | 277 | - 有两种算法可以对图进行遍历 278 | 279 | - 广度优先搜索 (Breadth-First Search, 简称 BFS) 280 | - 深度优先搜索 (Depth-First Search, 简称 DFS) 281 | 282 | 这两种遍历算法,都需要明确指定第一个被访问的顶点。 283 | 284 | - 遍历的注意点 285 | 286 | - 完全探索一个顶点要求我们便查看该顶点的每一条边。 287 | - 对于每一条所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。 288 | - 为了保证算法的效率:每个顶点至多访问两次。 289 | 290 | - 两种算法的思想 291 | 292 | - BFS 基于队列,入队列的顶点先被探索。 293 | - DFS 基于栈,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。 294 | 295 | 为了记录顶点是否被访问过,我们使用三种颜色来反应它们的状态。(或者两种颜色也可以) 296 | 297 | - **白色**表示该顶点还没有被访问。 298 | - **灰色**表示该顶点被访问过,但并未被探索过。 299 | - **黑色**表示该顶点被访问过且被完全探索过。 300 | 301 | ```js 302 | // 初始化顶点的颜色 303 | initializeColor() { 304 | // 白色:表示该顶点还没有被访问。 305 | // 灰色:表示该顶点被访问过,但并未被探索过。 306 | // 黑色:表示该顶点被访问过且被完全探索过。 307 | let colors = [] 308 | for (let i = 0; i < this.vertexes.length; i++) { 309 | colors[this.vertexes[i]] = "white" 310 | } 311 | return colors 312 | } 313 | ``` 314 | 315 | ### 广度优先搜索 (BFS) 316 | 317 | - 广度优先搜索算法的思路 318 | 广度优先算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深的访问顶点。 319 | 320 | - 图解 BFS 321 | ![img](../public/images/data-structure/img-64.png) 322 | 323 | - 广度优先搜索的实现 324 | 325 | 1. 创建一个队列 Q 326 | 2. 将 v 标注为被发现的 (灰色), 并将 v 将入队列 Q 327 | 3. 如果 Q 非空,执行下面的步骤: 328 | - 将 v 从 Q 中取出队列 329 | - 将 v 标注为被发现的灰色 330 | - 将 v 所有的未被访问过的邻接点(白色),加入到队列中 331 | - 将 v 标志为黑色 332 | 333 | - 广度优先搜索的代码 334 | 335 | ```js 336 | // 广度优先搜索 337 | bfs(handle) { 338 | // 1.初始化颜色 339 | let color = this._initializeColor() 340 | // 2. 创建队列 341 | let queue = new Queue 342 | // 3. 将传入的顶点放入队列 343 | queue.enqueue(this.vertexes[0]) 344 | // 4.依赖队列操作数据 队列不为空时一直持续 345 | while (!queue.isEmpty()) { 346 | // 4.1 拿到队头 347 | let qVal = queue.dequeue() 348 | // 4.2 拿到队头所关联(相连)的点并设置为访问中状态(灰色) 349 | let qAdj = this.adjList.get(qVal) 350 | color[qVal] = "gray" 351 | // 4.3 将队头关联的点添加到队尾 352 | // 这一步是完成bfs的关键,依赖队列的先进先出的特点。 353 | for (let i = 0; i < qAdj.length; i++) { 354 | let a = qAdj[i] 355 | if (color[a] === "white") { 356 | color[a] = "gray" 357 | queue.enqueue(a) 358 | } 359 | } 360 | // 4.5设置访问完的点为黑色。 361 | color[qVal] = "black" 362 | if (handle) [ 363 | handle(qVal) 364 | ] 365 | } 366 | } 367 | ``` 368 | 369 | - 测试代码 370 | 371 | ```js 372 | // 调用广度优先算法 373 | let result = ""; 374 | graph.bfs(graph.vertexes[0], function (v) { 375 | result += v + " "; 376 | }); 377 | console.log(result); // A B C D E F G H I 378 | ``` 379 | 380 | ### 深度优先搜索 (DFS) 381 | 382 | 深度优先搜索的思路: 383 | 384 | - 深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径知道这条路径最后被访问了。 385 | - 接着原路回退并探索下一条路径。 386 | - 图解 DFS 387 | ![img](../public/images/data-structure/img-65.png) 388 | 389 | 深度优先搜索算法的实现: 390 | 391 | 广度优先搜索算法我们使用的是队列,这里的深度优先搜索算法可以使用栈完成,也可以使用递归。(递归本质上就是函数栈的调用) 392 | 393 | - 深度优先搜索算法的代码: 394 | 395 | ```js 396 | // 深度优先搜索 397 | dfs(handle) { 398 | // 1.初始化颜色 399 | let color = this._initializeColor() 400 | // 2. 遍历所有顶点,开始访问 401 | for (let i = 0; i < this.vertexes.length; i++) { 402 | if (color[this.vertexes[i]] === "white") { 403 | this._dfsVisit(this.vertexes[i], color, handle) 404 | } 405 | } 406 | } 407 | // dfs 的递归方法 这里直接使用函数的调用栈 408 | _dfsVisit(val, color, handle) { 409 | // 1. 将颜色设置为访问中 410 | color[val] = "gray" 411 | // 2. 执行相应的回调 412 | if (handle) { 413 | handle(val) 414 | } 415 | // 3. 拿与该点相邻的点,对每个点操作 416 | let adj = this.adjList.get(val) 417 | for (let i = 0; i < adj.length; i++) { 418 | let w = adj[i] 419 | // 如果相邻点未未访问状态,开始访问。 420 | if (color[w] === "white") { 421 | this._dfsVisit(w, color, handle) 422 | } 423 | } 424 | // 4. 处理完后设置为访问过点。 425 | color[val] = "black" 426 | } 427 | ``` 428 | 429 | - 测试代码 430 | 431 | ```js 432 | // 调用深度优先算法 433 | result = ""; 434 | graph.dfs(function (v) { 435 | result += v + " "; 436 | }); 437 | // 输出深度优先 438 | console.log(result); //A B E I F C D G H 439 | ``` 440 | 441 | - 递归的代码较难理解一些,这副图来帮助理解过程: 442 | ![img](../public/images/data-structure/img-66.png) 443 | 444 | 445 | -------------------------------------------------------------------------------- /src/docs/data-structure/HashTable.md: -------------------------------------------------------------------------------- 1 | # 哈希表 2 | 3 | ## 认识哈希表 4 | 5 | 哈希表是一种非常重要的数据结构,几乎所有的编程语言都直接或者间接应用这种数据结构。 6 | 7 | 哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势: 8 | 9 | - 哈希表可以提供非常快速的 **插入 - 删除 - 查找** 操作。 10 | - 无论多少数据,插入和删除值都只需接近常量的时间,即 **O(1)** 的时间复杂度。实际上,只需要几个机器指令即可完成。 11 | - 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。 12 | - 哈希表相对于树来说编码要简单得多。 13 | 14 | 哈希表同样存在不足之处: 15 | 16 | - 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。 17 | - 通常情况下,哈希表中的 `key` 是不允许重复的,不能放置相同的 `key`,用于保存不同的元素。 18 | 19 | 哈希表是什么? 20 | 21 | - 哈希表并不好理解,不像数组、链表和树等可通过图形的形式表示其结构和原理。 22 | - 哈希表的结构就是数组,但它**神奇之处在于对下标值的一种变换**,这种变换我们可以称之为**哈希函数**,通过哈希函数可以获取 HashCode。 23 | 24 | 通过以下案例了解哈希表: 25 | 26 | - 案例一:公司想要存储 1000 个人的信息,每一个工号对应一个员工的信息。若使用数组,增加和删除数据时比较麻烦;使用链表,获取数据时比较麻烦。有没有一种数据结构,能把某一员工的姓名转换为它对应的工号,再根据工号查找该员工的完整信息呢?没错此时就可以使用哈希表的哈希函数来实现。 27 | 28 | - 案例二:存储联系人和对应的电话号码:当要查找张三的号码时,若使用数组:由于不知道存储张三数据对象的下标值,所以查找起来十分麻烦,使用链表时也同样麻烦。而使用哈希表就能通过哈希函数把张三这个名称转换为它对应的下标值,再通过下标值查找效率就非常高了。 29 | 30 | 也就是说:哈希表最后还是基于数组来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值,建立字符串和下标值的映射关系。 31 | 32 | ### 认识哈希化 33 | 34 | 为了把字符串转化为对应的下标值,需要有一套编码系统,为了方便理解我们创建这样一套编码系统:比如 a 为 1,b 为 2,c 为 3,以此类推 z 为 26,空格为 27(不考虑大写情况)。 35 | 36 | 有了编码系统后,将字母转化为数字也有很多种方案: 37 | 38 | - 方案一:数字相加。 39 | 40 | 例如 cats 转化为数字:`3 + 1 + 20 + 19 = 43`,那么就把 43 作为 cats 单词的下标值储存在数组中; 41 | 42 | 但是这种方式会存在这样的问题:很多的单词按照该方式转化为数字后都是 43,比如 was。而在数组中一个下标值只能储存一个数据,所以该方式不合理。 43 | 44 | - 方案二:幂的连乘。 45 | 46 | 我们平时使用的大于 10 的数字,就是用幂的连乘来表示它的唯一性的。 47 | 比如: `6543 = 6 * 10^3 + 5 * 10^2 + 4 * 10 + 3`;这样单词也可以用该种方式来表示:`cats = 3 * 27^3 + 1 * 27^2 + 20 * 27 + 17 = 60337`。 48 | 49 | 虽然该方式可以保证字符的唯一性,但是如果是较长的字符(如 aaaaaaaaaa)所表示的数字就非常大,此时要求很大容量的数组,然而其中却有许多下标值指向的是无效的数据(比如不存在 zxcvvv 这样的单词),造成了数组空间的浪费。 50 | 51 | 两种方案总结: 52 | 53 | - 第一种方案(让数字相加求和)产生的数组下标太少。 54 | - 第二种方案(与 27 的幂相乘求和)产生的数组下标又太多。 55 | 56 | 现在需要一种压缩方法,把幂的连乘方案系统中得到的**巨大整数范围压缩到可接受的数组范围中**。可以通过**取余**操作来实现。虽然取余操作得到的结构也有可能重复,但是可以通过其他方式解决。 57 | 58 | ### 哈希表的一些概念 59 | 60 | - **哈希化** 61 | 62 | 将**大数字**转化成**数组范围内下标**的过程,称之为哈希化。 63 | 64 | - **哈希函数** 65 | 66 | 我们通常会将单词转化成大数字,把大数字进行哈希化的代码实现放在一个函数中,该函数就称为哈希函数。 67 | 68 | - **哈希表** 69 | 70 | 对最终数据插入的数组进行整个结构的封装,得到的就是哈希表。 71 | 72 | ### 地址的冲突 73 | 74 | 在实际中,经过哈希函数哈希化过后得到的下标值可能有重复,这种情况称为冲突,冲突是不可避免的,我们只能解决冲突。 75 | 76 | 解决冲突常见的两种方案:链地址法(拉链法)和开放地址法。 77 | 78 | #### 链地址法(拉链法) 79 | 80 | 如下图所示,我们将每一个数字都对 10 进行取余操作,则余数的范围 0~9 作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。 81 | 82 | ![img](../public/images/data-structure/img-13.png) 83 | 84 | 这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。 85 | 86 | 总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。 87 | 88 | #### 开放地址法 89 | 90 | 开放地址法的主要工作方式是**寻找空白的单元格来放置冲突的数据项**。 91 | 92 | ![img](../public/images/data-structure/img-14.png) 93 | 94 | 根据探测空白单元格位置方式的不同,可分为三种方法: 95 | 96 | - 线性探测 97 | - 二次探测 98 | - 再哈希法 99 | 100 | ##### 线性探测 101 | 102 | - 当插入 13 时: 103 | 104 | 经过哈希化(对 10 取余)之后得到的下标值 index=3,但是该位置已经放置了数据 33。而线性探测就是从 index 位置 +1 开始向后一个一个来查找合适的位置来放置 13,所谓合适的位置指的是空的位置,如上图中 index=4 的位置就是合适的位置。 105 | 106 | - 当查询 13 时: 107 | 108 | - 首先 13 经过哈希化得到 index=3,如果 index=3 的位置存放的数据与需要查询的数据 13 相同,就直接返回; 109 | 不相同时,则线性查找,从 index+1 位置开始一个一个位置地查找数据 13。 110 | - 查询过程中不会遍历整个哈希表,只要查询到空位置,就停止,因为插入 13 时不会跳过空位置去插入其他位置。 111 | 112 | - 当删除 13 时: 113 | 114 | - 删除操作和上述两种情况类似,但需要注意的是,删除一个数据项时,不能将该位置下标的内容设置为 null,否则会影响到之后其他的查询操作,因为一遇到为 null 的位置就会停止查找。 115 | - 通常删除一个位置的数据项时,我们可以将它进行特殊处理(比如设置为 -1),这样在查找时遇到 -1 就知道要继续查找。 116 | 117 | 线性探测存在的问题: 118 | 119 | - 线性探测存在一个比较严重的问题,就是聚集。 120 | 121 | - 如哈希表中还没插入任何元素时,插入 23、24、25、26、27,这就意味着下标值为 3、4、5、6、7 的位置都放置了数据,这种一连串填充单元就称为聚集。 122 | 123 | - 聚集会影响哈希表的性能,无论是插入/查询/删除都会影响。 124 | 125 | - 比如插入 13 时就会发现,连续的单元 3~7 都不允许插入数据,并且在插入的过程中需要经历多次这种情况。二次探测法可以解决该问题。 126 | 127 | ![img](../public/images/data-structure/img-15.png) 128 | 129 | ##### 二次探测 130 | 131 | 上文所说的线性探测存在的问题: 132 | 133 | - 如果之前的数据是连续插入的,那么新插入的一个数据可能需要探测很长的距离; 134 | 135 | 二次探测是在线性探测的基础上进行了优化: 136 | 137 | - 线性探测:我们可以看成是步长为 1 的探测,比如从下表值 x 开始,那么线性探测就是按照下标值:x+1、x+2、x+3 等依次探测; 138 | 139 | - 二次探测:对步长进行了优化,比如从下标值 x 开始探测:x+1^2^、x+2^2^、x+3^3^ 。这样一次性探测比较长的距离,避免了数据聚集带来的影响。 140 | 141 | - 二次探测存在的问题: 142 | 143 | 当插入数据分布性较大的一组数据时,比如:13-163-63-3-213,这种情况会造成步长不一的一种聚集(虽然这种情况出现的概率较线性探测的聚集要小),同样会影响性能。 144 | 145 | ##### 再哈希法 146 | 147 | 在开放地址法中寻找空白单元格的最好的解决方式为再哈希化。 148 | 149 | - 二次探测的步长是固定的:1,4,9,16 依次类推。 150 | - 现在需要一种方法:产生一种依赖关键字 (数据) 的探测序列,而不是每个关键字探测步长都一样。 151 | - 这样,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。 152 | - 再哈希法的做法为:把关键字用另一个哈希函数,再做一次哈希化,用这次哈希化的结果作为该关键字的步长。 153 | 154 | 第二次哈希化需要满足以下两点: 155 | 156 | - 和第一个哈希函数不同,不然哈希化后的结果仍是原来位置; 157 | - 不能输出为 0,否则每次探测都是原地踏步的死循环; 158 | 159 | 优秀的哈希函数: 160 | 161 | - stepSize = constant - (key % constant); 162 | - 其中 constant 是质数,且小于数组的容量; 163 | - 例如:stepSize = 5 - (key % 5),满足需求,并且结果不可能为 0; 164 | 165 | 哈希化的效率 166 | 167 | 哈希表中执行插入和搜索操作效率是非常高的。 168 | 169 | - 如果没有发生冲突,那么效率就会更高; 170 | - 如果发生冲突,存取时间就依赖后来的探测长度; 171 | - 平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度会越来越长。 172 | 173 | #### 装填因子 174 | 175 | - 装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值; 176 | - 装填因子 = 总数据项 / 哈希表长度; 177 | - 开放地址法的装填因子最大为 1,因为只有空白的单元才能放入元素; 178 | - 链地址法的装填因子可以大于 1,因为只要愿意,拉链法可以无限延伸下去; 179 | 180 | #### 不同探测方式性能的比较 181 | 182 | - 线性探测 183 | 184 | 可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。 185 | 186 | ![img](../public/images/data-structure/img-16.png) 187 | 188 | - 二次探测和再哈希化的性能 189 | 190 | 二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。 191 | 192 | ![img](../public/images/data-structure/img-17.png) 193 | 194 | - 链地址法的性能 195 | 196 | 可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如 Java 中的 HashMap 中使用的就是链地址法。 197 | 198 | ![img](../public/images/data-structure/img-18.png) 199 | 200 | ### 哈希函数 201 | 202 | 哈希表的优势在于它的速度,所以哈希函数不能采用消耗性能较高的复杂算法。提高速度的一个方法是在哈希函数中尽量减少乘法和除法。 203 | 204 | 性能高的哈希函数应具备以下两个优点: 205 | 206 | - 快速的计算; 207 | - 均匀的分布; 208 | 209 | #### 快速计算 210 | 211 | 霍纳法则:在中国霍纳法则也叫做秦久韶算法,具体算法为: 212 | 213 | ![img](../public/images/data-structure/img-19.png) 214 | 215 | 求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求 n 次多项式 f(x) 的值就转化为求 n 个一次多项式的值。 216 | 217 | - 变换之前: 218 | 219 | - 乘法次数:n(n+1)/2 次; 220 | - 加法次数:n 次; 221 | 222 | - 变换之后: 223 | 224 | - 乘法次数:n 次; 225 | - 加法次数:n 次; 226 | 227 | 如果使用大 O 表示时间复杂度的话,直接从变换前的 O(N^2) 降到了 O(N)。 228 | 229 | #### 均匀分布 230 | 231 | 在设计哈希表时,我们已经有办法处理映射到相同下标值的情况:链地址法或者开放地址法。但是,为了提供效率,最好的情况还是让数据在哈希表中均匀分布。因此,我们需要在使用常量的地方,尽量使用质数。比如:哈希表的长度、N 次幂的底数等。 232 | 233 | Java 中的 HashMap 采用的是链地址法,哈希化采用的是公式为:index = HashCode(key) & (Length-1) 即将数据化为二进制进行与运算,而不是取余运算。这样计算机直接运算二进制数据,效率更高。但是 JavaScript 在进行较大数据的与运算时会出现问题,所以我们使用 JavaScript 实现哈希化时采用取余运算。 234 | 235 | ## 封装哈希表 236 | 237 | ### 哈希表常见操作 238 | 239 | - `put(key, value)` 插入或修改操作。 240 | - `get(key)` 获取哈希表中特定位置的元素。 241 | - `remove(key)` 删除哈希表中特定位置的元素。 242 | - `isEmpty()` 如果哈希表中不包含任何元素,返回 `trun`,如果哈希表长度大于 0 则返回 `false`。 243 | - `size()` 返回哈希表包含的元素个数。 244 | - `resize(value)` 对哈希表进行扩容操作。 245 | 246 | ### 哈希函数的简单实现 247 | 248 | 首先使用霍纳法则计算 hashCode 的值,通过取余操作实现哈希化,此处先简单地指定数组的大小。 249 | 250 | ```js 251 | hashFn(string, limit = 7) { 252 | 253 | // 自己采用的一个质数(无强制要求,质数即可) 254 | const PRIME = 31; 255 | 256 | // 1、定义存储 hashCode 的变量 257 | let hashCode = 0; 258 | 259 | // 2、使用霍纳法则(秦九韶算法),计算 hashCode 的值 260 | for (let item of string) { 261 | hashCode = PRIME * hashCode + item.charCodeAt(); 262 | } 263 | 264 | // 3、对 hashCode 取余,并返回 265 | return hashCode % limit; 266 | } 267 | ``` 268 | 269 | 哈希函数测试 270 | 271 | ```js 272 | console.log(hashFn("123")); //--> 5 273 | console.log(hashFn("abc")); //--> 6 274 | ``` 275 | 276 | ### 哈希表的实现 277 | 278 | #### 创建哈希表类 279 | 280 | 封装的哈希表的数据结构模型: 281 | 282 | ![img](../public/images/data-structure/img-20.png) 283 | 284 | 首先创建哈希表类 HashTable,并添加必要的属性和上面实现的哈希函数,再进行其他方法的实现。 285 | 286 | ```js 287 | class HashTable { 288 | constructor() { 289 | this.storage = []; // 哈希表存储数据的变量 290 | this.count = 0; // 当前存放的元素个数 291 | this.limit = 7; // 哈希表长度(初始设为质数 7) 292 | } 293 | } 294 | ``` 295 | 296 | #### put(key,value) 297 | 298 | 哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个 `[key, value]` 时,如果原来不存在该 key,那么就是插入操作,如果原来已经存在该 key,那么就是修改操作。 299 | 300 | ![img](../public/images/data-structure/img-21.png) 301 | 302 | 实现思路: 303 | 304 | - 首先,根据 key 获取索引值 index,将数据插入到 storage 的对应位置; 305 | - 然后,根据索引值取出 bucket,如果 bucket 不存在,先创建 bucket,随后放置在该索引值的位置; 306 | - 接着,判断新增还是修改原来的值。如果已经有值了,就修改该值;如果没有,就执行后续操作。 307 | - 最后,进行新增数据操作。 308 | 309 | 代码实现 310 | 311 | ```js 312 | // put(key, value) 往哈希表里添加数据 313 | put(key, value) { 314 | 315 | // 1、根据 key 获取要映射到 storage 里面的 index(通过哈希函数获取) 316 | const index = hashFn(key, this.limit); 317 | 318 | // 2、根据 index 取出对应的 bucket 319 | let bucket = this.storage[index]; 320 | 321 | // 3、判断是否存在 bucket 322 | if (bucket === undefined) { 323 | bucket = []; // 不存在则创建 324 | this.storage[index] = bucket; 325 | } 326 | 327 | // 4、判断是插入数据操作还是修改数据操作 328 | for (let i = 0; i < bucket.length; i++) { 329 | let tuple = bucket[i]; // tuple 的格式:[key, value] 330 | if (tuple[0] === key) { // 如果 key 相等,则修改数据 331 | tuple[1] = value; 332 | return; // 修改完 tuple 里数据,return 终止不再往下执行。 333 | } 334 | } 335 | 336 | // 5、bucket 新增数据 337 | bucket.push([key, value]); // bucket 存储元组 tuple,格式为 [key, value] 338 | this.count++; 339 | 340 | // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 341 | if (this.count / this.limit > this.loadFactor) { 342 | this.resize(this.getPrime(this.limit * 2)); 343 | } 344 | 345 | } 346 | ``` 347 | 348 | #### get(key) 349 | 350 | 实现思路: 351 | 352 | - 首先,根据 key 通过哈希函数获取它在 `storage` 中对应的索引值 `index`。 353 | - 然后,根据索引值获取对应的 `bucket`。 354 | - 接着,判断获取到的 `bucket` 是否为 `null`,如果为 `null`,直接返回 `null`。 355 | - 随后,线性遍历 `bucket` 中每一个 `key` 是否等于传入的 `key`。如果等于,直接返回对应的 `value`。 356 | - 最后,遍历完 `bucket` 后,仍然没有找到对应的 `key`,直接 `return null` 即可。 357 | 358 | 代码实现 359 | 360 | ```js 361 | // 根据 get(key) 获取 value 362 | get(key) { 363 | 364 | const index = hashFn(key, this.limit); 365 | const bucket = this.storage[index]; 366 | 367 | if (bucket === undefined) { 368 | return null; 369 | } 370 | 371 | for (const tuple of bucket) { 372 | if (tuple[0] === key) { 373 | return tuple[1]; 374 | } 375 | } 376 | return null; 377 | } 378 | ``` 379 | 380 | #### remove(key) 381 | 382 | 实现思路: 383 | 384 | - 首先,根据 key 通过哈希函数获取它在 `storage` 中对应的索引值 `index`。 385 | - 然后,根据索引值获取对应的 `bucket`。 386 | - 接着,判断获取到的 `bucket` 是否为 `null`,如果为 `null`,直接返回 `null`。 387 | - 随后,线性查找 `bucket`,寻找对应的数据,并且删除。 388 | - 最后,依然没有找到,返回 `null`。 389 | 390 | ```js 391 | // remove(key) 删除指定 key 的数据 392 | remove(key) { 393 | 394 | const index = hashFn(key, this.limit); 395 | const bucket = this.storage[index]; 396 | 397 | if (bucket === undefined) { 398 | return null; 399 | } 400 | 401 | // 遍历 bucket,找到对应位置的 tuple,将其删除 402 | for (let i = 0, len = bucket.length; i < len; i++) { 403 | const tuple = bucket[i]; 404 | if (tuple[0] === key) { 405 | bucket.splice(i, 1); // 删除对应位置的数组项 406 | this.count--; 407 | 408 | // 根据装填因子的大小,判断是否要进行哈希表压缩 409 | if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) { 410 | this.resize(this.getPrime(Math.floor(this.limit / 2))); 411 | } 412 | 413 | return tuple; 414 | } 415 | 416 | } 417 | 418 | } 419 | ``` 420 | 421 | #### isEmpty() 422 | 423 | ```js 424 | isEmpty() { 425 | return this.count === 0; 426 | } 427 | ``` 428 | 429 | #### size() 430 | 431 | ```js 432 | size() { 433 | return this.count; 434 | } 435 | ``` 436 | 437 | ## 哈希表的扩容与压缩 438 | 439 | 为什么需要扩容? 440 | 441 | - 前面我们在哈希表中使用的是长度为 7 的数组,由于使用的是链地址法,装填因子 (loadFactor) 可以大于 1,所以这个哈希表可以无限制地插入新数据。 442 | 443 | - 但是,随着数据量的增多,storage 中每一个 `index` 对应的 `bucket` 数组(链表)就会越来越长,这就会造成哈希表效率的降低。 444 | 445 | 什么情况下需要扩容? 446 | 447 | - 常见的情况是 `loadFactor > 0.75` 的时候进行扩容。 448 | 449 | 如何进行扩容? 450 | 451 | - 简单的扩容可以直接扩大两倍(关于质数,之后讨论)。 452 | - 扩容之后所有的数据项都要进行同步修改。 453 | 454 | 实现思路: 455 | 456 | - 首先,定义一个变量,比如 oldStorage 指向原来的 `storage`。 457 | - 然后,创建一个新的容量更大的数组,让 `this.storage` 指向它。 458 | - 最后,将 oldStorage 中的每一个 bucket 中的每一个数据取出来依次添加到 `this.storage` 指向的新数组中。 459 | 460 | ![img](../public/images/data-structure/img-22.png) 461 | 462 | ### resize() 的实现 463 | 464 | 装填因子 = 哈希表中数据 / 哈希表长度,即 `loadFactor = count / HashTable.length`。 465 | 466 | resize 方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩。 467 | 468 | ```js 469 | // 重新调整哈希表大小,扩容或压缩 470 | resize(newLimit) { 471 | 472 | // 1、保存旧的 storage 数组内容 473 | const oldStorage = this.storage; 474 | 475 | // 2、重置所有属性 476 | this.storage = []; 477 | this.count = 0; 478 | this.limit = newLimit; 479 | 480 | // 3、遍历 oldStorage,取出所有数据,重新 put 到 this.storage 481 | for (const bucket of oldStorage) { 482 | if (bucket) { 483 | for (const b of bucket) { 484 | this.put(b[0], b[1]); 485 | } 486 | } 487 | } 488 | } 489 | ``` 490 | 491 | - 通常情况下当装填因子 `laodFactor > 0.75` 时,对哈希表进行扩容。在哈希表中的添加方法(push 方法)中添加如下代码,判断是否需要调用扩容函数进行扩容。 492 | 493 | ```js 494 | // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 495 | if (this.count / this.limit > this.loadFactor) { 496 | this.resize(this.getPrime(this.limit * 2)); 497 | } 498 | ``` 499 | 500 | * 当装填因子 `laodFactor < 0.25` 时,对哈希表容量进行压缩。在哈希表中的删除方法(remove 方法)中添加如下代码,判断是否需要调用扩容函数进行压缩。 501 | 502 | ```js 503 | // 根据装填因子的大小,判断是否要进行哈希表压缩 504 | if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) { 505 | this.resize(this.getPrime(Math.floor(this.limit / 2))); 506 | } 507 | ``` 508 | 509 | ### 选择质数作为哈希表容量 510 | 511 | #### 质数判断 512 | 513 | > 1 不是质数 514 | 515 | - 方法一:针对质数的特点:只能被 1 和 number 整除,不能被 2 ~ (number-1) 整除。遍历 2 ~ (num-1) 。 516 | 517 | 这种方法虽然能实现质数的判断,但是效率不高。 518 | 519 | ```js 520 | function isPrime(number) { 521 | if (number <= 1) return false; 522 | for (let i = 2; i < number; i++) { 523 | if (number % i === 0) { 524 | return false; 525 | } 526 | } 527 | return true; 528 | } 529 | ``` 530 | 531 | - 方法二:只需要遍历 2 ~ num 的平方根即可。该方法性能较好。 532 | 533 | ```js 534 | function isPrime(number) { 535 | if (number <= 1 || number === 4) return false; 536 | const temp = Math.ceil(Math.sqrt(number)); 537 | for (let i = 2; i < temp; i++) { 538 | if (number % i === 0) { 539 | return false; 540 | } 541 | } 542 | return true; 543 | } 544 | ``` 545 | 546 | #### 实现扩容或压缩后的哈希表容量为质数 547 | 548 | 实现思路: 549 | 550 | 2 倍扩容或压缩之后,通过循环调用 `isPrime` 判断得到的容量是否为质数,不是则 +1,直到是为止。比如原长度:7,2 倍扩容后长度为 14,14 不是质数,`14 + 1 = 15` 不是质数,`15 + 1 = 16` 不是质数,`16 + 1 = 17` 是质数,停止循环,由此得到质数 17。 551 | 552 | - 第一步:首先需要为 HashTable 类添加判断质数的 `isPrime` 方法和获取质数的 `getPrime` 方法: 553 | 554 | ```js 555 | // getPrime(number) 根据传入的 number 获取最临近的质数 556 | getPrime(number) { 557 | while (!isPrime(number)) { 558 | number++; 559 | } 560 | return number; 561 | } 562 | ``` 563 | 564 | - 修改添加元素的 `put` 方法和删除元素的 `remove` 方法中关于数组扩容的相关操作: 565 | 566 | 在 `put` 方法中添加如下代码: 567 | 568 | ```js 569 | // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 570 | if (this.count / this.limit > this.loadFactor) { 571 | this.resize(this.getPrime(this.limit * 2)); 572 | } 573 | ``` 574 | 575 | 在 `remove` 方法中添加如下代码: 576 | 577 | ```js 578 | // 根据装填因子的大小,判断是否要进行哈希表压缩 579 | if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) { 580 | this.resize(this.getPrime(Math.floor(this.limit / 2))); 581 | } 582 | ``` 583 | 584 | ## 哈希表完整实现 585 | 586 | ```js 587 | class HashTable { 588 | constructor() { 589 | this.storage = []; // 哈希表存储数据的变量 590 | this.count = 0; // 当前存放的元素个数 591 | this.limit = 7; // 哈希表长度(初始设为质数 7) 592 | 593 | // 装填因子 (已有个数/总个数) 594 | this.loadFactor = 0.75; 595 | this.minLoadFactor = 0.25; 596 | } 597 | 598 | // getPrime(number) 根据传入的 number 获取最临近的质数 599 | getPrime(number) { 600 | while (!isPrime(number)) { 601 | number++; 602 | } 603 | return number; 604 | } 605 | 606 | // put(key, value) 往哈希表里添加数据 607 | put(key, value) { 608 | // 1、根据 key 获取要映射到 storage 里面的 index(通过哈希函数获取) 609 | const index = hashFn(key, this.limit); 610 | 611 | // 2、根据 index 取出对应的 bucket 612 | let bucket = this.storage[index]; 613 | 614 | // 3、判断是否存在 bucket 615 | if (bucket === undefined) { 616 | bucket = []; // 不存在则创建 617 | this.storage[index] = bucket; 618 | } 619 | 620 | // 4、判断是插入数据操作还是修改数据操作 621 | for (let i = 0; i < bucket.length; i++) { 622 | let tuple = bucket[i]; // tuple 的格式:[key, value] 623 | if (tuple[0] === key) { 624 | // 如果 key 相等,则修改数据 625 | tuple[1] = value; 626 | return; // 修改完 tuple 里数据,return 终止,不再往下执行。 627 | } 628 | } 629 | 630 | // 5、bucket 新增数据 631 | bucket.push([key, value]); // bucket 存储元组 tuple,格式为 [key, value] 632 | this.count++; 633 | 634 | // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 635 | if (this.count / this.limit > this.loadFactor) { 636 | this.resize(this.getPrime(this.limit * 2)); 637 | } 638 | } 639 | 640 | // 根据 get(key) 获取 value 641 | get(key) { 642 | const index = hashFn(key, this.limit); 643 | const bucket = this.storage[index]; 644 | 645 | if (bucket === undefined) { 646 | return null; 647 | } 648 | 649 | for (const tuple of bucket) { 650 | if (tuple[0] === key) { 651 | return tuple[1]; 652 | } 653 | } 654 | return null; 655 | } 656 | 657 | // remove(key) 删除指定 key 的数据 658 | remove(key) { 659 | const index = hashFn(key, this.limit); 660 | const bucket = this.storage[index]; 661 | 662 | if (bucket === undefined) { 663 | return null; 664 | } 665 | 666 | // 遍历 bucket,找到对应位置的 tuple,将其删除 667 | for (let i = 0, len = bucket.length; i < len; i++) { 668 | const tuple = bucket[i]; 669 | if (tuple[0] === key) { 670 | bucket.splice(i, 1); // 删除对应位置的数组项 671 | this.count--; 672 | 673 | // 根据装填因子的大小,判断是否要进行哈希表压缩 674 | if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) { 675 | this.resize(this.getPrime(Math.floor(this.limit / 2))); 676 | } 677 | 678 | return tuple; 679 | } 680 | } 681 | } 682 | 683 | isEmpty() { 684 | return this.count === 0; 685 | } 686 | 687 | size() { 688 | return this.count; 689 | } 690 | 691 | // 重新调整哈希表大小,扩容或压缩 692 | resize(newLimit) { 693 | // 1、保存旧的 storage 数组内容 694 | const oldStorage = this.storage; 695 | 696 | // 2、重置所有属性 697 | this.storage = []; 698 | this.count = 0; 699 | this.limit = newLimit; 700 | 701 | // 3、遍历 oldStorage,取出所有数据,重新 put 到 this.storage 702 | for (const bucket of oldStorage) { 703 | if (bucket) { 704 | for (const b of bucket) { 705 | this.put(b[0], b[1]); 706 | } 707 | } 708 | } 709 | } 710 | } 711 | ``` 712 | 713 | -------------------------------------------------------------------------------- /src/docs/data-structure/LinkedList.md: -------------------------------------------------------------------------------- 1 | # 单向链表 2 | 3 | ## 链表和数组 4 | 5 | 链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。 6 | 7 | ### 数组 8 | 9 | - 存储多个元素,数组(或列表)可能是最常用的数据结构。 10 | 11 | - 几乎每一种编程语言都有默认实现数组结构,提供了一个便利的 `[]` 语法来访问数组元素。 12 | 13 | - 数组缺点: 14 | 15 | 数组的创建需要申请一段连续的内存空间 (一整块内存),并且大小是固定的,当前数组不能满足容量需求时,需要扩容。 (一般情况下是申请一个更大的数组,比如 2 倍,然后将原数组中的元素复制过去) 16 | 17 | 在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。 18 | 19 | ### 链表 20 | 21 | - 存储多个元素,另外一个选择就是使用链表。 22 | 23 | - 不同于数组,链表中的元素在内存中不必是连续的空间。 24 | 25 | - 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用 (有些语言称为指针) 组成。 26 | 27 | - 链表优点: 28 | 29 | 内存空间不必是连续的,可以充分利用计算机的内存,实现灵活的内存动态管理。 30 | 31 | 链表不必在创建时就确定大小,并且大小可以无限延伸下去。 32 | 33 | 链表在插入和删除数据时,时间复杂度可以达到 **O(1)**,相对数组效率高很多。 34 | 35 | - 链表缺点: 36 | 37 | 访问任何一个位置的元素时,需要从头开始访问。(无法跳过第一个元素访问任何一个元素) 38 | 39 | 无法通过下标值直接访问元素,需要从头开始一个个访问,直到找到对应的元素。 40 | 41 | 虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。 42 | 43 | ## 单向链表 44 | 45 | 单向链表类似于火车,有一个火车头,火车头会连接一个节点,节点上有乘客,并且这个节点会连接下一个节点,以此类推。 46 | 47 | - 链表的火车结构 48 | 49 | ![img](../public/images/data-structure/img-05.png) 50 | 51 | - 链表的数据结构 52 | 53 | head 属性指向链表的第一个节点。 54 | 链表中的最后一个节点指向 `null`。 55 | 当链表中一个节点也没有的时候,head 直接指向 `null`。 56 | 57 | ![img.png](../public/images/data-structure/img-06.png) 58 | 59 | - 给火车加上数据后的结构 60 | 61 | ![img.png](../public/images/data-structure/img-07.png) 62 | 63 | ### 链表中的常见操作 64 | 65 | - `append(element)` 向链表尾部添加一个新的项。 66 | - `insert(position, element)` 向链表的特定位置插入一个新的项。 67 | - `get(position)` 获取对应位置的元素。 68 | - `indexOf(element)` 返回元素在链表中的索引。如果链表中没有该元素就返回 -1。 69 | - `update(position, element)` 修改某个位置的元素。 70 | - `removeAt(position)` 从链表的特定位置移除一项。 71 | - `remove(element)` 从链表中移除一项。 72 | - `isEmpty()` 如果链表中不包含任何元素,返回 `trun`,如果链表长度大于 0 则返回 `false`。 73 | - `size()` 返回链表包含的元素个数,与数组的 length 属性类似。 74 | - `toString()` 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。 75 | 76 | ### 单向链表的封装 77 | 78 | #### 创建单向链表类 79 | 80 | 先创建单向链表类 LinkedList,添加基本属性,再逐步实现单向链表的常用方法。 81 | 82 | ```js 83 | class LinkedList { 84 | // 初始链表长度为 0 85 | length = 0; 86 | 87 | // 初始 head 为 null,head 指向链表的第一个节点 88 | head = null; 89 | 90 | // 内部类(链表里的节点 Node) 91 | Node = class { 92 | data; 93 | next = null; 94 | constructor(data) { 95 | this.data = data; 96 | } 97 | }; 98 | } 99 | ``` 100 | 101 | #### 实现 append() 方法 102 | 103 | 代码实现: 104 | 105 | ```js 106 | // append() 往链表尾部追加数据 107 | append(data) { 108 | 109 | // 1、创建新节点 110 | const newNode = new this.Node(data); 111 | 112 | // 2、追加新节点 113 | if (this.length === 0) { 114 | 115 | // 链表长度为 0 时,即只有 head 的时候 116 | this.head = newNode; 117 | 118 | } else { 119 | // 链表长度大于 0 时,在最后面添加新节点 120 | let currentNode = this.head; 121 | 122 | // 当 currentNode.next 不为空时, 123 | // 循序依次找最后一个节点,即节点的 next 为 null 时 124 | while (currentNode.next !== null) { 125 | currentNode = currentNode.next; 126 | } 127 | 128 | // 最后一个节点的 next 指向新节点 129 | currentNode.next = newNode; 130 | } 131 | 132 | // 3、追加完新节点后,链表长度 + 1 133 | this.length++; 134 | } 135 | ``` 136 | 137 | 过程图解: 138 | 139 | - 首先让 `currentNode` 指向第一个节点。 140 | 141 | ![img.png](../public/images/data-structure/img-08.png) 142 | 143 | - 通过 `while` 循环使 `currentNode` 指向最后一个节点,最后通过 `currentNode.next = newNode`,让最后一个节点指向新节点 `newNode`。 144 | 145 | ![img.png](../public/images/data-structure/img-09.png) 146 | 147 | 代码测试: 148 | 149 | ```js 150 | const linkedList = new LinkedList(); 151 | // 测试 append 方法 152 | linkedList.append("A"); 153 | linkedList.append("B"); 154 | linkedList.append("C"); 155 | console.log(linkedList); 156 | ``` 157 | 158 | ![img.png](../public/images/data-structure/img-10.png) 159 | 160 | #### 实现 toString() 方法 161 | 162 | 代码实现: 163 | 164 | ```js 165 | toString() { 166 | let currentNode = this.head; 167 | let result = ''; 168 | 169 | // 遍历所有的节点,拼接为字符串,直到节点为 null 170 | while (currentNode) { 171 | result += currentNode.data + ' '; 172 | currentNode = currentNode.next; 173 | } 174 | 175 | return result; 176 | } 177 | ``` 178 | 179 | 代码测试: 180 | 181 | ```js 182 | // 测试 toString 方法 183 | console.log(linkedList.toString()); //--> AA BB CC 184 | ``` 185 | 186 | #### 实现 insert() 方法 187 | 188 | 代码实现: 189 | 190 | ```js 191 | // insert() 在指定位置(position)插入节点 192 | insert(position, data) { 193 | // position 新插入节点的位置 194 | // position = 0 表示新插入后是第一个节点 195 | // position = 1 表示新插入后是第二个节点,以此类推 196 | 197 | // 1、对 position 进行越界判断,不能小于 0 或大于链表长度 198 | if (position < 0 || position > this.length) return false; 199 | 200 | // 2、创建新节点 201 | const newNode = new this.Node(data); 202 | 203 | // 3、插入节点 204 | if (position === 0) { // position = 0 的情况 205 | // 让新节点的 next 指向 原来的第一个节点,即 head 206 | newNode.next = this.head; 207 | 208 | // head 赋值为 newNode 209 | this.head = newNode; 210 | } else { // 0 < position <= length 的情况 211 | 212 | // 初始化一些变量 213 | let currentNode = this.head; // 当前节点初始化为 head 214 | let previousNode = null; // head 的 上一节点为 null 215 | let index = 0; // head 的 index 为 0 216 | 217 | // 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode 218 | // 直到找到要插入的位置 219 | while (index++ < position) { 220 | previousNode = currentNode; 221 | currentNode = currentNode.next; 222 | } 223 | 224 | // 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向 225 | newNode.next = currentNode; 226 | previousNode.next = newNode; 227 | } 228 | 229 | // 更新链表长度 230 | this.length++; 231 | return newNode; 232 | } 233 | ``` 234 | 235 | 代码测试: 236 | 237 | ```js 238 | // 测试 insert 方法 239 | linkedList.insert(0, "123"); 240 | linkedList.insert(2, "456"); 241 | console.log(linkedList.toString()); //--> 123 AA 456 BB CC 242 | ``` 243 | 244 | #### 实现 getData() 方法 245 | 246 | 获取指定位置(position)的 data。 247 | 248 | 代码实现: 249 | 250 | ```js 251 | getData(position) { 252 | // 1、position 越界判断 253 | if (position < 0 || position >= this.length) return null; 254 | 255 | // 2、获取指定 position 节点的 data 256 | let currentNode = this.head; 257 | let index = 0; 258 | 259 | while (index++ < position) { 260 | currentNode = currentNode.next; 261 | } 262 | // 3、返回 data 263 | return currentNode.data; 264 | } 265 | ``` 266 | 267 | 代码测试: 268 | 269 | ```js 270 | // 测试 getData 方法 271 | console.log(linkedList.getData(0)); //--> 123 272 | console.log(linkedList.getData(1)); //--> AA 273 | ``` 274 | 275 | #### 实现 indexOf() 方法 276 | 277 | indexOf(data) 返回指定 data 的 index,如果没有,返回 -1。 278 | 279 | 代码实现: 280 | 281 | ```js 282 | indexOf(data) { 283 | 284 | let currentNode = this.head; 285 | let index = 0; 286 | 287 | while (currentNode) { 288 | if (currentNode.data === data) { 289 | return index; 290 | } 291 | currentNode = currentNode.next; 292 | index++; 293 | } 294 | 295 | return -1; 296 | } 297 | ``` 298 | 299 | 代码测试: 300 | 301 | ```js 302 | // 测试 indexOf 方法 303 | console.log(linkedList.indexOf("AA")); //--> 1 304 | console.log(linkedList.indexOf("ABC")); //--> -1 305 | ``` 306 | 307 | #### 实现 update() 方法 308 | 309 | update(position, data) 修改指定位置节点的 data。 310 | 311 | 代码实现: 312 | 313 | ```js 314 | update(position, data) { 315 | // 涉及到 position 都要进行越界判断 316 | // 1、position 越界判断 317 | if (position < 0 || position >= this.length) return false; 318 | 319 | // 2、痛过循环遍历,找到指定 position 的节点 320 | let currentNode = this.head; 321 | let index = 0; 322 | while (index++ < position) { 323 | currentNode = currentNode.next; 324 | } 325 | 326 | // 3、修改节点 data 327 | currentNode.data = data; 328 | 329 | return currentNode; 330 | } 331 | ``` 332 | 333 | 代码测试: 334 | 335 | ```js 336 | // 测试 update 方法 337 | linkedList.update(0, "12345"); 338 | console.log(linkedList.toString()); //--> 12345 AA 456 BB CC 339 | linkedList.update(1, "54321"); 340 | console.log(linkedList.toString()); //--> 12345 54321 456 BB CC 341 | ``` 342 | 343 | #### 实现 removeAt() 方法 344 | 345 | removeAt(position) 删除指定位置的节点。 346 | 347 | 代码实现: 348 | 349 | ```js 350 | removeAt(position) { 351 | // 1、position 越界判断 352 | if (position < 0 || position >= this.length) return null; 353 | 354 | // 2、删除指定 position 节点 355 | let currentNode = this.head; 356 | if (position === 0) { 357 | // position = 0 的情况 358 | this.head = this.head.next; 359 | 360 | } else { 361 | // position > 0 的情况 362 | // 通过循环遍历,找到指定 position 的节点,赋值到 currentNode 363 | 364 | let previousNode = null; 365 | let index = 0; 366 | 367 | while (index++ < position) { 368 | previousNode = currentNode; 369 | currentNode = currentNode.next; 370 | } 371 | 372 | // 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。 373 | previousNode.next = currentNode.next; 374 | } 375 | 376 | // 3、更新链表长度 -1 377 | this.length--; 378 | 379 | return currentNode; 380 | } 381 | ``` 382 | 383 | 代码测试: 384 | 385 | ```js 386 | // 测试 removeAt 方法 387 | linkedList.removeAt(3); 388 | console.log(linkedList.toString()); //--> 12345 54321 456 CC 389 | ``` 390 | 391 | #### 实现 remove() 方法 392 | 393 | remove(data) 删除指定 data 所在的节点。 394 | 395 | 代码实现: 396 | 397 | ```js 398 | remove(data) { 399 | this.removeAt(this.indexOf(data)); 400 | } 401 | ``` 402 | 403 | 代码测试: 404 | 405 | ```js 406 | // 测试 remove 方法 407 | linkedList.remove("CC"); 408 | console.log(linkedList.toString()); //--> 12345 54321 456 409 | ``` 410 | 411 | #### 实现 isEmpty() 方法 412 | 413 | isEmpty() 判断链表是否为空。 414 | 415 | 代码实现: 416 | 417 | ```js 418 | isEmpty() { 419 | return this.length === 0; 420 | } 421 | ``` 422 | 423 | 代码测试: 424 | 425 | ```js 426 | // 测试 isEmpty 方法 427 | console.log(linkedList.isEmpty()); //--> false 428 | ``` 429 | 430 | #### 实现 size() 方法 431 | 432 | size() 获取链表的长度。 433 | 434 | 代码实现: 435 | 436 | ```js 437 | size() { 438 | return this.length; 439 | } 440 | ``` 441 | 442 | 代码测试: 443 | 444 | ```js 445 | // 测试 size 方法 446 | console.log(linkedList.size()); //--> 3 447 | ``` 448 | 449 | #### 完整实现 450 | 451 | ```js 452 | class LinkedList { 453 | // 初始链表长度为 0 454 | length = 0; 455 | 456 | // 初始 head 为 null,head 指向链表的第一个节点 457 | head = null; 458 | 459 | // 内部类(链表里的节点 Node) 460 | Node = class { 461 | data; 462 | next = null; 463 | 464 | constructor(data) { 465 | this.data = data; 466 | } 467 | }; 468 | 469 | // ------------ 链表的常见操作 ------------ // 470 | 471 | // append() 往链表尾部追加数据 472 | append(data) { 473 | // 1、创建新节点 474 | const newNode = new this.Node(data); 475 | 476 | // 2、追加新节点 477 | if (this.length === 0) { 478 | // 链表长度为 0 时,即只有 head 的时候 479 | this.head = newNode; 480 | } else { 481 | // 链表长度大于 0 时,在最后面添加新节点 482 | let currentNode = this.head; 483 | 484 | // 当 currentNode.next 不为空时, 485 | // 循序依次找最后一个节点,即节点的 next 为 null 时 486 | while (currentNode.next !== null) { 487 | currentNode = currentNode.next; 488 | } 489 | 490 | // 最后一个节点的 next 指向新节点 491 | currentNode.next = newNode; 492 | } 493 | 494 | // 3、追加完新节点后,链表长度 + 1 495 | this.length++; 496 | } 497 | 498 | // insert() 在指定位置(position)插入节点 499 | insert(position, data) { 500 | // position 新插入节点的位置 501 | // position = 0 表示新插入后是第一个节点 502 | // position = 1 表示新插入后是第二个节点,以此类推 503 | 504 | // 1、对 position 进行越界判断,不能小于 0 或大于链表长度 505 | if (position < 0 || position > this.length) return false; 506 | 507 | // 2、创建新节点 508 | const newNode = new this.Node(data); 509 | 510 | // 3、插入节点 511 | if (position === 0) { 512 | // position = 0 的情况 513 | // 让新节点的 next 指向 原来的第一个节点,即 head 514 | newNode.next = this.head; 515 | 516 | // head 赋值为 newNode 517 | this.head = newNode; 518 | } else { 519 | // 0 < position <= length 的情况 520 | 521 | // 初始化一些变量 522 | let currentNode = this.head; // 当前节点初始化为 head 523 | let previousNode = null; // head 的 上一节点为 null 524 | let index = 0; // head 的 index 为 0 525 | 526 | // 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode 527 | // 直到找到要插入的位置 528 | while (index++ < position) { 529 | previousNode = currentNode; 530 | currentNode = currentNode.next; 531 | } 532 | 533 | // 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向 534 | newNode.next = currentNode; 535 | previousNode.next = newNode; 536 | } 537 | 538 | // 更新链表长度 539 | this.length++; 540 | return newNode; 541 | } 542 | 543 | // getData() 获取指定位置的 data 544 | getData(position) { 545 | // 1、position 越界判断 546 | if (position < 0 || position >= this.length) return null; 547 | 548 | // 2、获取指定 position 节点的 data 549 | let currentNode = this.head; 550 | let index = 0; 551 | 552 | while (index++ < position) { 553 | currentNode = currentNode.next; 554 | } 555 | 556 | // 3、返回 data 557 | return currentNode.data; 558 | } 559 | 560 | // indexOf() 返回指定 data 的 index,如果没有,返回 -1。 561 | indexOf(data) { 562 | let currentNode = this.head; 563 | let index = 0; 564 | 565 | while (currentNode) { 566 | if (currentNode.data === data) { 567 | return index; 568 | } 569 | currentNode = currentNode.next; 570 | index++; 571 | } 572 | 573 | return -1; 574 | } 575 | 576 | // update() 修改指定位置节点的 data 577 | update(position, data) { 578 | // 涉及到 position 都要进行越界判断 579 | // 1、position 越界判断 580 | if (position < 0 || position >= this.length) return false; 581 | 582 | // 2、痛过循环遍历,找到指定 position 的节点 583 | let currentNode = this.head; 584 | let index = 0; 585 | while (index++ < position) { 586 | currentNode = currentNode.next; 587 | } 588 | 589 | // 3、修改节点 data 590 | currentNode.data = data; 591 | 592 | return currentNode; 593 | } 594 | 595 | // removeAt() 删除指定位置的节点 596 | removeAt(position) { 597 | // 1、position 越界判断 598 | if (position < 0 || position >= this.length) return null; 599 | 600 | // 2、删除指定 position 节点 601 | let currentNode = this.head; 602 | if (position === 0) { 603 | // position = 0 的情况 604 | this.head = this.head.next; 605 | } else { 606 | // position > 0 的情况 607 | // 通过循环遍历,找到指定 position 的节点,赋值到 currentNode 608 | 609 | let previousNode = null; 610 | let index = 0; 611 | 612 | while (index++ < position) { 613 | previousNode = currentNode; 614 | currentNode = currentNode.next; 615 | } 616 | 617 | // 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。 618 | previousNode.next = currentNode.next; 619 | } 620 | 621 | // 3、更新链表长度 -1 622 | this.length--; 623 | 624 | return currentNode; 625 | } 626 | 627 | // remove() 删除指定 data 的节点 628 | remove(data) { 629 | this.removeAt(this.indexOf(data)); 630 | } 631 | 632 | // isEmpty() 判断链表是否为空 633 | isEmpty() { 634 | return this.length === 0; 635 | } 636 | 637 | // size() 获取链表的长度 638 | size() { 639 | return this.length; 640 | } 641 | 642 | // toString() 链表数据以字符串形式返回 643 | toString() { 644 | let currentNode = this.head; 645 | let result = ""; 646 | 647 | // 遍历所有的节点,拼接为字符串,直到节点为 null 648 | while (currentNode) { 649 | result += currentNode.data + " "; 650 | currentNode = currentNode.next; 651 | } 652 | 653 | return result; 654 | } 655 | } 656 | ``` 657 | 658 | -------------------------------------------------------------------------------- /src/docs/data-structure/Map.md: -------------------------------------------------------------------------------- 1 | # 字典 2 | 3 | ## 字典特点 4 | 5 | - 字典存储的是**键值对**,主要特点是**一一对应**。 6 | - 比如保存一个人的信息 7 | - 数组形式:`[19,"Tom", 1.65]`,可通过下标值取出信息。 8 | - 字典形式:`{"age": 19, "name": "Tom", "height": 165}`,可以通过 `key` 取出 `value`。 9 | - 此外,在字典中 key 是不能重复且无序的,而 Value 可以重复。 10 | 11 | ## 字典和映射的关系 12 | 13 | - 有些编程语言中称这种映射关系为**字典**,如 Swift 中的 `Dictonary`,Python 中的 `dict`。 14 | - 有些编程语言中称这种映射关系为 **Map**,比如 Java 中的 `HashMap` 和 `TreeMap` 等。 15 | 16 | ## 字典常见的操作 17 | 18 | - `set(key,value)` 向字典中添加新元素。 19 | - `remove(key)` 通过使用键值来从字典中移除键值对应的数据值。 20 | - `has(key)` 如果某个键值存在于这个字典中,则返回 `true`,反之则返回 `false`。 21 | - `get(key)` 通过键值查找特定的数值并返回。 22 | - `clear()` 将这个字典中的所有元素全部删除。 23 | - `size()` 返回字典所包含元素的数量。与数组的 `length` 属性类似。 24 | - `keys()` 将字典所包含的所有键名以数组形式返回。 25 | - `values()` 将字典所包含的所有数值以数组形式返回。 26 | 27 | ## 字典封装 28 | 29 | ### 代码实现 30 | 31 | ```js 32 | // 字典结构的封装 33 | class Map { 34 | constructor() { 35 | this.items = {}; 36 | } 37 | 38 | // has(key) 判断字典中是否存在某个 key 39 | has(key) { 40 | return this.items.hasOwnProperty(key); 41 | } 42 | 43 | // set(key, value) 在字典中添加键值对 44 | set(key, value) { 45 | this.items[key] = value; 46 | } 47 | 48 | // remove(key) 在字典中删除指定的 key 49 | remove(key) { 50 | // 如果集合不存在该 key,返回 false 51 | if (!this.has(key)) return false; 52 | delete this.items[key]; 53 | } 54 | 55 | // get(key) 获取指定 key 的 value,如果没有,返回 undefined 56 | get(key) { 57 | return this.has(key) ? this.items[key] : undefined; 58 | } 59 | 60 | // 获取所有的 key 61 | keys() { 62 | return Object.keys(this.items); 63 | } 64 | 65 | // 获取所有的 value 66 | values() { 67 | return Object.values(this.items); 68 | } 69 | 70 | // size() 获取字典中的键值对个数 71 | size() { 72 | return this.keys().length; 73 | } 74 | 75 | // clear() 清空字典中所有的键值对 76 | clear() { 77 | this.items = {}; 78 | } 79 | } 80 | ``` 81 | 82 | ### 代码测试 83 | 84 | ```js 85 | const map = new Map(); 86 | 87 | // set() 测试 88 | map.set("name", "XPoet"); 89 | map.set("age", 18); 90 | map.set("email", "i@xpoet.cn"); 91 | console.log(map); // {items: {name: "XPoet", age: 18, email: "i@xpoet.cn"}} 92 | 93 | // has() 测试 94 | console.log(map.has("name")); //--> true 95 | console.log(map.has("address")); //--> false 96 | 97 | // remove() 测试 98 | map.remove("name"); 99 | console.log(map); // {age: 18, email: "i@xpoet.cn"} 100 | 101 | // get() 测试 102 | console.log(map.get("age")); //--> 18 103 | 104 | // keys() 测试 105 | console.log(map.keys()); //--> ["age", "email"] 106 | 107 | // values() 测试 108 | console.log(map.values()); //--> [18, "i@xpoet.cn"] 109 | 110 | // size() 测试 111 | console.log(map.size()); //--> 2 112 | ``` 113 | -------------------------------------------------------------------------------- /src/docs/data-structure/PriorityQueue.md: -------------------------------------------------------------------------------- 1 | # 优先队列 2 | 3 | ## 场景 4 | 5 | 生活中类似**优先队列**的场景: 6 | 7 | - 优先排队的人,优先处理。 (买票、结账、WC)。 8 | - 排队中,有紧急情况(特殊情况)的人可优先处理。 9 | 10 | ## 优先队列 11 | 12 | 优先级队列主要考虑的问题: 13 | 14 | - 每个元素不再只是一个数据,还包含优先级。 15 | - 在添加元素过程中,根据优先级放入到正确位置。 16 | 17 | ## 优先队列的实现 18 | 19 | ### 代码实现 20 | 21 | ```js 22 | // 优先队列内部的元素类 23 | class QueueElement { 24 | constructor(element, priority) { 25 | this.element = element; 26 | this.priority = priority; 27 | } 28 | } 29 | 30 | // 优先队列类(继承 Queue 类) 31 | export class PriorityQueue extends Queue { 32 | constructor() { 33 | super(); 34 | } 35 | 36 | // enqueue(element, priority) 入队,将元素按优先级加入到队列中 37 | // 重写 enqueue() 38 | enqueue(element, priority) { 39 | // 根据传入的元素,创建 QueueElement 对象 40 | const queueElement = new QueueElement(element, priority); 41 | 42 | // 判断队列是否为空 43 | if (this.isEmpty()) { 44 | // 如果为空,不用判断优先级,直接添加 45 | this.items.push(queueElement); 46 | } else { 47 | // 定义一个变量记录是否成功添加了新元素 48 | let added = false; 49 | 50 | for (let i = 0; i < this.items.length; i++) { 51 | // 让新插入的元素进行优先级比较,priority 值越小,优先级越大 52 | if (queueElement.priority < this.items[i].priority) { 53 | // 在指定的位置插入元素 54 | this.items.splice(i, 0, queueElement); 55 | added = true; 56 | break; 57 | } 58 | } 59 | 60 | // 如果遍历完所有元素,优先级都大于新插入的元素,就将新插入的元素插入到最后 61 | if (!added) { 62 | this.items.push(queueElement); 63 | } 64 | } 65 | } 66 | 67 | // dequeue() 出队,从队列中删除前端元素,返回删除的元素 68 | // 继承 Queue 类的 dequeue() 69 | dequeue() { 70 | return super.dequeue(); 71 | } 72 | 73 | // front() 查看队列的前端元素 74 | // 继承 Queue 类的 front() 75 | front() { 76 | return super.front(); 77 | } 78 | 79 | // isEmpty() 查看队列是否为空 80 | // 继承 Queue 类的 isEmpty() 81 | isEmpty() { 82 | return super.isEmpty(); 83 | } 84 | 85 | // size() 查看队列中元素的个数 86 | // 继承 Queue 类的 size() 87 | size() { 88 | return super.size(); 89 | } 90 | 91 | // toString() 将队列中元素以字符串形式返回 92 | // 重写 toString() 93 | toString() { 94 | let result = ""; 95 | for (let item of this.items) { 96 | result += item.element + "-" + item.priority + " "; 97 | } 98 | return result; 99 | } 100 | } 101 | ``` 102 | 103 | ### 测试代码 104 | 105 | ```js 106 | const priorityQueue = new PriorityQueue(); 107 | 108 | // 入队 enqueue() 测试 109 | priorityQueue.enqueue("A", 10); 110 | priorityQueue.enqueue("B", 15); 111 | priorityQueue.enqueue("C", 11); 112 | priorityQueue.enqueue("D", 20); 113 | priorityQueue.enqueue("E", 18); 114 | console.log(priorityQueue.items); 115 | //--> output: 116 | // QueueElement {element: "A", priority: 10} 117 | // QueueElement {element: "C", priority: 11} 118 | // QueueElement {element: "B", priority: 15} 119 | // QueueElement {element: "E", priority: 18} 120 | // QueueElement {element: "D", priority: 20} 121 | 122 | // 出队 dequeue() 测试 123 | priorityQueue.dequeue(); 124 | priorityQueue.dequeue(); 125 | console.log(priorityQueue.items); 126 | //--> output: 127 | // QueueElement {element: "B", priority: 15} 128 | // QueueElement {element: "E", priority: 18} 129 | // QueueElement {element: "D", priority: 20} 130 | 131 | // isEmpty() 测试 132 | console.log(priorityQueue.isEmpty()); //--> false 133 | 134 | // size() 测试 135 | console.log(priorityQueue.size()); //--> 3 136 | 137 | // toString() 测试 138 | console.log(priorityQueue.toString()); //--> B-15 E-18 D-20 139 | ``` 140 | 141 | ## 数组、栈和队列图解 142 | 143 | ![image](../public/images/data-structure/img-04.png) 144 | -------------------------------------------------------------------------------- /src/docs/data-structure/Queue.md: -------------------------------------------------------------------------------- 1 | # 队列 2 | 3 | ## 认识队列 4 | 5 | 队列(Queue)是一种运算受限的线性表,特点:先进先出 (FIFO:First In First Out)。 6 | 7 | 受限之处: 8 | 9 | - 只允许在表的前端(front)进行删除操作。 10 | - 只允许在表的后端(rear)进行插入操作。 11 | 12 | 生活中类似队列结构的场景: 13 | 14 | - 排队,比如在电影院,商场,甚至是厕所排队。 15 | - 优先排队的人,优先处理。 (买票、结账、WC)。 16 | 17 | ![image](../public/images/data-structure/img-02.png) 18 | 19 | ### 队列图解 20 | 21 | ![image](../public/images/data-structure/img-03.png) 22 | 23 | ### 队列在程序中的应用 24 | 25 | - 打印队列:计算机打印多个文件的时候,需要排队打印。 26 | - 线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待 CPU 处理。 27 | 28 | ## 队列的实现 29 | 30 | 队列的实现和栈一样,有两种方案: 31 | 32 | - 基于数组实现。 33 | - 基于链表实现。 34 | 35 | ### 队列常见的操作 36 | 37 | - `enqueue(element)` 向队列尾部添加一个(或多个)新的项。 38 | - `dequeue()` 移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。 39 | - `front()` 返回队列中的第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息与 Map 类的 peek 方法非常类似)。 40 | - `isEmpty()` 如果队列中不包含任何元素,返回 `true`,否则返回 `false`。 41 | - `size()` 返回队列包含的元素个数,与数组的 length 属性类似。 42 | - `toString()` 将队列中的内容,转成字符串形式。 43 | 44 | ### 代码实现 45 | 46 | ```js 47 | class Queue { 48 | constructor() { 49 | this.items = []; 50 | } 51 | 52 | // enqueue(item) 入队,将元素加入到队列中 53 | enqueue(item) { 54 | this.items.push(item); 55 | } 56 | 57 | // dequeue() 出队,从队列中删除队头元素,返回删除的那个元素 58 | dequeue() { 59 | return this.items.shift(); 60 | } 61 | 62 | // front() 查看队列的队头元素 63 | front() { 64 | return this.items[0]; 65 | } 66 | 67 | // isEmpty() 查看队列是否为空 68 | isEmpty() { 69 | return this.items.length === 0; 70 | } 71 | 72 | // size() 查看队列中元素的个数 73 | size() { 74 | return this.items.length; 75 | } 76 | 77 | // toString() 将队列中的元素以字符串形式返回 78 | toString() { 79 | let result = ""; 80 | for (let item of this.items) { 81 | result += item + " "; 82 | } 83 | return result; 84 | } 85 | } 86 | ``` 87 | 88 | ### 测试代码 89 | 90 | ```js 91 | const queue = new Queue(); 92 | 93 | // enqueue() 测试 94 | queue.enqueue("a"); 95 | queue.enqueue("b"); 96 | queue.enqueue("c"); 97 | queue.enqueue("d"); 98 | console.log(queue.items); //--> ["a", "b", "c", "d"] 99 | 100 | // dequeue() 测试 101 | queue.dequeue(); 102 | queue.dequeue(); 103 | console.log(queue.items); //--> ["c", "d"] 104 | 105 | // front() 测试 106 | console.log(queue.front()); //--> c 107 | 108 | // isEmpty() 测试 109 | console.log(queue.isEmpty()); //--> false 110 | 111 | // size() 测试 112 | console.log(queue.size()); //--> 2 113 | 114 | // toString() 测试 115 | console.log(queue.toString()); //--> c d 116 | ``` 117 | 118 | ## 队列的应用 119 | 120 | 使用队列实现小游戏:**击鼓传花**。 121 | 122 | 分析:传入一组数据集合和设定的数字 number,循环遍历数组内元素,遍历到的元素为指定数字 number 时将该元素删除,直至数组剩下一个元素。 123 | 124 | ### 代码实现 125 | 126 | ```js 127 | // 利用队列结构的特点实现击鼓传花游戏求解方法的封装 128 | function passGame(nameList, number) { 129 | // 1、new 一个 Queue 对象 130 | const queue = new Queue(); 131 | 132 | // 2、将 nameList 里面的每一个元素入队 133 | for (const name of nameList) { 134 | queue.enqueue(name); 135 | } 136 | 137 | // 3、开始数数 138 | // 队列中只剩下 1 个元素时就停止数数 139 | while (queue.size() > 1) { 140 | // 不是 number 时,重新加入到队尾 141 | // 是 number 时,将其删除 142 | 143 | for (let i = 0; i < number - 1; i++) { 144 | // number 数字之前的人重新放入到队尾(即把队头删除的元素,重新加入到队列中) 145 | queue.enqueue(queue.dequeue()); 146 | } 147 | 148 | // number 对应这个人,直接从队列中删除 149 | // 由于队列没有像数组一样的下标值不能直接取到某一元素, 150 | // 所以采用,把 number 前面的 number - 1 个元素先删除后添加到队列末尾, 151 | // 这样第 number 个元素就排到了队列的最前面,可以直接使用 dequeue 方法进行删除 152 | queue.dequeue(); 153 | } 154 | 155 | // 4、获取最后剩下的那个人 156 | const endName = queue.front(); 157 | 158 | // 5、返回这个人在原数组中对应的索引 159 | return nameList.indexOf(endName); 160 | } 161 | ``` 162 | 163 | ### 测试代码 164 | 165 | ```js 166 | // passGame() 测试 167 | const names = ["lily", "lucy", "tom", "tony", "jack"]; 168 | const targetIndex = passGame(names, 4); 169 | console.log("击鼓传花", names[targetIndex]); //--> lily 170 | ``` 171 | -------------------------------------------------------------------------------- /src/docs/data-structure/Set.md: -------------------------------------------------------------------------------- 1 | # 集合 2 | 3 | 几乎每种编程语言中,都有集合结构。集合比较常见的实现方式是哈希表,这里使用 JavaScript 的 Object 进行封装。 4 | 5 | ## 集合特点 6 | 7 | - 集合通常是由一组**无序的**、**不能重复的**元素构成。 8 | 9 | - 数学中常指的集合中的元素是可以重复的,但是计算机中集合的元素不能重复。 10 | 11 | - 集合是特殊的数组。 12 | - 特殊之处在于里面的元素没有顺序,也不能重复。 13 | - 没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只会存在一份。 14 | 15 | ## 封装集合 16 | 17 | ES6 中的 `Set` 就是一个集合类,这里我们重新封装一个 `Set` 类,了解集合的底层实现。 18 | 19 | ### 集合常见的操作 20 | 21 | - `add(value)` 向集合添加一个新的项。 22 | - `remove(value)` 从集合移除一个值。 23 | - `has(value)` 如果值在集合中,返回 `true`,否则返回` false`。 24 | - `clear()` 移除集合中的所有项。 25 | - `size()` 返回集合所包含元素的数量。与数组的 `length` 属性类似。 26 | - `values()` 返回一个包含集合中所有值的数组。 27 | - 还有其他的方法,用的不多,这里不做封装。 28 | 29 | ### 代码实现 30 | 31 | ```js 32 | // 集合结构的封装 33 | class Set { 34 | constructor() { 35 | this.items = {}; 36 | } 37 | 38 | // has(value) 判断集合中是否存在 value 值,存在返回 true,否则返回 false 39 | has(value) { 40 | return this.items.hasOwnProperty(value); 41 | } 42 | 43 | // add(value) 往集合中添加 value 44 | add(value) { 45 | if (this.has(value)) return false; 46 | this.items[value] = value; 47 | return true; 48 | } 49 | 50 | // remove(value) 删除集合中指定的 value 51 | remove(value) { 52 | // 如果集合不存在该 value,返回 false 53 | if (!this.has(value)) return false; 54 | delete this.items[value]; 55 | } 56 | 57 | // clear() 清空集合中所有 value 58 | clear() { 59 | this.items = {}; 60 | } 61 | 62 | // size() 获取集合中的 value 个数 63 | size() { 64 | return Object.keys(this.items).length; 65 | } 66 | 67 | // values() 获取集合中所有的 value 68 | values() { 69 | return Object.keys(this.items); 70 | } 71 | } 72 | ``` 73 | 74 | ### 代码测试 75 | 76 | ```js 77 | const set = new Set(); 78 | 79 | // add() 测试 80 | set.add("abc"); 81 | set.add("abc"); 82 | set.add("123"); 83 | set.add("zxc"); 84 | console.log(set); //--> {items: {123: "123", abc: "abc", zxc: "zxc"}} 85 | 86 | // has() 测试 87 | console.log(set.has("123")); //--> true 88 | console.log(set.has("456")); //--> false 89 | 90 | // remove() 测试 91 | set.remove("abc"); 92 | console.log(set); //--> {items: {123: "123", zxc: "zxc"}} 93 | 94 | // size() 测试 95 | console.log(set.size()); //--> 2 96 | 97 | // values() 测试 98 | console.log(set.values()); //--> ["123", "zxc"] 99 | 100 | // clear() 测试 101 | set.clear(); 102 | console.log(set.values()); //--> [] 103 | ``` 104 | 105 | ## 集合间的操作 106 | 107 | - 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。 108 | - 交集:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合。 109 | - 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。 110 | - 子集:验证一个给定集合是否是另一个集合的子集。 111 | 112 | ![img](../public/images/data-structure/img-12.png) 113 | 114 | ### 并集的实现 115 | 116 | ```js 117 | // union() 求两个集合的并集 118 | union(otherSet) { 119 | // 1、创建一个新集合 120 | let unionSet = new Set(); 121 | 122 | // 2、将当前集合(this)的所有 value,添加到新集合(unionSet)中 123 | for (let value of this.values()) { 124 | unionSet.add(value); 125 | } 126 | 127 | // 3、将 otherSet 集合的所有 value,添加到新集合(unionSet)中 128 | for (let value of otherSet.values()) { 129 | unionSet.add(value); // add() 已经有重复判断 130 | } 131 | 132 | return unionSet; 133 | } 134 | ``` 135 | 136 | ### 交集的实现 137 | 138 | ```js 139 | // intersection() 求两个集合的交集 140 | intersection(otherSet) { 141 | 142 | // 1、创建一个新集合 143 | let intersectionSet = new Set(); 144 | 145 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在 146 | for (let value of this.values()) { 147 | if (otherSet.has(value)) { 148 | intersectionSet.add(value); 149 | } 150 | } 151 | 152 | return intersectionSet; 153 | } 154 | ``` 155 | 156 | ### 差集的实现 157 | 158 | ```js 159 | // difference() 差集 160 | difference(otherSet) { 161 | 162 | // 1、创建一个新集合 163 | let differenceSet = new Set(); 164 | 165 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,不存在的即为差集 166 | for (let value of this.values()) { 167 | if (!otherSet.has(value)) { 168 | differenceSet.add(value); 169 | } 170 | } 171 | 172 | return differenceSet; 173 | } 174 | ``` 175 | 176 | ### 子集的实现 177 | 178 | ```js 179 | // subset() 子集 180 | subset(otherSet) { 181 | 182 | // 从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,有不存在的返回 false 183 | // 遍历完所有的,返回 true 184 | for (let value of this.values()) { 185 | if (!otherSet.has(value)) { 186 | return false; 187 | } 188 | } 189 | return true; 190 | } 191 | ``` 192 | 193 | ## 集合的完整实现 194 | 195 | ```js 196 | // 集合结构的封装 197 | class Set { 198 | constructor() { 199 | this.items = {}; 200 | } 201 | 202 | // has(value) 判断集合中是否存在 value 值,存在返回 true,否则返回 false 203 | has(value) { 204 | return this.items.hasOwnProperty(value); 205 | } 206 | 207 | // add(value) 往集合中添加 value 208 | add(value) { 209 | if (this.has(value)) return false; 210 | this.items[value] = value; 211 | return true; 212 | } 213 | 214 | // remove(value) 删除集合中指定的 value 215 | remove(value) { 216 | // 如果集合不存在该 value,返回 false 217 | if (!this.has(value)) return false; 218 | delete this.items[value]; 219 | } 220 | 221 | // clear() 清空集合中所有 value 222 | clear() { 223 | this.items = {}; 224 | } 225 | 226 | // size() 获取集合中的 value 个数 227 | size() { 228 | return Object.keys(this.items).length; 229 | } 230 | 231 | // values() 获取集合中所有的 value 232 | values() { 233 | return Object.keys(this.items); 234 | } 235 | 236 | // ------- 集合间的操作 ------- // 237 | // union() 求两个集合的并集 238 | union(otherSet) { 239 | // 1、创建一个新集合 240 | let unionSet = new Set(); 241 | 242 | // 2、将当前集合(this)的所有 value,添加到新集合(unionSet)中 243 | for (let value of this.values()) { 244 | unionSet.add(value); 245 | } 246 | 247 | // 3、将 otherSet 集合的所有 value,添加到新集合(unionSet)中 248 | for (let value of otherSet.values()) { 249 | unionSet.add(value); // add() 已经有重复判断 250 | } 251 | 252 | return unionSet; 253 | } 254 | 255 | // intersection() 求两个集合的交集 256 | intersection(otherSet) { 257 | // 1、创建一个新集合 258 | let intersectionSet = new Set(); 259 | 260 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在 261 | for (let value of this.values()) { 262 | if (otherSet.has(value)) { 263 | intersectionSet.add(value); 264 | } 265 | } 266 | 267 | return intersectionSet; 268 | } 269 | 270 | // difference() 差集 271 | difference(otherSet) { 272 | // 1、创建一个新集合 273 | let differenceSet = new Set(); 274 | 275 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,不存在的即为差集 276 | for (let value of this.values()) { 277 | if (!otherSet.has(value)) { 278 | differenceSet.add(value); 279 | } 280 | } 281 | 282 | return differenceSet; 283 | } 284 | 285 | // subset() 子集 286 | subset(otherSet) { 287 | // 从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,有不存在的返回 false 288 | // 遍历完所有的,返回 true 289 | for (let value of this.values()) { 290 | if (!otherSet.has(value)) { 291 | return false; 292 | } 293 | } 294 | return true; 295 | } 296 | } 297 | ``` 298 | -------------------------------------------------------------------------------- /src/docs/data-structure/Stack.md: -------------------------------------------------------------------------------- 1 | # 栈 2 | 3 | 数组是一个线性结构,可以在数组的任意位置插入和删除元素。但是有时候,我们为了实现某些功能,必须对这种任意性加以限制。栈和队列就是比较常见的受限的线性结构。 4 | 5 | ## 什么是栈 6 | 7 | 栈(stack)是一种运算受限的线性表。 8 | 9 | - `LIFO(last in first out)`表示就是后进入的元素,第一个弹出栈空间。 10 | > 类似于自动餐托盘,最后放上的托盘,往往先被拿出去使用。 11 | - 仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。 12 | - 向一个栈插入新元素又称作**进栈**、**入栈**或**压栈**,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素; 13 | - 从一个栈删除元素又称作**出栈**或**退栈**,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 14 | 15 | ![image](../public/images/data-structure/img-01.png) 16 | 17 | 栈的特点:**先进后出,后进先出**。 18 | 19 | ## 程序中的栈结构 20 | 21 | - 函数调用栈:A(B(C(D()))): 22 | 即 A 函数中调用 B,B 调用 C,C 调用 D;在 A 执行的过程中会将 A 压入栈,随后 B 执行时 B 也被压入栈,函数 C 和 D 执行时也会被压入栈。所以当前栈的顺序为:A->B->C->D(栈顶);函数 D 执行完之后,会弹出栈被释放,弹出栈的顺序为 D->C->B->A; 23 | 24 | - 递归: 25 | 为什么没有停止条件的递归会造成栈溢出?比如函数 A 为递归函数,不断地调用自己(因为函数还没有执行完,不会把函数弹出栈),不停地把相同的函数 A 压入栈,最后造成栈溢出(stack overflow)。 26 | 27 | ## 练习 28 | 29 | 题目:有 6 个元素 6,5,4,3,2,1 按顺序进栈,问下列哪一个不是合法的出栈顺序? 30 | 31 | - A: 5 4 3 6 1 2 (√) 32 | - B: 4 5 3 2 1 6 (√) 33 | - C: 3 4 6 5 2 1 (×) 34 | - D: 2 3 4 1 5 6 (√) 35 | 36 | 题目所说的按顺序进栈指的不是一次性全部进栈,而是有进有出,进栈顺序为 6 -> 5 -> 4 -> 3 -> 2 -> 1。 37 | 38 | 解析: 39 | 40 | - A 答案:65 进栈,5 出栈,4 进栈出栈,3 进栈出栈,6 出栈,21 进栈,1 出栈,2 出栈(整体入栈顺序符合 654321)。 41 | - B 答案:654 进栈,4 出栈,5 出栈,3 进栈出栈,2 进栈出栈,1 进栈出栈,6 出栈(整体的入栈顺序符合 654321)。 42 | - C 答案:6543 进栈,3 出栈,4 出栈,之后应该 5 出栈而不是 6,所以错误。 43 | - D 答案:65432 进栈,2 出栈,3 出栈,4 出栈,1 进栈出栈,5 出栈,6 出栈。符合入栈顺序。 44 | 45 | ## 栈结构实现 46 | 47 | ### 栈常见的操作 48 | 49 | - `push()` 添加一个新元素到栈顶位置。 50 | - `pop()` 移除栈顶的元素,同时返回被移除的元素。 51 | - `peek()` 返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它)。 52 | - `isEmpty()` 如果栈里没有任何元素就返回 `true`,否则返回 `false`。 53 | - `size()` 返回栈里的元素个数。这个方法和数组的 `length` 属性类似。 54 | - `toString()` 将栈结构的内容以字符串的形式返回。 55 | 56 | ### JavaScript 代码实现栈结构 57 | 58 | ```js 59 | // 栈结构的封装 60 | class Stack { 61 | constructor() { 62 | this.items = []; 63 | } 64 | 65 | // push(item) 压栈操作,往栈里面添加元素 66 | push(item) { 67 | this.items.push(item); 68 | } 69 | 70 | // pop() 出栈操作,从栈中取出元素,并返回取出的那个元素 71 | pop() { 72 | return this.items.pop(); 73 | } 74 | 75 | // peek() 查看栈顶元素 76 | peek() { 77 | return this.items[this.items.length - 1]; 78 | } 79 | 80 | // isEmpty() 判断栈是否为空 81 | isEmpty() { 82 | return this.items.length === 0; 83 | } 84 | 85 | // size() 获取栈中元素个数 86 | size() { 87 | return this.items.length; 88 | } 89 | 90 | // toString() 返回以字符串形式的栈内元素数据 91 | toString() { 92 | let result = ""; 93 | for (let item of this.items) { 94 | result += item + " "; 95 | } 96 | return result; 97 | } 98 | } 99 | ``` 100 | 101 | ### 测试封装的栈结构 102 | 103 | ```js 104 | // push() 测试 105 | stack.push(1); 106 | stack.push(2); 107 | stack.push(3); 108 | console.log(stack.items); //--> [1, 2, 3] 109 | 110 | // pop() 测试 111 | console.log(stack.pop()); //--> 3 112 | 113 | // peek() 测试 114 | console.log(stack.peek()); //--> 2 115 | 116 | // isEmpty() 测试 117 | console.log(stack.isEmpty()); //--> false 118 | 119 | // size() 测试 120 | console.log(stack.size()); //--> 2 121 | 122 | // toString() 测试 123 | console.log(stack.toString()); //--> 1 2 124 | ``` 125 | 126 | ## 栈结构的简单应用 127 | 128 | 利用栈结构的特点封装实现十进制转换为二进制的方法。 129 | 130 | ### 代码实现 131 | 132 | ```js 133 | function dec2bin(dec) { 134 | // new 一个 Stack,保存余数 135 | const stack = new Stack(); 136 | 137 | // 当不确定循环次数时,使用 while 循环 138 | while (dec > 0) { 139 | // 除二取余法 140 | stack.push(dec % 2); // 获取余数,放入栈中 141 | dec = Math.floor(dec / 2); // 除数除以二,向下取整 142 | } 143 | 144 | let binaryString = ""; 145 | // 不断地从栈中取出元素(0 或 1),并拼接到一起。 146 | while (!stack.isEmpty()) { 147 | binaryString += stack.pop(); 148 | } 149 | 150 | return binaryString; 151 | } 152 | ``` 153 | 154 | ### 测试 155 | 156 | ```js 157 | // dec2bin() 测试 158 | console.log(dec2bin(100)); //--> 1100100 159 | console.log(dec2bin(88)); //--> 1011000 160 | ``` 161 | -------------------------------------------------------------------------------- /src/docs/data-structure/Tree.md: -------------------------------------------------------------------------------- 1 | # 树 2 | 3 | ## 真实的树 4 | 5 | ![img](../public/images/data-structure/img-23.png) 6 | 7 | ## 树的特点 8 | 9 | - 树一般都有一个根,连接着根的是树干; 10 | - 树干会发生分叉,形成许多树枝,树枝会继续分化成更小的树枝; 11 | - 树枝的最后是叶子; 12 | 13 | 现实生活中很多结构都是树的抽象,模拟的树结构相当于旋转 `180°` 的树。 14 | 15 | ![img](../public/images/data-structure/img-24.png) 16 | 17 | ## 树结构的优势 18 | 19 | 树结构对比于数组/链表/哈希表有哪些优势呢? 20 | 21 | 数组: 22 | 23 | - 优点:可以通过下标值访问,效率高; 24 | - 缺点:查找数据时需要先对数据进行排序,生成有序数组,才能提高查找效率;并且在插入和删除元素时,需要大量的位移操作; 25 | 26 | 链表: 27 | 28 | - 优点:数据的插入和删除操作效率都很高; 29 | - 缺点:查找效率低,需要从头开始依次查找,直到找到目标数据为止;当需要在链表中间位置插入或删除数据时,插入或删除的效率都不高。 30 | 31 | 哈希表: 32 | 33 | - 优点:哈希表的插入/查询/删除效率都非常高; 34 | - 缺点:空间利用率不高,底层使用的数组中很多单元没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素;而且不能快速找出哈希表中最大值或最小值这些特殊值。 35 | 36 | 树结构: 37 | 38 | - 优点:树结构综合了上述三种结构的优点,同时也弥补了它们存在的缺点(虽然效率不一定都比它们高),比如树结构中数据都是有序的,查找效率高;空间利用率高;并且可以快速获取最大值和最小值等。 39 | 40 | 总的来说:每种数据结构都有自己特定的应用场景。 41 | 42 | 树结构: 43 | 44 | - 树(Tree):由 n(n ≥ 0)个节点构成的有限集合。当 n = 0 时,称为空树。 45 | 46 | - 对于任意一棵非空树(n > 0),它具备以下性质: 47 | - 数中有一个称为根(Root)的特殊节点,用 `r` 表示; 48 | - 其余节点可分为 m(m > 0)个互不相交的有限集合 T1,T2,...,Tm,其中每个集合本身又是一棵树,称为原来树的子树(SubTree)。 49 | 50 | ## 树的常用术语 51 | 52 | ![img](../public/images/data-structure/img-25.png) 53 | 54 | - 节点的度(Degree):节点的子树个数,比如节点 B 的度为 2; 55 | - 树的度:树的所有节点中最大的度数,如上图树的度为 2; 56 | - 叶节点(Leaf):度为 0 的节点(也称为叶子节点),如上图的 H,I 等; 57 | - 父节点(Parent):度不为 0 的节点称为父节点,如上图节点 B 是节点 D 和 E 的父节点; 58 | - 子节点(Child):若 B 是 D 的父节点,那么 D 就是 B 的子节点; 59 | - 兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点,比如上图的 B 和 C,D 和 E 互为兄弟节点; 60 | - 路径和路径长度:路径指的是一个节点到另一节点的通道,路径所包含边的个数称为路径长度,比如 A->H 的路径长度为 3; 61 | - 节点的层次(Level):规定根节点在 1 层,其他任一节点的层数是其父节点的层数加 1。如 B 和 C 节点的层次为 2; 62 | - 树的深度(Depth):树种所有节点中的最大层次是这棵树的深度,如上图树的深度为 4; 63 | 64 | ## 树结构的表示方式 65 | 66 | ### 最普通的表示方法 67 | 68 | ![img](../public/images/data-structure/img-26.png) 69 | 70 | 如图,树结构的组成方式类似于链表,都是由一个个节点连接构成。不过,根据每个父节点子节点数量的不同,每一个父节点需要的引用数量也不同。比如节点 A 需要 3 个引用,分别指向子节点 B,C,D;B 节点需要 2 个引用,分别指向子节点 E 和 F;K 节点由于没有子节点,所以不需要引用。 71 | 72 | 这种方法缺点在于我们无法确定某一结点的引用数。 73 | 74 | ### 儿子 - 兄弟表示法 75 | 76 | ![img](../public/images/data-structure/img-27.png) 77 | 78 | 这种表示方法可以完整地记录每个节点的数据,比如: 79 | 80 | ```js 81 | //节点 A 82 | Node{ 83 | //存储数据 84 | this.data = data 85 | //统一只记录左边的子节点 86 | this.leftChild = B 87 | //统一只记录右边的第一个兄弟节点 88 | this.rightSibling = null 89 | } 90 | 91 | //节点 B 92 | Node{ 93 | this.data = data 94 | this.leftChild = E 95 | this.rightSibling = C 96 | } 97 | 98 | //节点 F 99 | Node{ 100 | this.data = data 101 | this.leftChild = null 102 | this.rightSibling = null 103 | } 104 | ``` 105 | 106 | 这种表示法的优点在于每一个节点中引用的数量都是确定的。 107 | 108 | ### 儿子 - 兄弟表示法旋转 109 | 110 | 以下为儿子 - 兄弟表示法组成的树结构: 111 | 112 | ![img](../public/images/data-structure/img-28.png) 113 | 114 | 将其顺时针旋转 45° 之后: 115 | 116 | ![img](../public/images/data-structure/img-29.png) 117 | 118 | 这样就成为了一棵二叉树,由此我们可以得出结论:任何树都可以通过二叉树进行模拟。但是这样父节点不是变了吗?其实,父节点的设置只是为了方便指向子节点,在代码实现中谁是父节点并没有关系,只要能正确找到对应节点即可。 119 | -------------------------------------------------------------------------------- /src/docs/foreword.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | ## 什么是数据结构 4 | 5 | ### 数据结构的定义 6 | 7 | - “数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。” --- 《数据结构、算法与应用》 8 | - “数据结构是 ADT(抽象数据类型 Abstract Data Type)的物理实现。” --- 《数据结构与算法分析》 9 | - “数据结构(data structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。” ---中文维基百科 10 | - 从自己角度认识,数据结构就是在计算机中,存储和组织数据的方式。 11 | 12 | ### 数据结构在生活中应用 13 | 14 | 我们知道,计算机中数据量非常庞大,如何以高效的方式组织和存储呢? 15 | 16 | 例如:一个庞大的图书馆中存放了大量的书籍,我们不仅仅要把书放进入,还应该在合适的时候能够取出来。 17 | 18 | 图书摆放要使得两个相关操作方便实现: 19 | 20 | - 操作 1:新书怎么插入? 21 | - 操作 2:怎么找到某本指定的书? 22 | 23 | 图书各种摆放方式: 24 | 25 | - 方法 1:随便放 26 | 27 | - 操作 1:哪里有空位放哪里。 28 | - 操作 2:盲目找,大海捞针。 29 | 30 | - 方法 2:按照书名的拼音字母顺序排放 31 | 32 | - 操作 1:新进一本《阿 Q 正传》,按照字母顺序找到位置,插入。 33 | - 操作 2:二分查找法。 34 | 35 | - 方法 3:把书架划分成几块区域,按照类别存放,类别中按照字母顺序。 36 | 37 | - 操作 1:先定类别,二分查找确定位置,移出空位。 38 | - 操作 2:先定类别,再二分查找。 39 | 40 | 结论: 41 | 42 | - 解决问题方法的效率,根据数据的组织方式有关。 43 | - 计算机中存储的数据量相对于图书馆的书籍来说数据量更大,更多。 44 | 45 | 以什么样的方式来存储和组织我们的数据,才能在使用数据时更加方便呢?这就是数据结构需要考虑的问题。 46 | 47 | ### 常见的数据结构 48 | 49 | - 数组(Array) 50 | - 栈(Stack) 51 | - 堆(Heap) 52 | - 链表(Linked List) 53 | - 图(Graph) 54 | - 散列表(Hash) 55 | - 队列(Queue) 56 | - 树(Tree) 57 | 58 | 数据结构与算法与语言无关,常见的编程语言都有**直接或间接**的使用上述常见的数据结构。 59 | 60 | ## 什么是算法 61 | 62 | ### 算法(Algorithm)的定义 63 | 64 | - 一个有限指令集,每条指令的描述不依赖于语言。 65 | - 接收一些输入(有些情况下不需要输入)。 66 | - 产生输出。 67 | - 一定在有限步骤之后终止。 68 | 69 | ### 算法通俗理解 70 | 71 | - Algorithm 这个单词本意就是解决问题的办法/步骤逻辑。 72 | - 数据结构的实现,离不开算法。 73 | 74 | ### 算法案例 75 | 76 | 假如上海和杭州之间有一条高架线,高架线长度是 1,000,000 米,有一天高架线中有其中一米出现了故障,请你想出一种算法,可以快速定位到处问题的地方。 77 | 78 | - 线性查找 79 | 80 | - 从上海的起点开始一米一米的排查,最终一定能找到出问题的线段。 81 | - 但是如果线段在另一头,我们需要排查 1,000,000 次,这是最坏的情况,平均需要 500,000 次。 82 | 83 | - 二分查找 84 | 85 | - 从中间位置开始排查,看一下问题出在上海到中间位置,还是中间到杭州的位置。 86 | - 查找对应的问题后,再从中间位置分开,重新锁定一般的路程。 87 | - 最坏的情况,需要多少次可以排查完呢?最坏的情况是 20 次就可以找到出问题的地方。 88 | - 怎么计算出来的呢?log(1000000, 2),以 2 位底,1000000 的对数 ≈ 20。 89 | 90 | 结论: 91 | 你会发现,解决问题的办法有很多,但是好的算法对比于差的算法,效率天壤之别。 92 | 93 | 94 | ## 学习数据结构与算法的好处 95 | 96 | - **解决问题的能力**:可以教会你如何系统地思考和解决复杂问题。例如:学会如何将一个大问题分解成更小、更可管理的问题,然后逐步解决这些问题。 97 | 98 | - **掌握基础知识**:许多高级的编程概念和技术(如数据库系统、操作系统、编译器等)都基于基础的数据结构和算法知识,掌握这些基础知识,可以更容易理解和学习这些高级技术。 99 | 100 | - **理解计算机科学的核心**:数据结构和算法是计算机科学的核心内容。通过学习它们,可以更深入地理解计算机的工作原理和计算理论。 101 | 102 | - **提高编程能力**:可以帮助你编写更高效的代码。能够选择合适的数据结构和算法,可以显著提升程序的性能和效率。 103 | 104 | - **编写可维护的代码**:选择合适的数据结构不仅能提高代码的效率,还能提高代码的可读性和可维护性。良好的数据结构设计能够使代码更清晰、更易于理解和修改。 105 | 106 | - **提高代码效率**:优秀的算法和数据结构设计能够显著减少程序的运行时间和内存消耗,这在处理大数据或实时系统中尤为重要。 107 | 108 | - **增强面试表现**:许多大公司的面试(如 Google、微软、字节跳动、腾讯等)都会考察应聘者对数据结构和算法的掌握情况。 109 | 110 | - **支持技术进步**:在科研和开发前沿技术时,数据结构和算法的创新和优化是必不可少的。无论是人工智能、机器学习,还是大数据处理,都离不开高效的算法设计。 -------------------------------------------------------------------------------- /src/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "JavaScript" 7 | text: "数据结构与算法" 8 | tagline: "从 0 到 1 开始学习 JavaScript 数据结构与算法" 9 | actions: 10 | - theme: brand 11 | text: 开始学习 12 | link: /foreword 13 | - theme: alt 14 | text: 数据结构 15 | link: /data-structure/Array 16 | - theme: alt 17 | text: 算法 18 | link: /algorithm/sort 19 | --- 20 | 21 | -------------------------------------------------------------------------------- /src/docs/public/CNAME: -------------------------------------------------------------------------------- 1 | data-structure-and-algorithm.xpoet.cn 2 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/BinarySearchTree/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 二叉搜索树 6 | 7 | 8 |

二叉搜索树的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/BinarySearchTree/index.js: -------------------------------------------------------------------------------- 1 | import { BinarySearchTree } from './tree.js'; 2 | // ---------------- 封装的二叉搜索树结构测试 ---------------- // 3 | console.log('// ----- 二叉搜索树结构测试 START -----//'); 4 | 5 | // 二叉搜索树测试 6 | // insert() 插入 7 | const binarySearchTree = new BinarySearchTree(); 8 | binarySearchTree.insert(11); 9 | binarySearchTree.insert(7); 10 | binarySearchTree.insert(5); 11 | binarySearchTree.insert(3); 12 | binarySearchTree.insert(9); 13 | binarySearchTree.insert(8); 14 | binarySearchTree.insert(10); 15 | binarySearchTree.insert(15); 16 | binarySearchTree.insert(13); 17 | binarySearchTree.insert(12); 18 | binarySearchTree.insert(14); 19 | binarySearchTree.insert(20); 20 | binarySearchTree.insert(18); 21 | binarySearchTree.insert(25); 22 | binarySearchTree.insert(19); 23 | console.log(binarySearchTree); 24 | 25 | 26 | console.log('前序遍历', binarySearchTree.preorderTraversal()); 27 | console.log('中序遍历', binarySearchTree.inorderTraversal()); 28 | console.log('后序遍历', binarySearchTree.postorderTraversal()); 29 | console.log('min', binarySearchTree.min()); 30 | console.log('max', binarySearchTree.max()); 31 | console.log('search(98)-递归实现', binarySearchTree.search(98)); 32 | console.log('search(10)-递归实现', binarySearchTree.search(10)); 33 | 34 | console.log('search(98)-while 循环实现', binarySearchTree.search2(98)); 35 | console.log('search(10)-while 循环实现', binarySearchTree.search2(10)); 36 | 37 | console.log('remove(20)'); 38 | binarySearchTree.remove(20); 39 | console.log(binarySearchTree); 40 | console.log(binarySearchTree.inorderTraversal()); 41 | 42 | console.log('// ----- 二叉搜索树结构测试 END -----//'); 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/BinarySearchTree/tree.js: -------------------------------------------------------------------------------- 1 | // 节点类 2 | class Node { 3 | constructor(key) { 4 | this.key = key; 5 | this.left = null; 6 | this.right = null; 7 | } 8 | } 9 | 10 | 11 | // 封装二叉搜索树(特点:左子树节点值 < 根节点,右子树节点值 > 根节点) 12 | export class BinarySearchTree { 13 | 14 | constructor() { 15 | this.root = null; 16 | } 17 | 18 | // insert(key) 插入数据 19 | insert(key) { 20 | const newNode = new Node(key); 21 | 22 | if (this.root === null) { 23 | this.root = newNode; 24 | } else { 25 | this.insertNode(this.root, newNode); 26 | } 27 | 28 | } 29 | 30 | insertNode(root, node) { 31 | 32 | if (node.key < root.key) { // 往左边查找插入 33 | 34 | if (root.left === null) { 35 | root.left = node; 36 | } else { 37 | this.insertNode(root.left, node); 38 | } 39 | 40 | } else { // 往右边查找插入 41 | 42 | if (root.right === null) { 43 | root.right = node; 44 | } else { 45 | this.insertNode(root.right, node); 46 | } 47 | 48 | } 49 | 50 | } 51 | 52 | // ----------- 二叉树遍历 ----------- // 53 | // 先序遍历(根左右 DLR) 54 | preorderTraversal() { 55 | const result = []; 56 | this.preorderTraversalNode(this.root, result); 57 | return result; 58 | } 59 | 60 | preorderTraversalNode(node, result) { 61 | if (node === null) return result; 62 | result.push(node.key); 63 | this.preorderTraversalNode(node.left, result); 64 | this.preorderTraversalNode(node.right, result); 65 | } 66 | 67 | // 中序遍历(左根右 LDR) 68 | inorderTraversal() { 69 | const result = []; 70 | this.inorderTraversalNode(this.root, result); 71 | return result; 72 | } 73 | 74 | inorderTraversalNode(node, result) { 75 | if (node === null) return result; 76 | this.inorderTraversalNode(node.left, result); 77 | result.push(node.key); 78 | this.inorderTraversalNode(node.right, result); 79 | } 80 | 81 | // 后序遍历(左右根 LRD) 82 | postorderTraversal() { 83 | const result = []; 84 | this.postorderTraversalNode(this.root, result); 85 | return result; 86 | } 87 | 88 | postorderTraversalNode(node, result) { 89 | if (node === null) return result; 90 | this.postorderTraversalNode(node.left, result); 91 | this.postorderTraversalNode(node.right, result); 92 | result.push(node.key); 93 | } 94 | 95 | // min() 获取二叉搜索树最小值 96 | min() { 97 | if (!this.root) return null; 98 | let node = this.root; 99 | while (node.left !== null) { 100 | node = node.left; 101 | } 102 | return node.key; 103 | } 104 | 105 | // max() 获取二叉搜索树最大值 106 | max() { 107 | if (!this.root) return null; 108 | let node = this.root; 109 | while (node.right !== null) { 110 | node = node.right; 111 | } 112 | return node.key; 113 | } 114 | 115 | // search(key) 查找二叉搜索树中是否有相同的key,存在返回 true,否则返回 false 116 | search(key) { 117 | return this.searchNode(this.root, key); 118 | } 119 | 120 | // 通过递归实现 121 | searchNode(node, key) { 122 | if (node === null) return false; 123 | if (key < node.key) { 124 | return this.searchNode(node.left, key); 125 | } else if (key > node.key) { 126 | return this.searchNode(node.right, key); 127 | } else { 128 | return true; 129 | } 130 | } 131 | 132 | // 通过 while 循环实现 133 | search2(key) { 134 | 135 | let node = this.root; 136 | 137 | while (node !== null) { 138 | if (key < node.key) { 139 | node = node.left; 140 | } else if (key > node.key) { 141 | node = node.right; 142 | } else { 143 | return true; 144 | } 145 | } 146 | 147 | return false; 148 | 149 | } 150 | 151 | // 删除节点 152 | remove(key) { 153 | 154 | let currentNode = this.root; 155 | let parentNode = null; 156 | let isLeftChild = true; 157 | 158 | // 循环查找到要删除的节点 currentNode,以及它的 parentNode、isLeftChild 159 | while (currentNode.key !== key) { 160 | 161 | parentNode = currentNode; 162 | 163 | // 小于,往左查找 164 | if (key < currentNode.key) { 165 | isLeftChild = true; 166 | currentNode = currentNode.left; 167 | 168 | } else { // 否则往右查找 169 | isLeftChild = false; 170 | currentNode = currentNode.right; 171 | } 172 | 173 | // 找到最后都没找到相等的节点,返回 false 174 | if (currentNode === null) { 175 | return false; 176 | } 177 | 178 | } 179 | 180 | 181 | // 1、删除的是叶子节点的情况 182 | if (currentNode.left === null && currentNode.right === null) { 183 | 184 | if (currentNode === this.root) { 185 | this.root = null; 186 | } else if (isLeftChild) { 187 | parentNode.left = null; 188 | } else { 189 | parentNode.right = null; 190 | } 191 | 192 | 193 | // 2、删除的是只有一个子节点的节点 194 | } else if (currentNode.right === null) { // currentNode 只存在左节点 195 | //-- 2.1、currentNode 只存在<左节点>的情况 196 | //---- 2.1.1、currentNode 等于 root 197 | //---- 2.1.2、parentNode.left 等于 currentNode 198 | //---- 2.1.3、parentNode.right 等于 currentNode 199 | 200 | if (currentNode === this.root) { 201 | this.root = currentNode.left; 202 | } else if (isLeftChild) { 203 | parentNode.left = currentNode.left; 204 | } else { 205 | parentNode.right = currentNode.left; 206 | } 207 | 208 | } else if (currentNode.left === null) { // currentNode 只存在右节点 209 | //-- 2.2、currentNode 只存在<右节点>的情况 210 | //---- 2.1.1 currentNode 等于 root 211 | //---- 2.1.1 parentNode.left 等于 currentNode 212 | //---- 2.1.1 parentNode.right 等于 currentNode 213 | 214 | if (currentNode === this.root) { 215 | this.root = currentNode.right; 216 | } else if (isLeftChild) { 217 | parentNode.left = currentNode.right; 218 | } else { 219 | parentNode.right = currentNode.right; 220 | } 221 | 222 | 223 | // 3、删除的是有两个子节点的节点 224 | } else { 225 | 226 | // 1、找到后续节点 227 | let successor = this.getSuccessor(currentNode); 228 | 229 | // 2、判断是否为根节点 230 | if (currentNode === this.root) { 231 | this.root = successor; 232 | } else if (isLeftChild) { 233 | parentNode.left = successor; 234 | } else { 235 | parentNode.right = successor; 236 | } 237 | 238 | // 3、将后续的左节点改为被删除的左节点 239 | successor.left = currentNode.left; 240 | } 241 | } 242 | 243 | // 获取后续节点,即从要删除的节点的右边开始查找最小的值 244 | getSuccessor(delNode) { 245 | 246 | // 定义变量,保存要找到的后续 247 | let successor = delNode; 248 | let current = delNode.right; 249 | let successorParent = delNode; 250 | 251 | // 循环查找 current 的右子树节点 252 | while (current !== null) { 253 | successorParent = successor; 254 | successor = current; 255 | current = current.left; 256 | } 257 | 258 | // 判断寻找到的后续节点是否直接就是要删除节点的 right 259 | if (successor !== delNode.right) { 260 | successorParent.left = successor.right; 261 | successor.right = delNode.right; 262 | } 263 | return successor; 264 | } 265 | 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/DoublyLinkedList/doublyLinkedList.js: -------------------------------------------------------------------------------- 1 | import { LinkedList, Node } from '../LinkedList/linkedList.js'; 2 | 3 | // 双向链表结构的封装 4 | 5 | // 双向链表的节点类(继承单向链表的节点类) 6 | class DoublyNode extends Node { 7 | constructor(element) { 8 | super(element); 9 | this.prev = null; 10 | } 11 | } 12 | 13 | // 双向链表类(继承单向链表类) 14 | export class DoublyLinkedList extends LinkedList { 15 | 16 | constructor() { 17 | super(); 18 | this.tail = null; 19 | } 20 | 21 | // ------------ 链表的常见操作 ------------ // 22 | // append(element) 往双向链表尾部追加一个新的元素 23 | // 重写 append() 24 | append(element) { 25 | 26 | // 1、创建双向链表节点 27 | const newNode = new DoublyNode(element); 28 | 29 | // 2、追加元素 30 | if (this.head === null) { 31 | this.head = newNode; 32 | this.tail = newNode; 33 | } else { 34 | // !!跟单向链表不同,不用通过循环找到最后一个节点 35 | // 巧妙之处 36 | this.tail.next = newNode; 37 | newNode.prev = this.tail; 38 | this.tail = newNode; 39 | } 40 | 41 | this.length++; 42 | } 43 | 44 | // insert(position, data) 插入元素 45 | // 重写 insert() 46 | insert(position, element) { 47 | // 1、position 越界判断 48 | if (position < 0 || position > this.length) return false; 49 | 50 | // 2、创建新的双向链表节点 51 | const newNode = new DoublyNode(element); 52 | 53 | // 3、判断多种插入情况 54 | if (position === 0) { // 在第 0 个位置插入 55 | 56 | if (this.head === null) { 57 | this.head = newNode; 58 | this.tail = newNode; 59 | } else { 60 | //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==// 61 | newNode.next = this.head; 62 | this.head.perv = newNode; 63 | this.head = newNode; 64 | } 65 | 66 | } else if (position === this.length) { // 在最后一个位置插入 67 | 68 | this.tail.next = newNode; 69 | newNode.prev = this.tail; 70 | this.tail = newNode; 71 | } else { // 在 0 ~ this.length 位置中间插入 72 | 73 | let targetIndex = 0; 74 | let currentNode = this.head; 75 | let previousNode = null; 76 | 77 | // 找到要插入位置的节点 78 | while (targetIndex++ < position) { 79 | previousNode = currentNode; 80 | currentNode = currentNode.next; 81 | } 82 | 83 | // 交换节点信息 84 | previousNode.next = newNode; 85 | newNode.prev = previousNode; 86 | 87 | newNode.next = currentNode; 88 | currentNode.prev = newNode; 89 | } 90 | 91 | this.length++; 92 | 93 | return true; 94 | } 95 | 96 | // getData() 继承单向链表 97 | getData(position) { 98 | return super.getData(position); 99 | } 100 | 101 | // indexOf() 继承单向链表 102 | indexOf(data) { 103 | return super.indexOf(data); 104 | } 105 | 106 | // removeAt() 删除指定位置的节点 107 | // 重写 removeAt() 108 | removeAt(position) { 109 | // 1、position 越界判断 110 | if (position < 0 || position > this.length - 1) return null; 111 | 112 | // 2、根据不同情况删除元素 113 | let currentNode = this.head; 114 | if (position === 0) { // 删除第一个节点的情况 115 | 116 | if (this.length === 1) { // 链表内只有一个节点的情况 117 | this.head = null; 118 | this.tail = null; 119 | } else { // 链表内有多个节点的情况 120 | this.head = this.head.next; 121 | this.head.prev = null; 122 | } 123 | 124 | } else if (position === this.length - 1) { // 删除最后一个节点的情况 125 | 126 | currentNode = this.tail; 127 | this.tail.prev.next = null; 128 | this.tail = this.tail.prev; 129 | 130 | } else { // 删除 0 ~ this.length - 1 里面节点的情况 131 | 132 | let targetIndex = 0; 133 | let previousNode = null; 134 | while (targetIndex++ < position) { 135 | previousNode = currentNode; 136 | currentNode = currentNode.next; 137 | } 138 | 139 | previousNode.next = currentNode.next; 140 | currentNode.next.perv = previousNode; 141 | 142 | } 143 | 144 | this.length--; 145 | return currentNode.data; 146 | } 147 | 148 | // update(position, data) 修改指定位置的节点 149 | // 重写 update() 150 | update(position, data) { 151 | // 1、删除 position 位置的节点 152 | const result = this.removeAt(position); 153 | 154 | // 2、在 position 位置插入元素 155 | this.insert(position, data); 156 | return result; 157 | } 158 | 159 | // remove(data) 删除指定 data 所在的节点(继承单向链表) 160 | remove(data) { 161 | return super.remove(data); 162 | } 163 | 164 | // isEmpty() 判断链表是否为空 165 | isEmpty() { 166 | return super.isEmpty(); 167 | } 168 | 169 | // size() 获取链表的长度 170 | size() { 171 | return super.size(); 172 | } 173 | 174 | 175 | // forwardToString() 链表数据从前往后以字符串形式返回 176 | forwardToString() { 177 | let currentNode = this.head; 178 | let result = ''; 179 | 180 | // 遍历所有的节点,拼接为字符串,直到节点为 null 181 | while (currentNode) { 182 | result += currentNode.data + '--'; 183 | currentNode = currentNode.next; 184 | } 185 | 186 | return result; 187 | } 188 | 189 | // backwardString() 链表数据从后往前以字符串形式返回 190 | backwardString() { 191 | let currentNode = this.tail; 192 | let result = ''; 193 | 194 | // 遍历所有的节点,拼接为字符串,直到节点为 null 195 | while (currentNode) { 196 | result += currentNode.data + '--'; 197 | currentNode = currentNode.prev; 198 | } 199 | 200 | return result; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/DoublyLinkedList/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 双向链表 6 | 7 | 8 |

双向链表的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/DoublyLinkedList/index.js: -------------------------------------------------------------------------------- 1 | import { DoublyLinkedList } from './doublyLinkedList.js'; 2 | 3 | // ---------------- 封装的双向链表结构测试 ---------------- // 4 | console.log('// ----- 双向链表结构测试 START -----//'); 5 | const doublyLinkedList = new DoublyLinkedList(); 6 | 7 | // append() 测试 8 | doublyLinkedList.append('ZZ'); 9 | doublyLinkedList.append('XX'); 10 | doublyLinkedList.append('CC'); 11 | console.log(doublyLinkedList); 12 | 13 | // insert() 测试 14 | doublyLinkedList.insert(0, '00'); 15 | doublyLinkedList.insert(2, '22'); 16 | console.log(doublyLinkedList); 17 | 18 | // getData() 测试 19 | console.log(doublyLinkedList.getData(1)); //--> ZZ 20 | 21 | // indexOf() 测试 22 | console.log(doublyLinkedList.indexOf('XX')); //--> 3 23 | console.log(doublyLinkedList); 24 | 25 | // removeAt() 测试 26 | // doublyLinkedList.removeAt(0); 27 | // doublyLinkedList.removeAt(1); 28 | // console.log(doublyLinkedList); 29 | 30 | // update() 测试 31 | console.log('update() 测试'); 32 | doublyLinkedList.update(0, '111111'); 33 | console.log(doublyLinkedList); 34 | 35 | // remove() 测试 36 | console.log('remove() 测试'); 37 | console.log(doublyLinkedList.remove('111111')); 38 | // console.log(doublyLinkedList.remove('22222')); 39 | console.log(doublyLinkedList); 40 | 41 | // forwardToString() 测试 42 | console.log(doublyLinkedList.forwardToString()); 43 | 44 | // backwardString() 测试 45 | console.log(doublyLinkedList.backwardString()); 46 | 47 | console.log('// ----- 双向链表结构测试 END -----//'); 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/HashTable/hashTable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 设计哈希函数,将传入的字符串哈希化,转换成 hashCode 3 | * @param string 要哈希化的字符串 4 | * @param limit 哈希表的最大个数(数组长度) 5 | * @returns {number} hashCode 6 | */ 7 | export function hashFn(string, limit = 7) { 8 | 9 | // 自己采用的一个质数(无强制要求,质数即可) 10 | const PRIME = 31; 11 | 12 | // 1、定义存储 hashCode 的变量 13 | let hashCode = 0; 14 | 15 | // 2、使用霍纳法则(秦九韶算法),计算 hashCode 的值 16 | for (let item of string) { 17 | hashCode = PRIME * hashCode + item.charCodeAt(); 18 | } 19 | 20 | // 3、对 hashCode 取余,并返回 21 | return hashCode % limit; 22 | } 23 | 24 | 25 | /** 26 | * 判断一个数是否为质数 27 | * @param number 28 | * @returns {boolean} 29 | */ 30 | // 方法一,性能比较低 31 | // export function isPrime(number) { 32 | // if (number <= 1) return false; 33 | // for (let i = 2; i < number; i++) { 34 | // if (number % i === 0) { 35 | // return false; 36 | // } 37 | // } 38 | // return true; 39 | // } 40 | 41 | // 方法二,性能较好 42 | export function isPrime(number) { 43 | if (number <= 1 || number === 4) return false; 44 | const temp = Math.ceil(Math.sqrt(number)); 45 | for (let i = 2; i < temp; i++) { 46 | if (number % i === 0) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | } 52 | 53 | // 哈希表的封装 54 | export class HashTable { 55 | 56 | constructor() { 57 | this.storage = []; // 哈希表存储数据的变量 58 | this.count = 0; // 当前存放的元素个数 59 | this.limit = 7; // 哈希表长度(初始设为质数 7) 60 | 61 | // 装填因子(已有个数/总个数) 62 | this.loadFactor = 0.75; 63 | this.minLoadFactor = 0.25; 64 | } 65 | 66 | // getPrime(number) 根据传入的 number 获取最临近的质数 67 | getPrime(number) { 68 | while (!isPrime(number)) { 69 | number++; 70 | } 71 | return number; 72 | } 73 | 74 | // put(key, value) 往哈希表里添加数据 75 | put(key, value) { 76 | 77 | // 1、根据 key 获取要映射到 storage 里面的 index(通过哈希函数获取) 78 | const index = hashFn(key, this.limit); 79 | 80 | // 2、根据 index 取出对应的 bucket 81 | let bucket = this.storage[index]; 82 | 83 | // 3、判断是否存在 bucket 84 | if (bucket === undefined) { 85 | bucket = []; // 不存在则创建 86 | this.storage[index] = bucket; 87 | } 88 | 89 | // 4、判断是插入数据操作还是修改数据操作 90 | for (let i = 0; i < bucket.length; i++) { 91 | let tuple = bucket[i]; // tuple 的格式:[key, value] 92 | if (tuple[0] === key) { // 如果 key 相等,则修改数据 93 | tuple[1] = value; 94 | return; // 修改完 tuple 里数据,return 终止,不再往下执行。 95 | } 96 | } 97 | 98 | // 5、bucket 新增数据 99 | bucket.push([key, value]); // bucket 存储元组 tuple,格式为 [key, value] 100 | this.count++; 101 | 102 | // 判断哈希表是否要扩容,若装填因子 > 0.75,则扩容 103 | if (this.count / this.limit > this.loadFactor) { 104 | this.resize(this.getPrime(this.limit * 2)); 105 | } 106 | 107 | } 108 | 109 | // 根据 get(key) 获取 value 110 | get(key) { 111 | 112 | const index = hashFn(key, this.limit); 113 | const bucket = this.storage[index]; 114 | 115 | if (bucket === undefined) { 116 | return null; 117 | } 118 | 119 | for (const tuple of bucket) { 120 | if (tuple[0] === key) { 121 | return tuple[1]; 122 | } 123 | } 124 | return null; 125 | } 126 | 127 | // remove(key) 删除指定 key 的数据 128 | remove(key) { 129 | 130 | const index = hashFn(key, this.limit); 131 | const bucket = this.storage[index]; 132 | 133 | if (bucket === undefined) { 134 | return null; 135 | } 136 | 137 | // 遍历 bucket,找到对应位置的 tuple,将其删除 138 | for (let i = 0, len = bucket.length; i < len; i++) { 139 | const tuple = bucket[i]; 140 | if (tuple[0] === key) { 141 | bucket.splice(i, 1); // 删除对应位置的数组项 142 | this.count--; 143 | 144 | // 根据装填因子的大小,判断是否要进行哈希表压缩 145 | if (this.limit > 7 && this.count / this.limit < this.minLoadFactor) { 146 | this.resize(this.getPrime(Math.floor(this.limit / 2))); 147 | } 148 | 149 | return tuple; 150 | } 151 | 152 | } 153 | 154 | } 155 | 156 | isEmpty() { 157 | return this.count === 0; 158 | } 159 | 160 | size() { 161 | return this.count; 162 | } 163 | 164 | // 重新调整哈希表大小,扩容或压缩 165 | resize(newLimit) { 166 | 167 | // 1、保存旧的 storage 数组内容 168 | const oldStorage = this.storage; 169 | 170 | // 2、重置所有属性 171 | this.storage = []; 172 | this.count = 0; 173 | this.limit = newLimit; 174 | 175 | // 3、遍历 oldStorage,取出所有数据,重新 put 到 this.storage 176 | for (const bucket of oldStorage) { 177 | if (bucket) { 178 | for (const b of bucket) { 179 | this.put(b[0], b[1]); 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/HashTable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 哈希表 6 | 7 | 8 |

哈希表的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/HashTable/index.js: -------------------------------------------------------------------------------- 1 | import { hashFn, HashTable, isPrime } from './hashTable.js'; 2 | 3 | // ---------------- 封装的哈希表结构测试 ---------------- // 4 | console.log('// ----- 哈希表结构测试 START -----//'); 5 | 6 | console.log('=== START 哈希函数测试 START === '); 7 | console.log(hashFn('123')); //--> 5 8 | console.log(hashFn('abc')); //--> 6 9 | console.log('=== END 哈希函数测试 END === '); 10 | 11 | const hashTable = new HashTable(); 12 | 13 | // put() 测试 14 | hashTable.put('name', 'XPoet'); 15 | hashTable.put('age', 18); 16 | hashTable.put('height', 178); 17 | hashTable.put('email', 'i@xpoet.cn'); 18 | hashTable.put('address', 'china'); 19 | console.log(hashTable); 20 | //--> {storage: Array(6), count: 5, limit: 7, loadFactor: 0.75, minLoadFactor: 0.25} 21 | 22 | hashTable.put('address2', 'china2'); 23 | console.log(hashTable); 24 | //--> {storage: Array(16), count: 6, limit: 17, loadFactor: 0.75, minLoadFactor: 0.25} 25 | 26 | // get() 测试 27 | console.log(hashTable.get('name')); //--> XPoet 28 | 29 | // remove() 测试 30 | hashTable.remove('address'); 31 | console.log(hashTable); 32 | //--> {storage: Array(16), count: 5, limit: 17, loadFactor: 0.75, minLoadFactor: 0.25} 33 | 34 | 35 | console.log('// ----- 哈希表结构测试 END -----//'); 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/LinkedList/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 单向链表 6 | 7 | 8 |

单向链表的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/LinkedList/index.js: -------------------------------------------------------------------------------- 1 | import { LinkedList } from './linkedList.js'; 2 | 3 | // ---------------- 封装的单向链表结构测试 ---------------- // 4 | console.log('// ----- 单向链表结构测试 START -----//'); 5 | const linkedList = new LinkedList(); 6 | 7 | // 测试 append 方法 8 | linkedList.append('AA'); 9 | linkedList.append('BB'); 10 | linkedList.append('CC'); 11 | console.log(linkedList); 12 | 13 | // 测试 toString 方法 14 | console.log(linkedList.toString()); //--> AA BB CC 15 | 16 | // 测试 insert 方法 17 | linkedList.insert(0, '123'); 18 | linkedList.insert(2, '456'); 19 | console.log(linkedList.toString()); //--> 123 AA 456 BB CC 20 | 21 | // 测试 getData 方法 22 | console.log(linkedList.getData(0)); //--> 123 23 | console.log(linkedList.getData(1)); //--> AA 24 | 25 | // 测试 indexOf 方法 26 | console.log(linkedList.indexOf('AA')); //--> 1 27 | console.log(linkedList.indexOf('ABC')); //--> -1 28 | 29 | // 测试 update 方法 30 | linkedList.update(0, '12345'); 31 | console.log(linkedList.toString()); //--> 12345 AA 456 BB CC 32 | linkedList.update(1, '54321'); 33 | console.log(linkedList.toString()); //--> 12345 54321 456 BB CC 34 | 35 | // 测试 removeAt 方法 36 | linkedList.removeAt(3); 37 | console.log(linkedList.toString()); //--> 12345 54321 456 CC 38 | 39 | // 测试 remove 方法 40 | linkedList.remove('CC'); 41 | console.log(linkedList.toString()); //--> 12345 54321 456 42 | 43 | // 测试 isEmpty 方法 44 | console.log(linkedList.isEmpty()); //--> false 45 | 46 | // 测试 size 方法 47 | console.log(linkedList.size()); //--> 3 48 | 49 | console.log('// ----- 单向链表结构测试 END -----//'); 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/LinkedList/linkedList.js: -------------------------------------------------------------------------------- 1 | // 封装节点类 2 | export class Node { 3 | constructor(data) { 4 | this.data = data; 5 | this.next = null; 6 | } 7 | } 8 | 9 | // 单向链表结构的封装 10 | export class LinkedList { 11 | 12 | constructor() { 13 | // 初始链表长度为 0 14 | this.length = 0; 15 | 16 | // 初始 head 为 null,head 指向链表的第一个节点 17 | this.head = null; 18 | } 19 | 20 | // ------------ 链表的常见操作 ------------ // 21 | 22 | // append(data) 往链表尾部追加数据 23 | append(data) { 24 | // 1、创建新节点 25 | const newNode = new Node(data); 26 | 27 | // 2、追加新节点 28 | if (this.length === 0) { 29 | // 链表长度为 0 时,即只有 head 的时候 30 | this.head = newNode; 31 | } else { 32 | // 链表长度大于 0 时,在最后面添加新节点 33 | let currentNode = this.head; 34 | 35 | // 当 currentNode.next 不为空时, 36 | // 循序依次找最后一个节点,即节点的 next 为 null 时 37 | while (currentNode.next !== null) { 38 | currentNode = currentNode.next; 39 | } 40 | 41 | // 最后一个节点的 next 指向新节点 42 | currentNode.next = newNode; 43 | } 44 | 45 | // 3、追加完新节点后,链表长度 + 1 46 | this.length++; 47 | } 48 | 49 | // insert(position, data) 在指定位置(position)插入节点 50 | insert(position, data) { 51 | // position 新插入节点的位置 52 | // position = 0 表示新插入后是第一个节点 53 | // position = 1 表示新插入后是第二个节点,以此类推 54 | 55 | // 1、对 position 进行越界判断,不能小于 0 或大于链表长度 56 | if (position < 0 || position > this.length) return false; 57 | 58 | // 2、创建新节点 59 | const newNode = new Node(data); 60 | 61 | // 3、插入节点 62 | if (position === 0) { 63 | // position = 0 的情况 64 | // 让新节点的 next 指向 原来的第一个节点,即 head 65 | newNode.next = this.head; 66 | 67 | // head 赋值为 newNode 68 | this.head = newNode; 69 | } else { 70 | // 0 < position <= length 的情况 71 | 72 | // 初始化一些变量 73 | let currentNode = this.head; // 当前节点初始化为 head 74 | let previousNode = null; // head 的 上一节点为 null 75 | let index = 0; // head 的 index 为 0 76 | 77 | // 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode 78 | // 直到找到要插入的位置 79 | while (index++ < position) { 80 | previousNode = currentNode; 81 | currentNode = currentNode.next; 82 | } 83 | 84 | // 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向 85 | newNode.next = currentNode; 86 | previousNode.next = newNode; 87 | } 88 | 89 | // 更新链表长度 90 | this.length++; 91 | return newNode; 92 | } 93 | 94 | // getData(position) 获取指定位置的 data 95 | getData(position) { 96 | // 1、position 越界判断 97 | if (position < 0 || position >= this.length) return null; 98 | 99 | // 2、获取指定 position 节点的 data 100 | let currentNode = this.head; 101 | let index = 0; 102 | 103 | while (index++ < position) { 104 | currentNode = currentNode.next; 105 | } 106 | 107 | // 3、返回 data 108 | return currentNode.data; 109 | } 110 | 111 | // indexOf(data) 返回指定 data 的 index,如果没有,返回 -1。 112 | indexOf(data) { 113 | let currentNode = this.head; 114 | let index = 0; 115 | 116 | while (currentNode) { 117 | if (currentNode.data === data) { 118 | return index; 119 | } 120 | currentNode = currentNode.next; 121 | index++; 122 | } 123 | 124 | return -1; 125 | } 126 | 127 | // update(position, data) 修改指定位置节点的 data 128 | update(position, data) { 129 | // 涉及到 position 都要进行越界判断 130 | // 1、position 越界判断 131 | if (position < 0 || position >= this.length) return false; 132 | 133 | // 2、痛过循环遍历,找到指定 position 的节点 134 | let currentNode = this.head; 135 | let index = 0; 136 | while (index++ < position) { 137 | currentNode = currentNode.next; 138 | } 139 | 140 | // 3、修改节点 data 141 | currentNode.data = data; 142 | 143 | return currentNode; 144 | } 145 | 146 | // removeAt(position) 删除指定位置的节点,并返回删除的那个节点 147 | removeAt(position) { 148 | // 1、position 越界判断 149 | if (position < 0 || position >= this.length) return null; 150 | 151 | // 2、删除指定 position 节点 152 | let currentNode = this.head; 153 | if (position === 0) { 154 | // position = 0 的情况 155 | this.head = this.head.next; 156 | } else { 157 | // position > 0 的情况 158 | // 通过循环遍历,找到指定 position 的节点,赋值到 currentNode 159 | 160 | let previousNode = null; 161 | let index = 0; 162 | 163 | while (index++ < position) { 164 | previousNode = currentNode; 165 | currentNode = currentNode.next; 166 | } 167 | 168 | // 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。 169 | previousNode.next = currentNode.next; 170 | } 171 | 172 | // 3、更新链表长度 -1 173 | this.length--; 174 | 175 | return currentNode; 176 | } 177 | 178 | // remove(data) 删除指定 data 的节点,并返回删除的那个节点 179 | remove(data) { 180 | return this.removeAt(this.indexOf(data)); 181 | } 182 | 183 | // isEmpty() 判断链表是否为空 184 | isEmpty() { 185 | return this.length === 0; 186 | } 187 | 188 | // size() 获取链表的长度 189 | size() { 190 | return this.length; 191 | } 192 | 193 | // toString() 链表数据以字符串形式返回 194 | toString() { 195 | let currentNode = this.head; 196 | let result = ''; 197 | 198 | // 遍历所有的节点,拼接为字符串,直到节点为 null 199 | while (currentNode) { 200 | result += currentNode.data + ' '; 201 | currentNode = currentNode.next; 202 | } 203 | 204 | return result; 205 | } 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 字典 6 | 7 | 8 |

字典的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Map/index.js: -------------------------------------------------------------------------------- 1 | import Map from './map.js'; 2 | 3 | // ---------------- 封装的字典结构测试 ---------------- // 4 | console.log('// ----- 字典结构测试 START -----//'); 5 | const map = new Map(); 6 | 7 | // set() 测试 8 | map.set('name', 'XPoet'); 9 | map.set('age', 18); 10 | map.set('email', 'i@xpoet.cn'); 11 | console.log(map); // {items: {name: "XPoet", age: 18, email: "i@xpoet.cn"}} 12 | 13 | // has() 测试 14 | console.log(map.has('name')); //--> true 15 | console.log(map.has('address')); //--> false 16 | 17 | // remove() 测试 18 | map.remove('name'); 19 | console.log(map); // {age: 18, email: "i@xpoet.cn"} 20 | 21 | // get() 测试 22 | console.log(map.get('age')); //--> 18 23 | 24 | // keys() 测试 25 | console.log(map.keys()); //--> ["age", "email"] 26 | 27 | // values() 测试 28 | console.log(map.values()); //--> [18, "i@xpoet.cn"] 29 | 30 | // size() 测试 31 | console.log(map.size()); //--> 2 32 | 33 | console.log('// ----- 字典结构测试 END -----//'); 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Map/map.js: -------------------------------------------------------------------------------- 1 | // 字典结构的封装 2 | export default class Map { 3 | 4 | constructor() { 5 | this.items = {}; 6 | } 7 | 8 | // has(key) 判断字典中是否存在某个 key 9 | has(key) { 10 | return this.items.hasOwnProperty(key); 11 | } 12 | 13 | // set(key, value) 在字典中添加键值对 14 | set(key, value) { 15 | this.items[key] = value; 16 | } 17 | 18 | // remove(key) 在字典中删除指定的 key 19 | remove(key) { 20 | // 如果集合不存在该 key,返回 false 21 | if (!this.has(key)) return false; 22 | delete this.items[key]; 23 | } 24 | 25 | // get(key) 获取指定 key 的 value,如果没有,返回 undefined 26 | get(key) { 27 | return this.has(key) ? this.items[key] : undefined; 28 | } 29 | 30 | // 获取所有的 key 31 | keys() { 32 | return Object.keys(this.items); 33 | } 34 | 35 | // 获取所有的 value 36 | values() { 37 | return Object.values(this.items); 38 | } 39 | 40 | // size() 获取字典中的键值对个数 41 | size() { 42 | return this.keys().length; 43 | } 44 | 45 | // clear() 清空字典中所有的键值对 46 | clear() { 47 | this.items = {}; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/PriorityQueue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 优先队列 6 | 7 | 8 |

优先队列的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/PriorityQueue/index.js: -------------------------------------------------------------------------------- 1 | import { PriorityQueue } from './priorityQueue.js'; 2 | 3 | // ---------------- 封装的优先队列结构测试 ---------------- // 4 | console.log('// ----- 优先队列结构测试 START -----//'); 5 | 6 | const priorityQueue = new PriorityQueue(); 7 | 8 | // 入队 enqueue() 测试 9 | priorityQueue.enqueue('A', 10); 10 | priorityQueue.enqueue('B', 15); 11 | priorityQueue.enqueue('C', 11); 12 | priorityQueue.enqueue('D', 20); 13 | priorityQueue.enqueue('E', 18); 14 | console.log(priorityQueue.items); 15 | //--> output: 16 | // QueueElement {element: "A", priority: 10} 17 | // QueueElement {element: "C", priority: 11} 18 | // QueueElement {element: "B", priority: 15} 19 | // QueueElement {element: "E", priority: 18} 20 | // QueueElement {element: "D", priority: 20} 21 | 22 | 23 | // 出队 dequeue() 测试 24 | priorityQueue.dequeue(); 25 | priorityQueue.dequeue(); 26 | console.log(priorityQueue.items); 27 | //--> output: 28 | // QueueElement {element: "B", priority: 15} 29 | // QueueElement {element: "E", priority: 18} 30 | // QueueElement {element: "D", priority: 20} 31 | 32 | // isEmpty() 测试 33 | console.log(priorityQueue.isEmpty()); //--> false 34 | 35 | // size() 测试 36 | console.log(priorityQueue.size()); //--> 3 37 | 38 | // toString() 测试 39 | console.log(priorityQueue.toString()); //--> B-15 E-18 D-20 40 | 41 | console.log('// ----- 优先队列结构测试 END -----//'); 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/PriorityQueue/priorityQueue.js: -------------------------------------------------------------------------------- 1 | import Queue from '../Queue/queue.js'; 2 | 3 | /** 4 | * 优先队列结构的封装 5 | */ 6 | 7 | // 优先队列内部的元素类 8 | class QueueElement { 9 | constructor(element, priority) { 10 | this.element = element; 11 | this.priority = priority; 12 | } 13 | } 14 | 15 | // 优先队列类(继承 Queue 类) 16 | export class PriorityQueue extends Queue { 17 | 18 | constructor() { 19 | super(); 20 | } 21 | 22 | // enqueue(element, priority) 入队,将元素按优先级加入到队列中 23 | // 重写 enqueue() 24 | enqueue(element, priority) { 25 | // 根据传入的元素,创建 QueueElement 对象 26 | const queueElement = new QueueElement(element, priority); 27 | 28 | // 判断队列是否为空 29 | if (this.isEmpty()) { 30 | // 如果为空,不用判断优先级,直接添加 31 | this.items.push(queueElement); 32 | } else { 33 | // 定义一个变量记录是否成功添加了新元素 34 | let added = false; 35 | 36 | for (let i = 0; i < this.items.length; i++) { 37 | // 让新插入的元素进行优先级比较,priority 值越小,优先级越大 38 | if (queueElement.priority < this.items[i].priority) { 39 | // 在指定的位置插入元素 40 | this.items.splice(i, 0, queueElement); 41 | added = true; 42 | break; 43 | } 44 | } 45 | 46 | // 如果遍历完所有元素,优先级都大于新插入的元素,就将新插入的元素插入到最后 47 | if (!added) { 48 | this.items.push(queueElement); 49 | } 50 | } 51 | } 52 | 53 | // dequeue() 出队,从队列中删除前端元素,返回删除的元素 54 | // 继承 Queue 类的 dequeue() 55 | dequeue() { 56 | return super.dequeue(); 57 | } 58 | 59 | // front() 查看队列的前端元素 60 | // 继承 Queue 类的 front() 61 | front() { 62 | return super.front(); 63 | } 64 | 65 | // isEmpty() 查看队列是否为空 66 | // 继承 Queue 类的 isEmpty() 67 | isEmpty() { 68 | return super.isEmpty(); 69 | } 70 | 71 | // size() 查看队列中元素的个数 72 | // 继承 Queue 类的 size() 73 | size() { 74 | return super.size(); 75 | } 76 | 77 | // toString() 将队列中元素以字符串形式返回 78 | // 重写 toString() 79 | toString() { 80 | let result = ''; 81 | for (let item of this.items) { 82 | result += item.element + '-' + item.priority + ' '; 83 | } 84 | return result; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Queue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 队列 6 | 7 | 8 |

队列的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Queue/index.js: -------------------------------------------------------------------------------- 1 | import Queue from './queue.js'; 2 | import passGame from './passGame.js'; 3 | 4 | // ---------------- 封装的队列结构测试 ---------------- // 5 | console.log('// ----- 队列结构测试 START -----//'); 6 | const queue = new Queue(); 7 | 8 | // enqueue() 测试 9 | queue.enqueue('a'); 10 | queue.enqueue('b'); 11 | queue.enqueue('c'); 12 | queue.enqueue('d'); 13 | console.log(queue.items); //--> ["a", "b", "c", "d"] 14 | 15 | // dequeue() 测试 16 | queue.dequeue(); 17 | queue.dequeue(); 18 | console.log(queue.items); //--> ["c", "d"] 19 | 20 | // front() 测试 21 | console.log(queue.front()); //--> c 22 | 23 | // isEmpty() 测试 24 | console.log(queue.isEmpty()); //--> false 25 | 26 | // size() 测试 27 | console.log(queue.size()); //--> 2 28 | 29 | // toString() 测试 30 | console.log(queue.toString()); //--> c d 31 | 32 | 33 | // 击鼓传花游戏算法封装 passGame() 测试 34 | const names = ['lily', 'lucy', 'tom', 'tony', 'jack']; 35 | const targetIndex = passGame(names, 4); 36 | console.log('击鼓传花', names[targetIndex]); //--> lily 37 | 38 | console.log('// ----- 队列结构测试 END -----//'); 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Queue/passGame.js: -------------------------------------------------------------------------------- 1 | import Queue from './queue.js'; 2 | 3 | // 利用队列结构的特点实现击鼓传花游戏求解方法的封装 4 | export default function passGame(nameList, number) { 5 | // 1、new 一个 Queue 对象 6 | const queue = new Queue(); 7 | 8 | // 2、将 nameList 里面的每一个元素入队 9 | for (const name of nameList) { 10 | queue.enqueue(name); 11 | } 12 | 13 | // 3、开始数数 14 | // 队列中只剩下 1 个元素时就停止数数 15 | while (queue.size() > 1) { 16 | // 不是 number 时,重新加入到队尾 17 | // 是 number 时,将其删除 18 | 19 | for (let i = 0; i < number - 1; i++) { 20 | // number 数字之前的人重新放入到队尾(即把队头删除的元素,重新加入到队列中) 21 | queue.enqueue(queue.dequeue()); 22 | } 23 | 24 | // number 对应这个人,直接从队列中删除 25 | // 由于队列没有像数组一样的下标值不能直接取到某一元素, 26 | // 所以采用,把 number 前面的 number - 1 个元素先删除后添加到队列末尾, 27 | // 这样第 number 个元素就排到了队列的最前面,可以直接使用 dequeue 方法进行删除 28 | queue.dequeue(); 29 | } 30 | 31 | // 4、获取最后剩下的那个人 32 | const endName = queue.front(); 33 | 34 | // 5、返回这个人在原数组中对应的索引 35 | return nameList.indexOf(endName); 36 | } 37 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Queue/queue.js: -------------------------------------------------------------------------------- 1 | // 队列结构的封装 2 | export default class Queue { 3 | 4 | constructor() { 5 | this.items = []; 6 | } 7 | 8 | // enqueue(item) 入队,将元素加入到队列中 9 | enqueue(item) { 10 | this.items.push(item); 11 | } 12 | 13 | // dequeue() 出队,从队列中删除队头元素,返回删除的那个元素 14 | dequeue() { 15 | return this.items.shift(); 16 | } 17 | 18 | // front() 查看队列的队头元素 19 | front() { 20 | return this.items[0]; 21 | } 22 | 23 | // isEmpty() 查看队列是否为空 24 | isEmpty() { 25 | return this.items.length === 0; 26 | } 27 | 28 | // size() 查看队列中元素的个数 29 | size() { 30 | return this.items.length; 31 | } 32 | 33 | // toString() 将队列中的元素以字符串形式返回 34 | toString() { 35 | let result = ''; 36 | for (let item of this.items) { 37 | result += item + ' '; 38 | } 39 | return result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Set/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 集合 6 | 7 | 8 |

集合的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Set/index.js: -------------------------------------------------------------------------------- 1 | import Set from './set.js'; 2 | 3 | // ---------------- 封装的栈结构测试 ---------------- // 4 | console.log('// ----- 集合结构测试 START -----//'); 5 | const set = new Set(); 6 | 7 | // add() 测试 8 | set.add('abc'); 9 | set.add('abc'); 10 | set.add('123'); 11 | set.add('zxc'); 12 | console.log(set); //--> {items: {123: "123", abc: "abc", zxc: "zxc"}} 13 | 14 | // has() 测试 15 | console.log(set.has('123')); //--> true 16 | console.log(set.has('456')); //--> false 17 | 18 | // remove() 测试 19 | set.remove('abc'); 20 | console.log(set); //--> {items: {123: "123", zxc: "zxc"}} 21 | 22 | // size() 测试 23 | console.log(set.size()); //--> 2 24 | 25 | // values() 测试 26 | console.log(set.values()); //--> ["123", "zxc"] 27 | 28 | // clear() 测试 29 | set.clear(); 30 | console.log(set.values()); //--> [] 31 | 32 | // ------- 集合的操作测试 ------- // 33 | const setA = new Set(); 34 | setA.add('111'); 35 | setA.add('222'); 36 | setA.add('333'); 37 | 38 | const setB = new Set(); 39 | setB.add('111'); 40 | setB.add('222'); 41 | setB.add('aaa'); 42 | setB.add('ccc'); 43 | 44 | // 求两个集合的并集 union() 测试 45 | console.log(setA.union(setB).values()); //--> ["111", "222", "333", "aaa", "ccc"] 46 | 47 | // 求两个集合的交集 intersection() 测试 48 | console.log(setA.intersection(setB).values()); //--> ["111", "222"] 49 | 50 | // 求集合 A 和 集合 B,集合 A 的差集,difference() 测试 51 | console.log(setA.difference(setB).values()); //--> ["333"] 52 | 53 | // 求集合 A 是否为 集合 B 的 子集,subset() 测试 54 | console.log(setA.subset(setB)); //--> false 55 | 56 | 57 | console.log('// ----- 集合结构测试 END -----//'); 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Set/set.js: -------------------------------------------------------------------------------- 1 | // 集合结构的封装 2 | export default class Set { 3 | 4 | constructor() { 5 | this.items = {}; 6 | } 7 | 8 | // has(value) 判断集合中是否存在 value 值,存在返回 true,否则返回 false 9 | has(value) { 10 | return this.items.hasOwnProperty(value); 11 | } 12 | 13 | // add(value) 往集合中添加 value 14 | add(value) { 15 | if (this.has(value)) return false; 16 | this.items[value] = value; 17 | return true; 18 | } 19 | 20 | // remove(value) 删除集合中指定的 value 21 | remove(value) { 22 | // 如果集合不存在该 value,返回 false 23 | if (!this.has(value)) return false; 24 | delete this.items[value]; 25 | } 26 | 27 | // clear() 清空集合中所有 value 28 | clear() { 29 | this.items = {}; 30 | } 31 | 32 | // size() 获取集合中的 value 个数 33 | size() { 34 | return Object.keys(this.items).length; 35 | } 36 | 37 | // values() 获取集合中所有的 value 38 | values() { 39 | return Object.keys(this.items); 40 | } 41 | 42 | // ------- 集合间的操作 ------- // 43 | // union() 求两个集合的并集 44 | union(otherSet) { 45 | // 1、创建一个新集合 46 | let unionSet = new Set(); 47 | 48 | // 2、将当前集合(this)的所有 value,添加到新集合(unionSet)中 49 | for (let value of this.values()) { 50 | unionSet.add(value); 51 | } 52 | 53 | // 3、将 otherSet 集合的所有 value,添加到新集合(unionSet)中 54 | for (let value of otherSet.values()) { 55 | unionSet.add(value); // add() 已经有重复判断 56 | } 57 | 58 | return unionSet; 59 | } 60 | 61 | // intersection() 求两个集合的交集 62 | intersection(otherSet) { 63 | 64 | // 1、创建一个新集合 65 | let intersectionSet = new Set(); 66 | 67 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在 68 | for (let value of this.values()) { 69 | if (otherSet.has(value)) { 70 | intersectionSet.add(value); 71 | } 72 | } 73 | 74 | return intersectionSet; 75 | } 76 | 77 | // difference() 差集 78 | difference(otherSet) { 79 | 80 | // 1、创建一个新集合 81 | let differenceSet = new Set(); 82 | 83 | // 2、从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,不存在的即为差集 84 | for (let value of this.values()) { 85 | if (!otherSet.has(value)) { 86 | differenceSet.add(value); 87 | } 88 | } 89 | 90 | return differenceSet; 91 | } 92 | 93 | // subset() 子集 94 | subset(otherSet) { 95 | 96 | // 从当前集合中取出每一个 value,判断是否在 otherSet 集合中存在,有不存在的返回 false 97 | // 遍历完所有的,返回 true 98 | for (let value of this.values()) { 99 | if (!otherSet.has(value)) { 100 | return false; 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Stack/dec2bin.js: -------------------------------------------------------------------------------- 1 | import Stack from './stack.js'; 2 | 3 | // 利用栈结构的特点封装实现十进制转换为二进制的方法 4 | export default function dec2bin(dec) { 5 | // new 一个 Stack,保存余数 6 | const stack = new Stack(); 7 | 8 | // 当不确定循环次数时,使用 while 循环 9 | while (dec > 0) { 10 | // 除二取余法 11 | stack.push(dec % 2); // 获取余数,放入栈中 12 | dec = Math.floor(dec / 2); // 除数除以二,向下取整 13 | } 14 | 15 | let binaryString = ''; 16 | // 不断地从栈中取出元素(0 或 1),并拼接到一起。 17 | while (!stack.isEmpty()) { 18 | binaryString += stack.pop(); 19 | } 20 | 21 | return binaryString; 22 | } 23 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Stack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

栈的实现和测试

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Stack/index.js: -------------------------------------------------------------------------------- 1 | import Stack from './stack.js'; 2 | import dec2bin from './dec2bin.js'; 3 | 4 | // ---------------- 封装的栈结构测试 ---------------- // 5 | console.log('// ----- 栈结构测试 START -----//'); 6 | const stack = new Stack(); 7 | 8 | // push() 测试 9 | stack.push(1); 10 | stack.push(2); 11 | stack.push(3); 12 | console.log(stack.items); //--> [1, 2, 3] 13 | 14 | // pop() 测试 15 | console.log(stack.pop()); //--> 3 16 | 17 | // peek() 测试 18 | console.log(stack.peek()); //--> 2 19 | 20 | // isEmpty() 测试 21 | console.log(stack.isEmpty()); //--> false 22 | 23 | // size() 测试 24 | console.log(stack.size()); //--> 2 25 | 26 | // toString() 测试 27 | console.log(stack.toString()); //--> 1 2 28 | 29 | // dec2bin() 测试 30 | console.log(dec2bin(100)); //--> 1100100 31 | console.log(dec2bin(88)); //--> 1011000 32 | 33 | console.log('// ----- 栈结构测试 END -----//'); 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/docs/public/data-structure/Stack/stack.js: -------------------------------------------------------------------------------- 1 | // 栈结构的封装 2 | export default class Stack { 3 | 4 | constructor() { 5 | this.items = []; 6 | } 7 | 8 | // push(item) 压栈操作,往栈里面添加元素 9 | push(item) { 10 | this.items.push(item); 11 | } 12 | 13 | // pop() 出栈操作,从栈中取出元素,并返回取出的那个元素 14 | pop() { 15 | if(this.isEmpty()) throw new Error('栈空了'); 16 | return this.items.pop(); 17 | } 18 | 19 | // peek() 查看栈顶元素 20 | peek() { 21 | if(this.isEmpty()) throw new Error('栈空了'); 22 | return this.items[this.items.length - 1]; 23 | } 24 | 25 | // isEmpty() 判断栈是否为空 26 | isEmpty() { 27 | return this.items.length === 0; 28 | } 29 | 30 | // size() 获取栈中元素个数 31 | size() { 32 | return this.items.length; 33 | } 34 | 35 | // toString() 返回以字符串形式的栈内元素数据 36 | toString() { 37 | let result = ''; 38 | for (let item of this.items) { 39 | result += item + ' '; 40 | } 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/docs/public/images/algorithm/img-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/algorithm/img-01.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-01.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-02.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-03.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-04.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-05.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-06.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-07.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-08.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-09.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-10.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-11.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-12.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-13.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-14.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-15.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-16.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-17.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-18.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-19.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-20.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-21.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-22.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-23.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-24.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-25.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-26.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-27.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-28.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-29.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-30.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-31.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-32.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-33.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-34.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-35.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-36.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-37.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-38.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-39.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-40.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-41.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-42.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-43.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-44.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-45.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-46.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-47.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-48.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-49.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-50.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-51.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-52.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-53.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-54.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-55.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-56.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-57.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-58.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-59.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-60.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-61.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-62.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-63.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-64.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-65.png -------------------------------------------------------------------------------- /src/docs/public/images/data-structure/img-66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/data-structure/img-66.png -------------------------------------------------------------------------------- /src/docs/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XPoet/js-data-structure-and-algorithm/c23d7256924df85461ce037bd6f70186d976b321/src/docs/public/images/logo.png --------------------------------------------------------------------------------