「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。
上一篇文章 写给前端开发的链表介绍(js) 中,介绍了链表,这篇文章就来实战一下,用 js 实现一个链表,再做几道 leetcode。
编码链表的一些小技巧
编码链表和编码数组是完全不一样的,这里有些小技巧,非常有用,链表类题目很多都是用这些小技巧,只有亲自写过才能体会。
- 循环链表使用 while,每次把当前节点指向当前节点的 next, 就向后面移动了一个节点。
while(head) {
do something...
head = head.next
}
- 设定哨兵节点,哨兵节点的 next 指向当前链表,这样不仅可以少写一些边界条件的判断,也能很好地操作第一个节点。
const cur = { // 哨兵 -> 1 -> 2 ->3
next: head
}
return cur.next // 返回 哨兵.next
- 设定临时链表,把当前链表赋值给一个临时变量,这样就可以随便操作临时变量,因为引用类型指向同一地址,操作临时链表,原链表也会变化。最后返回的时候返回原链表。
比如:
head: 1 -> 2 -> 3
let temp = head
temp = temp.next // 随便操作 temp
temp.val = 100
console.log(temp) 100 -> 3
console.log(head) 1 -> 100 -> 3 // head也会跟着变化
return head
js实现一个链表
JS 中的链表,是以嵌套的对象的形式来实现的
{
// 数据域
val: 1,
// 指针域,指向下一个结点
next: {
val:2,
next: ...
}
}
当然,我们也能自己写构造函数,把功能实现全面一点。
目标功能
- 新增节点
- 查找某一节点
- 查找某一节点的前一个节点
- 插入节点
- 删除节点
- 打印链表元素
- 获取链表长度
定义两个类,一个 Node 类来表示节点,一个 LinkedList 类来实现上述方法。
编码
class Node { // 节点构造函数
constructor (val) {
this.val = val
this.next = null
}
}
class LinkNodeList { // 链表构造函数
constructor () {
this.head = null
this.length = 0
}
// 新增节点
append (val) {
const node = new Node(val)
let temp = this.head
if (this.head) { // 如果有头节点,就把链表最后一个节点指向要创建的node节点
while (temp.next) {
temp = temp.next
}
temp.next = node
} else { // 如果没有头节点,就把要创建的node节点赋值给head
this.head = node
}
this.length++
}
// 查找节点
find (val) {
let temp = this.head
while (temp.val !== val) { // 找不到就继续循环
temp = temp.next
}
return temp // 找到了就返回节点
}
// 查找某一节点的前一个节点
findPre (val) {
let temp = this.head
while (!temp.next && temp.next.val !== val) { // 当前节点的 next 去和 val 做比较
temp = temp.next
}
return temp
}
// 插入节点
insert (val, insertVal) {
const newNode = new Node(insertVal)
const temp = this.find(val)
newNode.next = temp.next
temp.next = newNode
this.length++
}
// 删除节点
delete (val) {
const deleteNode = this.find(val)
this.findPre(deleteNode).next = deleteNode.next
this.length--
}
// 打印整个链表
print () {
let temp = this.head
let res = ''
if (this.head) {
while (temp) {
res += `${temp.val} -> `
temp = temp.next
}
console.log('打印链表 :>> ', res.slice(0, res.length - 4))
} else {
console.log('链表为空!')
}
}
// 获取链表长度
getLength () {
return this.length
}
}
测试一下:
const l1 = new LinkNodeList() // 定义一个链表
l1.append(1)
l1.append(2)
l1.append(3)
l1.append(4) // 添加 1,2,3,4
l1.insert(2, 100) // 在 2 后面添加 100
l1.delete(2) // 删除 2
l1.print() // 打印链表
用js实现这么一个链表,可以很好地体会链表里插入和删除节点时,节点之间的指向问题。
leetcode实战
203. 移除链表元素
真题描述:给你一个链表的头节点
head和一个整数val,请你删除链表中所有满足Node.val == val的节点,并返回 新的头节点 。
迭代法
循环链表,遇到和 val 值相同的就删除这个节点,充分利用哨兵节点和临时链表的技巧,代码如下:
const removeElements = function (head, val) {
const cur = { // 哨兵节点
next: head
}
let temp = cur // 临时变量
while (temp.next) {
if (temp.next.val === val) {
temp.next = temp.next.next // 如果相等,就删除链表元素
} else {
temp = temp.next // 否则就一直循环
}
}
return cur.next // 最后返回哨兵节点的next
}
时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)
递归法
这道题也可以用递归来实现:
- 终止条件:当前节点为空
- 递归条件:链表的每个节点都调用递归函数
const removeElements = function (head, val) {
if(!head) {
return head
}
head.next = removeElements(head.next, val)
return head.val === val ? head.next : head
}
时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。
空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。
82. 删除排序链表中的重复元素 II
真题描述:给定一个已排序的链表的头
head, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
这道题和上面那道类似,但是要复杂些,不过还是能用到前面总结的小技巧。
const deleteDuplicates = function (head) {
const cur = { // 设置哨兵节点
next: head
}
let temp = cur // 设置临时链表
while (temp.next && temp.next.next) { // 当前节点后面两个节点都有值才会去判断,有没有重复的元素
if (temp.next.val === temp.next.next.val) { // 如果有重复的元素
const val = temp.next.val // 记录下这个重复的值
while (temp.next && temp.next.val === val) { // 反复排查这个重复的值,如果遇到重复的,都删除掉
temp.next = temp.next.next
}
} else {
temp = temp.next // 没有重复的元素,就正常循环
}
}
return cur.next // 返回哨兵.next
}
时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)
141. 环形链表
真题描述:判断一个链表是否有环
这道经典面试题,一般有两种解法。
哈希表
用一个哈希表把走过的链表节点存起来,后面再遇到就说明有环。
js 里可以用 Set 和 Map 来存储节点。
const hasCycle = (head) => {
const cache = new Set() // 定义一个 Set
while (head) {
if (cache.has(head)) { // 有值,说明走到了之前的节点上,有环
return true
}
cache.add(head) // 每走过一个节点都把节点存进 Set
head = head.next // 循环链表
}
return false // 链表循环完了都没走到之前的节点上,无环
}
时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(n),主要是哈希表的开销。
我第一次解题时尝试这么写,没有通过:
const hasCycle = (head) => {
const obj = {}
while (head) {
if (obj[head]) {
return true
}
obj[head] = head
head = head.next
}
return false
}
因为我用 Object 来存,Object 的 key 只能是字符串,用 Object 存储链表节点(对象),都会自动转成这样的字符串。
es6 文档上讲述 Map 时也有相关说明:Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
这也是 Map 被设计出来的原因。
快慢指针
这题最巧妙的思路还是用快慢指针(双指针),说实话,不看题解,我是不可能想到这样的解题方法的。
快慢指针的思路是:定义两个指针,一个走得快,一个走得慢。
- 如果链表有环,那么快指针一定能追上慢指针。
- 如果链表没有环,那么快指针能走完这个链表。
就跟操场跑圈一样,跑得快的人可以套跑得慢的人圈。
代码如下:
const hasCycle = (head) => {
let slow = head // 定义慢指针
let fast = head // 定义快指针
while (fast && fast.next) {
slow = slow.next // 慢指针一次走一步
fast = fast.next.next // 快指针一次走两步
if (fast === slow) { // 如果快指针追上了慢指针,有环
return true
}
}
return false // 循环结束,快指针走完了链表,无环
}
时间复杂度:O(n),其中 n 是链表的长度。
空间复杂度:O(1)
小结
链表的题做了一些之后,我直呼,都是套路啊,如果没写过直接让你来写,不可能会写的。
而如果写过几道类似的,其他的也可以融会贯通。
如果觉得上面几道题有难度,这篇文章里也有几道题,要相对简单一些,链接在这里,写给前端开发的链表介绍
往期算法相关文章