代码随想录算法训练营第60期第四天打卡

      大家好,今天刚刚参加完蓝桥杯,感觉自己要完蛋了,没几道会的题目,或许是自己的实力还不够强,或许是题目难,无论结果如何我还是继续努力把训练营布置的任务认真完成,在开始今天的题目之前我先要告诉大家一个道理,就是失败不可怕,拿不到奖更不可怕,重要的是你的精气神绝对不能丢,一定要坚持下去,那在短暂的平复心请之后,我们要开始今天的题目了,估计今天的题目对我会比较有难度,因为以前我没刷过,今天是第一次刷,那就来吧。

第一题对应力扣上编号为24的两两交换链表中的节点

       首先我们先来回顾一下,链表与数组是有很大的区别的,包括增删改都很不一样,同样今天的两两交换链表中的节点也是,昨天我们反复使用虚拟头节点,那其实在这道题目里也是需要虚拟头节点的,我们来看一下题目是如何说的?

    注意题目要求我们两两交换相邻的节点,我们应该如何考虑呢?大家一定先注意,我首先申请一段内存来创立一个虚拟头节点,因为我得在两个需要交换的节点前面我才能对这两个节点进行操作,这一点大家一定要明白,那究竟如何操作呢?我以代码随想录网站上的例子给大家讲解:

       大家来看,cur就是我当前的虚拟头节点,只有这样我才可以对1,2两个节点进行互换操作,就是我让虚拟头节点指向2节点,2指向1节点,1指向3节点不就得到最下面的链表了吗?那接下来还有一个需要考虑的问题就是我们的遍历啥时候结束,这个其实要看链表的长度了,因为我每次操作都是要往后走了两个节点的,同时要保证我后面要有两个节点供我操作,否则就没有必要了,当奇数节点时,我们应该是cur->next->next != null就可以,否则后面只剩一个节点没有节点与之交换,这是大家要了解的,还有一个就是如果是偶数节点的话就是cur->next != null就可以,因为后面交换完了就没有节点了,比方说上面的这个链表只有四个节点,那我交换完之后我目前的cur指针指向了4节点的位置,但其实链表已经交换完毕了,所以循环这时候就终止了,还有一个比较容易错的问题就是我写循环条件的时候cur->next->next != null && cur->next != null是不对的,因为我们没有保证cur->next != null,在没有保证的情况下就去访问会出现空指针报错的,所以两个应该反过来才对,那接下来我们看一下具体的代码应该如何写:

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移动两位,准备下一轮交换
        }
        ListNode* result = dummyHead->next;
        delete dummyHead;
        return result;
    }
};

      我们来仔细分析一下,就是我首先初始化了一个虚拟头节点,然后我将head赋值给虚拟头节点的next指针,同时我再定义一个cur来保存虚拟头节点,随后就是我们的循环条件我上面解释过大家千万不要写反了,我们要记录两个临时变量,一个保存第一个节点,一个保存第三个节点,因为我们在第一队节点交换完成后就要让第1个节点的next指针指向第三个节点了,也就是cur->next->next->next这样我们开始进行我们上面的图中的三步,第一步是将第二个节点赋值给虚拟头节点的next节点,随后再将tmp赋值给cur->next->next这样就实现了两个节点互换,随后还有很重要的一步就是就是将原来的第一个节点指向第三个节点,大家一定要注意目前的第一个节点是在第二个节点的next,因此最后一步应该是cur->next->next这是第二个节点,cur->next->next->next就是第一个节点最后一步就是cur->next->next->next = tmp1,因为tmp1就是我最初存下来的第三个节点,那虚拟头节点就应该后移两位再去下一对节点的交换,最后交换完的链表的头节点就是dummyhead->next,删除dummyhead,返回头节点就可以了,这个比较难,很容易就写出空指针或者是链表指针会混乱,一定要搞清这些变量究竟是哪一个节点,每赋值一步注意节点的next都可能会变,这个一定要搞清楚。

第二题删除链表的倒数第N个结点对应力扣编号是19

     接下来我们来到第二题,这道题其实题目名称就告诉我们这道题目要求我们做什么了,那我们也是看一下题目要求:

      比如说样例,n=2表示我要删除链表中倒数第二个结点,其实很好懂,但是代码如何实现呢?其实我们昨天就讲到过链表删除元素的操作,我们提到要引入虚拟头节点的操作,因为链表删除元素一般都是去找要删除的元素的上一个元素,那这里就多了一点,我们要先去到合适的位置才能删除,这里我们还是得引入一个虚拟头节点,同时我们需要两个指针,一个快指针,一个慢指针,快指针的主要作用是找到需要删除的元素,而慢指针的只要作用就是删除这个元素了,我们还要注意一点就是这里卡哥的思路非常好,简直发现新大陆了,我还真想不到如何找到倒数第n个节点,其实我们定义两个指针一个快指针,一个慢指针,其实我先让快指针走n步,然后快慢指针同时向后移,这样当快指针的next为空的时候,我的慢指针就指向了我要删除的元素,有朋友可能会不明白?其实我们这样来理解,我的快指针提前走了n个节点,其实就像赛跑一样我的快指针比慢指针多跑了100米,如果后来两个人的运动都是匀速而且速度一样,当我的快指针到达终点的时候其实慢指针就到了离终点100米的地方,这里同理啊,我快指针一直比慢指针多跑了n个节点,当我快指针跑完的时候也就是慢指针跑到了倒数第n个节点的位置,因为快指针始终比慢指针多跑了n个节点,但是大家要注意的是我删除当前节点可不是找这个节点,而是找当前这一个节点的前一个结点,也就是倒数第n+1个节点的位置,我们在快指针走完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; 
        
        
        return dummyHead->next;
    }
};

     大家有了我的思路后其实看这个就不难了,最后我们返回虚拟头节点的下一个节点就可以了,其实很我们这里就可以不释放内存了,因为我们最后还要用虚拟头节点去找到头节点,但当然也可以释放内存,那代码就变成这样,主要思路是不变的,这样避免内存的浪费

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 * result = dummyHead -> next;
        delete(dummyHead);
        
        return result;
    }
};

