目录
7.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL OJ链接
8.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。要求返回这个链表的深度拷贝。OJ链接
1. 删除链表中等于给定值 val 的所有结点。 OJ链接
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* pcur = head;
ListNode* newtail,*newhead ;
newtail = newhead = NULL;
while(pcur)//挨个判断链表节点是否满足 Node.val == val 的节点
{
if(pcur->val != val)
{
if(newhead == NULL)//将首个满足 Node.val == val 的节点作为头节点赋给newhead;
{
newhead = newtail = pcur;
}
else//将之后满足条件的节点串联在头节点后
{
newtail->next = pcur;
newtail = pcur;
}
}
pcur = pcur->next;
}
if(newhead != NULL)//如果原链表存在满足条件的节点构成新链表,将尾节点的next指针置为空指针。
newtail->next = NULL;
return newhead;// 返回新的头节点
}
2. 反转一个单链表。OJ链接
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
ListNode* newnode = NULL;
ListNode* pcur = head;
if(pcur == NULL)//原链表为空,直接返回空
return pcur;
while(pcur->next)
{
ListNode* p = pcur->next;//创建新的指针保存当前节点的下一个节点
pcur->next = newnode;//将当前节点的next指针指向newnode保存的上一个节点
//pcur为头节点时,newnode为空,则原链表头节点
//反转后作为新尾节点next指针正好指向空指针
newnode = pcur;//newnode保存当前指针
pcur = p;//pcur指向向下一个节点
}
pcur->next = newnode;//将最后一个节点的next指针指向它的原上一个指针,则链表反转完成
return pcur;//返回新链表头节点
}
3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接
本体笔者使用快慢指针的方法,通过创建两个指针,一个slow,一个fast,slow循环一次走到一步(slow做到下一个节点),fast一次走两个步(fast走到下下个节点),由于速度之比是1:2,因此当fast走到尾的时候,slow正好就在中间节点。但是节点的个数有奇偶之分,那对于这两种不同情况,这种方法是否都适用呢?
1.首先当链表节点个数为偶数时,中间节点只有一个,就在正中间,当fast走到尾节点时,slow就在该节点处,我们根据fast->next为空结束循环,直接返回slow即可。
2.首先当链表节点个数为奇数时,中间节点有两个,我们需要返回下一个中间节点,这种情况下,快慢指针也可以解决,当slow指针走到前一个中间节点时,fast才走到尾节点的前一个节点,但是需要注意的是,当slow走到下一个中间节点时,fast会走到尾节点的下一个指针,即fast会变成空指针,我们根据fast为空结束循环,直接返回slow。其实在这个情况下,尾节点的next指向的NULL指针可以视为一个新节点。这是节点个数为奇数的情况就可以视为偶数的情况了(即长度/2得到中间节点)。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode* slow = head;//创建快慢指针
ListNode* fast = head;
while(fast && fast->next)//fast为空对应节点个数为奇数的情况
{ //fast->next为空对应节点个数为偶数的情况
slow = slow->next;//慢指针走一步
fast = fast->next->next;//快指针走两步
}
return slow;//返回中间节点
}
3. 输入一个链表,输出该链表中倒数第k个结点。OJ链接
对于单向链表,我们无法直接获取链表的尾节点,同时本题还要求返回倒数第几个节点,这就要求我们能够判断相对位置。因此针对倒数第k个节点相对于尾节点的相对距离固定,本题笔者依旧创建两个指针slow、fast,指向头结点,根据要求求倒数第k个节点,我们让fast从头结点先走K步,之后我们再让slow,fast一起一次一步往前走,这是slow、fast之间就有固定的距离差即K个步。当fast走到尾节点next指针指向的NULL时,slow即为所求的倒数第K个节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int kthToLast(struct ListNode* head, int k){
struct ListNode* slow = head,*fast = head;//快满指针
while(k--)//要找倒数第K个节点,则fast先走k步(即K个节点)
{
fast = fast->next;
}
while(fast)//fast走到NULL循环结束
{
fast = fast->next;//快慢指针一起走
slow = slow->next;
}
return slow->val;//返回该节点的值。
}
4. 链表的回文结构。OJ链接
对于这一题,如果是数组的回文判断那么就非常简单,但是根据代码的注释部分可知,我们这题是基于单链表来完成的。首先,由于单链表自身缺陷,我们无法由节点直接找到它的前一个节点,及时存一个尾节点指针也不行。因此,我们这题无法直接从左右向中间比较。其次,根据题目要求空间复杂度O(1)的要求,我们也无法开辟数组来按序存储链表节点的值,再按照数组内的位置反过来循环遍历节点。
本题笔者首先通过前文讲到的快慢指针的方法,找到回文结构那个分界点即中间节点。当然,读者可能会疑惑节点的数量不是会有奇偶之分吗?这里我们同前文的题目一样,只返回后一个中间节点。这时,我们以中间节点为头,将中间节点及之后的节点反转。
需要注意的是我们讲中间节点及之后所有的节点反转,中间节点的前一个节点的next指针依然是指向中间节点的,中间节点的next指针反转后却会指向NULL.
对于奇数,我们指针A、Newhead,挨个遍历比较,如果有对应节点值不相等,我们直接返回false,原链表不是回文结构,当两个指针都走到空,则原链表是回文结构。
对于偶数,需要注意的是,A、和Newhead不会同时走到NULL,我们是根据Newnode为空为结束条件。
注:笔者目前并不会C++,但是牛客网该题不直接支持C语言,因此笔者在原C++代码上直接按C完成代码,若代码对读者造成误导,可以直接跳过。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
ListNode* Mid(ListNode* head)//快慢指针找到中间节点
{
struct ListNode*slow = head,*fast = head;
while(fast&&fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* Reverse(ListNode* head)//将输入节点及往后节点反转
{
struct ListNode*cur= head,*newhead = NULL;
while(cur)
{
struct ListNode* n = cur->next;
cur->next = newhead;
newhead = cur;
cur = n;
}
return newhead;
}
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
struct ListNode* mid = Mid(A);//找到中间节点
struct ListNode* Newhead = Reverse(mid);//将中间节点及往后节点反转,返回反转之后的新头结点
while(A&&Newhead)//
{
if(A->val != Newhead->val)
{
return false;//出现不符合条件的节点直接返回
}
A = A->next;
Newhead = Newhead->next;//两个链表一起往后挨个走
}
return true;//所有节点都比较完
}
};
5. 输两个链表,找出它们的第一个公共结点。OJ链接
需要注意的是,对于相交的单链表,一个节点定义时就没有两个next的指针,因此,一但出现相交节点,两个链表必然合二为一,不会出现相交后又分开的情况。 同时,将链表反转的思路也不可用。
不过如果直接循环比较链表节点,由于相交节点之前的节点个数存在的不同情况,我们如果直接比较会存在错位的情况,永远无法得出正确结果。因此在比较之前,我们需要先循环遍历两个链表,得出两个链表的长度,让长的链表先走两个链表长度差步,这样两个链表到相交节点前长度相等。
这样后,我们就可以直接进行循环遍历比较了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
int len1 = 0,len2 = 0;
struct ListNode *HeadA = headA,*HeadB = headB;
while(HeadA)//遍历得出headA的长度(节点数)
{
HeadA = HeadA->next;
len1++;
}
while(HeadB)//遍历得出headB的长度(节点数)
{
HeadB = HeadB->next;
len2++;
}
int gap = abs(len1 - len2);//长度差(长的链表比短的链表多出的节点个数)
struct ListNode *longer = headA,*shoter = headB;//为了减少代码量,使用假设法
if(len2 > len1)//根据测的长度检验假设是否正确,不正确更新一下
{
longer = headB;
shoter = headA;
}
while(gap--)//长的链表先走gap长度差步,这样长短链表就可以视为一样长
{
longer = longer->next;
}
while(longer&&shoter)//长短链表任意链表能到空,说明比较完链表
{
if(longer == shoter)
{
return longer;
}
longer = longer->next;
shoter = shoter->next;
}
return NULL;//比较完都没有相较的节点,两个原链表不相交
}
6. 给定一个链表,判断链表中是否有环。 OJ链接
判断链表是否存在环,我们这里创建快慢指针,slow每次走一步,fast每次走两步,fast先进环,slow后进环,当slow后进环,不管fast与slow之间的距离为多少,fast、slow之间的速度差恒为1,因此,这就成了一个速度差恒定的追击问题。
每追击一次距离减1,距离为0就追上,因此如果存在环,fast一定能追上slow,如果不存在环,fast会走到链表尾,fast会为空。
拓展:在上述方法中,slow是走一步,fast走两步,那么如果fast走三步、四步、五步、n步呢?
以fast走三步为例:slow,fast速度差为2
看起来,如果同时存在N是奇数且C是偶数(C-1是奇数),那么就永远追不上了,那么现在我们不妨在追问一下,这种情况真的能存在吗?接下来,我们跟据两者的速度,距离关系尝试证明一下。
综上当slow走一步、fast走三步也一定能追上。
fast走四步速度差为三时,也是类似的证明过程。笔者在此便不过多赘述了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head) {//快慢指针
struct ListNode *slow = head;//慢指针走一步
struct ListNode *fast = head;//快指针走两步
while(fast&&fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow)//快慢指针相遇存在环
{
return true;
}
}
return false ;//快慢指针无法相遇不存在环
}
7.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL OJ链接
该题在上一题的基础上要求再返回开始入环的第一个节点。
方法1:在创建meet指针记录fast、slow相遇的位置,并从当前节点就序往下走,创建head指针从链表第一个节点开始向后遍历,最后,head,meet一定会在开始入环的第一个节点相遇,这时再返回meet。但是,为什么head,meet一定会在开始入环的第一个节点相遇呢?笔者接下来论证一下。
我们可以看到L=(x-1)*C+C-N,因此,当head走完L的路程,mee会走完(X-1)圈回到fast、slow相遇的位置,再走C-N走到开始入环的第一个节点与head相遇.
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow = head; //快慢指针
struct ListNode *fast = head;
while(fast&&fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow)//是否判断存在环
{
struct ListNode *meet = slow;//如果存在环,在相遇出创建meet
while(meet != head)//meet与head相遇出处链表开始入环的第一个节点
{
meet = meet->next;
head = head->next;//head从第一个有效节点向开始入环的第一个节点走
}
return meet;//meet与head相遇出处链表开始入环的第一个节点
}
}
return NULL ;//不存在环
}
方法二:我们可以从slow、fast相遇处将环解开,这时返回链表开始入环的第一个结点问题,就转变成前文求两个链表的相加节点问题。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
int len1 = 0,len2 = 0;
struct ListNode *HeadA = headA,*HeadB = headB;
while(HeadA)//遍历得出headA的长度(节点数)
{
HeadA = HeadA->next;
len1++;
}
while(HeadB)//遍历得出headB的长度(节点数)
{
HeadB = HeadB->next;
len2++;
}
int gap = abs(len1 - len2);//长度差(长的链表比短的链表多出的节点个数)
struct ListNode *longer = headA,*shoter = headB;//为了减少代码量,使用假设法
if(len2 > len1)//根据测的长度检验假设是否正确,不正确更新一下
{
longer = headB;
shoter = headA;
}
while(gap--)//长的链表先走gap长度差步,这样长短链表就可以视为一样长
{
longer = longer->next;
}
while(longer&&shoter)//长短链表任意链表能到空,说明比较完链表
{
if(longer == shoter)
{
return longer;
}
longer = longer->next;
shoter = shoter->next;
}
return NULL;//比较完都没有相较的节点,两个原链表不相交
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow = head; //快慢指针
struct ListNode *fast = head;
while(fast&&fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow)//是否判断存在环
{
struct ListNode *meet = slow;//如果存在环,在相遇出创建meet
struct ListNode *newhead = meet->next;
meet->next = NULL;//断开环
return getIntersectionNode(newhead,head);;//返回交链表,没有返回空
}
}
return NULL ;//不存在环
}
8.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。要求返回这个链表的深度拷贝。OJ链接
本题深拷贝属于C++中概念,简单来说就是创建原链表的复制链表。
本题的主要的难点在于如何实现新链表的random的指针指向关系与原链表相似,需要注意的是,
新链表的random指向不是指向原链表中的节点,而是新的节点,得到的新链表与原链表出指针指向的空间不同,其他的val值,指针指向的相对位置关系都一致。除此之外因为random指针指向随机,因此如果直接做,创建新的链表的random,我们还需要求出其指向的节点在整个链表中的相对位置关系。因此不论是循环得出位置信息还是数组存储位置信息之类,时间空间复杂度会很大。
笔者的方法是在原链表节点后面开辟它的对应的复制节点,即将复制节点插入到原链表中,这时对于random指针,如果random指向NULL,那么后面的复制节点random指向NULL,如果random指向一个链表中的随机节点,复制节点random就 指向原节点random指向节点的next下一个节点(copy->random = cur->random->next),这是复制节点就指向其对应的前一个节点。例如图中复制,就指向原13节点random指向的原7节点的next指向的复制7节点。
random处理完后,我们只需要将复制节点都拿下来,组装成新链表,并将原链表恢复原样就可以了。
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
struct Node* copyRandomList(struct Node* head) {
struct Node* cur = head;
while(cur)//在原链表节点的后面插入对应的复制节点
{
struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
newnode->val = cur->val;
newnode->next = cur->next;
cur->next = newnode;
cur = newnode->next;
}
cur = head;//cur在原链表的第一个有效节点
struct Node* copy = head->next;//copy在第一个复制节点
while(cur)//复制原链表的random指针的指向关系
{
copy = cur->next;//需要注意的是cur走完原链表,指向NULL,那么后面就没有复制节点
//因此移动copy指针,一定要放在开头,cur不为空移动copy
if(cur->random == NULL)
{
copy->random = cur->random ;
}
else
{
copy->random = cur->random->next ;
}
cur = copy->next;//cur移动到复制节点的下一个原节点
//copy = cur->next;放在这就会出现cur为NULL是解引用的情况
}
cur = head;
struct Node*newhead = NULL;
struct Node*newtail = NULL;
while (cur)//将复制节点从链表上解下来组成新链表,并恢复原链表
{
copy = cur->next;
struct Node*next = copy->next;
if(newtail == NULL)
{
newhead = newtail = copy;
}
else
{
newtail->next = copy;
newtail = newtail->next;
}
cur->next = next;
cur = next;
}
return newhead;
}