这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战 | 创作学习持续成长,夺宝闯关赢大奖 - 掘金 (juejin.cn)
题目链接
- K 个一组翻转链表 leetcode-cn.com/problems/re…
- 旋转链表 leetcode-cn.com/problems/ro…
- 两两交换链表中的节点 leetcode-cn.com/problems/sw…
- 删除链表的倒数第 N 个结点 leetcode-cn.com/problems/re…
- 删除排序链表中的重复元素 leetcode-cn.com/problems/re…
题解及分析
K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
链表反转可以参考过往的文章 链表折磨专题一 - 掘金 (juejin.cn)
思路一:
- 我们在链表头插入一个虚拟节点,循环每个节点并用变量tail标识出每次翻转的最后一个节点,head和tail可以圈出我们需要翻转的部分。
- 定义一个pre节点,一开始指向表头,每一次翻转之后,我们把翻转完的链表段装回pre的位置,而pre则移到tail的位置,而head则指向tail的下一个节点 需要注意的是:
- head指向tail下一个节点的原因在于,head的作用是表示出下一段需要翻转的链表段
- pre指向tail的原因是,pre用于标识出更新后的链表段需要组装在链表中的位置
let reverse = (head, tail) => {
// 这个函数用于反转链表
let prev = tail.next;
let p = head;
while (prev !== tail) {
const nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return [tail, head];
}
let reverseKGroup = (head, k) => {
// 在列表头生成一个虚拟节点
const vertical = new ListNode(0);
vertical.next = head;
let pre = vertical;
while(head) {
// tail用来标识每一次翻转的最后一个节点
let tail = pre;
// tail的值通过循环k来确认
for(let i = 0; i < k; ++i) {
tail = tail.next;
if(!tail) {
// 没有tail时说明剩余链表不满足k值,返回剩余的链表
return vertical.next;
}
}
// next用来定位尾部的下一位
const next = tail.next;
[head, tail] = reverse(head, tail)
// 重新组装链表
pre.next = head;
tail.next = next;
pre = tail;
head = tail.next;
}
return vertical.next
};
思路二
- 声明一个虚拟节点插入链表头部,
- 每次翻转k位链表,翻转完之后直接装入链表的指定部分
// 另一种写法
let reverseKGroup = (head, k) => {
if(!head) return null
let ret = new ListNode(-1, head)
let pre = ret
do{
// 直接对某段链表进行翻转
pre.next = reverse(pre.next, k)
// 翻转之后,将链表直接装入原链表
for(let i = 0; i < k && pre; i++) {
pre = pre.next
}
if(!pre) break
}while(1)
return ret.next
}
let reverse = (head, n) => {
let pre = head
let cur = head
let count = n
// 用于校验翻转长度是否大于链表长度
while(--n && pre) {
pre = pre.next
}
if(!pre) return head
pre = null
while(count--) {
[cur.next, pre, cur] = [pre, cur, cur.next]
}
head.next = cur
return pre
}
旋转链表
给你一个链表的头节点head,旋转链表,将链表每个节点向右移动k个位置。
这道题实际上难度并不高,题意上就很明显能看出是一个链表环型题目,那么优化点也就在于如何判断出链表到底需要扭转哪些节点,换言之即是k对链表长度mod求余,余下的即是需要旋转的节点。
var rotateRight = function(head, k) {
if(!head || !head.next || !k) {
return head
}
// 计算链表的长度
let cur = head
let n = 1
while(cur.next) {
cur = cur.next
n++
}
/**
*判断需要扭转的节点有哪些
*如果mod后长度和链表长度相等,代表不需要修改链表
*/
let diff = n - k % n
if(diff === n) {
return head
}
// 链表成环
cur.next = head
// 查找操作完成后的尾节点
while(diff) {
cur = cur.next
diff--
}
// 重新组装链表
const ret = cur.next
cur.next = null
return ret
};
两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
一般这种题目的解法有迭代和递归两种
思路一:迭代
var swapPairs = function(head) {
if(!head || !head.next) return head
const dummyHead = new ListNode()
dummyHead.next = head
let temp = dummyHead
// 循环条件是下一个节点和下下个节点是否存在
while(temp.next && temp.next.next ) {
const node1 = temp.next
const node2 = temp.next.next
// 交换节点引用
temp.next = node2
node1.next = node2.next
node2.next = node1
temp = node1
}
return dummyHead.next
};
思路二:递归
var swapPairs = function(head) {
if (head === null|| head.next === null) {
return head;
}
// 用一个新的变量保存反转节点的下一个节点
const cache = head.next
// 每次将需要翻转的节点传入
// 这个函数实际上只是调换了cache.next(即head.next.next)和head.next的引用顺序
head.next = swapPairs(cache.next)
cache.next = head
return cache
};
删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。
这道题的问题在于,我们不知道链表的长度。那么大致的解法分为两种,一种是遍历循环链表,确认链表长度后再删除对应的节点;另一种则不考虑去获得链表的长度
思路一:将链表转换为数组,用处理完数组后再转为链表
var removeNthFromEnd = function(head, n) {
let newArr = []
let dummy = new ListNode()
let newList = dummy
// 循环链表,维护数组来存储节点值
while(head){
newArr.push(head.val)
head = head.next
}
newArr.splice(newArr.length - n, 1)
// 将数组转换成链表
for(let i = 0; i < newArr.length ;i++){
newList.next = new ListNode(newArr[i]);
newList = newList.next;
}
return dummy.next
};
思路二:数组中直接存储链表的头节点,利用数组方法来修改头节点的指向(好家伙直接卡js数组的bug啊) 与上个解法的区别在于,这个写法用数组维护链表的头节点
var removeNthFromEnd = function(head, n) {
const dummyNode = new ListNode(0, head)
const arr = new Array()
let pushList = dummyNode
while (pushList != null) {
arr.push(pushList)
pushList = pushList.next
}
// 链表长度和n等同时删去第一个
if(n === arr.length) return head.next
arr[arr.length - n -1].next = arr[arr.length - n -1].next.next
return dummyNode.next
};
思路三:堆栈 利用堆栈找出倒数第n个节点,然后与剩余的节点连接
var removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0, head)
const stack = new Array()
let pushList = dummy
while (pushList != null) {
stack.push(pushList)
pushList = pushList.next
}
// 弹出堆栈中的倒数n个值,找到被删除节点的前一个节点
for (let i = 0; i < n; i++) {
stack.pop()
}
let peek = stack[stack.length - 1]
peek.next = peek.next.next
return dummy.next
};
思路四:快慢指针 快指针先移动n个单位,然后同步移动慢指针和快指针,最后修改慢指针的指向
var removeNthFromEnd = function(head, n) {
if(!head || !n) return head
let fast = head
let slow = head
while(n) {
fast = fast.next
n--
}
if(!fast) {
return head.next
}
while(fast.next) {
slow = slow.next
fast = fast.next
}
slow.next = slow.next.next
return head
};
删除排序链表中的重复元素
给定一个已排序的链表的头head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
由于链表已经被排序,所以相同的值必定在相邻的两个节点,那么遍历链表,找到相同值的节点,再修改节点的指向就可以了。
var deleteDuplicates = function(head) {
if(!head) return head
let cur = head
while(cur.next) {
if(cur.val === cur.next.val) {
cur.next = cur.next.next
} else {
cur = cur.next
}
}
return head
};
题目总结
- 1,2,3道题属于链表翻转类题目,难点在于如何确认翻转的位置以及对临界情况的处理,需要考虑临界情况是否能够省略余下的步骤
- 4,5道题属于节点删除类题目。4题难点在于如果需要在忽略链表长度的情况下对某个节点进行操作,那么我们可以利用堆栈的特性/快慢指针来处理。5题只要直接遍历即可。