前端算法小结 | 链表篇
前端算法小结
写在前面
这是本系列小结的第三篇,今天来讲讲数据结构中的链表。
前端开发需要掌握的几种数据结构
还是先罗列一下前端开发所需要的掌握的数据结构:
- 数组
- 栈
- 队列
- 链表
- 树(二叉树)
本文将跟大家一起聊聊链表。
链表
链表常常用来与数组作比较。它与数组相似,但是最大的区别在于它是一种非连续的存储结构。数组在内存中一般是占据一段连续的存储空间,而链表中的节点则是分散在存储空间的各个角落里。
对于链表中的节点,我们一般需要关注它以下两部分内容:
该节点本身的数据
该节点的指针指向的下一个节点
在JS中我们常常这样表示链表的数据结构:
JS 中链表的常规操作
创建
链表的创建关键在于链表节点的创建:
添加与删除
在链表中,无论是添加还是删除节点,其实都是在对节点 next 指针进行操作。
添加链表节点分为两种情况:
在链表后面添加节点
这种场景比较简单,不过就是将 next 指向新的节点
在两个节点中间插入节点
在两个已有的节点中间插入新的节点,其实就是把第一个节点的 next 指向新的节点,再把新节点的 next 指向第二个节点
删除节点
删除节点其实就是把被删除节点的前一个节点的next,指向被删除节点原本的next(即被删除节点原本的下一个节点)
访问节点
链表节点的访问就不像节点操作那么轻松了。你必须要明确你要找的是第几个节点,然后再遍历链表去获取到该节点
小结
由上述可知:
- 链表中操作节点的复杂度是 O(1),因为它只需要改变被操作节点前后节点的指针指向,不涉及到其他节点的操作
- 链表中访问节点的复杂度是 O(n),因为它不像数组一样可以通过下标直接访问节点,而是需要遍历链表才能找到指定的节点
JS 中链表的骚操作
哨兵节点
通常,我们对链表节点的操作都是对节点本身以及其 next 指针的操作。但是在某些特殊场景下,我们还需要对当前节点的前驱节点进行操作。
这中情况下,为了确保链表的第一个节点也能按我们预期的算法去运行,我们就必须要为其创造一个前驱节点。这个思路与上一节 栈和队列篇 中介绍过的哨兵的原理是一样的,因此我也称之为 哨兵节点。(在很多题解中也称之为节点)
快慢指针
快慢指针其实是双指针的其中一种形态。通常情况下,我们借助双指针是为了解决一些比较耗时的操作,比如需要多次遍历的操作,可以借助双指针一次遍历搞定。
而在链表中,比较耗时的操作就是涉及到与链表位置相关的操作了:比如删除指定位置的节点 反转指定位置的链表等等。当遇到这种题时,我们要下意识的往双指针的方向去思考。
快慢指针特指同时从同一方向出发(链表第一个节点出发)的两个指针,其中慢指针走的慢一些(一次向后走一个节点),而快指针走的快一些(一次向后走 n 个节点,n 视情况而定)
反转链表
相信刷过题的各位都看到过这句话哈哈。这充分说明了反转链表在链表题中的地位。
反转链表顾名思义,就是操作一个链表中某段区间内的结点,把这些节点间的 next 指针指向进行反转,将原本指向下一个节点的指针改为指向其前驱结点。
而反转一个链表,我们必须知道三个结点:
- 当前结点(currentNode)
- 当前结点的前驱结点(preNode)
- 当前结点的后继结点(nextNode)
常规的反转操作如下:
而反转链表的操作远不止这些,这后面的真题解析中我会继续深入反转链表。