单链表OJ题练习讲解

目录

前言

1.返回链表的倒数第k个节点

2.判断链表的回文结构

实现寻找中间节点的函数:

链表逆置的函数:

判断回文结构:

3.链表的相交

4.带环链表的判断

1.怎么判断链表是否为带环链表呢?

2.快慢指针方法的可行性验证和补充

5.寻找带环链表的入环点

6.随机链表的复制(深拷贝)

后记


前言

大家好!欢迎来到小鸥的博客~

本期专栏:数据结构_海盗猫鸥的博客-CSDN博客

在前面的博客中我们已经讲解过了单链表的相关知识,并使用单链表实现了简易的通讯录。本期我们就来一起做几道单链表相关的OJ题吧!(附题目链接和参考代码)

1.返回链表的倒数第k个节点

链接:

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)

问题分析:

由单链表的知识我们可以知道,单链表要查找一个目标节点,只能使用从前往后遍历链表的方法来查找。

由此可以得到方法一:

1.先遍历一遍链表,得到整个链表的长度;

2.使用整个链表的节点数和k相减,根据这个差值再遍历一遍来查找到目标节点。

但如果题目要求只能遍历一遍链表呢?

方法二:

1.定义两个指针slow和fast都先指向首节点;

2.让fast指针先走k步,使两个指针的距离保持为k

3.两指针同时一步一步往后走,当fast指针走到NULL时,slow所指节点就是目标节点

这样,我们只遍历一遍链表就能找到倒数第k个节点的位置了。

代码:

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k){
    ListNode* slow = head;
    ListNode* fast = head;
    while(k--)
    {
        fast = fast->next;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow->val;
}

显然方法二的解题思路更好,这里只提供了方法二的参考代码,感兴趣的可以自己试试方法一。

2.判断链表的回文结构

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

问题分析:

题中的链表为单链表,只能通过从前往后遍历的方法来找到节点,所以如果想使用头尾节点数据相比较再分别++和--的方法的话,时间复杂度将会很高。那么有什么简化的方法呢?

解题思路:


1.找到链表的中间节点;

2.从中间节点开始,将后面部分的链表逆置;

3.比较第一个节点和中间节点的值,相等就分别++,直到完成比较,返回true,中途若出现不相等的情况则直接返回false。

实现寻找中间节点的函数:

ListNode* MidNode(ListNode* ptr)
{
    ListNode* slow = ptr,* fast = ptr;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

原理和注意事项:

1.使用快慢指针slow和fast,slow每走一步,fast就走两步,当fast走到末尾时,slow就正好是中间节点。

2.当链表有两个中间节点(即链表长度为偶数)时,返回的是第二个中间节点

3.需要注意的是,当链表节点数为偶数时,fast最终会走到NULL的位置;为奇数时,fast最终会走到尾节点,所以while循环判断条件要有两个。

图解:

4.while循环的判断条件的fast和fast->next是不能交换位置的,因为当fast为NULL时,fast->next就是野指针了,将会导致错误。先判断fast,就算fast是NULL,后面的判断条件也不会执行了。

链表逆置的函数:

ListNode* ReverseList(ListNode* ptr)
{
    ListNode* newhead = NULL;
    ListNode* pcur = ptr;
    while(pcur)
    {
        ListNode* next = pcur->next;
        pcur->next = newhead;
        newhead = pcur;
        pcur = next;
    }
    return newhead;
}

步骤总结:

1.定义一个newhead指针,用来指向逆置后的首节点;

2.其他节点使用头插的方式将其插入到当前newhead的前面,再将newhead指向新的首节点;

3.next用于暂存下一个节点的地址。

判断回文结构:

由于本题原题中没有C语言的环境选项,所以选择C++环境,代码是可以兼容的,直接在所给代码内部书写代码即可。

class PalindromeList {
public:
    bool chkPalindrome(ListNode* head) {
        ListNode* mid = MidNode(head);
        ListNode* newmid = ReverseList(mid);
        while(head && newmid)
        {
            if(head->val == newmid->val)
            {
                head = head->next;
                newmid = newmid->next;
            }
            else {
            
                return false;
            }
        }
        return true;
    }
};

步骤总结:

1.调用MidNode函数,得到中间节点的地址并存储在mid指针中

2.调用ReverseList函数,将从中间节点开始往后的节点逆置,返回新得中间节点地址存储到newmid指针中(即原链表的尾节点);

3.比较头节点指针head和newmid节点的值,相等就继续向下比较,直到NULL,不相等就直接返回结果false

4.循环正常结束返回true。

注意事项:

在逆置链表的过程中,虽然从中间节点开始,后面的节点都已经逆置了,但原链表的中间节点mid的前一个节点,它的next仍然是指向原中间节点mid,所以原链表的结构其实已经发生了改变(和下一题的相交链表有相似):

但是这并不影响我们判断回文结构,因为head和newmid指针最终都会同时到达NULL。

3.链表的相交

160. 相交链表 - 力扣(LeetCode)

问题分析:

题示中两个链表从c1节点开始重合,要想找到第一个相交的节点,就要的到第一个相交节点的地址,我们可以定义两个指针分别指向两个头,再通过遍历的方式来寻找答案。

思路一:

使用双层循环,A链表的每一个节点分别和B链表的每一个节点的指针进行比对,相同就返回当前节点地址。这个方法的时间复杂度为O(N^{2})。

思路一的方法明显虽然理解比较简单,但时间复杂度较高,有没有时间复杂度更低的方法呢?

 思路二:

1.先分别遍历A和B链表,得到两个链表的长度;

2.两个链表的长度相减,得到差值,让指向长链表的指针先走差值步数,然后再让指向两个链表的指针一起向前走,当两个指针相等时就找到了第一个相交节点。

 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    ListNode* curA = headA;
    ListNode* curB = headB;
    int lenA = 0,lenB = 0;
    while(curA)
    {
        curA = curA->next;
        lenA++;
    }
    while(curB)
    {
        curB = curB->next;
        lenB++;
    }
    // curA != curB则说明他们的尾节点都不相等
    if(curA != curB)
    {
        return NULL;
    }
    int gap = abs(lenA - lenB);
    ListNode* longList = headA,*shortList = headB;
    if(lenA < lenB)
    {
        longList = headB;
        shortList = headA;
    }
    while(gap--)
    {
        longList = longList->next;
    }
    while(longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return shortList;
}

代码分析:

1.定义curA,curB分别指向两个链表的头节点,lenA,lenB用于记录链表长度;

2.遍历计算链表长度,判断尾节点是否相等,不相等则不相交;

3.计算差值存到gap中,比较两个链表的长短;

4.长链表指针longList先走gap步,之后两个指针一起往后走,找到相交节点。

4.带环链表的判断

141. 环形链表 - 力扣(LeetCode)

带环链表指的就是原本的尾节点指向不再是NULL,而是一个节点的链表

1.怎么判断链表是否为带环链表呢?

使用快慢指针来解决问题:

慢指针走一步,快指针走两步:

如果快慢指针相遇,就说明链表是带环链表。

 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
    ListNode* slow = head,* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return true;
        }
    }
    return false;
}

