链表算法练习day.2

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

链接. - 力扣(LeetCode)

题目描述:

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

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

思路:

通过改变结点指针的指向来实现链表的结点两两交换的过程

使用虚拟头结点的来进行实现,便于改变结点指向时查找到前驱点

实现:

1.定义一个虚拟头结点,让其指向真正链表的头结点

2.定义一个指针cur,指向要交换的两个结点的前一个结点,初始化为虚拟头结点

3.遍历链表进行交换,循环条件是cur的下两个结点都不为空,即cur->next和cur->next->next都不为空,此时两个顺序也不能交换,如果先写cur->next->next不为空,当cur->next为空,那么此时就会对空指针进行操作,编译器就会出错

3.进行结点交换的操作,先保存cur的后第一个结点的信息,让cur->next指向其后面的第二个结点,再保存该结点的后继结点,再让该结点指向cur后的第一个结点,最后让该结点指向保存的第二个结点后继结点,此时两个结点交换完成

4.移动cur指针,依次交换后序的结点

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* swapPairs(struct ListNode* head) {
    //创建一个虚拟头结点
    struct ListNode *nhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    nhead->next = head;

    //创造循环变量指针
    struct ListNode *cur = nhead;

    //循环交换结点
    while(cur->next && cur->next->next)
    {
        //定义两个临时变量,记录交换之后可能会丢失的结点
        //记录要交换的第一个结点的位置
        struct ListNode *loca1 = cur->next;
        //记录要交换的第二个结点的后一个结点
        struct ListNode *loca2 = cur->next->next->next;
        //进行结点交换
        cur->next = cur->next->next; // 虚拟的头结点指向第二个结点(第一次交换)
        cur->next->next = loca1;  //第二个结点指向第一个结点
        loca1->next  = loca2; // 第一个结点指向第二个结点的后继结点

        cur = cur->next->next; //一次会操作两个结点,因此需要移动两位
    }

    return nhead->next;
}

易错点:

1.要注意cur指针的指向,如果要交换第一个结点和第二个结点的位置,那么cur的位置必须在虚拟头结点,否则就导致第一个结点的前驱点找不到,无法将虚拟头结点指向第二个结点,即cur指针的位置必须的要操作的两个结点的前面,因此也要注意操作结点时的边界条件。

2.在交换两个结点的指向时,如果没有保存有第一个要交换结点的位置,以及第二个要交换结点后继结点的位置,会导致结点信息的丢失,导致结点连接错误。

19.删除链表的倒数第n个结点

链接:. - 力扣(LeetCode)

题目描述:

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

思路:

使用虚拟头结点和使用快慢指针的方法,这样便于能够快速查找到要删除结点的前驱点

实现:

1.定义一个虚拟头结点,让该结点指向头结点

2.定义两个指针,分别为快指针和慢指针,让快指针先走n+1步,然后快慢指针一起走,当快指针到达空的位置时,慢指针所在在位置就是我们要删除结点的前驱点

3.将该结点从链表中删除,记得手动去释放该结点所占用的内存空间

问题:为什么要让快指针走先走n+1步而不是n步呢

答:因为快指针如果先走n步,后序快慢指针一起移动,那么当快指针恰好指向为空时,那么慢指针的位置恰好就是我们要删除的倒数第n个结点,这样我们就难以查找到该结点的前驱点,因此让快指针先走n步,后序一起走,当快指针停下时,慢指针刚好就为前驱点,便于我们去删除结点。

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    //定义一个虚拟头结点,让其指向头结点
    struct ListNode *nhead = (struct ListNode*)malloc(sizeof(struct ListNode));
    nhead->next = head;

    //定义快慢指针
    struct ListNode *fast = nhead;
    struct ListNode *slow = nhead;
    //快指针先走n+1步
    for(register int i = 0 ; i <= n ; i++)
        fast = fast->next;
    //快慢指针一起走,指定快指针为空
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    //删除倒数第n个结点
    struct ListNode *del = slow->next;//记录要删除的结点,需要释放内存
    slow->next = slow->next->next;
    free(del);

    //删除虚拟头结点
    head = nhead->next;
    free(nhead);
    
    return head;
}

160.相交链表

链接:. - 力扣(LeetCode)

题目描述:

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。

示例 2:

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA] == listB[skipB]

