链表学会了没,会做这些题就足够了,思路全在动图里了,不懂直接剁手《下篇》

459 阅读12分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

1.分割链表<难度系数⭐⭐>

📝 题述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序 (相对顺序不变),返回重新排列后的链表的头指针。

💨 示例1:

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

    输出:[1,2,2,4,3,5]

💨 示例2:

    输入:head = [2,1], x = 2

    输出:[1,2]

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路,双链表,比x小的尾插至一个链表中,比x大的尾插至另一个链表中。最后再链接两链表

请添加图片描述

leetcode原题

牛客网原题

#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
	int val;
	struct ListNode *next;
};
struct ListNode* partition(struct ListNode* head, int x)
{
	struct ListNode *SmallHead, *BigHead, *SmallTail, *BigTail;
    //开辟二个节点
    SmallHead = SmallTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    BigHead = BigTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    //拷贝头节点
    struct ListNode* cur = head;
    //大小分类
    while(cur)
    {
    	if(cur->val < x)
        {
        	SmallTail->next = cur;
            SmallTail = cur;
        }
        else
        {
        	BigTail->next = cur;
            BigTail = cur;
        }
            cur = cur->next;
    }
	//再将最后一个节点这里置为空尤为重要,否则可能会造成死循环
	BigTail->next = NULL;
	//链接二节点
	SmallTail->next = BigHead->next;
	struct ListNode* temp = SmallHead->next;
	free(SmallHead);
	free(BigHead);
	
	return temp;
}
int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n1->val = 1;

	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n2->val = 2;

	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n3->val = 6;

	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n4->val = 3;

	struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n5->val = 4;

	struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n6->val = 5;

	struct ListNode* n7 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n7->val = 6;

	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	n5->next = n6;
	n6->next = n7;
	n7->next = NULL;

	partition(n1, 3);
	return 0;
}

2.链表的回文结构<难度系数⭐⭐>

📝 题述:对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

💨 示例1:

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

    输出:true

💨 示例2:

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

    输出:true

💨 示例3:

    输入:head = [1,2]

    输出:false

在这里插入图片描述

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路一, 钻牛角尖,使用数组 (OJ能过,但面试官不能过)。

这里需要注意的是, 有些OJ题目,有时间复杂度和空间复杂度的要求,但是平台并不能很好的检查两者的参数,因为有很多客观存在的因素,所以并不能制定一个公平的规则,这也是目前牛客网需要去改进的地方。
但相比牛客网leetcode会严格些,包括测试用例的完整度,但也不能只用leetcode,因为就目前来看,大多数的笔试环境用的都是牛客。
(从这道题就可以看出时间复杂度为是符合要求的,但是空间复杂度不符号要求,且牛客网能跑的过去)

请添加图片描述

思路二, 逆置后半部分再比较。

请添加图片描述

牛客原题

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int SLTDataType;

struct ListNode
{
   int val;
   struct ListNode *next;
};
//思路一
bool chkPalindrome1(ListNode* phead)
{
   int a[900];
   struct ListNode* cur = phead;
   int n = 0;
   //将链表中的val拷贝至a数组
   while(cur)
   {
   	a[n++] = cur->val;
   	cur = cur->next;
   }
   //判断回文 
   int left = 0, right = n - 1;
   while(left < right)
   {
   	if(a[left] != a[right])
   	{
   		return false;
   	}
           left++;
           right--;
       }
       return true;
   }
}
struct ListNode* FindMidNode(struct ListNode* phead)
{
   //通过快慢指针查找中间节点
   struct ListNode *slow, *fast;
   slow = fast = phead;
   while (fast && fast->next)
   {
   	slow = slow->next;
   	fast = fast->next->next;
   }
   return slow;
}
struct ListNode* reverseList(struct ListNode* pmid)
{
   //使用头插逆置
   struct ListNode* cur = pmid;
   struct ListNode* newhead = NULL;
   while (cur)
   {
   	struct ListNode* next = cur->next;
   	cur->next = newhead;
   	newhead = cur;
   	cur = next;
   }
   return newhead;
}
//思路二
bool chkPalindrome2(struct ListNode* A)
{
   struct ListNode* mid = FindMidNode(A);
   struct ListNode* rHead = reverseList(mid);
   struct ListNode* cur = A;
   while (rHead && cur)
   {
   	if (rHead->val != cur->val)
   		return false;
   	rHead = rHead->next;
   	cur = cur->next;
   }
   return true;
}
int main()
{
   struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
   n1->val = 1;

   struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
   n2->val = 2;

   struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
   n3->val = 2;

   struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
   n4->val = 1;


   n1->next = n2;
   n2->next = n3;
   n3->next = n4;
   n4->next = NULL;

   isPalindrome1(n1)
   isPalindrome2(n1)
   return 0;
}

3.相交链表<难度系数⭐>

