资讯专栏INFORMATION COLUMN

「中高级前端」窥探数据结构的世界- ES6版

Lucky_Boy / 1022人阅读

摘要:单链表与双向链表单链表是表示一系列节点的数据结构,其中每个节点指向列表中的下一个节点。且分别称为该结点的左子树与右子树。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

1. 什么是数据结构?

数据结构是在计算机中组织和存储数据的一种特殊方式,使得数据可以高效地被访问和修改。更确切地说,数据结构是数据值的集合,表示数据之间的关系,也包括了作用在数据上的函数或操作。

1.1 为什么我们需要数据结构?

数据是计算机科学当中最关键的实体,而数据结构则可以将数据以某种组织形式存储,因此,数据结构的价值不言而喻。

无论你以何种方式解决何种问题,你都需要处理数据——无论是涉及员工薪水、股票价格、购物清单,还是只是简单的电话簿问题。

数据需要根据不同的场景,按照特定的格式进行存储。有很多数据结构能够满足以不同格式存储数据的需求。

1.2 八大常见的数据结构

    数组:Array

    堆栈:Stack

    队列:Queue

    链表:Linked Lists

    树:Trees

    图:Graphs

    字典树:Trie

    散列表(哈希表):Hash Tables

在较高的层次上,基本上有三种类型的数据结构:

堆栈和队列是类似于数组的结构,仅在项目的插入和删除方式上有所不同。

链表,树,和图 结构的节点是引用到其他节点。

散列表依赖于散列函数来保存和定位数据。

在复杂性方面:

堆栈和队列是最简单的,并且可以从中构建链表。

树和图 是最复杂的,因为它们扩展了链表的概念。

散列表和字典树 需要利用这些数据结构来可靠地执行。

就效率而已:

链表是记录和存储数据的最佳选择

而哈希表和字典树 在搜索和检索数据方面效果最佳。

2. 数组 - 知识补充

数组是最简单的数据结构,这里就不讲过多了。 贴一张每个函数都运行10,000次迭代:

10,000个随机密钥在10,000个对象的数组中查找的执行效率对比图:

[ { id: "key0", content: "I ate pizza 0 times" }, { id: "key1", content: "I ate pizza 1 times" }, { id: "key2", content: "I ate pizza 2 times" }, ... ] ["key284", "key958", "key23", "key625", "key83", "key9", ... ]

2.1 for... in为何这么慢?

for... in语法令人难以置信的缓慢。在测试中就已经比正常情况下慢近9倍的循环。

这是因为for ... in语法是第一个能够迭代对象键的JavaScript语句。

循环对象键({})与在数组([])上进行循环不同,

因为引擎会执行一些额外的工作来跟踪已经迭代的属性。

3. 堆栈:Stack

堆栈是元素的集合,可以在顶部添加项目,我们有几个实际的堆栈示例:

浏览器历史记录

撤消操作

递归以及其它。

三句话解释堆栈:

    两个原则操作:pushpopPush 将元素添加到数组的顶部,而Pop将它们从同一位置删除。

    遵循"Last In,First Out",即:LIFO,后进先出。

    没了。

3.1 堆栈的实现。

请注意,下方例子中,我们可以颠倒堆栈的顺序:底部变为顶部,顶部变为底部。

因此,我们可以分别使用数组unshiftshift方法代替pushpop

