🐱作者:一只大喵咪1201
🐱专栏:《数据结构与算法》
🔥格言:你只管努力,剩下的交给时间!
习题
在学习完链表以后,本喵带着大家来做一些链表的OJ练习题,既可以检测我们学习的效果,也可以巩固学习的成果。
✨1.移除链表元素
题目描述:
分析:
首先需要创建一个新的头newhead,由于头不能动,所以我们用另一个指针copy控制插入的节点位置。
思路:
- 在原链表中,将指针cur指向的节点中的数据和给定的val比较
- 若不相等,则将原链表中的节点尾插在新链表中
- 若相等,则继续比较原链表中的下一个节点,新链表保持不动。
代码实现:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* cur = head;//新指针指向链表
struct ListNode* newhead = NULL;//新的头指针
struct ListNode* copy = NULL;//新链表中移动的指针
while(cur)
{
//比较的俩个值不相等
if(cur->val!=val)
{
//不相等的值是第一个
if(copy==NULL)
{
//第一个值时,将新头和移动指针都指向这个节点
newhead=copy=cur;
}
//不相等的值不是第一个
else
{
copy->next=cur;//尾插
copy=copy->next;//指向新插入的节点
}
//无论相不相等,原链表都移动到下一个节点
cur=cur->next;
}
//比较的俩个值相等
else
{
struct ListNode* del = cur;//记录要删除的当前节点
cur=cur->next;//原链表中移动到下一个节点
free(del);//是否当前节点
}
}
//原链表比较完后
if(copy)
{
copy->next = NULL;//新链表中最后一个节点的next置为空
}
return newhead;//返回新头
}
需要注意的一点就是,新链表中给头指针赋值的时候,只能赋值原来表中当前正在比较且不相等节点的地址,不能直接赋值head。
否则,当原链表中第一个节点中的值相等的时候,这里的新头还是指向旧链表中的第一个节点,本应该指向旧链表中的第一个节点。
此时这个算法需要创建一个新的头,接下来本喵再介绍一种,不需要创建新的头。
分析:
这里我们创建俩个指针,一个cur指向当前节点,另一个prev指向前一个节点。
思路:
- cur指向的当前节点中的值与给定的val比较
- 如果不相等,则prev和cur同时向后移动一个节点
- 如果相等,则prev不动,cur继续向后移动一个节点,并且prev指向节点中的next指向cur移动后的节点
当遇到一次不相等并且改变了指向关系以后,成为了
这个样子。
- 删除掉一个节点以后新链表又类似原来的样子,继续按照上面的逻辑走下去,直到遍历完整个链表。
代码实现:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while(cur)
{
//如果比较的值相等
if(cur->val==val)
{
//当前节点是第一节点
if(cur==head)
{
//将头指针指向下一个节点
head=head->next;
free(cur);
cur=head;
}
//当前节点不是第一个节点
else
{
//越过相等的节点建立新的连接
prev->next=cur->next;
free(cur);
cur = prev->next;
}
}
//如果比较的值不相等
else
{
//同时向后移动一个节点
prev=cur;
cur=cur->next;
}
}
//不是空链表
if(prev)
{
//将最后节点中的next置为空
prev->next=NULL;
}
return head;
}
具体解释清看代码中的注释。
✨2.反转链表
题目描述:
分析:
创建了指向原链表当前节点的指针cur和当前节点的下一个节点next,新列表头指针newhead。
思路:
- 总体思路就是顺序访问原链表中的每个节点,每访问一个,将其头插到新列表
- 头插本喵在上篇文章的单链表讲解中有详细说明
代码实现:
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* cur = head;//指向当前节点的指针
struct ListNode* newhead = NULL;//新头指针
while (cur)
{
struct ListNode* next = cur->next;//记录当前节点的下一个节点
//头插
cur->next = newhead;//新节点的next指向心头
newhead = cur;//新头指向新节点
cur = next;//取原链表中的下一个节点
}
return newhead;
}
可以看到通过了测试。
注意:
原链表中的当前节点的下一个节点必须提前记录,因为在后面蓝色框中的会改变当前节点的值,如果没有提前记录的话,会找不到后面下一个节点。
这里我们是通过创建一个新的头指针来改变节点的指向关系实现的,还有一种不需要创建新头的方法。
分析:
创建三个指针,n1,n2,n3
思路:
- n2指向第一个节点,n1指向前一个节点,n3提前记录后一个节点,否则会找不到后一个节点
- 将三个指针向后迭代,直到n2指向的内容为空,此时n1就是新的头
代码实现:
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* n1 = NULL;
struct ListNode* n2 = head;//指向第一个节点
struct ListNode* n3 = NULL;
while(n2)
{
n3=n2->next;//n3是当前节点的下一个节点
n2->next=n1;//n2的next指向n1
//迭代
n1=n2;//n1向后移动
n2=n3;//n2向后移动
}
return n1;
}
注意这里n3没有改变指向关系,只是提前记录了当前节点的下一个节点。
✨3.链表的中间结点
习题描述:
分析:
思路:
- 当节点个数是奇数个时
- 快指针一次移动两步,慢指针一次移动一步
- 当快指针指向最后一个节点时,慢指针正好指向中间节点
思路:
- 当节点个数是偶数时
- 快指针一次移动两步,慢指针一次移动一步
- 当快指针指向最后一个节点之后的空指针时,慢指针正好指向中间的第二个节点
代码实现:
struct ListNode* middleNode(struct ListNode* head){
//创建快慢指针,初始化都指向头节点
struct ListNode* fast = head;
struct ListNode* slow = head;
//计数个节点时fast的next为空停止
//偶数个节点时fast本身为空停止
while(fast&&fast->next)
{
slow=slow->next;//慢指针一次移动一步
fast=fast->next->next;//块指针一次移动两步
}
return slow;//慢指针的节点就是中间节点
}
注意:
循环条件中,必须写红色框中的内容,不能写蓝色框中的,否则就会编译不通过。
原因:
- 当节点个数是偶数时,fast在指向空之前指向的节点是倒数第二个节点,此时fast本身以及其下一个节点都不是空,所以会继续进循环。
- 当从循环体中出来以后,fast指向的是空,此时就在条件判断时,按照从左到右的顺序,先判断逻辑与(&&)操作符左边的值
- 如果是红色框,那么因为fast是空,就直接结束循环,&&后面的fast->不会再进行判断。
- 如果是蓝色框,先判断的是fast->next,此时的fast已经是空,操作符(->)是对fast解引用,因为空指针不能解引用,所以就会报错,&&后面的fast也不会再进行判断。
✨4.链表中倒数第k个结点
题目链接
题目描述:
分析:
这里创建了俩个指针,一个快指针fast,一个慢指针slow
思路:
- 快指针先移动k步
- 再快慢指针一起移动
- 当快指针指向NULL的时候,慢指针正好指向倒数第k个节点
代码实现:
struct ListNode* getKthFromEnd(struct ListNode* head, int k){
struct ListNode* fast = head;
struct ListNode* slow = head;
//快指针先移动k步
while(k--)
{
//如果超出链表节点个数,直接返回空
if(fast==NULL)
return NULL;
fast=fast->next;
}
//快慢指针一起移动
while(fast)
{
fast=fast->next;
slow=slow->next;
}
return slow;
}
注意:
红色框中的内容必须有
- 假设链表中有5个节点,但是找的是倒数第6个节点,链表中没有这个节点
- 所以在快指针先移动的时候,移动到空指针的位置就直接返回空,意思是没有这个节点
✨5.合并两个有序链表
题目描述:
分析:
按照蓝色数字的顺序来操作,我们创建了俩个新的指针,一个是新链表指针,另一个是新链表的尾部指针。
思路:
- 俩个链表的节点相比较,比较小的节点尾插到新链表
- 尾指针指向新链表中插入的节点
- 当俩个链表中有一个比较完以后,新链表的最后一个节点指向另一个没有比完的链表。
代码实现:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
//创建哨兵头
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
guard->next=NULL;//初始化为空链表
struct ListNode* cur1 = list1;
struct ListNode* cur2 = list2;//转移指针
struct ListNode* tail = guard;//创建尾指针,初始指向哨兵头
//进行比较和链接,直到一个链表比较完
while(cur1&&cur2)
{
//链表1的节点小于链表2的节点时
if(cur1->val < cur2->val)
{
tail->next=cur1;//尾插链表1的节点
cur1=cur1->next;//指向下一个节点
}
//链表2的节点大于等于链表1的节点
else
{
tail->next=cur2;//尾插链表2的节点
cur2=cur2->next;//指向下一个节点
}
tail=tail->next;//尾指针指向新链表中最后的节点
}
//链表1没有比较完,剩下的都是大的节点
if(cur1)
tail->next=cur1;//新链表与链表1进行链接
//链表2没有比较完
if(cur2)
tail->next=cur2;//新链表与链表2进行链接
struct ListNode* head = guard->next;//记录新链表的头
free(guard);//释放哨兵位
guard=NULL;
return head;//返回新链表的头
}
按照上面的思路,在具体的代码中,
- 我们创建了移动的节点指针cur1和cur2,分别在链表1和链表2中进行移动比较
- 创建了一个哨兵头,哨兵头的next指向的就是第一个节点,也就相当于头指针,指向有哨兵位会更加方便,无需单独出来第一个节点插入新链表的情况
- 创建了尾指针,让它永远指向新链表的尾部,以便于我们尾插
- 通过不停的比较和尾插,最后会有一个链表没有被比较完,此时将新链表的尾指向没有比较完的链表
✨6.链表分割
题目描述:
分析:
创建了用于比较的指针cur,俩个新链表的哨兵头,以及俩个新链表的尾指针,分别用来存放比val小的节点和大的节点。
思路:
- 将原链表中的每个节点与给定的值val相比较
- 比val小的放在存放小节点的新链表中
- 比val大的放在存放大节点的新链表中
- 新链表中的尾指针随时都要指向链表中的最后一个节点
- 当原链表比较完以后,将存放比val值大的新链表链接在存放比val值小的新链表后面。
代码实现:
ListNode* partition(ListNode* pHead, int x) {
// 创建存放小节点新链表的哨兵头
ListNode* bigguard = (ListNode*)malloc(sizeof(ListNode));
// 创建存放大节点新链表的哨兵头
ListNode* smallguard = (ListNode*)malloc(sizeof(ListNode));
bigguard->next=smallguard->next=NULL;//初始化为空链表
//将新链表各自的尾指针初始化指向哨兵头
ListNode* bigtail = bigguard;
ListNode* smalltail = smallguard;
//创建用于指向原链表比较节点的指针
ListNode* cur = pHead;
//挨个比较
while(cur)
{
//当前节点的值小于给定的值
if(cur->val<x)
{
//尾插在存放小节点的新链表
smalltail->next=cur;
smalltail=smalltail->next;//尾指针指向链表最后一个节点
}
//当前节点的值大于等于给定的值
else
{
//尾插在存放大节点的新链表
bigtail->next=cur;
bigtail=bigtail->next;//尾指针指向链表最后一个节点
}
cur=cur->next;//指向原链表的下一个节点
}
//将小节点的新链表与大节点新链表链接起来
smalltail->next=bigguard->next;
//最后形参的新链表的最后节点指向空
bigtail->next=NULL;
//记录新链表头指针
pHead=smallguard->next;
//释放哨兵头
free(smallguard);
smallguard=NULL;
free(bigguard);
bigguard=NULL;
//返回
return pHead;
}
可以看到,使用哨兵头非常的方便,可以不用单独处理新链表为空的情况。
✨7.链表的回文结构
习题描述:
分析:
实现该要求我们可以分为三个部分
- 找到中间节点
当节点个数是偶数时,中间节点是中间的第二个节点
当节点个数是奇数时,中间节点就是最中间的那个
在前面的习题链表的中间结点有讲解如何找到中间节点。
- 反转中间节点之后的链表
偶数个节点
奇数个节点:
在前面的习题反转链表中有讲解如何反转。
- 进行比较
偶数个节点
思路:
- 通过俩个当前指针cur和recur同时向后移动进行比较
- 如果相等继续移动,如果部相等,直接返回
- 如果一直相等,直到recur为空,说明是回文列表
奇数个节点:
思路:
- 和偶数个节点的思路一样,但是有一个不同点
- 当cur指向rehead的前一个节点时,recur指向没有反转之前的中间节点
因为
在没有反转之前,节点2的下一个节点是3- 所以当cur和recur同时移动时,它们回都指向原本的中间节点
代码实现:
//寻找中间节点
struct ListNode* middleNode(struct ListNode* head){
//创建快慢指针,初始化都指向头节点
struct ListNode* fast = head;
struct ListNode* slow = head;
//计数个节点时fast的next为空停止
//偶数个节点时fast本身为空停止
while(fast&&fast->next)
{
slow=slow->next;//慢指针一次移动一步
fast=fast->next->next;//块指针一次移动两步
}
return slow;//慢指针的节点就是中间节点
}
//反转链表
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* cur = head;//指向当前节点的指针
struct ListNode* newhead = NULL;//新头指针
while (cur)
{
struct ListNode* next = cur->next;//记录当前节点的下一个节点
//头插
cur->next = newhead;//新节点的next指向心头
newhead = cur;//新头指向新节点
cur = next;//取原链表中的下一个节点
}
return newhead;
}
bool isPalindrome(struct ListNode* head){
struct ListNode* midhead = middleNode(head);//找到中间节点
struct ListNode* rehead = reverseList(midhead);//将中间节点之后的链表反转
struct ListNode* cur = head;//未反转部分当前指针
struct ListNode* recur = rehead;//反转部分当前指针
//反转部分和未反转部分进行比较
while(cur&&recur)
{
//如果有一个节点不相等
if(cur->val != recur->val)
{
//直接返回假
return false;
}
//俩部分链表均移动一步
cur = cur->next;
recur = recur->next;
}
//全部节点都相等,返回真
return true;
}
这里我们寻找链表的中间节点和反转链表都是直接复制的前面本喵讲解的题中的代码。
✨8.相交链表
题目描述:
分析:
首先我们要知道一个概念,链表相交的节点并不是俩个节点的值相等,该可以得出链表在此处相交,而是俩个链表中某俩个节点所在的地址相等,才能说明俩个链表在此处相交。
相信各位小伙伴都可以通过暴力求解将它解出来,但是此时的时间复杂度是O(N^2),效率并不是很高,接下来本喵给大家介绍一种时间复杂度是O(N)的方法,效率更高。
这里我们创建了俩个指向当前位置的指针curA和curB
思路:
- 分别求出俩个链表的长度,然后相减得出它们相差的元素个数k
- 让长的链表先移动k步,然后再让俩个链表的cur一起向后移动,并且比较是否相等
- 如果不相等则继续同时往下移动,直到比较完毕,返回空
- 如果相等,说明该位置就是交叉点,直接返回该位置
代码实现:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//创建指向俩个链表的当前指针
struct ListNode* curA = headA;
struct ListNode* curB = headB;
//求链表a的长度
int lenA = 1;
while(curA->next)
{
curA=curA->next;
lenA++;
}
//求链表b的长度
int lenB = 1;
while(curB->next)
{
curB=curB->next;
lenB++;
}
//如果俩个链表最后的节点不相同,说明没有交点
if(curA!=curB)
{
return NULL;
}
//求俩个链表节点个数之差
int k = abs(lenA-lenB);//由于不直到谁大,所以结果用绝对值表示
//由于不知道哪个链表长
struct ListNode* longlist = headA;//假设A长
struct ListNode* shortlist = headB;//假设B短
//验证假设是否正确
if(lenA<lenB)
{
//求出真正长链表和短链表
longlist=headB;
shortlist=headA;
}
//长链表先走k步
while(k--)
{
longlist=longlist->next;
}
//俩个链表进行比较
while(longlist!=shortlist)
{
//不相等,同时进行下一步
longlist=longlist->next;
shortlist=shortlist->next;
}
//返回值就是交点,如果是空说明没有交点
return longlist;
}
注意:
这段代码是为了当链表没有交点时直接结束,就不用再继续往下走了。
原因:
- 单链表中只有一个next指针,指向的就只有下一个节点,如果俩个链表最后一个节点都不相同,那么说明这俩个链表一定没有交点
我们求出了俩个链表节点的个数之差,但是并不知道具体是哪个链表长,所以我们这里需要判断一下,将长的链表由longlist指向,短的链表由shortlist指向。
✨9.环形链表
题目描述:
分析:
我们创建俩个指针,分别是快指针fast和慢指针fast
思路:
- 快指针一次移动俩步,慢指针一次移动一步
- 快慢指针同时移动,快指针一定回先进入环
- 当快指针fast进入环以后就回沿着环安一次移动俩步不停的移动
- 在fast移动一段时间后,慢指针slow也会进入环
- 当fast和slow都在环中时,假设它们之间相差N步
- 由于fast一次移动俩步,slow一次移动一步,所以再移动一次后fast和slow相差N-1步
- 依次类推,会相差N-2,N-3,N-4…
- 最终fast会追上slow
代码实现:
bool hasCycle(struct ListNode *head) {
//如果是空链表指针返回假
if(head==NULL)
{
return false;
}
//定义快慢指针
struct ListNode* fast = head->next;//快指针初始化未第二个节点
struct ListNode* slow = head;//慢指针初始化为第一个节点
//当快慢指针不相等时,持续移动
while(fast!=slow)
{
//快指针或者快指针的下一个节点为空
if(fast==NULL||fast->next==NULL)
{
//说明没有成环
return false;
}
fast = fast->next->next;//快指针移动俩步
slow = slow->next;//慢指针移动一步
}
//被追上后说明成环
return true;
}
解释:
初始时,快指针指向第二个节点,慢指针指向第一个节点,这样做的好处是,当第一个节点就成环的时候,快指针和慢指针是相等的,就不会进入循环,之间返回真。
这里必须判断fast指针指向的节点和它下一个节点是否为空,因为fast每次移动的俩步,如果fast指向是最后一个节点,那么它就无法移动俩步,因为会报错。
✨10.环形链表返回成环位置
题目描述:
分析:
思路:
- 慢指针slow和快指针fast从开始处出发,通过上面那个题我们知道,最终会快慢指针会在环里相遇
- 假设fast和slow在距离成环点X处相遇,没有成环部分的长度是L
- 相遇时,slow走过的距离是L+X,fast走过的距离是L+X+NC,fast比slow走的多的部分就是在环里循环的次数,所以是NC,其中C是环的长度
- slow和fast之间的数量关系是2*(L+X) = L+X+N*C
- 最后可以简化为L=(N-1)*C + C-X
此时我们可以得出一个结论,一个指针从头走,另一个指针从相遇点开始走,它们的相遇点是在成环处。
代码实现:
struct ListNode *detectCycle(struct ListNode *head) {
//创建快慢指针
struct ListNode* slow = head;
struct ListNode* fast = head;
//寻找相交点
while(fast&&fast->next)
{
fast = fast->next->next;//一次移动俩步
slow = slow->next;//一次移动一步
if(fast==slow)
{
struct ListNode* meet = slow;//记录相遇点指针
//一个指针从开始移动,另一个指针从相遇点移动
while(head!=meet)
{
//每次均移动一次
head=head->next;
meet=meet->next;
}
//相遇点就是成环点,之间返回
return meet;
}
}
//没有相交点,返回空指针
return NULL;
}
难点在于数学公式的推导。
本喵还有一种方法,很容易理解,但是代码实现起来比较费事。
分析:
思路:
- 将快指针fast和慢指针slow在环内相遇点处的下一个点当成另一条链表的头rehead
- 原本链表的头head是原本链表的头
- 新形成的链表和原本链表的相交处就是成环处
代码实现:
//求链表的相交点
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//创建指向俩个链表的当前指针
struct ListNode* curA = headA;
struct ListNode* curB = headB;
//求链表a的长度
int lenA = 1;
while(curA->next)
{
curA=curA->next;
lenA++;
}
//求链表b的长度
int lenB = 1;
while(curB->next)
{
curB=curB->next;
lenB++;
}
//如果俩个链表最后的节点不相同,说明没有交点
if(curA!=curB)
{
return NULL;
}
//求俩个链表节点个数之差
int k = abs(lenA-lenB);//由于不直到谁大,所以结果用绝对值表示
//由于不知道哪个链表长
struct ListNode* longlist = headA;//假设A长
struct ListNode* shortlist = headB;//假设B短
//验证假设是否正确
if(lenA<lenB)
{
//求出真正长链表和短链表
longlist=headB;
shortlist=headA;
}
//长链表先走k步
while(k--)
{
longlist=longlist->next;
}
//俩个链表进行比较
while(longlist!=shortlist)
{
//不相等,同时进行下一步
longlist=longlist->next;
shortlist=shortlist->next;
}
//返回值就是交点,如果是空说明没有交点
return longlist;
}
struct ListNode *detectCycle(struct ListNode *head) {
//创建快慢指针
struct ListNode* fast = head;
struct ListNode* slow = head;
//寻找相遇点
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
{
struct ListNode* meet = slow;//记录相遇点
struct ListNode* rehead = meet->next;//将相遇点的下一个点记为新链表的头
meet->next=NULL;//将相遇点的下一个点置为空,制造出一个开环链表
return getIntersectionNode(head,rehead);//得到新旧链表的相交位置
}
}
return NULL;
}
其中,求俩个链表的相交点,本喵在前面的习题讲解过。
✨11.复制带随机指针的链表
题目描述:
分析:
思路:
- 新链表中的每个节点都插入到要原来链表中要复制节点和下一个节点之间,如图中红色节点所对应的红色箭头指示方向。
- 原链表中random的指向的节点在新链表中是该节点后面插入的新节点,就像图中黑色箭头的指向关系那样,它复制的是原链表中第二个节点中random的指向
- 最后将红色的新节点链接在一起,就像图中蓝色箭头那样。
代码实现:
struct Node* copyRandomList(struct Node* head) {
struct Node* cur = head;//创建当前指针
struct Node* copy = NULL;//创建复制节点的新指针
struct Node* next = NULL;//创建指向原链表下一个节点的指针
//插入
while(cur)
{
next = cur->next;//记录原链表下一个节点
copy = (struct Node*)malloc(sizeof(struct Node));//新节点
copy->val = cur->val;//复制值
cur->next = copy;//被复制节点的next指向复制的新节点
copy ->next = next;//复制的新节点的next指向原链表的下一个节点
cur = next;//迭代
}
cur = head;//重新指向原链表开头
//开始复制random
while(cur)
{
copy = cur->next;//记录复制的新节点
//如果原链表中节点的random指向空
if(cur->random==NULL)
copy->random = NULL;//复制节点的random也置空
else//原链表中节点的random指向的不是空
//复制节点中的random指向原节点中random指向节点的下一个节点
copy->random = cur->random->next;
cur = copy->next;//进行迭代,指向原链表中的下一个节点
}
//创建新链表的头指针和尾指针
struct Node* copyhead = NULL;
struct Node* copytail = NULL;
cur = head;//重新指向原链表开头
//将新节点链接起来
while(cur)
{
copy = cur->next;//记录心节点
next = copy->next;//记录原链表的下一个节点
//处理第一个新节点的情况
if(copytail==NULL)
{
//新链表的头指针和尾指针都指向新节点
copyhead=copytail=copy;
}
//通常情况
else
{
copytail->next=copy;//链接下一个新节点
copytail=copy;//迭代新链表
}
//恢复原本的链接关系
cur->next=next;
//迭代往下走
cur=next;
}
return copyhead;//返回复制链表的头
}
注意:
复制链表完成后,要将原来链表的指向关系恢复。
这11个题充分利用了链表的特点,如果掌握了,对链表的理解会更加深入。