「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
链表初识
链表和数组相似,它们都是有序的列表、都是线性结构(有且仅有一个前驱、有且仅有一个后继)。不同点在于,链表中,数据单位的名称叫做“结点”,而结点和结点的分布,在内存中可以是离散的。
数组和链表的区别
数组在内存中是一段连续的内存空间。每个元素的内存地址可以根据其索引距离数组头部的距离计算出来。因此对数组来说,每一个元素都可以通过数组的索引下标直接定位。
而链表中的结点,则允许散落在内存空间的各个角落里。通过指针(next)来记录后继结点的位置。
在链表中,每一个结点的结构都包括了两部分的内容:数据域和指针域。JS 中的链表,是以嵌套的对象的形式来实现的:
{
// 数据域
val: 1,
// 指针域,指向下一个结点
next: {
val:2,
next: ...
}
}
要想访问链表中的任何一个元素,我们都得从起点结点开始,逐个访问 next,一直访问到目标结点为止。为了确保起点结点是可抵达的,我们有时还会设定一个 head 指针来专门指向链表的开始位置。
画一张图来总结一下:
链表结点的创建、插入和删除
创建
创建一个 1 -> 2 的链表
需要一个构造函数
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
const node = new ListNode(1)
node.next = new ListNode(2)
这样就创建了一个数据域为1,指向数据域为2的链表。
插入
1 -> 2 变成 1 -> 3 -> 2
const node3 = new ListNode(3)
node3.next = node1.next
node1.next = node3
删除
1 -> 2 -> 3 变成 1 -> 3
node1.next = node1.next.next
链表和数组的增删、访问操作
数组的访问
因为数组在内存中对应一串连续的空间地址,每个元素的内存地址可以根据其索引距离数组头部的距离计算出来,所以时间复杂度是 O(1)。
数组的增删
因为数组在内存中对应一串连续的空间地址,所以增加元素,后面的所有元素都要后移一位; 同理,删除元素,后面所有的元素都要前移一位,所以时间复杂度是 O(n)。
链表的访问
因为链表只能通过 next 一个一个去找,所以访问的时间复杂度是 O(n)
链表的增删
不管链表里面的结点个数 n 有多大,只要我们明确了要插入/删除的目标位置,那么我们需要做的都仅仅是改变目标结点及其前驱/后继结点的指针指向,所以增删的时间复杂度是 O(1)
小结一下:
操作 | 数组 | 链表 |
---|---|---|
访问 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
相关 leetcode
合并两个有序链表
真题描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
迭代法
不断用变量的旧值递推新值
const mergeTwoLists = function (l1, l2) {
const head = new ListNode() // 定义头结点,确保链表可以被访问到
let cur = head // 定义 cur 指针,用来把 l1 和 l2 串起来
while (l1 && l2) { // 遍历 l1 和 l2
if (l1.val < l2.val) { // 如果 l1 的值比较小,就串起 l1 ,反之串起 l2
cur.next = l1 // cur 指向 l1
l1 = l1.next // l1 向前走一个结点
} else {
cur.next = l2 // l2 和 l1 同理
l2 = l2.next
}
}
cur = cur.next // 每一轮遍历, cur向前走一个结点
cur.next = l1 || l2 // 处理 l1 和 l2 长度不同的情况
return head.next // 返回起始结点
}
时间复杂度:O(n + m)
空间复杂度:O(1)
递归法
const mergeTwoLists = function(l1, l2) {
if (l1 === null) {
return l2;
} else if (l2 === null) {
return l1;
} else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
时间复杂度:O(n + m)
空间复杂度:O(n + m)
虽然递归看似高大上且代码简洁,还是用迭代法比较好,空间复杂度更低。
删除排序链表中的重复元素
真题描述:给定一个已排序的链表的头
head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例:
输入: 1->1->2
输出: 1->2
输入: 1->1->2->3->3
输出: 1->2->3
解法
const deleteDuplicates = function (head) {
let cur = head // 设定 cur 指针,初始位置为链表第一个结点
while (cur && cur.next) { // 遍历 cur
if (cur.val === cur.next.val) { // 若当前结点和它后面一个结点值相等(重复)
cur.next = cur.next.next // 删除后面那个节点(去重)
} else {
cur = cur.next // 若不重复,继续遍历
}
}
return head
}
时间复杂度:O(n)
空间复杂度:O(1)
反转链表
真题描述:给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
示例:
输入:1 -> 2 -> 3 -> 4
输出:4 -> 3 -> 2 -> 1
解法
const reverseList = function (head) {
let pre = null // 定义两个指针: pre 和 cur ;pre 在前 cur 在后。
let cur = head
while(cur) {
const temp = cur.next // 临时变量 temp 记录 cur 的 next,因为做反转操作时 cur 的 next 会变化,但后面还需要用到最开始时 cur 的 next
cur.next = pre // 每次让 cur 的 next 指向 pre ,实现一次局部反转
pre = cur // cur 和 pre 同时往前移动一个位置
cur = temp
}
return pre // 返回pre
}
很难受,直接写没写出来,看了答案后在脑海里想也想不出来,画出来才理解了,算法还是不能空想,容易卡壳,要把过程画出来。