快慢指针指的是两个一前一后的指针,两个指针往同一个方向走,只是一个快一个慢。快慢指针严格来说只能有俩,不过实际做题中,可能会出现一前、一中、一后的三个指针,这种超过两个指针的解题方法也叫“多指针法”。
快慢指针——删除链表的倒数第 N 个结点
真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个结点后,链表变为 1->2->3->5.
var removeNthFromEnd = function(head, n) {let dummy = new ListNode();dummy.next = head;let fast = dummy;let slow = dummy;while(n) {fast = fast.next;n--;}while(fast.next) {fast = fast.next;slow = slow.next;}slow.next = slow.next.next;return dummy.next;};
多指针法——链表的反转
完全反转一个链表
真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
思路解读
——处理链表的本质,是处理链表结点之间的指针关系。
next 指针的指向给反过来就行了
接下来我们需要琢磨的是如何去反转指针的指向,这里我们需要用到三个指针,它们分别指向目标结点(cur)、目标结点的前驱结点(pre)、目标结点的后继结点(next)。这里咱们随便找个结点来开刀:
那 next 不是完全没用到吗?
当然有用,咱们反转完链表变成啥样了:
要是不用 next 给你指着 cur 原本的后继结点,你上哪去定位下一个结点呢?遍历都没法继续了嗷。
咱们从第一个结点开始,每个结点都给它进行一次 next 指针的反转。到最后一个结点时,整个链表就已经被我们彻底反转掉了。
/*** @param {ListNode} head* @return {ListNode}*/const reverseList = function(head) {// 初始化前驱结点为 nulllet pre = null;// 初始化目标结点为头结点let cur = head;// 只要目标结点不为 null,遍历就得继续while (cur !== null) {// 记录一下 next 结点let next = cur.next;// 反转指针cur.next = pre;// pre 往前走一步pre = cur;// cur往前走一步cur = next;}// 反转结束后,pre 就会变成新链表的头结点return pre};
局部反转一个链表
反转链表真是座金矿,反转完整体反转局部,反转完局部还能每 k 个一组花式反转(最后这个略难,我们会放在真题训练环节来做)。虽然难度依次进阶,但只要把握住核心思想就没问题,下面咱们来看看如何反转局部:
真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
思路解读
我们仍然是从指针反转来入手:

按照题中的示例,假如我们需要反转的是链表的第 2-4 之间的结点,那么对应的指针逆序后会是这个样子:
4指3,3指2,这都没问题,关键在于,如何让1指向4、让2指向5呢?这就要求我们在单纯的重复“逆序”这个动作之外,还需要对被逆序的区间前后的两个结点做额外的处理:
由于我们遍历链表的顺序是从前往后遍历,那么为了避免结点1和结点2随着遍历向后推进被遗失,我们需要提前把1结点缓存下来。而结点5就没有这么麻烦了:随着遍历的进行,当我们完成了结点4的指针反转后,此时 cur 指针就恰好指在结点5上:
此时我们直接将结点2的 next 指针指向 cur、将结点1的 next 指针指向 pre 即可。
/*** @param {ListNode} head* @param {number} m* @param {number} n* @return {ListNode}*/// 入参是头结点、m、nconst reverseBetween = function(head, m, n) {// 定义pre、cur,用leftHead来承接整个区间的前驱结点let pre,cur,leftHead// 别忘了用 dummy 嗷const dummy = new ListNode()// dummy后继结点是头结点dummy.next = head// p是一个游标,用于遍历,最初指向 dummylet p = dummy// p往前走 m-1 步,走到整个区间的前驱结点处for(let i=0;i<m-1;i++){p = p.next}// 缓存这个前驱结点到 leftHead 里leftHead = p// start 是反转区间的第一个结点let start = leftHead.next// pre 指向startpre = start// cur 指向 start 的下一个结点cur = pre.next// 开始重复反转动作for(let i=m;i<n;i++){let next = cur.nextcur.next = prepre = curcur = next}// leftHead 的后继结点此时为反转后的区间的第一个结点leftHead.next = pre// 将区间内反转后的最后一个结点 next 指向 curstart.next=cur// dummy.next 永远指向链表头结点return dummy.next};
