Bootstrap

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

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

今天的题目我大部分都三刷过了,所以基本都是一遍过的,不过第一遍做的时候还是很有难度的。链表章节今天就结束了

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

题目链接/文章讲解/视频讲解: https://programmercarl.com/0024.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.html

思路

这题抓住两两交换节点的步骤(尤其是先后操作顺序)就没有问题了,直接上图

在这里插入图片描述

  • 交换节点

    这里我们使用的顺序是1->3 , 2->1 , cur->2,但是需要注意的是当1的next指向3时,此时2无法再访问到,所以需要先用temp指针保存节点2(当然如果你就像先cur->2,再2->1 1>3,也没问题,只不过此时你需要用temp指针保存节点1,因为cur的next指向2后,1无法被访问

    • 总结一下就是指向x节点的next指针如果先被改变,那么就让temp指针指向x
  • 边界条件

    1. 通过上图可以发现我们操作的节点是cur的后两个节点,所以后两个指针不能为空指针。

    2. 而且cur每次走两步,一开始在dummyhead处,那么最终结束时有两种情况,第一就是节点总数是奇数,那么最后cur会位于倒数第二个节点处(此时cur->next->next==nullptr),第二种情况是节点总数是偶数,那么最后cur会位于最后一个节点处(此时cur->next==nullptr

代码

/**
 * 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* swapPairs(ListNode* head) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead;
        while(cur->next!=nullptr && cur->next->next!=nullptr)
        {
            //存下2
            ListNode* temp = cur->next->next;
            //交换,操作3下
            cur->next->next = temp->next;
            temp->next = cur->next;
            cur->next = temp;

            //cur往前走两步
            cur = cur->next->next;
        
        }
        return dummyhead->next;
    }
};

删除链表的导数第N个结点

题目链接/文章讲解/视频讲解:https://programmercarl.com/0019.%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACN%E4%B8%AA%E8%8A%82%E7%82%B9.html

思路

  1. 暴力解法

    暴力方法很简单,就是先遍历链表得出一共有多少个节点,再让cur从dummyhead前进 size - N步(注意:前面(上一篇博客)已经讲过删除节点需要cur位于需要删除节点处的上一个位置

    两次for循环,应该很好实现,我就不写代码了

  2. 双指针法

    这题的双指针法的思想就是一开始快慢指针都位于dummyhead处,接着让快指针比慢指针先走n步,这样当快指针走到终点时,慢指针就位于要删除节点的前面了。

    • 我们说过双指针法最重要的一点就是理解两个指针分别的作用:

      快指针:探寻最后一个节点

      慢指针:探寻需要的删除节点

    在这里插入图片描述

如上图所示,加入我们要删除的是倒数第二个节点,那么fast先走两步到2处,两者再同步向右走,直至fast指向最后一个节点,此时slow也处于要删除节点的前一位了

代码

/**
 * 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) {
        //双指针法 cur是快指针,pre是慢指针
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* pre = dummyhead;
        ListNode* cur = pre;
        //倒数第n个,那两个指针就相隔几(下标相减为n)
        for(int i =0; i<n ; i++)
        {
            cur = cur->next;
        }
        //当cur的下一个是nullptr时,说明此时pre已经位于要删除节点的前面了
        while(cur->next!=nullptr)
        {
            pre = pre->next;
            cur = cur->next;
        }
        //删除操作
        ListNode* temp = pre->next;
        pre->next = pre->next->next;
        delete temp;
        return dummyhead->next;
    }
};

链表相交

题目链接/文章讲解:https://programmercarl.com/%E9%9D%A2%E8%AF%95%E9%A2%9802.07.%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4.html

思路

这题算需要注意的是:数值相等不一定是相同节点,一定要指针相同(即地址相同)

  1. 暴力解法:

    这个没啥好说的,就是两层for循环,对两个链表一个一个检查是否是同一个节点

  2. 双指针法:

    既然暴力解法是双层for循环,那么双指针法一定是值得我们考虑的(前面说过双指针法就是化两层for循环为一层for循环),这题处理双指针的关键就是抓住两个链表的长度差

    • 两个指针遍历链表计算出两个两个链表的长度,并计算长度差dif = lenA - lenB

    • 让指向长度更长的链表的指针curA先走lenA步,此时curA和curB就位于与交点处同样距离处了,让他们同时前进直至相等

      在这里插入图片描述

代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0;
        int lenB = 0;
        while(curA!=nullptr)
        {
            ++lenA;
            curA = curA->next;
        }
        while(curB!=nullptr)
        {
            ++lenB;
            curB = curB->next;
        }
        //别忘了返回起点
        curA = headA;
        curB = headB;
        //为了方便操作,让curA指向长的那一个链表
        if(lenA<lenB)
        {
            swap(lenA,lenB);
            swap(curA,curB);
        }
        int dif = lenA - lenB;
        //计算长度差,差几就让curA提前走几步,最后curA就会和curB相遇
        while(dif--)//走n步,就while(n--),这个可以看出模版了
        {
            curA = curA->next;
        }
        while(curA!=nullptr)
        {
            //一般都是先进行该循环内该做的事,在最后结束循环时才让指针前进
            if(curA==curB)
            {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return nullptr;
    }
};

细心的读者肯定发现这道题并没有使用dummyhead,别急,稍后我会总结

环形链表

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

思路

  1. 哈希表标记:

    看到这题我的第一反应就是使用一个数组来标记走过的节点,这样就可以检测到重复走过的节点。也就是说没走一个节点都需要查一次数组,那么使用哈希表的时间复杂度是最低的,因为哈希表查表的时间复杂度是O(1)

  2. 双指针法:

    这题用双指针法其实就是纯数学题了

    在这里插入图片描述

    如上图所示

    • 我们让slow每次走一步,fast走两步—在这种情况下,fast相对于slow的速度就是一步,也就不会出现fast跳过slow的情况了
    • 都从头结点开始走,那么一定是fast先进入链表环,并在环形中相遇。那么假设头结点与环形入口节点距离为x,按顺时针方向环形入口节点与相遇节点距离y,相遇节点与环形入口节点距离为z(如图)。
    • 第三点非常重要:两个指针一定在slow的第一个循环中相遇
      • fast想要追尾必须要比slow多走一圈
      • 假设当slow第一次进入环形入口时,fast与slow相距d(按顺时针方向),那么以slow为参考系,fast速度为1,想要追尾slow就需要相对slow走z+y-d步,时间也就为z+y-d,所以当fast追上slow时,slow连自己的第一个循环都没走完(slow走一个循环需要z+y步,速度为1,也即时间也是z+y)
      • 此时你也可以发现刚好slow走过的距离就是y嘛(因为slow走过z+y-d相遇,而走过y也是相遇),所以z+y-d=y ,即z=d
    • 所以$2x = n(y+z)+d+x=n(y+z)+z+x \rightarrow^{令n=0} x =z $,等式左边是slow走的路程的两倍,右边是fast走过的路程(n为slow进入环形入口前走过的圈数),因为fast的速度为slow的两倍,所以该等式成立
    • x=z意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

(看完上面的推导我觉得不少人会有点晕吧,还是第一种方法好呢)

代码

/**
 * 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;
//----------------------------------    
        //哈希表法
        unordered_set<ListNode*> mark;
        while(head!=nullptr)
        {
            //检查是否遍历到过
            if(mark.count(head)!=0)
            {
                return head;
            }
            //第一次遍历到就放入哈希表中
            mark.insert(head);
            head = head->next;
        }
        //没有环
        return nullptr;
    }
    
    
    }
};

总结:

对于有增删改,用虚拟头结点

而对于只有查操作的时候,无需使用

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;