最近在看数据结构和算法,努力总结出道~
TL;DR
- 链表就是嵌套的对象,
{val:1,next:{val:2,next:...}}
- 链表里的指针,听起来很抽象,其实就是部分链表,依旧是嵌套对象,p 是
{val:1,next:{val:2,next:...}}
- 指针往后挪动,其实就是
p = p.next
,也就是 p 变成{val:2,next:...}
- 链表的最后一个节点,
next
肯定是空的 - 处理链表的时候,一般为了边界问题,很多时候先创建一个空节点,指向链表,有备无患吧
- 删除节点必须要知道前置节点
- 反转链表就是反转指针
- 反复遍历链表的时候,考虑下快慢指针和多指针
练习:删除倒数第 n 个结点
先说说,初始想法:
链表里遍历,只能从前往后,所以删除倒数第 n 个结点,就是删除第len+1-n
个,比如总共 5 个结点,删除倒数第 1 个结点,其实就是第 5 个结点,当然链表里删除节点需要知道前置节点,也就是知道倒数第n+1
个结点。
于是遍历链表知道链表的长度,然后再遍历链接到第len-n
个结点处开始删除第len+1-n
个节点,但这样就是嵌套遍历。
怎么能一次遍历就能成功呢?
其实这里,想想双指针法,可以记录遍历的进度。
倒数第n+1
个结点距离最后一个节点是n
步,换言之,如果有两个指针相隔n
步,那么前面的指针到达终点时,后一个指针恰好到了倒数第n+1
的结点处,就可以顺手的删掉倒数第n
个结点了。
这种两个指针,同个方向,一前一后,就是快慢指针法,解决差距的问题。
- 处理边界问题,提前创建空节点
- 快指针先走 n 步
- 然后快慢指针一起走,直到快指针走到终点,那么慢指针也就到了倒数第
n+1
个结点处 - 删除后面一个节点
var removeNthFromEnd = function (list, n) {
// 处理边界问题,加个空节点
let head = new ListNode();
head.next = list;
// 定义快慢指针
let slow = head;
let fast = head;
// 快指针先走n步
for (let count = 0; count < n; count++) {
fast = fast.next;
}
// 快慢一起走,直到快指针到终点,慢指针也就到了倒数第n+1个结点处
while (fast && slow && fast.next && slow.next) {
fast = fast.next;
slow = slow.next;
}
// 删除倒数第n个结点
slow.next = slow.next.next ? slow.next.next : null;
return head.next;
};
可以看下官方题解的其他方案
练习:反转链表
处理链表的本质,就是处理指针。
所以反转链表的本质,就是反转指针。
先看下只有两个结点的链表反转:1->2->null
const list = { val: 1, next: { val: 2, next: null } };
// null 1->2
// 建个空节点方便指定
let p0 = null;
let p1 = list;
// 把1后面的链表(这里只有2),先存下,p2就是{val:2,next:null}
let p2 = p1.next;
// 先把1的指针置空,p1就是{val:1,next:null}
// 此时变成 1->null 2
p1.next = p0;
// 再2的指针指向1,p2就是{val:2,next:p1}
p2.next = p1;
三个结点的链表反转:1->2->3->null
,其实道理是类似的,但是 p0 和 p1 指针向前移动。
var reverseList = function (list) {
let pre = null;
let cur = list;
while (cur) {
// 必须先保存下一个结点,不然置空cur指针之后,拿不到cur的下一个结点
let next = cur.next;
// 反转cur指针
cur.next = pre;
// pre和cur指针往前走
pre = cur;
cur = next;
}
// cur为空的时候,说明pre就是首节点
return pre;
};
练习:反转局部链表
这题的第一感觉是和上面的很像,只要先找到要反转的链表区间,然后反转,再重新连接就可以了。
// 官网的注释很明确,直接使用了
var reverseBetween = function (head, left, right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
const head = new ListNode();
head.next = head;
let pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
// 建议写在 for 循环里,语义清晰
for (let i = 0; i < left - 1; i++) {
pre = pre.next;
}
// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
let rightNode = pre;
for (let i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
// 第 3 步:切断出一个子链表(截取链表)
let leftNode = pre.next;
let curr = rightNode.next;
// 注意:切断链接
pre.next = null;
rightNode.next = null;
// 第 4 步:同第 206 题,反转链表的子区间
reverseLinkedList(leftNode);
// 第 5 步:接回到原来的链表中
pre.next = rightNode;
leftNode.next = curr;
return dummyNode.next;
};
const reverseLinkedList = (head) => {
let pre = null;
let cur = head;
while (cur) {
const next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
};
时间复杂度是 O(n),空间复杂度是 O(1)。
但是这里遍历了两次列表,想想还能不能继续优化。
新思路:在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。
根据上面的思路,写出代码
var reverseBetween = function (list, left, right) {
let head = new ListNode();
head.next = list;
let left_pre = head;
// 将left_pre指针移到在前一个节点处,且以后一直在这个位置
for (let i = 0; i < left - 1; i++) {
left_pre = left_pre.next;
}
// cur指针也始终不变
let cur = left_pre.next;
// 反转right - left次
for (let i = 0; i < right - left; i++) {
// next始终是cur的下一个节点
let next = cur.next;
// 先修改cur的下一个指针指向
cur.next = next.next;
// 然修改next的下一个指针指向
next.next = left_pre.next;
// 最后修改left_Pre的下一个指针指向
left_pre.next = next;
}
return head.next;
};