这是我参与更文挑战的第9天,活动详情查看: 更文挑战
前言
链表这个数据结构,在我们前端开发中用的比较少,但是抵不住源码中有这样的数据结构。如果你对链表中的基本操作你都不是很熟悉的话。你很难看懂源码中的一些方法。本篇文章还是结合Leecode带你领略链表中的奥秘。
定义
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
实现js版本的链表
// 单个链表节点
class ListNode {
constructor(value, next) {
this.value = value || 0
this.next = next || null
}
}
// 所有链表的节点
class LinkList {
constructor(arr) {
this.list = LinkList.arr2List(arr)
}
// 数组转链表的一个方法
static arr2List(arr) {
if (!Array.isArray(arr) && !arr.length) {
throw 'errr'
}
let head = new ListNode(arr[0])
let current = head
arr.slice(1).forEach((item) => {
let node = new ListNode(item)
current.next = node
current = node
})
return head
}
}
上面这段主要是定义了链表节点属性和链表,以及一个链表上一个静态方法: 数组转链表。
链表的遍历
forEach() {
let current = this.list
let arr = []
// 一直遍历节点
while (current) {
arr.push(current.value)
current = current.next
}
return arr
}
链表的遍历,其实就是不断取next节点。
插入一个链表节点
// k 表示位置 node 节点
insert(k, node) {
let current = this.list
let i = 0
while (current) {
if (i === k) {
let behind = current.next
current.next = node
node.next = behind
}
current = current.next
i++
}
return this.list
}
在某个位置插入链表节点, 就当前链表的节点next 指向后一个, k位置的下一个节点执向当前节点。
反转链表
reverse() {
let cur = this.list
let pre = null
// 遍历当前链表
while (cur !== null) {
// 将当前指针指向前一个 并且 得记住当前后面的节点
let temp = cur.next
// 将当前节点指向上一个节点
cur.next = pre
// 改变上一个节点
pre = cur
cur = temp
}
return pre
}
链表的反转就是定义一个节点,不断改变节点的指向。
判断是不是回文链表
// 判断当前链表是不是回文链表
isParionList() {
let current = this.list
let res = []
while (current) {
res.push(current.val)
current = current.next
}
// 利用数组reverse 方法
return res.reverse().toString() == res.toString()
}
利用数组reverse去判断,虽然比较蠢,但是也是实现了。
求两个链表相交的地方
static getIntersectionNode(headA, headB) {
// 用一个集合去存储每个节点
const set = new Set()
let current = headA
while (current !== null) {
set.add(current)
current = current.next
}
current = headB
while (current !== null) {
// 如果存在节点相同 就返回就好
if (set.has(current)) {
return current
}
current = current.next
}
return current
}
总结
至此,我们对链表这个数据结构有了一定的认知,由于其非连续内存的特性导致链表非常适用于频繁插入、删除的场景,而不见长于读取场景,这跟数组的特性恰好形成互补,所以现在也可以回到题目中的问题了,链表的特性与数组互补,各有所长,而且链表由于指针的存在可以形成环形链表,在特定场景也非常有用,因此链表的存在是很有必要的。