day4:第二章 链表 Part02

今日任务(2024/05/19~2024/05/21)

本是2024/05/11的任务,又花了好些天才补完。

这段时间在面试中常常遇到类似的题目,甚至是原题(特别是链表篇章的内容),而写解析的过程让我把很多细节都想得更清楚了,所以在面试遇到的时候觉得轻松很多,也觉得很值得。

  • 24.两两交换链表中的节点
  • 19.删除链表的倒数第N个节点
  • 面试题02.07.链表相交
  • 142.环形链表Ⅱ
  • 总结

24. 两两交换链表中的节点

题目建议

  • 这道题比较基础,用虚拟头结点可以方便地以同一种方式处理所有节点
  • 需要注意的是:当只有1个节点或者没有节点时,是不需要做交换的。这也要求我们在交换操作之前先判断是否有2个节点。

题目:24. 两两交换链表中的节点 - 力扣(LeetCode)

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:
示例1

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

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

虚拟头节点[ 用时: 11 m 8 s ]

思路

设置虚拟头节点,其后每2个节点做一次交换,交换的步骤如下:

初始化pre = dummyHead; cur1 = head;,要交换的是pre后的2个节点:cur1cur2

  • 保存cur2->nexttemp
  • pre指向cur2
  • cur2指向cur1
  • cur1指向temp

经过以上操作,完成两个节点的交换,后面还需要更新节点:

  • pre更新为cur1
  • cur1更新为temp

注意,cur2暂不更新,因为需要判断下面2个条件是否满足,如果不满足则说明后面没有节点 或者 只有1个节点了,这时就不需要交换了:

  1. cur1不为空节点
  2. 条件1满足时,cur1->next也不能为节点

代码

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode *dummyHead = new ListNode(-1, head);
        ListNode *pre = dummyHead, *cur1 = head;
        while (cur1 != nullptr && cur1->next != nullptr) {
            ListNode *cur2 = cur1->next;
            ListNode *temp = cur2->next;
            // 交换节点
            pre->next = cur2;
            cur2->next = cur1;
            cur1->next = temp;
            // 更新节点
            pre = cur1;
            cur1 = temp;
        }
        return dummyHead->next;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)

  • 空间复杂度: O ( 1 ) O(1) O(1)

Carl思路

代码随想录 (programmercarl.com)

19.删除链表的倒数第N个节点

题目建议

  • 双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点

题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

示例1

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

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

**进阶:**你能尝试使用一趟扫描实现吗?

两次扫描[ 用时: 8 m 15 s ]

思路

暴力解法:

  • 扫描1:获取链表总长度len,确定要删除的是第 ( l e n − n + 1 ) (len - n + 1) (lenn+1)个节点
  • 扫描2:删除节点

本题涉及到删除节点,还是设置一个虚拟头节点来便于操作。