(上面的代码就是本题题解)

图解:

慢指针走一步,快指针走两步:

由此可知,fast指针一定能够追上slow指针,所以可以判断是否是带环链表。

2.快慢指针方法的可行性验证和补充

上面我们可以看出,慢指针走一步,快指针走两步时,fast一定可以追上slow。

但如果慢指针走一步,快指针走3步 4步 5步 n步呢?一定能追上吗?怎么证明解释?

快指针走三步为例:

1.当N为奇数时fast指针会错过slow指针,进行新一轮的追击;

2.此时距离变成了C-1,当C-1为偶数时,第二次循环就能fast就能追上slow指针;而C-1也是奇数时,fast就永远都会错过slow指针,导致陷入死循环。

总结:

其他步数就是类似的。

总结出追不上的条件为:

N是奇数且C是偶数

但这种条件是否真的存在呢?

证明:(以fast走三步slow走一步为例)

设进环之前的链表长度为L,环长为C;

当slow刚进环时:

slow路程为:L

fast路程为:L+x*C + C-N(x为fast已经走过的圈数);

又因为fast的速度是slow的三倍;

所以路程关系也是三倍;

有:

偶数减去一个奇数,不可能的到一个偶数,因此N为奇数和C为偶数的情况不可能同时存在

从而反证出,永远追不上的条件不成立

结论:

所以快慢指针的验证方法一定可行

5.寻找带环链表的入环点

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

