【算法】链表反转并不简单

337 阅读5分钟

【算法】链表反转并不简单

  • 作者: Wel2018
  • 文章状态: 更新中
  • 系列状态: 未完结
  • 阅读预计用时:10 分钟
  • 编程语言:C++

本文概要

如果在求职面试过程中遇到【链表反转】这道题,很可能是面试官对你非常满意,并没有为难你的意思,对整个链表反转大部分人都能一次通过。但也不可高兴太早,面试官看你代码写的如同行云流水,说不定他一高兴表示要给题目加点限制,比如要求 实现一下链表的局部反转、局部旋转、两两旋转、分组反转 等等,这些题有难有易,但是只要平时好好分析一下,相信也能一次通过的。

更新记录

  • 2022-08-11:发布

待更新:K 个一组翻转链表。

整体反转

方法 1:迭代法

思路:使用三个指针分别记录当前位置、上一个和下一个位置,那么“在指针滑行的途中”呢:

  • 先备份下一个位置
  • 修改指向关系,完成指针当前位置的反转
  • 迭代,重复上述操作
  • 遍历完成,此时当前位置到达 nullptr,新的头结点位于上一个位置

代码:

class Solution {
public:
    // 迭代法、三指针
    ListNode* reverseList(ListNode* head) 
    {
        // 1、参数检查:至少要有两个节点
        if(!head || !head->next) return head;
        
        // 2、准备工作
        ListNode* pre = nullptr;
        auto cur = head;
        ListNode* nex = nullptr;
        
        // 3、反转
        while(cur) {
            nex = cur->next;  // 备份下一个位置
            cur->next = pre;  // 当前位置链接前一个节点
            pre = cur;
            cur = nex;
        }

        // 4、返回
        return pre;
    }
};

注:

  • auto cur = head 没问题,写 auto pre = nullptr 是错的,因为 nullptr 是 nullptr_t 类型,此处无法推导出 ListNode 指针类型,会提示 error: assigning to 'nullptr_t' from incompatible type 'ListNode *' (看起来很低级,但面试时一紧张可能出现各种难以想到的失误)
  • cur->next = pre 操作很好理解,关键是其他几个指针操作的顺序, nex = cur->next 本来是写在最后的,但是由于反转操作,导致 cur 的 next 会丢失,因此要将它写在反转操作之前。
  • 返回 pre 而不是 cur:while(cur) 的退出条件时 cur == null ,此时 pre 停留在最后一个有效元素,它为链表的新 head。

方法 2:递归法

image.png

class Solution {
public:
    // 递归法
    ListNode* reverseList(ListNode* head) {
        // 1、参数检查:至少要有两个节点
        if(!head || !head->next) return head;
        
        // 2、准备工作
        ListNode* cur = head;
        ListNode* nex = cur->next;
        
        // 3、反转
        // 返回 nex 链表反转后的头结点
        auto nh = reverseList(nex);   // (1)
        // 让 nex 指向 cur
        cur->next = nullptr;
        nex->next = cur;

        // 4、返回 (2)
        return nh;  
    }
};

注:

  • (1)必须写在修改指向之前,因为 nex->next = cur 修改了 nex 的指向;
  • (2)返回的是新头结点,由递归(前序遍历)到最后一个有效节点时返回。

局部反转

  • 题目要求:给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表
  • 链接:92. 反转链表 II
  • 方法:穿针引线法

对于如下示例:

输入:head = [1,2,3,4,5], left = 2, right = 4 
输出:[1,4,3,2,5]

反转前后的指针位置如图所示:

image.png

具体步骤如下:

  • 给定的区间范围是从 1 开始的,先建立头部伪节点
  • 穿针引线,确定两个边界,将待反转部分切分出来
  • 对切分部分直接反转
  • 将反转完成的局部链表拼接回去

代码:

class Solution {
    ListNode* reverseList(ListNode* head) {
	    ...
    }

public:
    ListNode* reverseBetween(ListNode* head, int left, int right) 
    {
        if(!head) return head;

        // 1、给定的区间范围是从 1 开始的,先建立头部伪节点
        auto dummy = new ListNode(-1);
        dummy->next = head;
        auto p_left = dummy, p_right = dummy; 
        auto p = p_left->next;

        // 2、确定两个边界 L、R,将中间待反转部分 [p_l, p_r] 提取出来
        for(int i = 0; i < left-1; i++) {
            p = p->next;
            p_left = p_left->next;
        }
        auto L = p_left;

        for(int i = 0; i < right; i++) {
            p_right = p_right->next;
        }
        auto R = p_right->next;
        p_right->next = nullptr;
        
        // 3、对中间待反转部分直接反转
        auto nh = reverseList(p);
        p = nh; 

        // 4、再拼接回去
        L->next = nh;
        while(p && p->next) p = p->next;
        p->next = R;
		
	// 5、返回
        return dummy->next;
    }
};

局部旋转

  • 题目要求:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
  • 链接:61. 旋转链表 - 力扣(LeetCode)
  • 方法:迭代法(穿针引线)

旋转前后示意图如下:

image.png

代码:

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) 
    {
        // 当只有一个或没有节点时,直接返回
        if(!head || !head->next) return head;
        
        // 求链表长度 
        int n = 0;
        auto p = head;
        while(p) { n++; p = p->next; }
        k %= n;
        if(k == 0) return head;

        // 准备
        auto dummy = new ListNode(-1);
        dummy->next = head;
        auto p1 = dummy->next;
        auto p2 = dummy;
        int n1 = n - k;
        while(n1--) { p2 = p2->next; }

        // 根据 k 将链表分为两组
        auto q1 = p2->next;
        auto q2 = q1;
        while(q2 && q2->next) { q2 = q2->next; }

        // 交换这两个子链表的位置
        p2->next = nullptr;
        dummy->next = q1;
        q2->next = p1;

        // 返回
        auto ret = dummy->next;
        delete dummy;
        return ret;
    }
};

两两反转

  • 题目要求:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
  • 链接: 24. 两两交换链表中的节点 - 力扣(LeetCode)
  • 方法:迭代法

反转前后示意图如下:

image.png

代码:

class Solution {
public:
    // 迭代法
    ListNode* swapPairs(ListNode* head) {
        // check
        if(!head) return nullptr;
        auto dummy = new ListNode(-1);
        dummy->next = head;
        auto p = dummy;
        
        // [p, p1, p2] => [p, p2, p1]
        while(p->next && p->next->next) {
            auto p1 = p->next;
            auto p2 = p->next->next;
            // 更新链接
            p->next = p2; 
            auto p3 = p2->next;
            p2->next = p1;
            p1->next = p3;
            // 指向下一位置
            p = p1;
        }
        return dummy->next;
    }
};

总结

注:待内容全部同步完成后再做总结。

鸣谢

说明

  • 【算法】系列相关博客正在更新中,感兴趣的朋友欢迎 star,您的支持是我继续更新下去的最大动力!
  • 由于本人水平、精力有限,文中可能存在疏漏之处,欢迎读者大佬们指正。
  • 对于高质量、格式规范的建议,确认无误后会合并到博客中,并将贡献者加入【鸣谢】名单中。
  • 可以随意转载,但要注明出处。