class Stack { constructor(...items) { this.reverse = false; this.stack = [...items]; } push(...items) { return this.reverse ");

4. 队列:Queue

在计算机科学中,一个队列(queue)是一种特殊类型的抽象数据类型或集合。集合中的实体按顺序保存。

而在前端开发中,最著名的队列使用当属浏览器/NodeJs中 关于宏任务与微任务,任务队列的知识。这里就不再赘述了。

在后端领域,用得最广泛的就是消息队列:Message queue:如RabbitMQActiveMQ等。

以编程思想而言,Queue可以用两句话描述:

只是具有两个主要操作的数组:unshiftpop

遵循"Fist In,first out"即:FIFO,先进先出。

4.1 队列的实现

请注意,下方例子中,我们可以颠倒堆队列的顺序。

因此,我们可以分别使用数组unshiftshift方法代替pushpop

class Queue { constructor(...items) { this.reverse = false; this.queue = [...items]; } enqueue(...items) { return this.reverse ");

5. 链表:Linked Lists

与数组一样,链表是按顺序存储数据元素。

链表不是保留索引,而是指向其他元素。

第一个节点称为头部(head),而最后一个节点称为尾部(tail)。

单链表与双向链表:

单链表是表示一系列节点的数据结构,其中每个节点指向列表中的下一个节点。

链表通常需要遍历整个操作列表,因此性能较差。

提高链表性能的一种方法是在每个节点上添加指向列表中上一个节点的第二个指针。

双向链表具有指向其前后元素的节点。

链表的优点:

链接具有常量时间 插入和删除,因为我们可以只更改指针。

与数组一样,链表可以作为堆栈运行。

链表的应用场景:

链接列表在客户端和服务器上都很有用。

在客户端上,像Redux就以链表方式构建其中的逻辑。

React 核心算法 React Fiber的实现就是链表。

React Fiber之前的Stack Reconciler,是自顶向下的递归mount/update,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验。

React Fiber解决过去Reconciler存在的问题的思路是把渲染/更新过程(递归diff)拆分成一系列小任务,每次检查树上的一小部分,做完看是否还有时间继续下一个任务,有的话继续,没有的话把自己挂起,主线程不忙的时候再继续。

在服务器上,像Express这样的Web框架也以类似的方式构建其中间件逻辑。当请求被接收时,它从一个中间件管道输送到下一个,直到响应被发出。

5.1 单链表实现

单链表的操作核心有:

push(value) - 在链表的末尾/头部添加一个节点

pop() - 从链表的末尾/头部删除一个节点

get(index) - 返回指定索引处的节点

delete(index) - 删除指定索引处的节点

isEmpty() - 根据列表长度返回true或false

print() - 返回链表的可见表示

class Node { constructor(data) { this.data = data this.next = null } } class LinkedList { constructor() { this.head = null this.tail = null // 长度非必要 this.length = 0 } push(data) { // 创建一个新节点 const node = new Node(data) // 检查头部是否为空 if (this.head === null) { this.head = node this.tail = node } this.tail.next = node this.tail = node this.length++ } pop(){ // 先检查链表是否为空 if(this.isEmpty()) { return null } // 如果长度为1 if (this.head === this.tail) { this.head = null this.tail = null this.length-- return this.tail } let node = this.tail let currentNode = this.head let penultimate while (currentNode) { if (currentNode.next === this.tail) { penultimate = currentNode break } currentNode = currentNode.next } penultimate.next = null this.tail = penultimate this.length -- return node } get(index){ // 处理边界条件 if (index === 0) { return this.head } if (index < 0 || index > this.length) { return null } let currentNode = this.head let i = 0 while(i < index) { i++ currentNode = currentNode.next } return currentNode } delete(index){ let currentNode = this.head if (index === 0) { let deletedNode currentNode.next = this.head deletedNode = currentNode this.length-- return deletedNode } if (index < 0 || index > this.length) { return null } let i = 0 let previous while(i < index) { i++ previous = currentNode currentNode = currentNode.next } previous.next = currentNode.next this.length-- return currentNode } isEmpty() { return this.length === 0 } print() { const list = [] let currentNode = this.head while(currentNode){ list.push(currentNode.data) currentNode = currentNode.next } return list.join(" => ") } }

测试一下:

const l = new LinkedList() // 添加节点 const values = ["A", "B", "C"] values.forEach(value => l.push(value)) console.log(l) console.log(l.pop()) console.log(l.get(1)) console.log(l.isEmpty()) console.log(l.print())

5.2 双向链表实现

1. 双向链表的设计

类似于单链表,双向链表由一系列节点组成。每个节点包含一些数据以及指向列表中下一个节点的指针和指向前一个节点的指针。这是JavaScript中的简单表示:

class Node { constructor(data) { // data 包含链表项应存储的值 this.data = data; // next 是指向列表中下一项的指针 this.next = null; // prev 是指向列表中上一项的指针 this.prev = null; } }

还是敲一遍吧:

class DoublyLinkedList { constructor() { this.head = null; this.tail = null; } // 各种操作方法 // ... }

2. 双向链表的操作方法

Append & AppendAt: 在链表的尾部/ 指定位置添加节点

append( item ) { let node = new Node( item ); if(!this.head) { this.head = node; this.tail = node; } else { node.prev = this.tail; this.tail.next = node; this.tail = node } }

appendAt( pos, item ) { let current = this.head; let counter = 1; let node = new Node( item ); if( pos == 0 ) { this.head.prev = node node.next = this.head this.head = node } else { while(current) { current = current.next; if( counter == pos ) { node.prev = current.prev current.prev.next = node node.next = current current.prev = node } counter++ } } }

Remove & RemoveAt: 在链表的尾部/ 指定位置删除节点

remove( item ) { let current = this.head; while( current ) { if( current.data === item ) { if( current == this.head && current == this.tail ) { this.head = null; this.tail = null; } else if ( current == this.head ) { this.head = this.head.next this.head.prev = null } else if ( current == this.tail ) { this.tail = this.tail.prev; this.tail.next = null; } else { current.prev.next = current.next; current.next.prev = current.prev; } } current = current.next } }

removeAt( pos ) { let current = this.head; let counter = 1; if( pos == 0 ) { this.head = this.head.next; this.head.prev = null; } else { while( current ) { current = current.next if ( current == this.tail ) { this.tail = this.tail.prev; this.tail.next = null; } else if( counter == pos ) { current.prev.next = current.next; current.next.prev = current.prev; break; } counter++; } } }

Reverse: 翻转双向链表

reverse(){ let current = this.head; let prev = null; while( current ){ let next = current.next current.next = prev current.prev = next prev = current current = next } this.tail = this.head this.head = prev }

Swap:两节点间交换。

swap( nodeOne, nodeTwo ) { let current = this.head; let counter = 0; let firstNode; while( current !== null ) { if( counter == nodeOne ){ firstNode = current; } else if( counter == nodeTwo ) { let temp = current.data; current.data = firstNode.data; firstNode.data = temp; } current = current.next; counter++; } return true }

IsEmpty & Length:查询是否为空或长度。

length() { let current = this.head; let counter = 0; while( current !== null ) { counter++ current = current.next } return counter; } isEmpty() { return this.length() < 1 }

Traverse: 遍历链表

traverse( fn ) { let current = this.head; while( current !== null ) { fn(current) current = current.next; } return true; }

每一项都加10

Search:查找节点的索引。

search( item ) { let current = this.head; let counter = 0; while( current ) { if( current.data == item ) { return counter } current = current.next counter++ } return false; }

6. 树:Tree

计算机中经常用到的一种非线性的数据结构——树(Tree),由于其存储的所有元素之间具有明显的层次特性,因此常被用来存储具有层级关系的数据,比如文件系统中的文件;也会被用来存储有序列表等。

在树结构中,每一个结点只有一个父结点,若一个结点无父节点,则称为树的根结点,简称树的根(root)。

每一个结点可以有多个子结点。

没有子结点的结点称为叶子结点。

一个结点所拥有的子结点的个数称为该结点的度。

所有结点中最大的度称为树的度。树的最大层次称为树的深度。

6.1 树的分类

常见的树分类如下,其中我们掌握二叉搜索树即可。

二叉树:Binary Search Tree

AVL树:AVL Tree

红黑树:Red-Black Tree

线段树: Segment Tree - with min/max/sum range queries examples

芬威克树:Fenwick Tree (Binary Indexed Tree)

6.2 树的应用

