LeetCode/NowCoder-链表经典算法OJ练习4

·人的才华就如海绵的水,没有外力的挤压,它是绝对流不出来的。流出来后,海绵才能吸收新的源泉。💓💓💓

目录

说在前面

题目一:环形链表

 题目二:环形链表 II

题目三:随机链表的复制

SUMUP结尾


说在前面

 dear朋友们大家好!💖💖💖我们又见面了,接着上一个篇目,我们接着继续练习有关链表的面试题、OJ题,希望大家和我一起学习,共同进步~

 👇👇👇

友友们!🎉🎉🎉点击这里进入力扣leetcode学习🎉🎉🎉


​以下是leetcode题库界面:

 👇👇👇

🎉🎉🎉点击这里进入牛客网NowCoder刷题学习🎉🎉🎉
​以下是NowCoder题库界面:

​​

 ​​

题目一:环形链表

题目链接:141. 环形链表 - 力扣(LeetCode)

题目描述:

题目分析:

 思路:快慢指针法。创建快慢指针fast、slow,快指针每次走两步,慢指针每次走一步,如果fast追击上slow(即fast最终等于slow),则链表带环,否则不带环。

代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
    ListNode* slow = head, *fast = head;//创建快慢指针fast、slow
    while(fast && fast->next)//fast走两步,slow走一步
    {
        slow = slow->next;
        fast = fast->next->next;

        if(slow == fast)//fast追击上slow,则链表带环
            return true;
    }
    return false;
}

思考下列问题:

 当然,虽然这道题比较简单,但是有些问题我们还是需要搞清楚:

1、为什么一定会相遇,有没有可能会错过,永远追不上?请证明。

答:不会错过。

证明:

假设slow进环时,fast和slow之间的距离为d,那么在fast追击slow的过程中,距离变化为d、d-1、d-2、d-3、...、2、1、0,最终会减到0。当距离为0时,fast追上slow,也就是每追击一次距离d减小1,所以一定能追上。

2、slow一次走1步,fast走3步可以吗?4步、5步、n步呢?请证明。

答:不稳定,有可能很久都追不上。

证明:

我们先考虑slow走1步,fast走3步的情况,剩下的都是如法炮制。同样假设fast和slow之间的距离为d,那么在fast追击slow的过程中,距离变化为d、d-2、d-4、d-6、...,此时就需要讨论d的奇偶性。如果d是偶数,那么d可以减到0,即fast最终会追上slow,但是如果d是奇数,那么d不是2的倍数,它会错过slow并新的距离为C-1(假设C是环的节点数),进入新一轮的追击过程,直到他们两的距离是2的倍数,此时这一轮就可以追上了。

永远追不上的条件:同时存在C是偶数且距离d(或N)为奇数,那么就会永远追不上。 

那是否真的会有这种情况呢?我们需要寻找C和N之间的关系:

证明:

假设slow进环时,fast和slow的距离是N,带环部分的节点数为C,链表不带环部分的节点数为L,slow进环时fast已经在环中转了x圈,则有:

slow走的距离是:L

fast走的距离是:L + xC + (C - N)

由于fast走的距离是slow的三倍,则有:3L = L + xC + C - N

化简得到:2L = (x + 1)C - N

显然C为偶数且N为奇数不能同时成立,也就是说,fast最终总是会追击上slow。

 ​​

 题目二:环形链表 II

题目链接:142. 环形链表 II - 力扣(LeetCode)

题目描述:

题目分析:

 思路1:快慢指针法。用题目一中的方法找到fast追击上slow的节点,记为meet,再将head记为cur,让meet和cur同时走,它们会再第一个相交节点相遇,即入环的第一个节点。

那为什么会在第一个相交节点相遇呢?

解:

假设fast追上slow时的节点,即meet节点,逆时针距离入环的第一个节点的距离为N,不带环部分节点数为L,带环部分节点数为C,则有:

slow走的距离是:L + N(fast一次走2步,在一圈内一定能追上)

fast走的距离是:L + xC + N

又因为fast走的距离是slow的两倍,则有:2(L + N) = L + xC + N

化简得到:L = xC- N

显然随着x的变化,指针fast在环内距离入环的第一个节点的距离f(x)是一个周期函数,周期为T = 1,那么L(x)也是关于x的周期函数,周期T = 1。

所以:L = C - N(就比如sinπ、sin3π和sin5π都是相等的)