📝 题述:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。(保证整个链式结构中不存在环且函数返回结果后,链表必须保持其原始结构。)

⚠ 注意,其一,以下这种结构实际并不存在,因为3和1可以同时指向4,但4不能同时指向5和6。其二,这里比较的是节点的地址,不是节点的值

在这里插入图片描述

💨 示例1:

    输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3

    输出:Intersected at '8'

在这里插入图片描述

💨 示例2:

    输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1

    输出:Intersected at '2'

在这里插入图片描述

💨 示例3:

    输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2

    输出:null

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

常规暴力求解,A链表的每个节点和B链表的每个节点比较,如果有相同的就是交点,如果没有就不相交 (注意是节点的地址),时间复杂度O(N^2) 对常规的方法进行优化:分别算出A和B的长度 lenA、lenB,让长的链表先走|lenA-lenB|,再同时走找交点,时间复杂度O(N)

请添加图片描述

leetcode原题

#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
	int val;
	struct ListNode *next;
};

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    int lenA = 0, lenB = 0;
    //计算链表的长度
    while(curA)
    {
        lenA++;
        curA = curA->next;
    }
    while(curB)
    {
        lenB++;
        curB = curB->next;
    }
    //先假设A长B短
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    //如果假设错了,就交换下
    if(lenA < lenB)
    {
        longList = headB;
        shortList = headA;
    }
    //让长的走差距步
    int gap = abs(lenA-lenB);
    while(gap--)
    {
        longList = longList->next;
    }
    //长的和短的同时走
    while(longList && shortList)
    {
        //找到了,相交
        if(longList == shortList)
        {
            return longList;
        }
        //没找到继续走
        longList = longList->next;
        shortList = shortList->next;
    }
    //没找到,不相交
    return NULL;
}
int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n1->val = 4;

	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n2->val = 1;

	struct ListNode* m1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	m1->val = 5;

	struct ListNode* m2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	m2->val = 0;
	
	struct ListNode* m3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	m3->val = 1;

	struct ListNode* u1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	u1->val = 8;

	struct ListNode* u2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	u2->val = 4;
	
	struct ListNode* u3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	u3->val = 5;
	//上节点
	n1->next = n2;
	n2->next = u1;
	u1->next = u2;
	u2->next = u3;
	u3->next = NULL;
	//下节点
	m1->next = m2;
	m2->next = m3;
	m3->next = u1;
	u1->next = u2;
	u2->next = u3;
	u3->next = NULL;
	
	getIntersectionNode(n1, m1);
	return 0;
}

4.环形链表<难度系数⭐>

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

在这里插入图片描述

带环链表不能轻易的去遍历,否则会死循环

💨 示例1:

    输入:head = [3,2,0,-4], pos = 1

    输出:true

在这里插入图片描述

💨 示例2:

    输入:head = [1,2], pos = 0

    输出:true

在这里插入图片描述

💨 示例3:

    输入:head = [1], pos = -1

    输出:false

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

快慢指针,如果快指针和慢指针相遇了就是带环的,否则快指针指向空时就是不带环的

请添加图片描述

leetcode原题

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>


typedef int SLTDataType;

struct ListNode
{
	int val;
	struct ListNode *next;
};
bool hasCycle(struct ListNode* head) 
{
    struct ListNode *slow = head, *fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        //相遇即带环
        if(slow == fast)
        {
            return true;
        }
    }
    //不带环
    return false;
}
int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n1->val = 1;

	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n2->val = 2;

	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n3->val = 3;

	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n4->val = 4;
	
	struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n5->val = 5;

	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	n5->next = n3;
	
	hasCycle(n1);
	return 0;
}

其实这道题并不难,相对较难的是这道题的延伸:

🧿 slow一次走一步,fast一次走两步,fast一定可以相遇slow吗?请证明

请添加图片描述

slow一次走一步,fast一次走两步时,它们一定可以相遇

slow进环时,fast已经在环里走一会了,那么这时在环内就产生了fast追赶slow的现象。假设环的长度是C、slow进环时,fast和slow之间的距离是N,那么进环后fast和slow之间的距离变化是:
N
N-1
N-2
...
2
1
0   
slow和fast之间的距离是0的时候就能相遇,同时也说明了N为奇或偶时都能相遇 最简单的理解就是:fast每次都以一步的距离追赶slow,slow走一步,fast走二步可以等同于slow不走,fast走一步,

🧿 slow一次走一步,fast一次走n步(n > 2,3,4,5...),fast一定可以相遇slow吗?请证明

请添加图片描述

slow一次走一步,fast一次走n步时,它们不一定可以相遇 假设slow一次走1步,fast一次走3步,假设环的长度是C、slow进环时,fast和slow之间的距离是N。那么进环后fast和slow之间的距离变化是:

偶     奇
N      N
N-2   N-2
N-4   N-4
...      ...
2     1
0    -1   -1相当于它们的距离变成了C-1

