今天我们来学习一下链表这种数据结构。看看它有些什么特点。
1. 链表的概念
链表和数组一样,都是有序的,线性的。它和数组不一样的地方是:数组是连续的,而链表是离散的。
数组中的值一个紧接着一个,而链表可以不是。链表由节点组成,每个节点由值和地址组成,只要它的地址指向了下一个值,那么这个节点处于哪里都无所谓。
链表的结构大概长这样:
它的第一个值的地址指向下一个值。
在JS中我们可以用对象来模拟链表这种数据结构,比如:
head = {
val: 1,
next: {
val: 2,
next: {
val: 3,
next: {
val: 4,
next: null
}
}
}
}
这就是一个链表,由对象嵌套而成。最外面的这个对象就叫头节点,里面的对象就是子节点。
当我们想要访问第一个值时,就要这样写:
head.val
要访问后续的值就这样写:
head.val
head.next.val
head.next.next.val
head.next.next.next.val
我们想访问第二个节点的值,就必须先知道第一个节点的地址;想访问第三个节点的值,就必须知道第二个节点的地址。这就是链表的特性。
如果我们想往2和3之间添加一个x节点,应该怎么做呢?我们可以让2节点的地址指向x节点,再让x节点的地址指向3节点就行了。
如果我们想要删除3节点,我们可以直接让2节点的地址指向4节点,跳过3节点。
那我们怎么创建一个链表呢?我们可以自己来写一个构造函数:
function ListNode(val) {
this.val = val;
this.next = null;
}
const node = new ListNode(1)
node.next = new ListNode(2)
node.next.next = new ListNode(3)
这样node就是一个节点了,我们再去定义node的地址指向下一个节点。这样就会形成一个链表。
那链表相较于数组的优势是什么?如果我们想在数组中增删某一个值,是不是会带来时间复杂度,这个值后面所有的值都得往后挪,而链表不需要。链表中删除一个节点只需要找到它的上一个节点的地址,让它指向这个节点的下一个节点就行了。
但是在数组中访问一个值就比链表方便,直接用下标访问。而在链表中想访问一个节点,就必须知道它的上一个节点,知道上一个节点的上一个节点...
所以链表有这两个特性:
- 高效的增删节点
- 低效的访问节点
如果我们想遍历访问链表的每一个值,我们要这样访问:
head = {
val: 1,
next: {
val: 2,
next: {
val: 3,
next: {
val: 4,
next: null
}
}
}
}
const index = 4
let node = head
for (let i = 0; i < index && node; i++) {
console.log(node.val)
node = node.next
}
先定义一个变量node等于头节点head,每循环一次让node等于它的下一个节点。这样就可以实现一个链表的遍历。
这样我们差不多学完了链表的概念,让我们一起来做几道关于链表常考的算法题。
2. leet21 合并两个有序链表
原题:合并两个有序链表
题目是说:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]
示例 2:
输入: l1 = [], l2 = []
输出: []
示例 3:
输入: l1 = [], l2 = [0]
输出: [0]
这道题还是比较容易的。我们这样想想看:如果这道题目是让我们将两个有序数组合并,我们就会准备两个指针,分别指向每个数组的第一个元素,然后拿这两个指针去比较大小。小的就放入一个新数组中,然后这个指针向后挪一位,再去比大小。
链表也可以用这个思想。我们分别获取两个链表的头节点,然后去比较头节点的值的大小。小的就存入新链表中,然后让这个头节点等于自己的子节点再去比大小。
有了思路代码就呼之欲出了。
var mergeTwoLists = function (list1, list2) {
let l1 = list1
let l2 = list2
let res = new ListNode()
let head = res
};
我们声明两个变量 l1 和 l2 用来获取list1和list2的头节点,再准备一个新链表res。可以调用题目已经给我们打造好的构造函数ListNode创建。我们先将这个新链表的头节点head保存下来,一般要对新链表做操作都要将头节点保存下来。此时这个新链表res的头节点值为0,我们会从res的下一个节点开始存入新的值,到时候返回head.next就行了。
就按照我们的思路来,去比较两个链表头节点的值。
var mergeTwoLists = function (list1, list2) {
let l1 = list1
let l2 = list2
let res = new ListNode()
let head = res
if (l1.val <= l2.val) {
res.next = l1
l1 = l1.next
} else {
res.next = l2
l2 = l2.next
}
};
值更小的节点存入res.next中,然后让它等于自己的子节点,再去比较。这个过程是要重复执行的,所以我们要写个循环。
var mergeTwoLists = function (list1, list2) {
let l1 = list1
let l2 = list2
let res = new ListNode()
let head = res
while (l1 && l2) {
if (l1.val <= l2.val) {
res.next = l1
l1 = l1.next
} else {
res.next = l2
l2 = l2.next
}
res = res.next
}
};
循环条件就是 l1 && l2 都要存在。每存入一个节点给res,res也要赋值为自己的字节点,去进行下一次存值。
这里会有一个问题:有可能l1已经走到终点了,不存在了,而l2后面还有很多值没存入,此时循环就不会执行了。所以我们需要处理一下。如果 l1 已经走到终点 l2 里还有值,说明 l2 中剩余的值一定是最大的了,因为链表是有序的,所以我们直接将剩余的全部存入res中就行了。
var mergeTwoLists = function (list1, list2) {
let l1 = list1
let l2 = list2
let res = new ListNode()
let head = res
while (l1 && l2) {
if (l1.val <= l2.val) {
res.next = l1
l1 = l1.next
} else {
res.next = l2
l2 = l2.next
}
res = res.next
}
res.next = l1 !== null ? l1 : l2
return head.next
};
我们这样写:res.next = l1 !== null ? l1 : l2。用一个三元运算符,如果l1不为null,说明 l1 中还有剩余值,直接存入res中;如果l1为null,就将l2中剩余的值存入res中。
最后返回 head.next 就行了。head头节点值为0,我们只要它的字节的就行。
这样就完成了这道题目的解答,还是比较容易的,我们继续。
3. leet83 删除排序链表中的重复元素
原题: 删除排序链表中的重复元素
我们再来看一下力扣上的第83题。
题目说:给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。
示例 1:
输入: head = [1,1,2]
输出: [1,2]
示例 2:
输入: head = [1,1,2,3,3]
输出: [1,2,3]
注意题目说:返回已排序的链表。它不让我们创建一个新的链表,而是在原来的链表上进行操作。
应该怎么做呢?我们最好不要在头节点上进行操作,因为到时候我们需要返回它,所以我们准备一个当前节点cur,先赋值为头节点,再对cur去进行操作。
我们说过,在链表中删除元素其实就是将上一个节点的地址指向下一个节点,跳过这个我们要删除的节点就行。所以,我们可以拿cur与cur的子节点的值进行比较,如果发现相等,就跳过这个相等的节点,让cur的子节点指向下一个子节点;如果不相等,就向后挪一位,让cur等于自己的子节点,再去比较。
var deleteDuplicates = function (head) {
let cur = head
if (cur.val === cur.next.val) {
cur.next = cur.next.next
} else {
cur = cur.next
}
};
其实逻辑就是这样,然后我们要重复去比较,所以我们要写一个循环。循环条件就为cur && cur.next都要存在。当cur.next不存在时,说明cur已经走到最后一个节点上了,就不需要再进行比较了。循环结束后返回head就行。
var deleteDuplicates = function (head) {
let cur = head
while (cur && cur.next) {
if (cur.val === cur.next.val) {
cur.next = cur.next.next
} else {
cur = cur.next
}
}
return head
};
4. leet82 删除排序链表中的重复元素 II
我们继续,再来看力扣上的第82题。
原题: 删除排序链表中的重复元素 II
题目说:给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回已排序的链表。
示例 1:
输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]
示例 2:
输入: head = [1,1,1,2,3]
输出: [2,3]
这道题是上一道题的进阶版,上一道题是去重,这道题是只要出现了重复的值就要一起去掉。
这道题的思路其实和上面那道差不多,只不过这次是如果发现了重复的节点,所有重复的节点都要跳过。
这里还有个细节:因为头节点也可能是重复的值,也可能被去除,而要是头节点都被去除了,我们就不可能访问到后面的节点了。所以我们要准备一个虚拟节点dummy,让它成为这个链表的头节点,到时候返回dummy.next就行了。
var deleteDuplicates = function (head) {
let dummy = new ListNode()
dummy.next = head
let cur = dummy
return dummy.next
};
因为此时dummy就是链表的头节点了,我们不对它进行操作,再定义一个变量cur等于dummy,我们去对cur进行操作。
应该怎么做呢?因为此时cur是那个虚拟节点,所以我们要从cur.next开始去操作。我们还是拿着cur.next去和它的子节点进行比较,如果不相等,就挪一位。这里同样要写一个循环,循环条件就是cur.next && cur.next.next存在。
var deleteDuplicates = function (head) {
let dummy = new ListNode()
dummy.next = head
let cur = dummy
while (cur.next && cur.next.next) {
if (cur.next.val === cur.next.next.val) {
}
} else {
cur = cur.next
}
}
return dummy.next
};
那如果相等呢?逻辑应该怎么写呢?我们就将这个相等的值保存起来,然后从第一个出现重复值的节点开始,将所有等于这个值的节点移除,直到碰到不等于这个值的节点。这样我们就可以跳过出现重复值的节点了。
var deleteDuplicates = function (head) {
let dummy = new ListNode()
dummy.next = head
let cur = dummy
while (cur.next && cur.next.next) {
if (cur.next.val === cur.next.next.val) {
let value = cur.next.val
while (cur.next && cur.next.val === value) {
cur.next = cur.next.next
}
} else {
cur = cur.next
}
}
return dummy.next
};
这道题其实和上一道题差不多,就是多了两个步骤。一,要设置一个虚拟节点,防止头节点被破坏;二,如果出现了重复的值,要从这个出现重复值的节点开始,移除所有等于这个重复值的节点,直到碰到不等于重复值的节点。
5. leet206 反转链表
原题:反转链表
我们再来看最后一道题:反转链表。这是一道很经典的面试题。我们一起来看一下。
题目是说:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
示例 2:
输入: head = [1,2]
输出: [2,1]
示例 3:
输入: head = []
输出: []
这道题应该怎么做呢?我们要反转链表,就是要让每个节点的指向都指反,原本是1指向2指向3指向4指向5,就要变成5指向4指向3指向2指向1。假如当前节点是2,我们就要让2指向节点1;假如当前节点是3,我们就要让3指向节点2。规律就是让每一个节点指向它的前一个节点,
同样,我们也不去动头节点,我们准备一个当前节点cur为head。还需要准备一个前节点pre,用来保存当前节点的前一个节点,到时候就要让cur指向pre。当改变了指向后,我们就要让cur和pre都往后挪一位,所以我们还需要准备一个后节点next保存当前节点的下一个值,当改变了指向后,就让cur等于next,让pre等于cur,让next = next.next。
当然开头就要判断一下,如果头节点为空,直接返回头节点。
var reverseList = function (head) {
if (!head) return head
let pre = null
let cur = head
let next = cur.next
cur.next = pre
pre = cur
cur = next
next = next.next
};
因为我们将cur赋值为了头节点,头节点就要指向null,作为反转后的链表的尾节点,所以这里pre初始值为null。然后去执行我们分析好的逻辑,让cur.next = pre,改变了指向后,pre,cur和next都要向后挪一位。这里要重复执行,所以我们要写一个循环。
var reverseList = function (head) {
if (!head) return head
let pre = null
let cur = head
let next = cur.next
while (cur) {
cur.next = pre
pre = cur
cur = next
next = next.next
return pre
};
循环条件就是当前节点cur要存在,不存在说明已经走到最后一个节点了,就不需要循环了。而此时,会有一个问题,当执行最后一次循环时,cur = next语句就将cur变为不存在了,说明next已经是不存在了,就不需要再next = next.next,因为此时next.next根本没有这个节点,就会报错。所以,在这里,我们要加一个判断:
var reverseList = function (head) {
if (!head) return head
let pre = null
let cur = head
let next = cur.next
while (cur) {
cur.next = pre
pre = cur
cur = next
if (next) {
next = next.next
}
}
return pre
};
当next存在时再去执行next = next.next,最后一次循环next为null了,就不会执行next = next.next,也就不会报错。最后pre会成为新链表的头节点,返回pre即可。