Skip to content

反转链表

统计信息:字数 10308 阅读21分钟

反转链表这里一共有三个题目供大家训练。分别是原地单链表的反转、两个一组反转链表和K个一组反转链表,难度由阶梯式上升。

而在面试当中凡是遇到链表,反转类的题目出现的频率也是数一数二的,因此把它当做链表开篇的训练类型。

No.1 简单的反转链表

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

来源: LeetCode 第 206 题

循环解决方案

这道题是链表中的经典题目,充分体现链表这种数据结构操作思路简单, 但是实现上并没有那么简单的特点。

那在实现上应该注意一些什么问题呢?

保存后续节点。作为新手来说,很容易将当前节点的 next指针直接指向前一个节点,但其实当前节点下一个节点的指针也就丢失了。因此,需要在遍历的过程当中,先将下一个节点保存,然后再操作next指向。

链表结构声定义如下:

function ListNode(val) {
  this.val = val;
  this.next = null;
}

实现如下:

let reverseList = (head) => {
    if (!head) {
      return null;
    }
    let pre = null, cur = head;
    while (cur) {
        // 关键: 保存下一个节点的值
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};

由于逻辑比较简单,代码直接一气呵成。不过仅仅写完还不够,对于链表问题,边界检查的习惯能帮助我们进一步保证代码的质量。具体来说:

  • 当 head 节点为空时,我们已经处理,通过✅
  • 当链表只包含一个节点时, 此时我们希望直接返回这个节点,对上述代码而言,进入循环后 pre 被赋值为cur,也就是head,没毛病,通过✅

运行在 LeetCode, 成功 AC ✌

但作为系统性的训练而言,单单让程序通过未免太草率了,我们后续会尽可能地用不同的方式去解决相同的问题,达到融会贯通的效果,也是对自己思路的开拓,有时候或许能达到更优解。

递归解决方案

由于之前的思路已经介绍得非常清楚了,因此在这我们贴上代码,大家好好体会:

let reverseList = (head) =>{
  let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 节点
    let next = cur.next;
    cur.next = pre;
    reverse(cur, next);
  }
  return reverse(null, head);
}

No.2 区间反转

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明: 1 ≤ m ≤ n ≤ 链表长度。

示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

来源: LeetCode 第 92 题

思路

这一题相比上一个整个链表反转的题,其实是换汤不换药。我们依然有两种类型的解法:循环解法和递归解法。

需要注意的问题就是前后节点的保存(或者记录),什么意思呢?看这张图你就明白了。

关于前节点和后节点的定义,大家在图上应该能看的比较清楚了,后面会经常用到。

反转操作上一题已经拆解过,这里不再赘述。值得注意的是反转后的工作,那么对于整个区间反转后的工作,其实就是一个移花接木的过程,首先将前节点的 next 指向区间终点,然后将区间起点的 next 指向后节点。因此这一题中有四个需要重视的节点: 前节点、后节点、区间起点和区间终点。接下来我们开始实际的编码操作。

循环解法

/**
 * @param {ListNode} head
 * @param {number} m
 * @param {number} n
 * @return {ListNode}
 */
var reverseBetween = function(head, m, n) {
    let count = n - m;
    let p = dummyHead = new ListNode();
    let pre, cur, front, tail;
    p.next = head;
    for(let i = 0; i < m - 1; i ++) {
        p = p.next;
    }
    // 保存前节点
    front = p;
    // 同时保存区间首节点
    pre = tail = p.next;
    cur = pre.next;
    // 区间反转
    for(let i = 0; i < count; i++) {
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // 前节点的 next 指向区间末尾
    front.next = pre;
    // 区间首节点的 next 指向后节点(循环完后的cur就是区间后面第一个节点,即后节点)
    tail.next = cur;
    return dummyHead.next;
};

递归解法

对于递归解法,唯一的不同就在于对于区间的处理,采用递归程序进行处理,大家也可以趁着复习一下递归反转的实现。

var reverseBetween = function(head, m, n) {
  // 递归反转函数
  let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 节点
    let next = cur.next;
    cur.next = pre;
    return reverse(cur, next);
  }
  let p = dummyHead = new ListNode();
  dummyHead.next = head;
  let start, end; //区间首尾节点
  let front, tail; //前节点和后节点
  for(let i = 0; i < m - 1; i++) {
    p = p.next;
  }
  front = p; //保存前节点
  start = front.next;
  for(let i = m - 1; i < n; i++) {
    p = p.next;
  }
  end = p;
  tail = end.next; //保存后节点
  end.next = null;
  // 开始穿针引线啦,前节点指向区间首,区间首指向后节点
  front.next = reverse(null, start);
  start.next = tail;
  return dummyHead.next;
}