slow和fast之间的距离是0的时候就能相遇,同时说明N是偶数时,就一定能相遇(假设N是奇数,且C-1也是奇数,就永远不能相遇) 最简单的理解就是:fast每次都以2步的距离追赶slow,slow走一步,fast走三步可以等同于slow不走,fast走二步。

💨 总结

fast一次走x步,slow一次走y步都是可以的,但最重要的是追赶过程中的步差x-y。步差是1,一定能追上;步差不是1,不一定能追上

5.环形链表<难度系数⭐⭐>

📝 题述:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。说明:不允许修改给定的链表。进阶:使用 O(1) 空间解决此题

💨 示例1:

    输入:head = [3,2,0,-4], pos = 1

    输出:返回索引为 1 的链表节点

在这里插入图片描述

💨 示例2:

    输入:head = [1,2], pos = 0

    输出:返回索引为 0 的链表节点

在这里插入图片描述

💨 示例3:

        输入:head = [1], pos = -1

    输出:返回 null

在这里插入图片描述

🧷 平台:Visual studio 2017 && windows

🔑 核心思想:

思路一,一个指针从相遇点走,另一个指针从head走,它们会在入口点相遇(待证)

请添加图片描述

思路二,把相遇点的位置断开,这时就变成了相交问题(调用之前实现的相交函数)

请添加图片描述

leetcode原题

#include<stdio.h>
#include<stdlib.h>

typedef int SLTDataType;

struct ListNode
{
	int val;
	struct ListNode *next;
};
//思路一
struct ListNode* detectCycle1(struct ListNode *head)
{
    struct ListNode *slow = head, *fast = head;
    //结束条件:无环,快指针走到空了;有环,返回入环的节点
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        //相遇了
        if(slow == fast)
        {
            //记录相遇的节点
            struct ListNode* meet = slow;
            //头节点和相遇点同时走,它们会在入口点相遇,也就是说在入口点时,头节点和相遇点是相同的
            while(head != meet)
            {
                head = head->next;
                meet = meet->next;
            }
            //有环,返回相遇节点
            return meet;
        }
    }
    //无环,返回空
    return NULL;
}
//思路二
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
	    struct ListNode* curA = headA;
    struct ListNode* curB = headB;
    int lenA = 0, lenB = 0;
    //计算链表的长度
    while(curA)
    {
        lenA++;
        curA = curA->next;
    }
    while(curB)
    {
        lenB++;
        curB = curB->next;
    }
    //先假设A长B短
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    //如果假设错了,就交换下
    if(lenA < lenB)
    {
        longList = headB;
        shortList = headA;
    }
    //让长的走差距步
    int gap = abs(lenA-lenB);
    while(gap--)
    {
        longList = longList->next;
    }
    //长的和短的同时走
    while(longList && shortList)
    {
        //找到了,相交
        if(longList == shortList)
        {
            return longList;
        }
        //没找到继续走
        longList = longList->next;
        shortList = shortList->next;
    }
    //没找到,不相交
    return NULL;
}
struct ListNode* detectCycle2(struct ListNode *head)
{
   struct ListNode *slow, *fast;
   slow = fast = head;
   while(fast && fast->next)
   {
       slow = slow->next;
       fast = fast->next->next;

       //带环相遇点
       if(slow == fast)
       {
           //断开
           struct ListNode* meet = slow;
           struct ListNode* next = meet->next;
           meet->next = NULL;
           return getIntersectionNode(head, next);
       }
   }
   return NULL;
}
int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n1->val = 1;

	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n2->val = 2;

	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n3->val = 3;

	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n4->val = 4;
	
	struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n5->val = 5;

	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	n5->next = n3;
	
	detectCycle1(n1);
	detectCycle2(n1);
	return 0;
}

🧿 证明:一个指针从相遇点走,另一个指针从head走,它们会在入口点相遇 假设链表头到入口点的距离是L,假设相遇点到入口点的距离是X,假设环的长度是C。 错误推论:当快慢指针相遇时,慢指针走的长度是L+X,快指针走的长度是L+C+X,由此就可以推出2(L+X) = L+C+X,再推出L+X=C,L= C-X。看似没毛病,但实际这个推导是错的,因为slow进环前,fast不一定在环里只走一圈

请添加图片描述

正确推论:慢指针走的长度是L+X,快指针走的长度是L+N * C+X (N表示圈数&&N>=1)。由此就可以推出2(L+X) = L+N * C+X ,再推出L+X=N*C,L=N * C-X,可分解为L=(N-1) * C+C-X,(N-1) * C表示从相遇点走了N-1圈,C-X表示相遇点到入口的距离。最后通过这个公式就可以推出一个指针从链表头走,一个指针从相遇点走,会在入口点相遇。

请添加图片描述

请添加图片描述