由此得到不带环部分的节点数等于meet距离入环的第一个节点的节点数相等,所以它们以相同速度移动,一定会再第一个相交节点相遇。

代码如下: 

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
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)//fast追上slow
        {
            ListNode* meet = slow;
            ListNode* cur = head;
            while (meet != cur)//meet和cur相遇,则为第一个相交节点
            {
                meet = meet->next;
                cur = cur->next;
            }
            return meet;
        }
    }
    return NULL;//没有相交节点则返回NULL
}

思路2:先用思路1的方法找到meet,然后将meet->next设置为newhead,然后让环断裂,使其转化为相交链表找第一个相交节点(上个篇目解析过这类题)。 

这个思路可能更好想,但是代码却要更加复杂一些。

代码如下: 

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
//相交单链表寻找第一个相交节点
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) {
	ListNode* cur1 = headA, * cur2 = headB;
	int lenA = 0;
	int lenB = 0;
	while (cur1->next)//统计链表A的长度
	{
		lenA++;
		cur1 = cur1->next;
	}
	while (cur2->next)//统计链表B的长度
	{
		lenB++;
		cur2 = cur2->next;
	}
	if (cur1 != cur2)//判断是否有交点
		return NULL;
    //假设法,设置长短链表
	ListNode* LongList = headA, * ShortList = headB;
	if (lenA < lenB)
	{
		LongList = headB;
		ShortList = headA;
	}
	int gap = abs(lenA - lenB);//两链表节点差值
	while (gap--)//让长的先走差值的步数
	{
		LongList = LongList->next;
	}
	while (LongList != ShortList)//让两链表一起走,第一个相等的就是交点
	{
		LongList = LongList->next;
		ShortList = ShortList->next;
	}
	return LongList;
}
//带环链表寻找第一个相交节点
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;
           ListNode* newhead = meet->next;//新单链表的头
           meet->next = NULL;//尾部置为NULL
           ListNode* cur = getIntersectionNode(newhead, head);
           return cur;
        }
    }   
    return NULL;
}

第一个函数我们在上一篇目中有详细解析,这里直接CV就可以了。 虽然能够通过,但是修改了链表,本质上不符合题目要求了,但是也是个值得思考的思路。

 ​​

题目三:随机链表的复制

题目链接:138. 随机链表的复制 - 力扣(LeetCode)

题目描述:

注:深拷贝:拷贝一个值个指向和当前链表一模一样的链表。

题目分析:

 思路:快先将新的节点交错插入到原随机链表中,然后完成设置random后再将新的节点尾插到新链表newhead中。

这道题目创建节点其实不难,难就难在random是随机的,应该如何处理random的指向是这道题目的难点。我们如果错位插入,就可以得到新节点和旧链表之间的关系,那么新的节点的random指针都会指向插入了新节点的链表的random的next指针(空指针除外)。

比如,上面原来的第二个节点的random指向了第一个节点,那么新的第二个节点的random就指向第一个节点的next节点。

代码如下:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
struct Node* copyRandomList(struct Node* head) {
    Node* cur = head;
    while (cur)//创建新节点并错位插入旧节点
    {
        Node* copy = (Node*)malloc(sizeof(Node));
        copy->val = cur->val;
        copy->next = cur->next;
        cur->next = copy;
        cur = copy->next;
    }
    cur = head;
    while (cur)//设置random指针的指向
    {
        Node* copy = cur->next;
        if (!cur->random)
            copy->random = NULL;
        else
        {
            copy->random = cur->random->next;
        }
        cur = copy->next;
    }
    cur = head;
    //创建新链表
    Node* newhead = (Node*)malloc(sizeof(Node));
    Node* newtail = newhead;
    while (cur)//将新节点尾插到新链表
    {
        Node* copy = cur->next;
        newtail->next = copy;
        newtail = newtail->next;
        cur->next = copy->next;//恢复原链表
        cur = copy->next;
    }
    if (newtail)
        newtail->next = NULL;
    return newhead->next;
}

这道题目不论是思路还是代码书写都很有难度,如果你看了我的讲解能够独立地写出上面代码(不一定非要和我一样),那么恭喜你,在当前阶段你的链表已经过关了!

 

SUMUP结尾

数据结构就像数学题,需要刷题才能对它有感觉。之后还会更新数据结构相关的练习题、面试题,希望大家一起学习,共同进步~

如果大家觉得有帮助,麻烦大家点点赞,如果有错误的地方也欢迎大家指出~

  • 59
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法数据结构基础》等,来深入理解这种算法思想。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值