算法训练Day4

2023年3月4日

今天的任务有四个

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

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

简单来说,节点之间的交换就是指针交换而并非值交换,所以不能单纯的用双指针同步移动进行值交换

具体的步骤应为下面这张图

即先用cur->next = cur->next->next 进行步骤一,然后用cur->next->next = cur->next 进行步骤二,再用cur->next = cur->next->next->next进行步骤三,在这个过程中设置两个临时指针会比较好,以防在交换过程中出现野指针的情况,源码如下(带注释)

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        return dummyHead->next;
    }
};

//以下是我自己按照思路写的一遍
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode * dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode * cur = dummyHead;
        while(cur->next!=nullptr && cur->next->next!=nullptr){
            ListNode * temp1 = cur->next;
            ListNode * temp2 = cur->next->next->next;

            cur->next = cur->next->next;
            cur->next->next = temp1;
            cur->next->next->next = temp2;

            cur = cur->next->next; 
        }
        return dummyHead->next;
    }
};

注意,关于NULL和nullptr的区别可以简单理解为NULL是0,nullptr是空指针,具体讲解请移步(5条消息) C++中NULL和nullptr的区别_nullptr 和null_csu_zhengzy~的博客-CSDN博客

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

一开始写的时候就简简单单认为直接指针移动到倒数第n+1个节点进行删除不就行了,让最后一个节点指向第一个节点,变成一个单向循环链表,然后再计算链表长度,数学计算啥的,最后找到倒数第n+1个节点进行删除,虽然麻烦但是解决问题

看到代码随想录中提供的思路我觉得真的是抄了“捷径”,通过设置快慢指针,直接让一个慢指针指向了待删除结点的前一个节点(指向前一个节点便于删除),直接省略了我前面的所有步骤,这种方法让我对快慢指针有了新的理解

快慢指针说本质还是双指针,一开始的双指针是相邻的,便于进行数值的交换,后来变成一左一右,向中间移动进行查找,即二分查找,再后来是快慢指针,一是进行数组的覆盖,当两个指针指向同一个数的时候,快指针向右移动一位而慢指针不移动,然后用fast的值对slow的值进行覆盖,然后再同时向右移动快慢指针,直到slow指针指到下一个要删除的元素,再用fast对slow进行覆盖,对于这道题,跟数组的原理是一样的,让一个指针指向待删除的元素,只不过让fast先移动n位我是没想过,由此可见对于双指针的用法还是不太灵活,需要继续加强理解

言归正传,这道题经过刚刚的思路分析,代码如下

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete nth;
        
        return dummyHead->next;
    }
};

这个代码在不考虑时间复杂度的情况下可以考虑改进的就是那个n--的部分,可以从头到尾遍历一遍求出链表的长度,然后直接用len-n就是fast移动的步数,如果不想让fast移动到null节点的话,让fast移动到最后一个节点也是可以的,这样的话fast和slow同时走,当fast指向最后一个节点的时候,slow也就指向了被删除节点的前一个节点,两种方法应该都是可以的

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

注意,这道题说的相交指的是地址相交,即节点中的地址是一样的,并不能简单理解为值相等,所以这道题就是需要找到地址一样的两个节点

我一开始想的是,直接设置两个指针分别指向A和B,然后向右移动找到地址一样的两个节点不就行了,代码如下

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * cura = headA;
        ListNode * curb = headB;
        while(cura != curb){
            cura = cura->next;
            curb = curb->next;
        }
        return cura;
    }
};

但是是没法ac的,会出现报错:runtime error: member access within null pointer of type 'ListNode' (solution.cpp),这句话的意思是引用了空指针,也就是说在while循环里面进行判断的时候,即执行cura = cura->next这句话或者curb = curb->next这句话的时候,变成了空指针,会出现报错。那么如果我加上不是空指针呢?如下所示

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * cura = headA;
        ListNode * curb = headB;
        while(cura->next != nullptr && curb->next != nullptr && cura != curb){
            cura = cura->next;
            curb = curb->next;
        }
        return cura;
    }
};

还是没法ac过,这时候的原因就是链表长度不一致导致的问题,因为这种代码默认了两个链表在相同位置产生了相同的节点地址,但实际情况可能并不是这样的,这时候再加上长度的考虑,这时候就有两种方案了,一种是使用双循环,固定一个查找另一个,代码如下

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * cura = headA;
        ListNode * curb = headB;
        if(cura == NULL){
            return NULL;
        }
        if(curb == NULL){
            return NULL;
        }
        if(cura == curb){
            return curb;
        }
        for(cura = headA;cura->next!=NULL;cura=cura->next){
            for(curb = headB;curb->next!=NULL;curb=curb->next){
                if(cura==curb){
                    return curb;
                }
            }
        }
        return NULL;
    }
};

这种方式依然有问题,通过41/45,为什么呢?因为你判断的时候总是判断的下一个节点,即cura/b->next 是不是null,相当于你最后一个节点压根没判断,将next去掉即可,代码如下

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * cura = headA;
        ListNode * curb = headB;
        if(cura == NULL){
            return NULL;
        }
        if(curb == NULL){
            return NULL;
        }
        if(cura == curb){
            return curb;
        }
        for(cura = headA;cura!=NULL;cura=cura->next){
            for(curb = headB;curb!=NULL;curb=curb->next){
                if(cura==curb){
                    return curb;
                }
            }
        }
        return NULL;
    }
};

我这种方式的时间复杂度是O(n^2),虽然说通过了,但算法的目的就是让时间复杂度尽可能的减少,下面是代码随想录中给的思路

简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。

为了方便举例,假设节点元素数值相等,则节点指针相等。

看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:

我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:

此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。

否则循环退出返回空指针。

代码如下

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != NULL) { // 求链表A的长度
            lenA++;
            curA = curA->next;
        }
        while (curB != NULL) { // 求链表B的长度
            lenB++;
            curB = curB->next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            swap (lenA, lenB);
            swap (curA, curB);
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap--) {
            curA = curA->next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != NULL) {
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

这种方法成功将时间复杂度降到O(n+m)了,是一个新的思路,通过改变遍历的位置减少一个for循环

#环形链表II 142. 环形链表 II - 力扣(LeetCode)

我一开始想的是,直接设置一前一后两个指针,当后面指针指向前一个指针的时候,后面指针所指位置就是环形链表入口,后来仔细想一下,发现这种并不普适,有时候是后面的指针指向前一个指针的时候,后面指针是环形链表入口,有时候是两个指针相交的时候是环形链表入口

经过学习我发现,这种题基本都是一个套路:设置快慢指针,快指针一次走两位,慢指针一次走一位,当两个指针“相撞”的时候,就说明有环,之后再对两个指针所指向的位置进行记录,一个指针在当前位置,另一个指针指向最开始的位置,两个指针同时移动,当两个指针“相撞”的时候,就说明这个位置是环形链表入口,代码随想录中写的已经不能更清楚了,我在这里只附上源码,具体思路请移步代码随想录 (programmercarl.com)

代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};

那么今天就到这里吧,希望能继续坚持下去,明天见!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值