代码随想录算法训练营第4天 | 24.两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题 02.07. 链表相交 142.环形链表II

原计划2023年3月4日,2023年3月6日 补打卡


● day 1 任务以及具体安排:训练营一期day 1

● day 2 任务以及具体安排:day 2 第一章数组

● day 3 任务以及具体安排:3 第二章 链表

今日任务

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

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

● 面试题 02.07. 链表相交

● 142.环形链表II

● 总结


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

【链接】(文章,视频,题目)

用虚拟头结点,这样会方便很多。

本题链表操作就比较复杂了,建议大家先看视频,视频里我讲解了注意事项,为什么需要temp保存临时节点。

题目链接/文章讲解/视频讲解: 代码随想录

【第一想法与实现(困难)】

手动实现虚拟头结点

  • 关键是想明白pre, cur, next之间的下一个关系,从而实现两两交换

  • 最后要手动设置新链表的头并返回

【看后想法】

  • 交换1,2,需要1的前继,2的后继指针,因此本身最好用几个指针来提升代码可读性。这里我使用的是cur, next1, next2, next3

  • 最后返回的新链表头

【实现困难】看明白了,没啥困难,整体链表为了提升泛化性,会增加虚拟头结点,dummy_head很好用

【自写代码】

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 虚拟头结点
        ListNode* dummy_head = new ListNode(0, head);
        ListNode* cur = dummy_head;
        while (cur && cur->next && cur->next->next) {
            ListNode* next1 = cur->next;
            ListNode* next2 = cur->next->next;
            ListNode* next3 = cur->next->next->next;
            // 交换next1与next2。从0123变成0213
            cur->next = next2;
            next2->next = next1;
            next1->next = next3;
            cur = cur->next->next; // 后跳两个结点
        }
        head = dummy_head->next;
        delete dummy_head;
        return head;
    }
};

【收获与时长】半小时


【链接】(文章,视频,题目)

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

双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点,建议先看视频。

题目链接/文章讲解/视频讲解:代码随想录

【第一想法与实现(困难)】

遍历了两次,一次找到最大size,并确认需要删除的index,第二次才做删除

【看后想法】根据“倒数”的想法设计双指针。

双指针,快慢指针,快指针比慢指针多走n+1步。这样,快指针指向链表尾巴fast == nullptr时,慢指针指向待删除的倒数第n个结点的前一个

【实现困难】理解思路之后不难,关键是想到双指针各自的定义是什么

【自写代码】

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 双指针,快慢指针,快指针比慢指针多走n+1步。这样,快指针指向链表尾巴fast == nullptr时,慢指针指向待删除的倒数第n个结点的前一个
        ListNode* dummy_head = new ListNode(0, head);
        ListNode* fast = dummy_head;
        ListNode* slow = dummy_head;
        while (n-- && fast) { // fast移动n步
            fast = fast->next;
        }
        // fast多移动一步,使得慢指针指向待删除的倒数第n个结点的前一个
        fast = fast->next;
        while (fast) {
            fast = fast->next;
            slow = slow->next;
        }
        // 删除slow的下一个
        ListNode* next1 = slow->next;
        ListNode* next2 = slow->next->next;
        slow->next = next2;
        delete next1;

        head = dummy_head->next;
        delete dummy_head;
        return head;
    }
};

【收获与时长】半小时,比较巧妙的双指针设计方法


【链接】(文章,视频,题目)

面试题 02.07. 链表相交

本题没有视频讲解,大家注意 数值相同,不代表指针相同。

题目链接/文章讲解:代码随想录

【第一想法与实现(困难)】

  • 暴力,两个循环,遇到问题,curB定义完成之后,需要在第二层while循环复位成curB = headB,才是完成了两个循环的操作。因此循环要注意循环变量初始化。

  • 时间复杂度O(mn),性能比较差

【看后想法】

  • 避免循环嵌套,希望线性时间复杂度,因此各自遍历一次,先求取size / length

  • 如果有交叉,其尾部必然是对齐的。那么只要遍历两次就可以了

  • 长短的判断,不需要多写一次条件,只需要std::swap(lengthA, lengthB); std::swap(curA, curB)即可

【实现困难】

  • 线性时间复杂度,各自求长度,对齐末尾再遍历

  • 判断条件不是值相等,而是指针相等(地址相同)

【自写代码】

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 线性时间复杂度
        int lengthA = 0;
        int lengthB = 0;
        ListNode* curA = headA;
        ListNode* curB = headB;
        // 各自求长度
        while (curA) {
            lengthA++;
            curA = curA->next;
        }
        while (curB) {
            lengthB++;
            curB = curB->next;
        }
        curA = headA;
        curB = headB;
        // 设置A为较长的,B为较短的
        if (lengthB > lengthA) {
            std::swap(lengthA, lengthB);
            std::swap(curA, curB);
        }
        // 对齐末尾
        int gap = lengthA - lengthB;
        while (gap--) {
            curA = curA->next;
        }
        while (curA && curB) {
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return nullptr;
    }
};

【收获与时长】不到一小时,一开始没有转过弯来


142.环形链表II

【链接】(文章,视频,题目)

算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口,建议先看视频。

题目链接/文章讲解/视频讲解:https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html

【第一想法与实现(困难)】

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 暴力,记录已经访问过的结点地址集合
        std::set<ListNode*> node_set;
        ListNode* cur = head;
        int max_size = 1e4;
        while (cur && max_size--) {
            if (node_set.find(cur) != node_set.end()) {
                return cur;
            }
            node_set.insert(cur);
            cur = cur->next;
        }
        return nullptr;
    }
};

【看后想法】

  • 看来想要常数空间复杂度,一般都需要用快慢指针,用两个指针代替掉需要额外存储的O(n)数据结构

  • 看了整个数学推导证明,自己负数一遍。借用卡哥的图片和notation

  • 已知快慢指针相遇,也即有环,环入口前长度x,环部分相遇前长度y,环部分相遇后长度z

  • 快慢指针关系有2(x+y) = x + n(y+z) + y,其中n为快指针走的完整环圈数,n>=1。如果n=0,则快指针都还没走完完整环,快慢指针无法相遇

  • 所求为环入口位置x = (n-1)(y+z) + z, 其中n>=1,因此在头结点与相遇处各自开始,每次走一步,会在环入口相遇。

  • 感性理解为,相遇处走z之后到达环入口,n=1时候x=z,直接相遇。n>=2,头结点index到达环入口时,相遇结点index2多走了n-1圈,但是也肯定在环入口,因此他们会在环入口相遇

【实现困难】

  • 先开始走(fast两步,slow一步),再判断相遇,否则头结点就直接相遇了

【自写代码】

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 快慢指针,快指针走两步,慢指针走一步,相遇说明有环。相遇结点与头结点再重新走各一步,在环入口碰上
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast && fast->next) {
            fast = fast->next->next;
            slow = slow->next;
            // 快慢结点相遇说明有环
            if (fast == slow) {
                // 从头结点,相遇结点开始单步前进,在环入口相遇,返回环入口
                ListNode* index1 = head;
                ListNode* index2 = fast;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index1;
            }
        }
        return nullptr;
    }
};

【收获与时长】

2小时


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值