基础数据结构,最基础的就只有两种,一个是数组,一个是链表。其余的数据结构都是在它们之上抽象出来的,好比,栈,队列,散列表,树,图等。javascript
数组在内存中是申请的一组连续的内存空间,在一些强类型语言中,初始化数组时是要事先指定数组大小和类型的,一旦完成,则数组大小和类型不可改变。咱们常说的对数组的动态扩容等,其实也是申请更大的数组,而后把旧数组里的数据搬移到新数组里。在 Javascript 中,数组却没有这样的限制,能够随意增长内容,随意改变数组的大小,且数组里元素类型能够不同。JavaScript 底层中实现数组时,若是元素是同类型的,则使用的是连续的内存空间,若是是不一样类型的元素,则应该使用的是相似哈希结构。Javascript 中的TypeArray就是使用的连续的内存空间。html
对数组中任意位置元素的访问,是十分高效的。咱们能够根据下标值,很快就能够在 O(1)时间内完成访问,而链表则须要移动屡次 head 指针才能完成。因为它是一组连续的内存空间,计算机能够一次性把它所有读入内存缓冲区中,下次访问其余位置元素,只须要计算偏移量就能够从内存缓冲区中读取,速度是很是快的。可是对于频繁的插入和删除操做,可能就涉及动态扩容或者维护数据的有序性,那么就会存在额外的数据搬移工做,额外花费的时间多是 O(n)。java
链表是由一个一个节点连接起来的,每一个节点会存储当前节点的值,还会有一个 next 指针,指向下一个节点。对于第一个节点,会有一个 head 指针指向它,最后一个节点的 next 指向 null。它天生就支持动态扩容或者缩容,因为对内存空间不要求连续,对内存利用率更高。若是须要扩容,就增长节点,插入到链表的某一个节点后面。若是要缩容,就删除释放掉部分不用的节点。git
/* 链表节点 链表节点用于组成单向链表,双向链表,循环链表等。 */
class LinkedNode<Item> {
public val: Item
public next: LinkedNode<Item>
public pre: LinkedNode<Item>
constructor(val: Item, next: LinkedNode<Item>, pre: LinkedNode<Item> = null) {
this.val = val
this.next = next
this.pre = pre
}
}
复制代码
因为它的不连续性,咱们在访问链表中某个位置的节点数据时,须要从头开始遍历 head 指针,直到 head 指向要访问的节点,须要的时间复杂度为 O(n)。对于删除或者插入数据,只须要简单的改变上一个节点和当前节点的 next 指针便可,不须要额外的搬移其余节点,时间复杂度通常为 O(1)。github
链表有多种变体,好比双向链表,循环链表,双向循环链表等。双向链表,就是节点不只有一个 next 指针,还有一个 pre 指针,指向前一个节点。因为单向链表只有一个 next 指针,因此只能日后遍历,而双向链表,既能够日后遍历,也能够根据 pre 指针往前遍历,使用很是方便,而且只须要多存储一个 pre 指针便可。在实际应用中,更多的是使用双向链表。循环链表,就是最后一个节点的 next 不指向 null,而是指向第一个节点,从而造成了一个环。typescript
因为链表中每一个节点不只存储了值,还须要额外的空间存储 next 指针(双向链表还须要存储 pre 指针),因此对于相同数据而言,链表花费的内存空间比数组要大。数组
对于链表的掌握,我作了以下一些练习,你能够看看,浏览器
访问任意位置元素 | 插入或者删除某一个元素 | 内存空间 | 使用内存大小 | |
---|---|---|---|---|
数组 | O(1) | O(n) | 连续 | 较小 |
链表 | O(n) | O(1) | 不连续 | 较大 |
栈是一种抽象的 LIFO(last in , first out)数据结构。用数组实现的栈,通常称为顺序栈,用链表实现的栈,通常称为链式栈;实际应用中,顺序栈使用较多。栈通常暴露出来的操做,只有出栈和入栈,可能还会有清空,查找等其余辅助操做。它遵循后进,先出的策略,只有经过不停的出栈操做才能遍历或者访问它最开始加入的数据。数据结构
函数调用栈,就是用的这种结构,在一个函数 A 中调用另一个函数 B,就会先把函数 B 压入到执行栈里,当函数 B 执行完毕以后,就会把函数 B 出栈,继续执行栈顶函数 A。特别是对于递归调用,咱们要控制终止条件,否则就会出现递归次数过多,抛出maximum-call-stack-size-exceeded-error 的错误。解决办法能够将递归转化为迭代,或者使用尾递归优化。函数
对于栈的掌握,我作了以下一些练习,你能够看看,
队列是一种抽象的 FIFO(first in, first out)数据结构。同理,队列也能够用数组或者链表实现。实际应用中,顺序队列使用比较多。队列通常暴露出来的操做,只有入队列和出队列,可能还有清空队列,查找等其余辅助操做。它遵循先进,先出的策略,后加入的元素放在队尾,相似于咱们生活中排队买票同样。
JavaScript 中常说的event loop,就是队列的应用之一。它会不断的从可执行队列中出队列,取出一个可执行的函数,而后将它放入执行栈中执行。咱们在实现 IO 操做,事件监听,或者setTimeout
时就会入队列操做,将执行函数放入队列末尾。若是更加深刻,JavaScript 的 event loop 分为两种队列,一个是 macrotask,一个是 microtask,这里不作更加深刻的探讨。
在使用广度优先搜素(BFS)遍历图时,队列也是经常使用的数据结构。初始时,随机选择一个节点入队列,而后经过每次从队列里出队列一个节点,访问它,而后把它全部的关联节点都入队列。这样当队列为空时,整个图全部节点就都被访问到了。
队列也有变体,循环队列,优先级队列。循环队列,跟循环链表相似,循环队列只是咱们思惟抽象上的环。因为队列入队列时,只能加到队尾,当一个固定大小的队列的尾部有元素时,咱们就没法再执行入队列了,即便队列前面有空的位置,这将致使内存空间的浪费。解决办法之一就是咱们每次执行出队列时,都移动队列中元素,填充第一个空的位置,这样虽然能够防止队列空间的浪费,可是每次搬移队列中数据,将致使性能急剧降低。解决办法之二就是使用咱们的循环队列,计算队尾位置时,并非咱们固定的数组最后一个位置,而是结合队首空的位置来计算。优先级队列,出队列逻辑并非先入队列的元素出队列,而是优先级高的元素先出队列,若是优先级相同,则先入队列的元素先出队列。堆的应用之一就是优先级队列。
对于队列的掌握,我作了以下一些练习,你能够看看,
散列表又叫哈希表,一般是经过键(key)来存储一个值(value),也就是经常使用的 key-value 结构。散列表是基于数组抽象出来的,不过它是经过一个 key 来访问一个 value 的,时间复杂度也是 O(1)。当咱们存储一个 key-value 时,会先经过散列函数和 key 计算出一个非负整数 index,再把 value 存在下标为 index 的位置。经过 key 查询 value 时,过程也是相似的,也是先经过散列函数和 key 计算出下标 index,而后返回数组中下标为 index 的位置值。
const index = hash(key)
复制代码
一个好的散列函数,既要计算过程简单,不能消耗太多时间,也要知足生成的下标 index 随机且分布均匀。若是散列函数计算过程很是复杂,每次插入或者查询时都将花费更多的时间,影响性能。若是散列函数生成的 index 不够随机分布,就会增长发生散列冲突的几率,解决散列冲突也会花费额外的时间,也会影响性能。
若是 k1 经过散列函数获得 i1,咱们把 v1 存在数组下标为 i1 的位置;若是 k2 经过散列函数也到 i1,因为 i1 的位置已经被 v1 使用了,v2 不能直接存在 i1 的位置,这个时候就发生了散列冲突。散列冲突的几率不只受散列函数的影响,也受当前装载因子大小的影响。当装载因子太高时,能够启动动态扩容,减小散列冲突,当装载因子太低时,能够启动动态缩容,释放没有使用的内存空间。
装载因子 = 数组已经使用的元素个数 / 数组的长度
解决散列冲突常见的方法一种是开放寻址法,一种是链表法。开放寻找法中有一种是线性探测,简单点说就是若是当前位置 i1 已经被使用了,就继续遍历数组后面的位置,直到找到一个为空的位置,而后将 v2 放入。链表法就是数组中存储的是一个链表的地址,经过散列函数获得的下标 index,而后将数组插入当前链表的尾部。这里不作深刻说明了,想继续深刻的,能够看下面的资料,