No.3 两个一组翻转链表

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

来源: LeetCode 第 24 题

示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.

思路

如图所示,我们首先建立一个虚拟头节点(dummyHead),辅助我们分析。

首先让 p 处在 dummyHead 的位置,记录下 p.next 和 p.next.next 的节点,也就是 node1 和 node2。

随后让 node1.next = node2.next, 效果:

然后让 node2.next = node1, 效果:

最后,dummyHead.next = node2,本次翻转完成。同时 p 指针指向node1, 效果如下:

依此循环,如果 p.next 或者 p.next.next 为空,也就是找不到新的一组节点了,循环结束。

循环解决

思路清楚了,其实实现还是比较容易的,代码如下:

var swapPairs = function(head) {
    if(head == null || head.next == null) 
        return head;
    let dummyHead = p = new ListNode();
    let node1, node2;
    dummyHead.next = head;
    while((node1 = p.next) && (node2 = p.next.next)) {
        node1.next = node2.next;
        node2.next = node1;
        p.next = node2;
        p = node1;
    }
    return dummyHead.next;
};

递归方式

var swapPairs = function(head) {
    if(head == null || head.next == null)
        return head;
    let node1 = head, node2 = head.next;
    node1.next = swapPairs(node2.next);
    node2.next = node1;
    return node2;
};

利用递归方式之后,是不是感觉代码特别简洁?😃😃😃

希望你能好好体会一下递归调用的过程,相信理解之后对自己是一个很大的提升。

No.4 K个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例 :

给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5

说明 :

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

来源: LeetCode 第 25 题

思路

思路类似No.3中的两个一组翻转。唯一的不同在于两个一组的情况下每一组只需要反转两个节点,而在 K 个一组的情况下对应的操作是将 K 个元素的链表进行反转。

递归解法

这一题我觉得递归的解法更容易理解,因此,先贴上递归方法的代码。

提醒

以下代码的注释中首节点、尾结点等概念都是针对反转前的链表而言的。

/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var reverseKGroup = function(head, k) {
    let pre = null, cur = head;
    let p = head;
    // 下面的循环用来检查后面的元素是否能组成一组
    for(let i = 0; i < k; i++) {
        if(p == null) return head;
        p = p.next;
    }
    for(let i = 0; i < k; i++){
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // pre为本组最后一个节点,cur为下一组的起点
    head.next = reverseKGroup(cur, k);
    return pre;
};

循环解法

重点都放在注释里面了。

var reverseKGroup = function(head, k) {
    let count = 0;
    // 看是否能构成一组,同时统计链表元素个数
    for(let p = head; p != null; p = p.next) {
        if(p == null && i < k) return head;
        count++;
    }
    let loopCount = Math.floor(count / k);
    let p = dummyHead = new ListNode();
    dummyHead.next = head;
    // 分成了 loopCount 组,对每一个组进行反转
    for(let i = 0; i < loopCount; i++) {
        let pre = null, cur = p.next;
        for(let j = 0; j < k; j++) {
            let next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        // 当前 pre 为该组的尾结点,cur 为下一组首节点
        let start = p.next;// start 是该组首节点
        // 开始穿针引线!思路和2个一组的情况一模一样
        p.next = pre;
        start.next = cur;
        p = start;
    }
    return dummyHead.next;
};

Last update: November 9, 2024