在《数据结构与算法篇初阶3:线性表—链表相关知识点讲解》中,为大家详细讲解了线性表中的单链表相关知识,这一讲主要承接单链表的内容进行针对性的笔试面试OJ刷题训练,带领读者了解单链表的应用场景及应用特征。
目录
1、OJ训练题1:移除指定链表元素
2、OJ训练题2:反转链表
3、OJ训练题3:找链表的中间结点
4、OJ训练题4:返回链表倒数第k个结点
5、OJ训练题5:合并两个有序链表
6、结语
训练题链接:203. 移除链表元素 - 力扣(LeetCode)
题目概述:
解法一:直接在原链表中删除结点。创建两个结点prev=NULL和cur=head,其中cur定义为当前结点,作为遍历整个链表,在程序设计过程中,分以下情况考虑:
1、当链表的第一个结点也就是head结点所在位置就是需要删除的对象时,此时cur结点等于head,将cur结点指向的下一个结点重新赋给head,作为链表新的头结点,然后删除当前位置结点。代码所示如下:
2、当头结点不是需要删除的对象时,此时将cur结点指向的下一个结点赋给prev结点指向的下一个结点,释放当前结点,然后将prev结点指向的下一个结点作为新的当前结点。代码所示如下:
3、当cur->val不等于val时,此时将当前结点赋给prev,然后cur结点继续向后走,找下一个结点。在整个过程中,prev结点用于记录保留下的链表中的结点,然后cur进行更新。代码所示如下:
4、最终返回可能修改也可能没有修改过的头结点head。完整代码展示如下所示:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode*prev=NULL;
struct ListNode*cur=head;
while(cur)
{
if(cur->val==val)
{
//如果第一个位置就是在头节点
if(cur==head)
{
head=cur->next;
free(cur);
cur=head;
}
else
{
//删除结点
prev->next=cur->next;
free(cur);
cur=prev->next;
}
}
//如果不相等
else
{
prev=cur;
cur=cur->next;
}
}
return head;
}
解法二:利用在新链表中利用尾插思想尾插数据。创建两个结点tail=NULL和cur=head,其中cur定义为当前结点,作为遍历整个链表,tail结点用于将保留的数据尾插在新链表中。在程序设计过程中,分以下情况考虑:
1、当当前结点cur就是需要删除的对象时,可以通过创建一个新结点del保存当前结点,然后保存cur结点指向的下一个结点的位置,然后释放del。代码所示如下:
2、当cur结点不是需要删除的对象时,此时需要做一个判断,即:判断tail结点是不是为空,如果为空,则需要先将curf赋值给tail和head,然后再将cur指向的下一个结点作为当前结点。代码所示如下:
3、当cur结点不是需要删除的对象且不是上述情况时,利用单链表的尾插数据方法,直接在tail结点后面插入保留的数据,然后再将cur指向的下一个结点作为当前结点。代码所示如下:
4、在最后我们需要判断tail为不为空,如果不为空,当程序执行完前面的循环时,此时需要将tail指向的下一个结点置为NULL。
5、注意特殊情况:如果所有的结点都是需要删除的,那么一开始我们需要先将head置为空,如果不这样做,则执行程序的时候不会进入循环,然后直接renturn head,则就会出错。完整代码展示如下所示:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode*tail=NULL;
struct ListNode*cur=head;
//当所有的结点都是需要删除的对象时,如果不把头节点初始化为NULL,
//则在后面的循环中,返回的头结点就不对了
head=NULL;
while(cur)
{
//找到符合的结点,直接删除
if(cur->val==val)
{
struct ListNode*del=cur;
cur=cur->next;
free(del);
}
else
{
//尾插,当tail==NULL时,需要先将cur赋给tail
if(tail==NULL)
{
head=tail=cur;
}
//直接利用尾插方式
else
{
tail->next=cur;
tail=tail->next;
}
cur=cur->next;
}
}
if(tail)
{
tail->next=NULL;
}
return head;
}
解法三:利用创建带哨兵位的头节点。创建两个结点tail=NULL和cur=head,其中cur定义为当前结点,作为遍历整个链表,tail结点用于将保留的数据尾插在新链表中。在程序设计过程中,分以下情况考虑:
1、创建哨兵位的头结点:该结点不存储数据,多用于尾插情况,因为利用该结点不需要判断原链表为不为空的情况。代码展示如下:
2、当当前结点cur就是需要删除的对象时,可以通过创建一个新结点del保存当前结点,然后保存cur结点指向的下一个结点的位置,然后释放del。代码所示如下:
3、当cur结点不是需要删除的对象,利用单链表的尾插数据方法,直接在tail结点后面插入保留的数据,然后再将cur指向的下一个结点作为当前结点。代码所示如下:
4、我们在利用哨兵位的头节点时,在程序的最后,需要将其删除。但删除之前,需要先将哨兵位头节点所指向的下一个结点作为新的头节点保存并返回,然后再释放该哨兵位头节点。代码所示如下:
5、综合前面所述,完整代码展示如下所示:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode*tail = NULL;//tail指针用于记录保留的节点
struct ListNode*cur = head;//表示当前位置节点
//创建哨兵位的头节点
head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while (cur)
{
if (cur->val == val)
{
struct ListNode*del = cur;
cur = cur->next;
free(del);
}
else
{
tail->next = cur;
tail = tail->next;
cur = cur->next;
}
}
tail->next = NULL;
struct ListNode*del = head;
head = head->next;
free(del);
return head;
}
训练题链接:206. 反转链表 - 力扣(LeetCode)
题目概述:
解法1:利用单链表的头插思想。创建两个结点newhead和cur,其中newhead表示反转链表的头结点,cur结点表示当前位置,然后遍历原链表,并头插进新链表,进而实现链表的反转。分四步进行:
1、创建新结点作为保存当前结点的下一个结点位置;
2、然后将当前结点cur的cur->next指向newhead;
3、更新新的头结点newhead;
4、更新当前结点cur;
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode*newhead = NULL;
struct ListNode*cur = head;
while (cur)
{
//利用的是头插
struct ListNode*next = cur->next;//记录原始节点的下一个位置
cur->next = newhead;//然后将当前节点cur的next连接到新的节点中,
newhead = cur;//此时新的链表的头结点需要改变
cur = next;//在原始链表中更新cur;
}
return newhead;
}
解法2:创建三个指针。其中n1表示原链表的前一个结点,n2表示原链表的当前结点位置;n3表示原链表下一个结点位置。
1、初始时刻,先判断链表为不为空,为空直接返回;
2、初始三个指针指向的结点:n1=NULL;n2=head;n3=n2->next;从这里应该就可以看出已经有了反转链表的味道;
3、如何向后以此遍历方法,代码展示如下:即先将n2->next指向n1,然后将n2赋给n1,将n3赋给n2,然后n3=n3->next,从而实现在原结点中向后遍历。但是在这里我们要注意,n3会提前访问到空,所以需要有一个判断,最后循环结束返回n1,此时的n1即为反转链表的头节点。完整代码展示如下所示:
struct ListNode* reverseList(struct ListNode* head)
{
if (head == NULL)
{
return NULL;
}
struct ListNode*n1, *n2, *n3;
n1 = NULL;
n2 = head;
n3 = n2->next;
while (n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3)
{
n3 = n3->next;
}
}
return n1;
}
训练题链接:876. 链表的中间结点 - 力扣(LeetCode)
题目概述:
解法:针对这类寻找链表的中间结点,在这里为大家提供一种“快慢指针”的思想。思想如下:
1、创建两个指针,一个快指针fast=head,一个慢指针slow=head;
2、在链表的遍历过程中,二者同时走,但要保证快指针fast始终比慢指针slow多走一步;
3、当快指针fast==NULL或者fast->next==NULL时,遍历结束,此时慢指针所指的结点位置即为链表的中间结点。完整代码展示如下:
struct ListNode* middleNode(struct ListNode* head)
{
//采用快慢指针思维,即当快指针走的是慢指针的2倍时,就可以实现找到中间节点;
struct ListNode*slow = head;
struct ListNode*fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
训练题链接:链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)
题目概述:
解法:针对这类返回第k个结点位置的解法,同样可以用到上一训练题讲解的“快慢指针”思想。思想如下:
1、创建两个指针,一个快指针fast=head,一个慢指针slow=head;
2、在链表的遍历过程中,快指针先走k步,然后二者再同时走
3、当快指针fast==NULL遍历结束,此时慢指针所指的结点位置即为链表的倒数第k个结点。完整代码展示如下:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{
//同样可以参考快慢指针思想,即最开始让快指针先走K步,然后再一起走,
//当快指针为空时此时慢指针指向即为倒数第k个节点
struct ListNode*fast = pListHead;
struct ListNode*slow = pListHead;
while (k--)
{
//注意判断,如果fast还没有走出K步,链表没有K步长;
if (fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while (fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
训练题链接:21. 合并两个有序链表 - 力扣(LeetCode)
题目概述:
解法1:利用归并排序思想,创建两个指针tail和head,其中head用来作为排序后的新结点的头节点,tail指针用来记录尾插。
1、因为两个链表本身就是有序的,所以从头开始比较,取小的尾插到新的链表中。
2、在尾插的过程中,我们要留意初始时刻头节点,即当tail==NULL时,令head=tail=list1或者head=tail=list2。然后再利用尾插思想思想进行尾插。
3、当list1&&list2中有一个为空时,此时我们不能确定还有哪一个链表还有剩余结点,为此需要单独对两个链表进行判断,不为空则继续尾插到新链表中。
4、当我们完成了前面的三个过程以后,我们还需要留意如果最开始就有某一个结点就是为空的,则此时直接将不为空的链表的头结点作为返回值。完整代码展示如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
//先判断是否存在空链表情况
if (list1 == NULL)
return list2;
if (list2 == NULL)
return list1;
//利用归并思想:从头开始比较,取小的尾插到新的链表
struct ListNode*head=NULL;
struct ListNode*tail=NULL;
while (list1&&list2)
{
if (list1->val < list2->val)
{
if (tail == NULL)
{
head = tail = list1;
}
else
{
tail->next = list1;
tail = tail->next;
}
list1 = list1->next;
}
else
{
if (tail == NULL)
{
head = tail = list2;
}
else
{
tail->next = list2;
tail = tail->next;
}
list2 = list2->next;
}
}
if (list1)
tail->next = list1;
if (list2)
tail->next = list2;
return head;
}
解法2:仍然利用归并排序思想,创建两个指针tail和head,其中head用来作为排序后的新结点的头节点,tail指针用来记录尾插。但因为是利用尾插思想,我们可以利用创建带哨兵位的头节点,这样就可以不用单独判断最开始tail为不为空的情况。尾插方法如方法1所示,在这里不在赘述。
不过在这里值得补充一点的就是关于在创建哨兵位的头结点时,如何销毁的方法。在这里我提供了两种方式:
1、第一种是直接创建新结点list存储哨兵位头节点的下一个结点,然后释放哨兵位头节点;
2、第二中就是创建新结点del存储哨兵位的头节点,然后保存哨兵位头结点指向的下一个结点,并释放del。
//方法二:借助哨兵位
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode*head, *tail;
head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while (list1&&list2)
{
if (list1->val < list2->val)
{
tail->next = list1;
tail = tail->next;
list1 = list1->next;
}
else
{
tail->next = list2;
tail = tail->next;
list2 = list2->next;
}
}
if (list1)
tail->next = list1;
if (list2)
tail->next = list2;
//struct ListNode*list = head->next;
//free(head);
//return list;
//或者这样表示:
struct ListNode*del = head;
head = head->next;
free(del);
return head;
}
本节主要为读者们带来了单链表OJ面试经典题型中常见的五种题型,相信通过这五个题型的讲解,大家对单链表的使用及理解会更进一步。后面关于链表的经典题型会为大家陆续带来详细讲解。欢迎大家点赞、支持、关注!!!