代码

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummyHead = new ListNode(-1, head);
        // 扫描1:确定链表长度
        int len = 0;
        ListNode *pre = dummyHead;
        while (pre->next != nullptr) {
            ++len;
            pre = pre->next;
        }
        int toDel = len - n + 1;
        // 扫描2:找到第(len - n)个节点
        pre = dummyHead;    // 我们要删除的是pre->next
        while (--toDel) {
            pre = pre->next;
        }
        // 删除第(len - n + 1)个节点
        ListNode *temp = pre->next;
        pre->next = temp->next;
        delete temp;    // 释放内存空间
        temp = nullptr; // 避免野指针
        
        return dummyHead->next;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)

    • 2次扫描,时间复杂度为 O ( 2 n ) O(2n) O(2n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

进阶:一次扫描

我的思路[ 用时: 10 m 45 s ]

如果做一次扫描,我有一个想法,但是空间复杂度为 O ( n ) O(n) O(n)

  1. 通过一次扫描完成以下事情:
    • 将链表上每个节点及其对应位置index存放在一个容器vector<ListNode*> node
    • 确定链表长度len
  2. 删除第 ( l e n − n + 1 ) (len - n + 1) (lenn+1)个节点,因此要获取第 ( l e n − n ) (len - n) (lenn)个节点,即node[len - n]
  3. 删除node[len - n]的下一个节点

代码

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummyHead = new ListNode(-1, head);
        ListNode* node[31] = {dummyHead}; // 避免频繁emplace_back()开销
        // 扫描1:1.存储节点;2.获取节点总数
        // 节点总数
        int len = 0;
        ListNode *pre = dummyHead;
        while (pre->next != nullptr) {
            node[++len] = pre->next;    // 下标从1开始
            pre = pre->next;
        }
        // 获取第(len - n)个节点
        pre = node[len - n];
        // 删除第(len - n + 1)个节点
        ListNode *temp = pre->next;
        pre->next = temp->next;
        delete temp;    // 释放内存
        temp = nullptr; // 避免野指针

        return dummyHead->next;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

Carl思路:双指针法

代码随想录 (programmercarl.com)

  1. 设置快慢指针,均初始化为dummyHead
  2. fast先走n + 1个节点
  3. fastslow一起向后扫描,直到fastnullptr,此时slow指向倒数第n + 1个节点(即要删除的节点的前一个)
  4. 删除slow->next节点
代码
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummyHead = new ListNode(-1, head);
        ListNode *fast = dummyHead, *slow = dummyHead;
        // fast先前进n + 1个节点
        for (int i = 0; i < n + 1; ++i) {
            fast = fast->next;
        }
        // 扫描1:找到倒数第n + 1个节点
        while (fast != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        // 删除倒数第n个节点
        ListNode *temp = slow->next;
        slow->next = temp->next;
        delete temp;
        temp = nullptr;

        return dummyHead->next;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

面试题 02.07. 链表相交

题目建议

  • 注意 数值相同,不代表指针相同

题目:面试题 02.07. 链表相交 - 力扣(LeetCode)

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交**:**
示例

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:
示例1

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

示例2

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

示例3

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

**进阶:**你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

暴力解法

这2个链表在后半段可能重合,困难的地方在于重合点之前的节点数不一定一样,如果暴力解决有2个思路:

  • 思路1:利用后半段相同,倒序查找?这很离谱,无法实现,因为链表是单向的
  • 思路2:扫描链表1,使用unordered_set<ListNode*> st1记录链表1的所有节点地址(值相等没有用),然后扫描链表2,逐个判断st1中是否存在该地址,第一个找到的节点就是重合节点

思路2

  1. 扫描链表1,使用unordered_set<ListNode*> st1记录链表1的所有节点地址(值相等没有用)
  2. 扫描链表2,逐个判断st1中是否存在该地址,第一个找到的节点就是重合节点

代码

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 扫描链表1
        unordered_set<ListNode*> st1;
        ListNode *preA = headA;
        while (preA != nullptr) {
            st1.insert(preA);
            preA = preA->next;
        }
        // 扫描链表2
        ListNode *preB = headB;
        while (preB != nullptr) {
            if (st1.find(preB) != st1.end()) return preB;
            preB = preB->next;
        }
        return nullptr;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)

  • 空间复杂度: O ( n ) O(n) O(n)

进阶:时间 O ( n ) O(n) O(n)内存 O ( 1 ) O(1) O(1)

在前面暴力解法分析时提到过*“困难的地方在于重合点之前的节点数不一定一样”*,这里Carl的思路其实就是解决这个问题——把两个链表的节点数调一致

Carl思路

代码随想录 (programmercarl.com)

通过两次扫描,确定两个链表长度;第三次扫描确定重合点:

  1. 扫描链表1:确定链表1长度sz1

  2. 扫描链表2:确定链表2长度sz2

  3. 求出长度差

  4. 尾端对齐:

    • 将长链表前置多余节点走完(现在的位置即为出发点sta1
    • 短链表的出发点即为头节点sta2 = headA或headB

    也可以通过swap()使得**sta1始终为长链表起始点**

  5. 同时扫描链表1、2:找到重合点

代码

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int sz1 = 0, sz2 = 0;
        ListNode *cur1 = headA, *cur2 = headB;
        // 扫描1:获取链表A长度
        while (cur1) {
            cur1 = cur1->next;
            ++sz1;
        }
        // 扫描2:获取链表B长度
        while (cur2) {
            cur2 = cur2->next;
            ++sz2;
        }
        // 令cur1为长链表
        cur1 = headA;
        cur2 = headB;
        if (sz1 < sz2) {
            swap(cur1, cur2);
            swap(sz1, sz2);
        }
        // 尾端对齐
        while (sz1 != sz2) {
            cur1 = cur1->next;
            --sz1;
        }
        // 扫描3:查找重合点
        while (cur1 != nullptr && cur2 != nullptr) {
            if (cur1 == cur2) return cur1;  // 找到重合点(注意:重合点不是节点值相同,而是节点地址相同)
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        return nullptr;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

Carl思路优化

Carl做了3次扫描解决了问题,实际上我觉得上面的步骤1~4可以合并在一次扫描中完成,总共2次扫描。

这个具体的实现过程还没想好。

142.环形链表II

题目建议

  • 算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口
  • 拼多多一面遇到了,我突然想不清楚怎么找到环长度,面试官还详细解释了一下,并且跟我说没关系,这个其实是个智力题hh

题目:142. 环形链表 II - 力扣(LeetCode)

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

示例1

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

示例2

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

示例3

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

**进阶:**你是否可以使用 O(1) 空间解决此题?

双指针法[ 用时: 44 m 29 s ]

思路

采用快慢指针方法,快慢指针步长分别为2、1:

  1. 判断是否有环:以不同速度从头节点出发,当指向同一个节点时,说明有环;否则无环。
  2. 寻找到环入口:双指针,指针1从头节点出发,指针2从重合节点出发,步长均为1,相遇节点即为环入口(这个证明过程稍稍有些复杂)
    • 假设在步骤1中,快指针在与慢指针相遇前已在环内转了n圈,则有 y − x = n ⋅ l o o p y-x=n\cdot loop yx=nloop(其中y和x分别为快慢指针所走的路程)。因为快指针步长是慢指针2倍,所以路程也是2倍的关系 y = 2 x y=2x y=2x,代入上式可得: x = n ⋅ l o o p x=n\cdot loop x=nloop
    • 假设在步骤1中,慢指针进环后所走长度为 d x + m ⋅ l o o p dx+m\cdot loop dx+mloop,进环前所走长度为 z z z,则有 x = z + d x + m ⋅ l o o p x=z+dx+m\cdot loop x=z+dx+mloop,又因为 x = n ⋅ l o o p x=n\cdot loop x=nloop,所以有 d x = ( n − m ) ⋅ l o o p − z dx=(n-m)\cdot loop-z dx=(nm)loopz
    • 在步骤2中,因为指针步长一样,假设指针1、2所走长度均为 w w w,当且仅当 w = k ⋅ l o o p − d x w=k\cdot loop-dx w=kloopdx w = z w=z w=z时,指针1、2才能在环入口处相遇。
    • d x = ( n − m ) ⋅ l o o p − z dx=(n-m)\cdot loop-z dx=(nm)loopz代入 w w w的表达式,可以得到 w = k ⋅ l o o p − ( n − m ) ⋅ l o o p + z w=k\cdot loop-(n-m)\cdot loop+z w=kloop(nm)loop+z,整理得 w = ( k − n + m ) ⋅ l o o p + z w=(k-n+m)\cdot loop+z w=(kn+m)loop+z
    • 这代表着指针2可能会在环内跑几圈,但是两个指针确实会在环入口处相遇(指针1进了环之后就永远不能和指针2相遇了呀,因为步长是相同的)。

补充:

  1. 计算环长度:令快指针从重合节点出发(步长为1),慢指针一直在重合节点这等着,当快指针又到达重合节点时,所走的路程就是环长度。

代码

  • 注意,在判断是否有环时,while遍历的条件容易写错。
class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        // 判断是否有环
        ListNode *fast = head, *slow = head;
     	// 【易错】
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
            // 找到重合点,有环
            if (slow == fast) {
                // 寻找环入口
                ListNode *cur1 = head, *cur2 = slow;
                while (cur1 != cur2) {
                    cur1 = cur1->next;
                    cur2 = cur2->next;
                }
                return cur1;
            }
        }
        return nullptr; // 无环
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)

  • 空间复杂度: O ( 1 ) O(1) O(1)

Carl思路

代码随想录 (programmercarl.com)

总结

  • 24.两两交换链表中的节点

    • 虚拟头节点的设置
    • 注意:当后面不足2个节点时,就不需要再交换了
    while (cur1 != nullptr && cur1->next != nullptr)
    
  • 19.删除链表的倒数第N个节点

    • 也用了虚拟头节点
    • 暴力解法(2次扫描):
      • 一次用于确定总长度,一次用于删除倒数第n个节点
      • 注意:要删除某个点,需要找到其前一个节点
    • 双指针法(1次扫描):
      • 快指针先行n + 1步,后面和慢指针一起同步前进
      • 这样当快指针指向末尾的时候,慢指针正好在倒数第n个节点的前一个节点
  • 面试题02.07.链表相交

    • 3次扫描:
      • 扫描1、2用于确定2个链表的长度
      • 用一次小小的扫描移动长链表的起始节点,使两个链表尾端对齐
      • 扫描3用于找到相交点
  • 142.环形链表Ⅱ

    • 快慢指针法的灵活应用
    • 关键在于数学推导的过程,俺觉得确实是需要智力才能理解的hh
    • 注意:快慢指针遍历时的判断条件很容易错(跟前面的24.两两交换链表中的节点是一样的哦)
    while (fast != nullptr && fast->next != nullptr)
    
  • 本文若存在侵权,烦请指出,本人会立马删除相关内容;
  • 注意:当后面不足2个节点时,就不需要再交换了
while (cur1 != nullptr && cur1->next != nullptr)
  • 19.删除链表的倒数第N个节点

    • 也用了虚拟头节点
    • 暴力解法(2次扫描):
      • 一次用于确定总长度,一次用于删除倒数第n个节点
      • 注意:要删除某个点,需要找到其前一个节点
    • 双指针法(1次扫描):
      • 快指针先行n + 1步,后面和慢指针一起同步前进
      • 这样当快指针指向末尾的时候,慢指针正好在倒数第n个节点的前一个节点
  • 面试题02.07.链表相交

    • 3次扫描:
      • 扫描1、2用于确定2个链表的长度
      • 用一次小小的扫描移动长链表的起始节点,使两个链表尾端对齐
      • 扫描3用于找到相交点
  • 142.环形链表Ⅱ

    • 快慢指针法的灵活应用
    • 关键在于数学推导的过程,俺觉得确实是需要智力才能理解的hh
    • 注意:快慢指针遍历时的判断条件很容易错(跟前面的24.两两交换链表中的节点是一样的哦)
    while (fast != nullptr && fast->next != nullptr)
    
  • 本文若存在侵权,烦请指出,本人会立马删除相关内容;
  • 本文内容若有不正确或不规范指出,请大家不吝赐教~
  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值