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

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

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

超过20w字的「前端面试与进阶指南」可以移步github



对于不少开发者而言,链表(linked list)这种数据结构既熟悉又陌生,熟悉是因为它确实是非常基础的数据结构,陌生的原因是我们在业务开发中用到它的几率的确不大.

在很多情况下,我们用数组就能很好的完成工作,而且不会产生太多的差异,那么链表存在的意义是什么?链表相比于数组有什么优势或者不足吗?

什么是链表

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

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

当然,链表也有不同的形态,主要分为三种:单向链表、双向链表、循环链表.

单向链表

单向链表的节点通常由两个部分构成,一个是节点储存的值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后执行

小结

至此,我们对链表这个数据结构有了一定的认知,由于其非连续内存的特性导致链表非常适用于频繁插入、删除的场景,而不见长于读取场景,这跟数组的特性恰好形成互补,所以现在也可以回到题目中的问题了,链表的特性与数组互补,各有所长,而且链表由于指针的存在可以形成环形链表,在特定场景也非常有用,因此链表的存在是很有必要的。

那么,现在有一个非常常见的一个面试向的思考题:

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



原文地址:https://www.cnblogs.com/duxinyi/p/11555940.html

时间: 2024-10-30 20:21:49

# 面试官: 既然已经有数组了,为什么还要链表的相关文章

【真实面试经历】我和阿里面试官的一次“邂逅”(附问题详解)

本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助. 本文主要涵盖下面的内容: 分布式商城系统:架构图讲解: 消息队列相关:削峰和解耦: Redis 相关:缓存穿透问题的解决: 一些基础问题: 网络相关:1.浏览器输入 URL 发生了什么? 2.TCP 和 UDP 区别? 3.TCP 如何保证传输可靠性? Java 基础:1. 既然有了字节流,为什么还要有字符流? 2.深拷贝 和 浅拷贝有啥区别呢? 下面是正文! 面试开始,坐在我前面

前端面试高频题:删除数组重复元素的多种方法

最近在想着换工作,去了一家中关村的上市公司,面试官随便问了几个问题其中就提到了怎么删除数组元素的方法,表示这已经是第三次遇到了这个问题了,由于知识口头说说所以就随便说了下,之前用的时候都是直接找的方法库虽然知道大致是怎么写的但是还没有真正写过,网上的方法还是蛮多的这里也给大家分享一个作者写的吧,希望对初学者有所帮助: //数组去重的方法 Array.prototype.unique=function(){ //集中声明变量 var oldArr=this, newArr=[oldArr[0]],

算法数据结构面试分享(六)数组排序问题(2) - 计数排序

数组排序问题(2) 昨天我们留了一道题目"给你一个整型数组,里面出现的数在[0-100] 之间,能用最优化的方法帮我排序吗". 1. 确保我们理解了问题,并且尝试一个例子,确认理解无误. 这是一道排序算法题,我们学过很多排序的算法.不一样的是,它给定一个额外的条件,数组里的每个数字都在1-100之间.如果我们采取传统的排序算法,这个条件我们好像用不上.大家在面试的时候如果发现有条件没有用上,基本上我们给出的算法可能不是最优的,或者我们没有解决它最原始的需求.举个例子{50, 46, 5

记一次面试一直出现的题——数组去重

作为一个猪脑子,请时时刻刻记住了,es6一行代码去重是 var a = [1,2,2,2,2,3,3,3,3,3,5,5,55,6,,6,66,,6,8,4,8,45,5] console.log(new Set(a)) //一脸秀死面试官 这是其中一个方法 敲黑板!!!...(三点运算符)只是纯粹的遍历数组出来,不是去重,记住了 还有其他方法: 利用数组中的filter方法 var arr = [1,2,2,2,2,23,3,3,3,5,5,4,7,8,6,1,2']; var r = arr

面试官问你斐波那契数列的时候不要高兴得太早

前言 假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了.如果真这么想,那就危险了. 递归求斐波那契数列 递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法.斐波那契数列的计算表达式很简单: 1F(n) = n; n = 0,12F(n) = F(n-1) + F(n-2),n >= 2; 因此,我们能很快根据表达式写出递归版的代码: 1/*fibo.c*/ 2#include <stdio.h> 3#include <stdlib.h&

【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)

原文:[面试]吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏) 万字长文,干货满满. 是数据结构而非类型 很多文章都会说,redis支持5种常用的数据类型,这其实是存在很大的歧义.redis里存的都是二进制数据,其实就是字节数组(byte[]),这些字节数据是没有数据类型的,只有把它们按照合理的格式解码后,可以变成一个字符串,整数或对象,此时才具有数据类型. 这一点必须要记住.所以任何东西只要能转化成字节数组(byte[])的,都可以存到redis里.管你是字符串.数字.

【深度】扒开V8引擎的源码,我找到了你们想要的前端算法(下次面试官再问算法,用它怼回去!)

算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索`Array.prototype.sort`函数下的算法实现. 来,先把你用过的和听说过的排序算法都列出来: * 快速排序 * 冒泡排序 * 插入排序 * 归并排序 * 堆排序 * 希尔排序 * 选择排序 * 计数排序 * 桶排序 * 基数排序 * ... 答题环节到了, sort 函数使用的以上哪一种算法? 如果你在网上搜索过关于 sort 源码的文章,可能会告诉你数组长度小于10用插入排序,否则用快速排序. 开始我也是

面试官: 聊一聊Babel

点击关注本公众号获取文档最新更新,并可以领取配套于本指南的 <前端面试手册> 以及最标准的简历模板. 前言 Babel 是现代 JavaScript 语法转换器,几乎在任何现代前端项目中都能看到他的身影,其背后的原理对于大部分开发者还属于黑盒,不过 Babel 作为一个工具真的有了解背后原理的必要吗? 如果只是 Babel 可能真没有必要,问题是其背后的原理在我们开发中应用过于广泛了,包括不限于: eslint jshint stylelint css-in-js prettier jsx v

面试官: 写过『通用前端组件』吗?

前言 设计前端组件是最能考验开发者基本功的测试之一,因为调用Material design.Antd.iView 等现成组件库的 API 每个人都可以做到,但是很多人并不知道很多常用组件的设计原理. 能否设计出通用前端组件也是区分前端工程师和前端api调用师的标准之一,那么应该如何设计出一个通用组件呢? 下文中提到的组件库通常是指单个组件,而非集合的概念,集合概念的组件库是 Antd iView这种,我们所说的组件库是指集合中的单个组件,集合性质的组件库需要考虑的要更多. 文章目录 前端组件库的