链表反转
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
迭代法:
- 需要一个变量保存cur后的节点,因为中间会修改cur.next的指向
- 循环结束时,pre为反转后链表的第一个节点,cur为反转链表部分的后一个节点
var reverseList = function(head) {
let pre = null, cur = head, next = null
while (cur) {
next = cur.next
cur.next = pre
pre = cur
cur = next
}
return pre
};
递归法:
假设head后的节点已经反转了,只需要将head添加到反转后的链表的尾部
head.next.next为子链表翻转后的最后一个节点,将head节点添加在其后面
var reverseList = function(head) {
if (head === null || head.next === null) return head
const res = reverseList(head.next)
head.next.next = head
head.next = null
return null
};
反转链表II
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
反转逻辑同反转链表类似,但是需要知道链表反转部分的前一个节点和后一个节点
后一个节点就是反转逻辑中的cur, 前一个节点则需要额外变量保存
var reverseBetween = function(head, left, right) {
let dummy = new ListNode(-1)
dummy.next = head
let nodeBeforeLeft = dummy
// 找到整数left对应的前一个节点
for (let i = 0; i < left - 1; i++) {
nodeBeforeLeft = nodeBeforeLeft.next
}
// 反转left到right部分的链表
let pre = null, next = null, cur = nodeBeforeLeft.next
for (let i = 0; i < right - left + 1; i++) {
next = cur.next
cur.next = pre
pre = cur
cur = next
}
// 反转后的链表部分接入到原链表
nodeBeforeLeft.next.next = cur
nodeBeforeLeft.next = pre
return dummy.next
};
k个一组翻转链表
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k **的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解法一:
将链表中的节点保存到数组中,按照翻转后的顺序依次访问。
时间复杂度:
空间复杂度:,因为需要额外的数组保存节点
var reverseKGroup = function(head, k) {
// 保存数组
const arr = []
let cur = head
while (cur) {
arr.push(cur)
cur = cur.next
}
// 按组翻转链表
const n = Math.floor(arr.length / k)
let dummy = new ListNode(-1)
dummy.next = head
cur = dummy
for (let i = 0; i < n; i++) {
for (let j = k - 1; j >= 0; j--) {
cur.next = arr[j + k * i]
cur = cur.next
}
}
// 处理链表未被整除部分
for (let i = 0; i < arr.length - n * k; i++) {
cur.next = arr[n * k + i]
cur = cur.next
}
// 当节点长度刚好被k整除时,此时的cur为节点,需要手动将其next置为null
if (cur) cur.next = null
return dummy.next
};
解法二:
与反转链表II的解法类似,但是会循环多组,注意在一组链表反转后,p0需要相应的更新
function getListLength(head) {
if (!head) return 0
let length = 0
while (head) {
length++
head = head.next
}
return length
}
var reverseKGroup = function(head, k) {
let dummy = new ListNode(-1, head), p0 = dummy
const length = getListLength(head)
const n = Math.floor(length / k)
for (let i = 0; i < n; i++) {
let cur = p0.next, pre = null, next = null
// k个一组翻转链表
for (let j = 0; j < k; j++) {
next = cur.next
cur.next = pre
pre = cur
cur = next
}
const tmp = p0.next // 保存p0的下一个位置
p0.next.next = cur
p0.next = pre
p0 = tmp // 更新p0
}
return dummy.next
};
快慢指针
链表的中间节点
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
var middleNode = function(head) {
let slow = head, fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
}
return slow
};
环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
解法分析:
使用快指针(一次走两步)、慢指针(一次走一步),快指针每次比慢指针多走一步,如果存在环的话,那么快慢指针必定会在环中相遇。
而且快慢指针相遇时,慢指针肯定没有走完全部环。因为假如慢指针走了环的一圈的话,则快指针肯定比慢指针多走了环的一圈的距离,快慢指针肯定已经相遇了。
var hasCycle = function(head) {
if (!head) return false
let slow = head, fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
return true
}
}
return false
};
环形链表II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环.不允许修改 链表.
解法分析:
假设链表头到环入口的距离为a,快慢指针相遇的地方正序距环入口的距离为b,快慢指针相遇的地方逆序距环入口的距离为c。正序意思是按照指针的指向顺序。
快指针走的距离:
慢指针走的距离:
那么:
从上面的推导可以看出:当快慢指针相遇时,一个指针从相遇点,一个指针从链表头,两者同时前进,那么这两个指针必定是在环的入口相遇。由此代码如下:
var detectCycle = function(head) {
if (!head || !head.next) return null
let slow = head, fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) break
}
if (!fast || !fast.next) return null // 链表无环
let res = head
while (res !== fast) {
res = res.next
fast = fast.next
}
return res
};
重排链表
给定一个单链表 L **的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换
解法分析:
将链表从中间分为两段,后半段先进行反转,然后依次从两段链表中取出节点进行重新排列。
var reorderList = function(head) {
if (!head || !head.next) return head
const midNode = midList(head) // 获取中间节点
const head2 = reverseList(midNode) // 链表后半段反转
let first = head, second = head2, first_next = null, second_next = null
while (second.next) {
first_next = first.next
second_next = second.next
first.next = second
second.next = first_next
first = first_next
second = second_next
}
return head
};
链表删除
删除链表中的节点
给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点
解法分析:注意本题只给了要删除的节点node,并未给链表head, 所以无法获取node的前一个节点。但是链表中的值是不同的,所以可以将node.next的值给node,把node.next删除掉。
var deleteNode = function(node) {
node.val = node.next.val
node.next = node.next.next
};
删除链表的倒数第n个节点
给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点
解法分析:
- 倒数第n个节点,该节点可能是head节点,因此需要使用哨兵节点,确保第一个节点是存在的。
- 如果先获取链表长度,再正向遍历到 长度-n ,将该节点删除。此方法需要两次扫描链表
- 一次扫描的话,可以使用快慢指针,相距n,当快指针为null时,慢指针自然指向倒数第n个节点
var removeNthFromEnd = function (head, n) {
let dummy = new ListNode(-1, head)
let fast = head, slow = head, pre = dummy
for (let i = 0; i < n; i++) {
fast = fast.next
}
while (fast) {
pre = pre.next
slow = slow.next
fast = fast.next
}
// slow此时指向倒数第n个节点
pre.next = slow.next
slow = null
return dummy.next
}
解法优化:
最关键的是我们需要获得倒数第n+1个节点,因此上面的解法引入了pre变量。但其实pre变量不是必须的,修改循环的条件即可。
var removeNthFromEnd = function (head, n) {
let dummy = new ListNode(-1, head)
let fast = dummy, slow = dummy
for (let i = 0; i < n; i++) {
fast = fast.next
}
while (fast.next) {
slow = slow.next
fast = fast.next
}
// slow此时指向倒数第n+1个节点
slow.next = slow.next.next
return dummy.next
}
删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
// 复杂写法
var deleteDuplicates = function(head) {
if (!head) return null
let cur = head
while (cur) {
let next = cur.next
while (next) {
if (next.val === cur.val){
next = next.next
} else {
break
}
}
cur.next = next
cur = next
}
return head
};
// 优雅写法
var deleteDuplicates = function(head) {
if (!head) return null
let cur = head
while (cur.next) {
if (cur.next.val === cur.val) {
cur.next = cur.next.next
} else {
cur = cur.next
}
}
return head
};
删除排序链表中的重复元素II
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
解法分析:
- head节点可能会被删除,需要在头部添加哨兵节点,其值小于head节点的值
- 所有重复数字的节点均被删除,表示cur也可能被删除,因此需要保存pre变量,表示无重复数字链表的尾部。
// 复杂写法
var deleteDuplicates = function(head) {
if (!head) return null
let dummy = new ListNode(head.val - 1, head)
let pre = dummy, cur = head, next = null
while (cur) {
next = cur.next
while (next) {
if (next.val === cur.val) {
next = next.next
} else {
break
}
}
if (next === cur.next) {
pre = cur
cur = next
} else {
pre.next = next
cur = next
}
}
return dummy.next
};
// 优雅写法
var deleteDuplicates = function(head) {
if (!head) return null
let dummy = new ListNode(head.val - 1, head)
let cur = dummy
// 两个一组判断
while (cur.next && cur.next.next) {
let val = cur.next.val
if (cur.next.next.val === val) {
while (cur.next && cur.next.val === val) {
cur.next = cur.next.next
}
} else {
cur = cur.next
}
}
return dummy.next
};