    DOM树。每个网页都有一个树数据结构。

2. VueReactVirtual DOM也是树。

6.3 二叉树:Binary Search Tree

二叉树是一种特殊的树,它的子节点个数不超过两个。

且分别称为该结点的左子树(left subtree)与右子树(right subtree)。

二叉树常被用作二叉查找树和二叉搜索树、或是二叉排序树(BST)。

6.4 二叉树的遍历

按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次,这个操作被称为树的遍历,是对树的一种最基本的运算。

由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

按照根节点访问的顺序不同,二叉树的遍历分为以下三种:前序遍历,中序遍历,后序遍历;

前序遍历Pre-Order

根节点->左子树->右子树

中序遍历In-Order

左子树->根节点->右子树

后序遍历Post-Order

左子树->右子树->根节点

因此我们可以得之上面二叉树的遍历结果如下:

前序遍历:ABDEFGC

中序遍历:DEBGFAC

后序遍历:EDGFBCA

6.5 二叉树的实现

class Node { constructor(data) { this.left = null this.right = null this.value = data } } class BST { constructor() { this.root = null } // 二叉树的各种操作 // insert(value) {...} // insertNode(root, newNode) {...} // ...

1. insertNode& insert:插入新子节点/节点

insertNode(root, newNode) { if (newNode.value < root.value) { // 先执行无左节点操作 (!root.left) ");

2. removeNode& remove:移除子节点/节点

removeNode(root, value) { if (!root) { return null } // 从该值小于根节点开始判断 if (value < root.value) { root.left = this.removeNode(root.left, value) return root } else if (value > root.value) { root.right = tis.removeNode(root.right, value) return root } else { // 如果没有左右节点 if (!root.left && !root.right) { root = null return root } // 存在左节点 if (root.left) { root = root.left return root // 存在右节点 } else if (root.right) { root = root.right return root } // 获取正确子节点的最小值以确保我们有有效的替换 let minRight = this.findMinNode(root.right) root.value = minRight.value // 确保删除已替换的节点 root.right = this.removeNode(root.right, minRight.value) return root } } remove(value) { if (!this.root) { return "Tree is empty!" } else { this.removeNode(this.root, value) } }

3. findMinNode:获取子节点的最小值

findMinNode(root) { if (!root.left) { return root } else { return this.findMinNode(root.left) } }

4. searchNode & search:查找子节点/节点

searchNode(root, value) { if (!root) { return null } if (value < root.value) { return this.searchNode(root.left, value) } else if (value > root.value) { return this.searchNode(root.right, value) } return root } search(value) { if (!this.root) { return "Tree is empty" } else { return Boolean(this.searchNode(this.root, value)) } }

    Pre-Order:前序遍历

preOrder(root) { if (!root) { return "Tree is empty" } else { console.log(root.value) this.preOrder(root.left) this.preOrder(root.right) } }

    In-Order:中序遍历

inOrder(root) { if (!root) { return "Tree is empty" } else { this.inOrder(root.left) console.log(root.value) this.inOrder(root.right) } }

    Post-Order:后序遍历

postOrder(root) { if (!root) { return "Tree is empty" } else { this.postOrder(root.left) this.postOrder(root.right) console.log(root.value) } }

7. 图:Graph

图是由具有边的节点集合组成的数据结构。图可以是定向的或不定向的。

图的介绍普及,找了一圈文章,还是这篇最佳:

Graphs—-A Visual Introduction for Beginners

7.1 图的应用

在以下场景中,你都使用到了图:

使用搜索服务,如Google,百度。

使用LBS地图服务,如高德,谷歌地图。

使用社交媒体网站,如微博,Facebook

图用于不同的行业和领域:

GPS系统和谷歌地图使用图表来查找从一个目的地到另一个目的地的最短路径。

社交网络使用图表来表示用户之间的连接。

Google搜索算法使用图 来确定搜索结果的相关性。

运营研究是一个使用图 来寻找降低运输和交付货物和服务成本的最佳途径的领域。

甚至化学使用图 来表示分子!

图,可以说是应用最广泛的数据结构之一,真实场景中处处有图。

7.2 图的构成

图表用于表示,查找,分析和优化元素(房屋,机场,位置,用户,文章等)之间的连接。

1. 图的基本元素

节点:Node,比如地铁站中某个站/多个村庄中的某个村庄/互联网中的某台主机/人际关系中的人.

边:Edge,比如地铁站中两个站点之间的直接连线, 就是一个边。

2. 符号和术语

|V|=图中顶点(节点)的总数。

|E|=图中的连接总数(边)。

在下面的示例中

|V| = 6 |E| = 7

3. 有向图与无向图

图根据其边(连接)的特征进行分类。

1. 有向图

在有向图中,边具有方向。它们从一个节点转到另一个节点,并且无法通过该边返回到初始节点。

如下图所示,边(连接)现在具有指向特定方向的箭头。 将这些边视为单行道。您可以向一个方向前进并到达目的地,但是你无法通过同一条街道返回,因此您需要找到另一条路径。

有向图
2. 无向图

在这种类型的图中,边是无向的(它们没有特定的方向)。将无向边视为双向街道。您可以从一个节点转到另一个节点并返回相同的“路径”。

4. 加权图

在加权图中,每条边都有一个与之相关的值(称为权重)。该值用于表示它们连接的节点之间的某种可量化关系。例如:

权重可以表示距离,时间,社交网络中两个用户之间共享的连接数。

或者可以用于描述您正在使用的上下文中的节点之间的连接的任何内容。

著名的Dijkstra算法,就是使用这些权重通过查找网络中节点之间的最短或最优的路径来优化路由。

5. 稀疏图与密集图

当图中的边数接近最大边数时,图是密集的。

密集图

当图中的边数明显少于最大边数时,图是稀疏的。

稀疏图

6. 循环

如果你按照图中的一系列连接,可能会找到一条路径,将你带回到同一节点。这就像“走在圈子里”,就像你在城市周围开车一样,你走的路可以带你回到你的初始位置。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6707.html

相关文章

  • 摩拜前端周刊第8期

    摘要:地址前端词典提高幸福感的个技巧推荐文章介绍了个更加简洁优雅的使用技巧。这些技巧确实在实际开发中十分常用,作者总结的很好,特别是针对降级问题又学到了一个新思路。值得奋战在一线的攻城狮们阅读学习。Ladies and 乡亲们,摩拜前端周刊起航啦~ 摩拜前端团队会收集每周前端优秀文章,每周五发布至掘金平台,欢迎关注我们~ 过个没什么了不起的一天,耀眼一些,你有资格 Top 榜 「中高级前端」...

    lykops 评论0 收藏0
  • 窥探 Script 标签(步入现代 Web 开发魔法世界

    摘要:而且默认带有执行的顺序是,,即便是内联的,依然具有属性。模块脚本只会执行一次必须符合同源策略模块脚本在跨域的时候默认是不带的。通常被用作脚本被禁用的回退方案。最后标签真的令人感到兴奋。 窥探 Script 标签 0x01 什么是 script 标签? script 标签允许你包含一些动态脚本或数据块到文档中,script 标签是非闭合的,你也可以将动态脚本或数据块当做 script 的...

    Terry_Tai 评论0 收藏0
  • 窥探 Script 标签(步入现代 Web 开发魔法世界

    摘要:而且默认带有执行的顺序是,,即便是内联的,依然具有属性。模块脚本只会执行一次必须符合同源策略模块脚本在跨域的时候默认是不带的。通常被用作脚本被禁用的回退方案。最后标签真的令人感到兴奋。 窥探 Script 标签 0x01 什么是 script 标签? script 标签允许你包含一些动态脚本或数据块到文档中,script 标签是非闭合的,你也可以将动态脚本或数据块当做 script 的...

    gaosboy 评论0 收藏0
  • 双十二大前端工程师读书清单

    摘要:本文最早为双十一而作,原标题双大前端工程师读书清单,以付费的形式发布在上。发布完本次预告后,捕捉到了一个友善的吐槽读书清单也要收费。这本书便从的异步编程讲起,帮助我们设计快速响应的网络应用,而非简单的页面。 本文最早为双十一而作,原标题双 11 大前端工程师读书清单,以付费的形式发布在 GitChat 上。发布之后在读者圈群聊中和读者进行了深入的交流,现免费分享到这里,不足之处欢迎指教...

    happen 评论0 收藏0
  • 双十二大前端工程师读书清单

    摘要:本文最早为双十一而作,原标题双大前端工程师读书清单,以付费的形式发布在上。发布完本次预告后,捕捉到了一个友善的吐槽读书清单也要收费。这本书便从的异步编程讲起,帮助我们设计快速响应的网络应用,而非简单的页面。 本文最早为双十一而作,原标题双 11 大前端工程师读书清单,以付费的形式发布在 GitChat 上。发布之后在读者圈群聊中和读者进行了深入的交流,现免费分享到这里,不足之处欢迎指教...

    余学文 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<