代码随想录算法训练营Day04 | LeetCode24.两两交换链表中的节点、LeetCode19.删除链表的倒数第N个节点、面试题 02.07. 链表相交、

79 阅读6分钟

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

题目链接:leetcode.cn/problems/sw…

思路:

初始时,cur指向虚拟头结点,然后进行如下三步:

image.png

操作之后,链表如下:

image.png

image.png

C++代码如下:

/**
 * 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!=NULL) && (cur->next->next!=NULL))
        {
            ListNode* tmp0 = cur->next;     //记录临时节点
            ListNode* tmp1 = cur->next->next->next;   //记录临时节点

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

            cur = cur->next->next;   //更新cur,准备下一轮交换
        }
        return dummyHead->next;
    }
};

总结:一定要画图,若不画图,操作多个指针很容易乱,注意操作的先后顺序

LeetCode19. 删除链表的倒数第N个结点

题目链接:leetcode.cn/problems/re…

思路:看到这题的第一个想法就是:先把整个链表反转,找出想要删除的结点,再反转回来。

C++代码如下(正确):

/**
 * 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)
    {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* pre = NULL;
        ListNode* cur = head;

        while(cur != NULL)
        {
            ListNode* tmp = cur->next;   //暂存
            cur->next = pre;  //翻转
            
            pre = cur;  //更新
            cur = tmp;
        }
        //return pre;  //此时pre就是原链表的最后一个结点,翻转后链表的头结点
        
        ListNode* dummyHead02 = new ListNode(0);
        dummyHead02->next = pre;   //虚拟头结点指向翻转后链表的头结点

        ListNode* cur02 = dummyHead02;  

        n = n-1;
        while(n--)
        {
            cur02 = cur02->next;
        }

        ListNode* deleteNode = cur02->next;
        cur02->next = cur02->next->next;
        delete deleteNode;
        //return dummyHead02->next;  //此时返回的是翻转后且删除结点后的链表的头结点

        ListNode* pre03 = NULL;
        ListNode* cur03 = dummyHead02->next;
        while(cur03 != NULL)
        {
            ListNode* tmp03 = cur03->next;
            cur03->next = pre03;

            pre03 = cur03;
            cur03 = tmp03;
        }
        return pre03;
    }
};

思考:用双指针法,非常巧妙!总体思路如下(双指针的经典应用):如果要删除倒数第n个节点,让fast先移动n步,然后让fast和slow同时移动,直到fast指向链表末尾,此时删掉slow所指向的节点即可

C++代码如下:

/**
 * 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) 
    {
        ListNode* dummyHead = new ListNode(0);   //设置一个虚拟头结点
        dummyHead->next = head;
        ListNode* slowIndex = dummyHead;
        ListNode* fastIndex = dummyHead; 

        n = n+1;
        while(n--)
        {
            fastIndex = fastIndex->next;
        }

        while(fastIndex != NULL)
        {
            slowIndex = slowIndex->next;
            fastIndex = fastIndex->next;
        }
        //此时slowIndex->next就是要删除的节点
        slowIndex->next = slowIndex->next->next;
        return dummyHead->next;
    }
};

面试题 02.07. 链表相交

题目链接:leetcode.cn/problems/in…

思路:

  1. 分别求出两个链表的长度;
  2. 求出长度差gap;
  3. 将较长链表的辅助节点向后移动gap长度,目的是为了末尾位置对齐,让curA与curB在同一起点上;
  4. 遍历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 = new ListNode(0);
        curA->next = headA;
        int lenA = 0;
        while(curA->next != NULL)
        {
            lenA++;
            curA = curA->next;
        }
        //此时lenA为A链表的长度

        ListNode* curB = new ListNode(0);
        curB->next = headB;
        int lenB = 0;
        while(curB->next != NULL)
        {
            lenB++;
            curB = curB->next;
        }
        //此时lenB为B链表的长度
        curA = headA;  //重置一下
        curB = headB;

        //求出长度差gap
        int gap = 0;
        if(lenA >= lenB)
        {
            gap = lenA - lenB;
        }
        else
        {
            gap = lenB - lenA;
        }

        //把curA向后移动,让curA和curB在同一起点上(末尾位置对齐)
        if(lenA < lenB)    //目的是为了让长的为lenA,headA
        {
            swap(curA,curB);
            swap(headA,headB);
        }
        while(gap--)
        {
            curA = curA->next;
        }
        //此时curA到链表尾部的距离和curB到链表尾部的距离相同

        //遍历curA和curB,遇到相同则直接返回
        while((curA != NULL)&&(curA != curB))
        {
            curA = curA->next;
            curB = curB->next;
        }
        return curA;
    }
};

LeetCode142. 环行链表II

题目链接:leetcode.cn/problems/li…

思路:

首先,要判断链表是否有环:定义快慢指针,快指针fastIndex每次走2个节点,慢指针slowIndex每次走1个节点,如果fastIndex和slowIndex指针在途中相遇 ,说明这个链表有环。fastIndex走得比较快,如果能和slowIndex相遇,说明一定是在环内相遇的;至于为什么一定可以相遇,因为fastIndex每次走两步,slowIndex每次走一步,相对于slowIndex而言,fastIndex每次一个节点一个节点的靠近slowIndex,所以一定可以相遇。

接下来,有环的话,如何找到这个环的入口:假设从头结点到环形入口节点数为x,环形入口节点到 fastIndex指针与slowIndex指针相遇节点的节点数为y,从相遇节点再到环形入口节点节点数为z。

image.png

那么相遇时: slowIndex指针走过的节点数为: x + y, fastIndex指针走过的节点数:x + y + n (y + z),n为fastIndex指针在环内走了n圈才遇到slowIndex指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:(x + y) * 2 = x + y + n (y + z)

公式经过移项,有x = n (y + z) - y,即x = (n - 1) (y + z) + z,注意这里n一定是大于等于1的,因为 fastIndex指针至少要多走一圈才能相遇slowIndex指针。

先拿n等于1举例子,n=1时候,公式就是x = z,也就是说,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

n大于1时候,也就是fastIndex指针在环形转了n圈之后才遇到slowIndex指针。但是这种情况和n为1时候的效果是一样的,同样可以通过这个方法找到环形的入口节点,只不过,从相遇节点出发的指针在环里多转了( n-1)圈,然后再遇到从头结点出发的指针,相遇点依然是环形的入口节点。

C++代码如下:

/**
 * 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) 
    {
        //定义快慢指针,fastIndex每次走2个节点,slowIndex每次走1个节点
        ListNode* fastIndex = head;
        ListNode* slowIndex = head;

        //这样子它们一定会相遇的,因为相对于slowIndex来说,fastIndex每次一个节点一个节点的靠近slowIndex。
        while((fastIndex != NULL)&&(fastIndex->next != NULL))
        {
            fastIndex = fastIndex->next->next;  //移动2个节点
            slowIndex = slowIndex->next;  //移动1个节点

            if(fastIndex == slowIndex)  //两个指针相遇了
            {
                 //再定义两个指针,从头结点出发一个指针,从相遇节点出发一个指针,每次都只走1个节点,那么它们相遇的节点就是入环的第一个节点。
                ListNode* cur1 = fastIndex;  //从相遇节点出发
                ListNode* cur2 = head;   //从头结点出发

                while(cur1 != cur2)
                {
                    cur1 = cur1->next;
                    cur2 = cur2->next;
                }
                return cur1;
            }
        }
        return NULL;
    }
};

注意:

在定义快慢指针的时候,是定义成:

ListNode* fastIndex = head;
ListNode* slowIndex = head;

而不是定义成:

ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* fastIndex = dummyHead;
ListNode* slowIndex = dummyHead;

因为快指针与慢指针都要一起从头结点出发而不是从虚拟节点出发,否则结果(相遇的节点)就会出错,以致于一直找不到入环节点就会一直循环最后超时。