思路一:

  1. 先使用快慢指针(1,2),找到快慢指针相遇的节点;
  2. 定义一个指向第一个有效节点的head指针,以及一个指向快慢指针相遇节点的指针meet;
  3. Head和meet相遇的节点就是入环点。
 typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head) {
    ListNode* slow = head,* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            ListNode* meet = slow;
            while(head != meet)
            {
                head = head->next;
                meet = meet->next;
            }
            return meet;
        }
    }
    return NULL;
}

解法可行性验证:

如图设入环点之前的链表长度为L,入环点到快慢指针相遇节点的距离为N,环长C;

分别计算slow指针和fast指针的路程

Slow:L + N;

Fast:L + N + x*C(x为圈数,x>=1)

所以得到:

结论:

当x=1时,L = C-N;结合图就可知head和meet指针与入环点的距离相同;

而x>1时,变形可得L=(x-1)*C + C-N,从meet起始位置走(x-1)*C + C-N的距离,最终到达的还是入环点,且head走过L长度后也将到达入环点;

所以得出方法可行。

思路二:

  1. 使用快慢指针的到其相遇的节点,定义meet指针指向相遇节点;
  2. 定义newhead指针将meet指向节点的next给newhead;
  3. 将meet的next置为NULL(将链表从meet断开,成为相交的两条链表);
  4. 运用相交节点(第3题)的思路找到入环点。

不过本题提示已经表明不允许修改链表,可以在找到入环节点后,再重新将meet节点和newhead节点相连接。

这里就不演示思路二的代码了,感兴趣的读者可以自己试着敲一敲,原理是相似的。

6.随机链表的复制(深拷贝)

问题分析:

题中所谓深拷贝,指的就是拷贝一个和原链表的值和指针指向都一摸一样的链表
本题实际上就是单链表的拷贝问题,本题的难点不在拷贝上,而是在如何找到原链表random指针的相对指向上。

思路一:

1.先只拷贝原链表的值和next指针的内容,再用遍历的方法找到原链表的random指针所指向得节点的相对位置(即在链表中是第几个节点)。

2.找到相对位置之后,通过遍历链表的方式在拷贝链表中找到相对位置相同的节点,将它的地址给到当前节点的random。

这个方法中,查找每一个节点的random的相对位置时,都要遍历一遍链表,所以可以的到时间复杂度为O(N^{2})。

思路二:

1.拷贝链表时只拷贝节点的值,每拷贝一个节点,就将这个拷贝节点链接到原链表当前节点的后面,如图:

2.处理random指针:

每个拷贝节点的random,指向NULL的也指向NULL

而不为NULL的,由于第一步的拷贝,所有拷贝节点都在原节点的后面,所以拷贝节点的random指针只需要指向原节点random所指向节点的下一个节点即可

图中以值为13的节点为例:

原13节点的random指向原7节点,所以拷贝13节点就指向原7节点的下一个节点。

3.将拷贝节点取出,重新链接每个节点的next指针使其成为独立的链表,并恢复原链表。

代码实现:

struct Node* copyRandomList(struct Node* head) {
	struct Node* cur = head;
    //第一步,创建拷贝节点并链接到原节点后
    while(cur)
    {
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        assert(copy);
        copy->val = cur->val;
        //调整next指向,插入节点
        copy->next = cur->next;
        cur->next = copy;

        cur = copy->next;
    }
    //修改random指针
    cur = head;
    while(cur)
    {
        //cur指向原节点,copy指向对应的拷贝节点
        struct Node* copy = cur->next;
        if(cur->random == NULL)
        {
            copy->random = NULL;
        }
        else
        {
            copy->random = cur->random->next;
        }
        //cur始终指向原节点
        cur = copy->next;
    }

    //取出拷贝节点,并链接
    cur = head;
    struct Node* copyhead = NULL,* copytail = NULL;
    while(cur)
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;
        //尾插到新链表中
        if(copytail == NULL)
        {
            copyhead = copytail = copy;
        }
        else
        {
            copytail->next = copy;
            copytail = copy;
        }
        //恢复原链表的next指向
        cur->next = next;
        cur = next;
    }
    return copyhead;
}

后记

本篇到这里就结束啦!

这次我们不仅讲了普通单链表的一些OJ题,还介绍了相交链表,带环链表的判断等方法,也验证了方法的可行性,最后还讲到了随机链表的深拷贝。

后面的4 5 6题可能在解题思路或者实现上有一些难度,特别是带环链表中快慢指针方法可行性的证明,大家可以多理解一下。

如果有不足的地方大家可以在评论区或者私信指出来,谢谢大家的支持啦!

我们下期再见——

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值