思路:

求两个链表相交的结点,注意,两个结点相等的是指针而不是值

实现:

1.先分别求出两个链表的长度

2.求出两个链表的长度差

3.让链表尾部对其,移动指针,查找是否有相同的元素

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //定义指针来遍历链表,并计算出链表长度
    struct ListNode *lg = NULL; //记录较长的链表
    struct ListNode *shr = NULL;//记录较短的链表
    int lenA = 0 ,lenB = 0;  //记录两个链表的长度
    lg = headA;
    while(lg) //计算headA的长度
    {
        lg = lg->next;
        lenA++;
    }
    lg = headB;
    while(lg)//计算headB的长度
    {
        lg = lg->next;
        lenB++;
    }
    //求两个链表的长度差
    int add = 0;
    if(lenA > lenB)
    {
        lg = headA;
        shr = headB;
        add = lenA - lenB;
    }
    else
    {
        lg = headB;
        shr = headA;
        add = lenB-lenA;
    }

    //进行尾部对其
    while(add--)
        lg = lg->next;
    //查找是否有相同的元素
    while(lg)
    {   //查找到交叉的结点
        if(lg == shr)
            return lg;
        //没有查找到
        lg = lg->next;
        shr = shr->next;
    }
    //只要程序没有退出,则证明没有交叉的结点,返回为空
    return NULL;
}

142.环形链表

链接:. - 力扣(LeetCode)

题目描述:

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

思路:

判断链表是否有环:

使用快慢指针的方法来进行判断,如果链表有环,那么链表一定会相遇,因为快指针进入链表的环后,会在环里一直循环,当慢指针进入链表的环时,快慢指针一定会在某一个时刻在环里相遇,如果链表是一条直线,那么快慢指针不可能相遇,因为快慢指针移动的速度不同。

如何找到链表环的入口:

假如链表有环,则可以设链表头到环的入口的距离为x个结点,链表环入口到快慢指针相遇的距离设为y个结点,快慢指针相遇到另一个方向环出口的距离为y个结点。

如果快慢指针相遇,则慢指针走了x+y个结点,而快指针走了x+y+n(y+z),n为快慢指针相遇时,快指针在环里移动的圈数

实现:

判断链表是否有环:

定义快慢指针,都让他们指向链表的头,让快指针一次移动两个结点,慢指针移动一个结点,如果快慢指针都进入到链表的环中,那么相当于快指针一次以移动一个结点的速度去追赶慢指针,因此两个指针一定会相遇

找到环的出入口:

由于快指针的速度是慢指针的两倍,因此可以写出

2(x+y) = x+y+n(y+z),可求得x = n(y+z)-y; n一定大于等于1,即快指针至少要走一圈

如果去除一圈的距离可得 x = (n -1 )(y+z)+z; 假如n为1,则 x=z,此时代表着慢指针刚好到环入口时,快指针恰好到环的出口处,因此根据x = (n -1 )(y+z)+z可以得出,当慢指针到达环入口时,快指针在环里面转了n圈恰好位于环出口位置,因此

可以在相遇的位置定义一个指针index1,在起始位置定义一个Index2,两个指针以相同的速度去移动,此时两者相遇的位置就是环的入口处

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    //定义快慢指针,都指向链表头
    struct ListNode *fast = head;
    struct ListNode *slow = head;
    //让快指针一次移动两格,慢指针一次移动一格
    //快指针一次移动两个结点
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        //如果快慢指针相遇,说明找到了环
        if(fast == slow)
        {
            //定义一个指针记录相遇的位置
            struct ListNode *index1 = fast;
            //再定义一个指针指向链表头,让他们同时移动
            struct ListNode *index2 = head;
            //当这两个指针相遇,代表找到了环的入口
            while(index1 != index2)
            {
                index1 = index1->next;
                index2 = index2->next;
            }
            //如果退出循环,则返回环的入口位置
            return index1;
        }
    }
    //证明没有找到环
    return NULL;
}

总结:

在此次对链表的算法练习题中,能够更加熟练的去查找到某一个节点的前驱点,能够注意到了记录链表结点的信息,防止在对链表结点进行操作时,造成链表结点的丢失,更深刻的理解了虚拟头结点和双指针的用法,但对于环形链表的数学推导不熟悉,需要多加练习

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值