面试官: 既然已经有数组了,为何还要链表

本文发布于微信平台: 程序员面试官前端

超过20w字的「前端面试与进阶指南」能够移步githubnode


对于很多开发者而言,链表(linked list)这种数据结构既熟悉又陌生,熟悉是由于它确实是很是基础的数据结构,陌生的缘由是咱们在业务开发中用到它的概率的确不大.git

在不少状况下,咱们用数组就能很好的完成工做,并且不会产生太多的差别,那么链表存在的意义是什么?链表相比于数组有什么优点或者不足吗?程序员

什么是链表

链表是一种常见的基础数据结构,是一种线性表,可是并不会按线性的顺序存储数据,而是在每个节点里存到下一个节点的指针(Pointer).github

从本质上来说,链表与数组的确有类似之处,他们的相同点是都是线性数据结构,这与树和图不一样,而它们的不一样之处在于数组是一块连续的内存,而链表能够不是连续内存,链表的节点与节点之间经过指针来联系.面试

链表vs数组

固然,链表也有不一样的形态,主要分为三种:单向链表、双向链表、循环链表.算法

单向链表

单向链表的节点一般由两个部分构成,一个是节点储存的值val,另外一个就是节点的指针next.小程序

单向链表

链表与数组相似,也能够进行查找、插入、删除、读取等操做,可是因为链表与数组的特性不一样,致使不一样操做的复杂度也不一样.微信小程序

查找性能

单向链表的查找操做一般是这样的:设计模式

  1. 从头节点进入,开始比对节点的值,若是不一样则经过指针进入下一个节点
  2. 重复上面的动做,直到找到相同的值,或者节点的指针指向null

链表的查找性能与数组同样,都是时间复杂度为O(n).

插入删除性能

链表与数组最大的不一样就在于删除、插入的性能优点,因为链表是非连续的内存,因此不用像数组同样在插入、删除操做的时候须要进行大面积的成员位移,好比在a、b节点之间插入一个新节点c,链表只须要:

  1. a断开指向b的指针,将指针指向c
  2. c节点将指针指向b,完毕

这个插入操做仅仅须要移动一下指针便可,插入、删除的时间复杂度只有O(1).

链表的插入操做以下:

插入操做

链表的删除操做以下:

删除操做

读取性能

链表相比之下也有劣势,那就是读取操做远不如数组,数组的读取操做之因此高效,是由于它是一块连续内存,数组的读取能够经过寻址公式快速定位,而链表因为非连续内存,因此必须经过指针一个一个节点遍历.

好比,对于一个数组,咱们要读取第三个成员,咱们仅须要arr[2]就能快速获取成员,而链表则须要从头部节点进入,在经过指针进入后续节点才能读取.

应用场景

因为双向链表的存在,单向链表的应用场景比较少,由于不少场景双向链表能够更出色地完成.

可是单向链表并不是无用武之地,在如下场景中依然会有单向链表的身影:

  1. 撤销功能,这种操做最常见于各类文本、图形编辑器中,撤销重作在编辑器场景下属于屡见不鲜,单向链表因为良好的删除特性在这个场景很适用
  2. 实现图、hashMap等一些高级数据结构

双向链表

咱们上文已经提到,单向链表的应用场景并很少,而真正在生产环境中被普遍运用的正是双向链表.

双向链表与单向链表相比有何特殊之处?

双向链表

咱们看到双向链表多了一个新的指针prev指向节点的前一个节点,固然因为多了一个指针,因此双向链表要更占内存.

别小看双向链表多了一个前置指针,在不少场景里因为多了这个指针,它的效率更高,也更加实用.

好比编辑器的「undo/redo」操做,双向链表就更加适用,因为拥有先后指针,用户能够自由得进行先后操做,若是这个是一个单向链表,那么用户须要遍历链表这时的时间复杂度是O(n).

真正生产级应用中的编辑器采用的数据结构和设计模式更加复杂,好比Word就是采用Piece Table数据结构加上Command queue模式实现「undo/redo」的,不过这是后话了.

循环链表

循环链表,顾名思义,他就是将单向链表的尾部指针指向了链表头节点:

循环链表

循环链表一个应用场景就是操做系统的分时问题,好比有一台计算机,可是有多个用户使用,CPU要处理多个用户的请求极可能会出现抢占资源的状况,这个时候计算机会采起分时策略来保证每一个用户的使用体验.

每一个用户均可以当作循环链表上的节点,CPU会给每一个节点分配必定的处理时间,在必定的处理时间后进入下一个节点,而后无限循环,这样能够保证每一个用户的体验,不会出现一个用户抢占CPU而致使其余用户没法响应的状况.

固然,约瑟夫环的问题是单向循环链表的表明性应用,感兴趣的能够深刻了解下.

固然若是是双向链表首尾相接呢?这就是双向循环链表.

在Node中有一类场景,没有查询,可是却有大量的插入和删除,这就是Timer模块。 几乎全部的网络I/O请求,都会提供timeout操做控制socket的超时情况,这里就会大量使用到setTimeout,而且这些timeout定时器,绝大部分都是用不到的(数据按时正常响应),那么又会有响应的大量clearTimeout操做,所以node采用了双向循环链表来提升Timer模块的性能,至于其中的细节就再也不赘述了.

插入!
TimersList <-----> timer1 <-----> timer2 <-----> timer4 <-----> timer3 <-----> ......
                1000ms后执行     1050ms后执行    1100ms后执行    1200ms后执行
复制代码

Node.js 核心模块 Timers 详解

小结

至此,咱们对链表这个数据结构有了必定的认知,因为其非连续内存的特性致使链表很是适用于频繁插入、删除的场景,而不见长于读取场景,这跟数组的特性刚好造成互补,因此如今也能够回到题目中的问题了,链表的特性与数组互补,各有所长,并且链表因为指针的存在能够造成环形链表,在特定场景也很是有用,所以链表的存在是颇有必要的。

那么,如今有一个很是常见的一个面试向的思考题:

咱们平时在用的微信小程序会有最近使用的功能,时间最近的在最上面,按照时间顺序日后排,当用过的小程序大于必定数量后,最不经常使用的小程序就不会出现了,你会如何设计这个算法?

2019-09-07-01-20-00


相关文章
相关标签/搜索