链表是由多个节点共同组成的一组线性数据结构。每个节点都包含一个数据域和一个指针域,数据域用于存储数据,指针域用于存储下一个节点的地址。
1class Node {2value: number3next: Node | null4}
在内存的存储上,链表并不关注内存空间是否连续,而是通过相同类型的指针将多个节点串联在一起。因此,和数组相比
和数组不同,我们并不需要完整的定义链表结构,只需要定义链表的首节点、以及链表节点的类型即可。
1class Node {2value: number3next: Node | null4}56const linkedList = new Node(1, null)
在后续的操作过程中,如果有新的节点加入,只需要将新节点的 next
指针指向当前的节点即可。
1const newNode = new Node(2, null)2linkedList.next = newNode
我们也可以再定义一个变量,用于记录链表的尾节点
1const node = new Node(2, null)23// 头节点4const head = node5// 尾节点6const tail = node
在末尾插入一个节点时
1// 将新节点插入到尾节点后面2const newNode = new Node(3, null)3tail.next = newNode4// 更新尾节点5tail = newNode
用老的尾节点的 next
指针,指向新的节点,表示新的节点插入到链表的末尾
然后,我们还要更新尾节点的指针,指向新的尾节点
注意观察 tail
的变化。链表在写法上是最简单的数据结构,但是,由于过于灵活,因此,代码写出来之后,理解起来会比较绕,新手朋友很难快速的理解链表的各个指针变来变去的到底在干啥,因此,学习链表时要
稍微把速度放慢一点,结合图例慢慢消化
由于链表不是连续的内存结构,因此,我们不能直接用下标来访问链表中的元素。而是需要从头节点开始,依次遍历,直到找到第 n 个节点。
10function getNode(head: Node, n: number) {20let current = head30if (n === 0) {40return current50}60for (let i = 1; i < n; i++) {70if (!current) {80return null90}10current = current.next11}12return current13}
时间复杂度为 ,因为需要遍历链表中的每个节点,直到找到第 n 个节点。所以链表的访问效率比数组低很多。
由于我们只定义了头结点和尾节点的指针,因此,如果在中间插入一个节点,需要先找到插入的位置,然后修改指针的指向。
10function insertNode(head: Node, n: number, value: number) {20const newNode = new Node(value, null)3040if (n === 0) {50newNode.next = head60head = newNode70return head80}9010let current = head1112for (let i = 1; i < n - 1; i++) {13current = current.next14}15const next = current.next16current.next = newNode17newNode.next = next1819return head20}
图例演示如下
对于链表而言,删除节点的操作比较好理解。例如,我们删除给定节点的下一个节点,只需要将给定节点的 next
指针,指向给定节点的下一个节点的下一个节点即可。
10function deleteNode(current: Node) {20if (!current.next) {30return null40}5060// 获取要删除的节点70const p = current.next8090// 将当前节点的 next 指针,指向要删除的节点的下一个节点10current.next = p.next1112// 释放要删除的节点13p.next = null1415return p16}
但是有的时候,我们很难找到要删除的节点是哪一个。例如,我想要删除给定节点的上一个节点。此时就需要从头部开始遍历,找到给定节点的前一个节点。所以成本很高,这个时候,我们可以构建一个双向链表,这样就可以从任意一个节点,快速找到前一个节点和后一个节点。
10function deletePrevNode(current: Node) {20if (!current.prev) {30return null40}5060// 获取要删除的节点70const p = current.prev80// p 的前一个节点90const prev = p.prev1011// 将当前节点的 prev 指针,指向要删除的节点的上一个节点12current.prev = prev13// 将 p 的前一个节点的 next 指针,指向当前节点14prev.next = current1516// 释放要删除的节点17p.prev = null18p.next = null1920return p21}
一定要注意指针的变动。反复体会。链表是一个理解起来比较容易的概念,但是在代码的表现上理解起来会很困难,因为指针的变动可读性本身就比较差。所以,学习的时候一定要速度放慢,理解清楚了再往下走。
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
1输入:l1 = [1, 2, 4], l2 = [1, 3, 4]2输出:[1, 1, 2, 3, 4, 4]
10/**20* Definition for singly-linked list.30* class ListNode {40* val: number50* next: ListNode | null60* constructor(val?: number, next?: ListNode | null) {70* this.val = (val===undefined ? 0 : val)80* this.next = (next===undefined ? null : next)90* }10* }11*/1213function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null {1415};
链表的基础知识比较少,我们在后续的学习中,还会基于链表实现栈、队列等数据结构,后续的每日一题中,还会基于链表解决更多的难题。
接下来还会学习循环链表、双向链表、双向循环链表等。