第三题面试题对应力扣上编号为面试题 02.07. 链表相交

      来到第三题,这是一道面试题,其实看出以后找工作面试官会考察你对链表的理解,因为其实是考察对基础数据理解的一个很好的考察方向,我们一起来看一下这一道面试题,如果你在面试现场你是否能解出来?题目是这样描述的:

      我们要找两个链表的相交节点,其实这个似乎有难度,首先我们要明白相交不是数值相等而是指针相等,

 我们来看下面这两个链表,起初两个指针都指向链表的头节点,我们可以先计算出长度,然后让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;
    }
};

       我们首先要求出两个链表的长度,这个估计不难办,一个指针一个变量就可以解决,得到了以后我们规定一个指针为最长链表的头,我们代码中规定的是curA,因此我们要加一句如果B的长度大于A,我们就交换长度与指针,始终保持curA是最长链表的头指针,还有一句话强调,我们用curA,curB求完了链表长度要再用headA,headB赋值,否则我们就没法遍历了已经到了链表尾部了,这样我们求出两个链表长度的差值,这样先让curA 指针先移动,这样我们让curA,curB可以同时到达终点,这样我们才可以找到了返回相交元素找不到返回null。为啥我不在开头就直接遍历非要在尾部对齐不在头部对齐,其实大家看一下题目给的样例,就拿第一个样例来说:

如果头部对齐:A:4 -> 1 -> 8 -> 4 -> 5

                         B:   5 -> 0 -> 1 -> 8 -> 4 -> 5我们无法找到相交节点

如果尾部对齐:

A :        4 -> 1 -> 8 -> 4 -> 5

B:  5 -> 0 -> 1 -> 8 -> 4 -> 5 这样我们就可以发现我提前走长度差个节点当我两个指针相等的时候就是相交元素。

第四题环形链表II对应力扣上编号142

       这是今天的最后一道题,环形链表还是第一次听说,那我们一起来看看题目是如何描述的?

       我们要明白有环的时候是有什么不一样的地方,其实很明显有环的话是可以转回来的,也就是指针会慢慢指向已经遍历过的元素,我们要如何判断是否有环呢?我直接给出思路,就是我定义两个指针,一个快指针,一个慢指针,我让快指针一次走两个节点,慢指针一次走一个节点,如果有环它俩一定会相遇,首先很明显如果没有环他俩是一定不会相遇的,具体动画大家可以去看一下代码随想录的网站,上面有两个指针相遇的情况,那究竟为啥载环内会相遇呢?首先我们了解快指针一定是先入环的,那这样在环内其实是快指针去追慢指针,而且每次其实我们可以理解成慢指针静止,快指针每次走一个节点,这样是一定可以追上的,所以不会出现快指针跳过慢指针的情况,那接下来就是如何去找到环的入口,这里有一个数学推导,我们来看一下代码随想录给出的图:

    接下来的推导其实是我在相遇时慢指针走过的节点数x + y, 快指针走过的节点数是x + y+ n (y + z),n表示快指针转过的圈数,很明显快指针至少走一圈才可能追上慢指针,因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:我们可以得到(x + y) * 2 = x + y + n (y + z),这样化简可以得到x = (n - 1) (y + z) + z ,特殊的,n = 1的话,x = z,这就意味着从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。大家可以明白吗?代码随想录上的讲解比我的理解要好一些。(来自代码随想录)

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

接下来我们来看一下代码如何写:

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;
    }
};

     我们具体看一下代码,快指针一次走两步,慢指针一次走一步,当有环时就会在环内相遇,同时我在相遇点处定义一个指针,头节点处定义一个指针,两个指针同时一步走,相遇的点就是环的入口,其实这个思路我建议大家背一下,如果没学过真的很难想到这个思路。

今天的题目结束了,总结一下

    今天的题目其实很有意思,都对链表考察很深入的,多练这种题,其实今天我打蓝桥杯虽说考的一塌糊涂,可能省三也没有,但是我发现自己还是思路不够活,基本功不熟练,大家一定要坚持,努力下去,多思考,我们对链表其实这几天的题目也比较了解了,它是一种不同于数组的数据结构,今天的讲解就到这里,感谢大家观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值