目录
前言
一、OJ练习
1.移除链表元素
2.链表的中间结点
3.链表中倒数第k个结点
4.反转链表
5. 合并两个有序链表
6.链表分割
7. 链表的回文结构
8.相交链表
9.环形链表
10. 环形链表 II
总结
学习完单链表的增删查改,我们就需要练习来巩固,单链表中的题目无非就是增删查改的排列组合,根据实际情况采用较为方便的方式解题,这里小帅带大家练习链表经典题目。
方法一:双指针
使用两个指针,一个在前一个在后,前指针找到目标值开始删除操作,但是这个方法有许多坑
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode *cur=head,*prev=NULL;
while(cur)//遍历
{
if(cur->val != val)
{
prev = cur;
cur = cur->next;
}
else
{
if(cur == head)
{
head = cur->next;
free(cur);
cur = head;//从头开始
}
else
{
prev->next = cur->next;
free(cur);
cur = prev->next;//不能再使用cur赋值,因为它已经被释放了
}
}
}
return head;
}
方法二:创建新链表,比较后赋值(双指针)
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* newList = NULL, *tail = NULL, *cur = head;//tail记录新链表的尾节点,如果是记录头节点,每次都要找尾节点,时间繁琐
while(cur)//遍历
{
if(cur->val != val)
{
if(tail == NULL)//这时新链表为空
{
newList = cur;//将头指针更新
tail = newList;//赋值tail
}
else//新链表不为空
{
tail->next = cur;
tail = tail->next;
}
cur = cur->next;//cur后一一个节点
}
else
{
struct ListNode* next = cur->next;//保存下一个节点,为cur释放后赋值
free(cur);
cur = next;
}
}
if(tail)//如果是空指针就不能进行下面的赋值操作
tail->next = NULL;
return newList;
}
力扣想调试,就自己手写一个简单的main函数,快速手搓一个链表
方法:快慢指针
慢指针一次走一个节点,快指针一次走两个节点 。
情况分奇偶节点数:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode *fast = head, *slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast ->next ->next;
}
return slow;
}
方法:快慢指针
快指针走法有两种,由于倒数第k个到最后1个之间的距离是k-1
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
struct ListNode* fast, * slow;
fast = slow = pListHead;
while (k--)//循环k次,如果是--k是k-1次
{
if (fast == NULL)//判断k是否大于链表长度
{
return NULL;
}
fast = fast->next;
}
while (fast)//直到fast为空,两指针同时走
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
方法一:三指针反转链表
//方法一:三指针法,反转链表
struct ListNode* reverseList(struct ListNode* head)
{
if (head == NULL)//head不能为空
{
return NULL;
}
struct ListNode* n1, * n2, * n3;
n1 = NULL;
n2 = head;//n1, n2负责反转
n3 = n2->next;//n3负责找到下一个节点
while (n2)
{
//反转
n2->next = n1;
//移动
n1 = n2;
n2 = n3;
//n3为NULL时就不能进行赋值操作,所以要判断一下
if(n3)
n3 = n3->next;
}
return n1;
}
方法二:创建新的头节点,头插
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head, * newlist = NULL;
while (cur)
{
struct ListNode* next = cur->next;
//头插
cur->next = head;
head = cur;
//移动
cur = next;
}
return newlist;
}
方法一: 比大小,小的尾插至新链表
但是要注意链表为空的情况
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if (list1 == NULL)//判断链表是否为空,如果不处理的话,后面程序会出现NULL错误
return list2;
if (list2 == NULL)
return list1;
struct ListNode* cur1 = list1, * cur2 = list2;//两个指针负责移动
struct ListNode* head = NULL, * tail = NULL;//tail负责找尾
while (cur1 && cur2)
{
if (cur1->val < cur2->val)
{
if (head == NULL)
{
head = tail = cur1;//链表为空,进行赋值
}
else
{
tail->next = cur1;//尾插
tail = tail->next;
}
cur1 = cur1->next;
}
else
{
if (head == NULL)//同理
{
head = tail = cur2;
}
else
{
tail->next = cur2;
tail = tail->next;
}
cur2 = cur2->next;
}
}
if (cur1)//如果链表一没有放完,直接把cur1之后都链上
tail->next = cur1;
if (cur2)//如果链表二没有放完,直接把cur1之后都链上
tail->next = cur2;
return head;
}
方法二:同方法一,但是加上了哨兵位
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* cur1 = list1, * cur2 = list2;
struct ListNode* guard = NULL, * tail = NULL;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));//哨兵位必须单独开辟节点
tail->next = NULL;//控制哨兵位的next为空
while (cur1 && cur2)
{
if (cur1->val < cur2->val)
{
tail->next = cur1;//因为tail不为空,所以不需要判断为空的情况
tail = tail->next;
cur1 = cur1->next;
}
else
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
}
if (cur1)//如果链表一没有放完,直接把cur1之后都链上
{
tail->next = cur1;
}
if (cur2)//如果链表二没有放完,直接把出cur2之后都链上
{
tail->next = cur2;
}
struct ListNode* head = guard->next;//注意!如果不释放单独开辟的guard指针,会造成内存泄露
free(guard);
guard = NULL;
//返回的是头节点,不是哨兵位,所以返回哨兵位的next
return head;
}
方法一:创建两个链表,分别存放小于x,大于等于x,最后再链接
NULL情况太多,建议使用哨兵位
单链表题目中,无非是增删查改的操作的组合,掌握基本增删查改函数是基本功。保持简单的头插或者尾插是一个简单的解题思路,如果要中间插入或删除,试着想一想能头插尾插吗?
要在一个链表内又删除又插入,是一个情况繁多新手很难把握的程序,例如此题,我们可能会有把链表中大于x的进行尾插的想法,但是稍微一分析,就会发现有很多情况要分,删除操作要找上一个节点和下一个节点,上一个节点是否为头节点?尾插一直循环,到何时停止?
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
struct ListNode* gGuard, * gTail, * lGuard, * lTail;//两个哨兵位,以及两个尾指针
gGuard = gTail = (struct ListNode*)malloc(sizeof(struct ListNode));//为哨兵位赋值结构体指针
lGuard = lTail = (struct ListNode*)malloc(sizeof(struct ListNode));
gTail->next = lTail->next = NULL;//置空
struct ListNode* cur = pHead;
while (cur)//cur遍历
{
if (cur->val < x)
{
lTail->next = cur;
lTail = lTail->next;//指向尾节点
}
else
{
gTail->next = cur;
gTail = gTail->next;//指向尾节点
}
cur = cur->next;//cur移动
}
lTail->next = gGuard->next;//链接
gTail->next = NULL;//尾节点next置空,不置空会出现链表循环
pHead = lGuard->next;//存放链表头节点return
free(gGuard);//释放
free(lGuard);
return pHead;
}
};
方法:先找到中间节点 ,再将中间节点之后的链表逆置,再比较是否相等即可
(如果是逆置整个链表是不行的,因为逆置改变了原链表,只能复制一个原链表在进行逆置)
struct ListNode
{
int val;
struct ListNode* next;
};
//寻找中间节点函数
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* fast = head, * slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
//链表逆置函数
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL) { //head不能为空
return NULL;
}
struct ListNode* n1, * n2, * n3;
n1 = NULL;
n2 = head;//n1, n2负责反转
n3 = n2->next;//n3负责找到下一个节点
while (n2) {
//反转
n2->next = n1;
//移动
n1 = n2;
n2 = n3;
//n3为NULL时就不能进行赋值操作,所以要判断一下
if (n3)
n3 = n3->next;
}
return n1;
}
bool chkPalindrome(ListNode* head)
{
struct ListNode* mid = middleNode(head);
struct ListNode* rhead = reverseList(mid);
while (head && rhead)//有一个链表为空就停止比较
{
if (head->val != rhead->val)
return false;
head = head->next;
rhead = rhead->next;
}
return true;
}
方法:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* tailA = headA, *tailB = headB;//两个指针负责找到两个链表的最后节点
int lenA = 1, lenB = 1;//记录两个链表的长度
while(tailA->next)//找到最后一个节点,并记录长度
{
tailA = tailA->next;
++lenA;
}
while(tailB->next)
{
tailB = tailB->next;
++lenB;
}
if(tailA != tailB)//判断是否相交
return NULL;
int gap =fabs(lenA-lenB);//算相差长度
struct ListNode* longList = headA, *shortList = headB;//从头比较
if(lenAnext;
}
while(longList != shortList)//比较两链表节点地址是否相同
{
longList = longList->next;
shortList = shortList->next;
}
return longList;//返回相遇的节点地址
}
方法:快慢指针,追击问题
struct ListNode
{
int val;
struct ListNode* next;
};
bool hasCycle(struct ListNode* head)
{
struct ListNode* fast = head, * slow = head;
while (fast && fast->next)//因为fast一次走两步,所以要两种判断
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)//相遇,就返回真
return true;
}
return false;//循环退出,那么就表明没有循环
}
拓展问题:
- 为什么slow1步,fast走2步,它们会相遇?会不会错过?请证明
- 如果slow1步,fast走m步(m>=3),它们是否会相遇?是否会错过?请证明
分析:
结论:一个指针从相遇点走,一个指针从起始点走,会在入口点相遇
//方法一:理论推导 L = n*C - X
struct ListNode* detectCycle(struct ListNode* head)
{
struct ListNode* fast = head, * slow = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
struct ListNode* meet = slow;
struct ListNode* start = head;
while (meet != start)
{
meet = meet->next;
start = start->next;
}
return meet;
}
}
return NULL;
}
方法二:将相遇点与相遇点的下一节点之间断开,转换成相交链表找交点的问题
// 方法二:将相遇点与相遇点的下一节点之间断开,转换成相交链表找交点的问题
struct ListNode* detectCycle(struct ListNode* head)
{
struct ListNode* fast = head, * slow = head;
int len1 = 1, len2 = 1;//置为1是因为要找到两个链表的最后一个节点,循环条件是tail->next != NULL,少算一个所以初值为1
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
//断开节点
struct ListNode* start = slow->next;
slow->next = NULL;
//寻找相交节点
struct ListNode* tail1 = head, * tail2 = start;
while(tail1->next)
{
len1++;
tail1 = tail1->next;
}
while (tail2->next)
{
len2++;
tail2 = tail2->next;
}
int gap = fabs(len1 - len2);
struct ListNode* longlist = head, *shortlist = start;
if (len1 < len2)
{
longlist = start;
shortlist = head;
}
while (gap--)
{
longlist = longlist->next;
}
while (longlist != shortlist)
{
longlist = longlist->next;
shortlist = shortlist->next;
}
return longlist;
}
}
return NULL;
}
分析:本题关键就在于如何复制各节点内random指针指向的节点的信息 ,因为题目要求新链表各节点不能指向原链表,所以直接赋值给新节点是错误的,那么我们思考如何储存当前指针cur与其random指针指向的节点之间的相对位置,遍历数组找到与cur->random->val值相同的节点?不可以,因为如果有大于等于2个节点的val相同,那么random就可能会找错,造成错误。既然找相同值不可以,那我们找与cur->random地址相同的节点,并使用计数器记录找到该节点共经过了几个节点,对每个节点的random都遍历一次链表,或者用指针数组存各节点random的值,再建立一个数组,遍历链表与指针数组值比较,将计数器记录下来的值存放至数组,这两种方法都可行,但很显然时间复杂度很大。
这里有很优秀的解法:
如果链表掌握不是很优秀的话,即使知道了解题思路也很难直接通过,可能总是会因指针问题调试,所以独立完成此题就是检验链表是否完美掌握的标志
struct Node
{
int val;
struct Node* next;
struct Node* random;
};
struct Node* copyRandomList(struct Node* head)
{
//1.创建新节点,插入链表
struct Node* cur = head;
while (cur)
{
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
struct Node* next = cur->next;
copy->val = cur->val;
copy->next = next;
cur->next = copy;
cur = next;
}
//2.将当前节点random->next赋值给下一个节点的random
cur = head;
while (cur)
{
struct Node* copy = cur->next;
if (cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = cur->next->next;
}
//3.解下来,尾插
struct Node* newlist = NULL, *tail = NULL;
cur = head;
while (cur)
{
struct Node* copy = cur->next;//cur不可能为空,那么copy会为空,那么tail->next就会为空,
//所以最后不需要再加tail->next为空,反而会把程序搞错
struct Node* next = copy->next;
if (newlist == NULL)
{
newlist = tail = copy;
}
else
{
tail->next = copy;
tail = tail->next;
}
cur->next = next;
cur = next;
}
return newlist;
}
至此我们练习了较为经典的单链表题目,更加深刻的理解结构体指针与单链表增删查改操作,跨过诸多链表小坑,想要在做题中有较为优秀的思路,只能多做多练,见得多自然也就有优秀的解题思路了。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!