链表12道经典OJ题——助你理解链表原理
Posted aaaaaaaWoLan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表12道经典OJ题——助你理解链表原理相关的知识,希望对你有一定的参考价值。
文章目录
注:部分图片从leetcode转载
后面偷懒了,有些图没画
个人认为11题是难度较高的题,如果有大佬觉得不过如此,还望指导指导
1.移除链表元素
OJ链接
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
思路一:设置两个指针遍历链表,cur去查找删除元素,prev保持删除后与后一个节点的联系
需考虑第一个节点就为val的情况,并修改head指向
以示例1为例分析:
第一步:prev为空,cur指向第一个节点,cur指向的1不是要删除的元素,cur,prev均向后走
也就是prev=cur,cur=cur->next
第二步:prev指向1,cur指向2,cur指向的2不是要删除的元素,cur,prev均向后走
第三步:prev指向2,cur指向6,cur指向的6是要删除的元素,free掉6这个节点,cur向后走,prev继续指向2,但节点2的next不再指向6,而是指向3,也就是cur = cur->next,prev->next = cur
第四步:把6删掉,prev指向2,cur指向3,cur指向的3不是要删除的元素,cur,prev均向后走
第五步:prev指向3,cur指向4,cur指向的4不是要删除的元素,cur,prev均向后走
第六步:prev指向4,cur指向5,cur指向的5不是要删除的元素,cur,prev均向后走
第七步:prev指向5,cur指向6,cur指向的6是要删除的元素,free掉6这个节点,cur向后走,prev继续指向5,但节点5的next不再指向6,而是指向NULL,此时cur也指向了NULL,遍历结束
当然,存在首元素就是要删除元素的情况,或者全部都是要删除元素的情况:
此时cur=cur->next,但是prev->next=cur还成立吗?
答案显然是不成立,prev此时还是个空指针,这样显然是非法访问了,所以不能这么写了
删除之后:我们可以假设为将之前的6删掉了,cur还是指向第一个节点,prev当然就还是NULL不用改变,但是此时head就要改变指向了
由此可以直到,所有节点都要删除无非是重复刚才的步骤
代码(只实现函数):
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while (cur != NULL)//遍历链表,cur指向空时遍历结束
{
if (cur->val == val)//遇到需删除元素的情况
{
if (prev == NULL)//第一个节点就为val的情况
{
struct ListNode*new = cur->next;
free(cur);
cur = new;
head = cur;//此时head就不指向第一个节点了
}
else//第一个节点不为val的情况
{
struct ListNode*new = cur->next;
free(cur);
cur = new;
prev->next = cur;
}
}
else//未遇到需删除元素,prev与cur均向后走
{
prev = cur;
cur = cur->next;
}
}
return head;
}
思路二:设置一个新的哨兵guard结构体,
guard->next=head,prev=guard,这样就不用单独考虑第一个节点就是要删除元素的情况了,而且删除了第一个节点,head也会随之改变,因为prev->next会连接要删除元素的下一个节点,prev->next就是head,head也就改变了,如果第一个节点不是要删除元素,prev->next就不再指向head,head也就不会再改变了,最后记得free(guard)
代码:
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
guard->next = head;
struct ListNode* prev = guard;
struct ListNode* cur = head;
while (cur != NULL)
{
if (cur->val == val)
{
struct ListNode* new = cur->next;
free(cur);
prev->next = new;
cur = new;
}
else
{
prev = cur;
cur = cur->next;
}
}
head = guard->next;
free(guard);
guard = NULL;
return head;
}
2.反转链表
OJ链接
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
思路一:创建三个指针n1,n2,n3
初始化n1为空,n2指向1,使1指向NULL也就是n1,2指向1也就是n2,再依次将n1,n2,n3向后走,如此循环往复
以示例1为例分析:
第一步:n2->next = n1,使1指向空,n1 = n2, n1再指向1,n2 = n3, n2再指向2,n3 = n3->next,n3再指向3
第二步:n2->next = n1,使2指向1,n1 = n2, n1再指向2,n2 = n3, n2再指向3,n3 = n3->next,n3再指向4
第三步:n2->next = n1,使3指向2,n1 = n2, n1再指向3,n2 = n3, n2再指向4,n3 = n3->next,n3再指向5
第四步:n2->next = n1,使4指向3,n1 = n2, n1再指向4,n2 = n3, n2再指向5,n3 = n3->next,n3就指向NULL了
这里注意,下一次就不能再使n3 = n3->next了,否则就是非法访问了,所以要加个判断条件if (n3 != NULL),才能执行
n3 = n3->next
第五步:n2->next = n1,使5指向4,n1 = n2, n1再指向5,n2 = n3, n2再指向NULL,n3已经是NULL了,就不能再向后走了
最后的结果就是:此时遍历结束,完成反转
代码(只实现函数):
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL || head->next == NULL)
{
return head;
}
struct ListNode* n1 = NULL;
struct ListNode* n2 = head;
struct ListNode* n3 = head->next;
while (n2 != NULL)
{
//翻转
n2->next = n1;
//迭代
n1 = n2;
n2 = n3;
if (n3 != NULL)
{
n3 = n3->next;
}
}
return n1;
}
思路二:头插(不创建新节点)
创建cur,newhead两个指针用头插的方式完成反转
初始化cur为head,newhead为NULL,即假设从一个空链表(说是新链表,但实际没有开辟空间)开始,向前依次插入1,2,3,4,5
以示例1为例分析(图中简称newhead为new):
第一步:一开始为空链表,向空链表头插1,头部就变成了1,new此时指向1,1也要重新指向NULL,创建临时指针tmp保存1指向的2,cur向后指向2,注意这里不能free(cur),因为1这个空间只开辟了一份,如果free了就不存在了,头插就类似把原节点转移位置
第二步:…
第三步:…
…
如此往复,就可以把所有1,2,3,4,5均头插至新链表,最后的头部也就是5,newhead指向的就是5
(别问为什么没图,问就是太难画)
代码(只实现函数):
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* cur = head;
struct ListNode* newhead = NULL;
while (cur != NULL)
{
struct ListNode* tmp = cur->next;
cur->next = newhead;
newhead = cur;
//不能free
cur = tmp;
}
return newhead;
}
3.链表的中间结点
OJ链接
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路:利用快慢指针
慢指针走一步,快指针走两步,当快指针指向NULL时(偶数个数字的情况)或快指针的next指向NULL时(奇数个数字的情况)
慢指针指向的位置就是所求节点
以示例1和2为例分析:
示例1:
第一步:slow和quick一开始均指向1,然后slow走一步,quick走两步
第二步:slow此时指向2,quick此时指向3,然后slow走一步,quick走两步
第三步:quick的next已经指向空了,slow此时就是要找的位置
示例2:
第一步:slow和quick一开始均指向1,然后slow走一步,quick走两步
第二步:slow此时指向2,quick此时指向3,然后slow走一步,quick走两步
第三步:slow此时指向3,quick此时指向5,然后slow走一步,quick走两步
第四步:quick此时已经指向NULL了,slow就是要查找的位置
代码(只实现函数):
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* quick = head;
struct ListNode* slow = head;
while (quick != NULL && quick->next != NULL)
{
slow = slow->next;
quick = quick->next->next;
}
return slow;
}
4.链表中倒数第k个结点
输入一个链表,输出该链表中倒数第k个结点。
注:一般题目说输入一个链表,没有特别说明,就是指不带头节点的单链表
思路一:遍历链表,计算链表长度,再遍历一次,找到(链表长度-k)个节点
该方法较简单,不作详细解释
思路二:快慢指针,快指针先走k步,慢指针再走,快指针指向空时,慢指针指向节点即为所求
考虑极端情况:1.k>链表长度; 2.链表为空
以示例1为例分析:
第一步:一开始slow 和 quick都指向1,quick向后先k走,也就是1步
第二步:slow和quick一起向后走,直到quick指向空
第三步:quick已经指向空,slow所指的节点5就是所求节点
两种极端情况的解决方法:
1.k>链表长度
k>链表长度,找倒数第k个节点说明找到的是空,所以返回空
2.链表为空,直接返回空
代码(只实现函数部分):
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k) {
// write code here
struct ListNode* quick = pListHead;
struct ListNode* slow = pListHead;
if (pListHead == NULL)
return NULL;
while ((k--) != 0)//快指针先走
{
if (quick == NULL)//处理k>链表长度的情况
{
return NULL;
}
quick = quick->next;
}
while (quick != NULL)//快慢同时出发
{
slow = slow->next;
quick = quick->next;
}
return slow;
}
5.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:类似于合并两个有序数组,创建两个指针分别指向两个链表,将两个指针指向的val分别进行比较,取小的尾插到另一个大的节点后面,并且创建一个尾指针来维护新链表的末尾
考虑链表为空的情况:可能链表一为空链表二不为空,也可能链表二为空链表一不为空,也可能两者都为空
以示例1为例分析:
第一步:先取一个较小的节点做新链表的第一个节点,方便后面尾插(前提是两指针不为空)
假设当cur2指向的元素大于cur1指向的元素时,将cur1所指元素尾插至新链表后,否则将cur2所指元素尾插至新链表后
cur1指向红色链表,cur2指向紫色链表,此时cur1指向的1等于cur2指向的1,所以将cur2指向的1作为头部将cur1指向的1尾插至cur2指向的1之后
同时让新链表的头部phead指向紫色链表的节点1,方便之后返回结果
分离得到:
要设置指针保存指向紫色链表3的位置,才能再尾插,也就是使 cur2 = cur2->next
我们发现实际上我们并没有创建新链表,而是将其中一个作为新链表
第二步:比较cur1指向1与cur2指向的3,1<3,所以将节点1尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点1
得到结果:
第三步:比较cur1指向2与cur2指向的3,2<3,所以将节点2尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点2
得到结果:
第四步:比较cur1指向4与cur2指向的3,3<4,所以将节点3尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点3
得到结果:
第五步:比较cur1指向4与cur2指向的4,cur2指向的4不大于cur1指向的4,所以将节点4尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点4
得到结果:
第六步:cur2已经指向空了,再将另一个链表剩下的节点插至新链表后即可,注意,这里只需要连接一次,因为圣血的节点本就是相连的
代码(只实现函数):
上述分析中的cur1,cur2在代码中是l1,l2
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
//判断是否为空
if (l1 == NULL)
{
return l2;
}
if (l2 == NULL)
{
return l1;
}
struct ListNode* phead = NULL;
struct ListNode* tail = NULL;
if (l2->val > l1->val)//先取小节点作新链表的第一个节点
{
phead = tail = l1;
l1 = l1->next;
}
else
{
phead = tail = l2;
l2 = l2->next;
}
while (l1 != NULL && l2 != NULL)
{
if (l2->val > l1->val)
{
tail->next = l1;
tail = tail->next;
l1 = l1->next;
}
else
{
tail->next = l2;
tail = tail->next;
l2 = l2->next;
}
}
//拷贝剩下的节点
if (l2 == NULL)
{
tail->next = l1;
}
else
{
tail->next = l2;
}
return phead;
}
还有一种哨兵解法,就是创建一个新节点,同样也是取小的尾插至新节点之后,省去了判断两个链表是否为空的情况,但返回前需要free掉该空间
初始时:
后面与上述方法一致
代码:
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
//创建哨兵
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
//维护新空间的尾部
struct ListNode* tail = guard;
while (l1 != NULL && l2 != NULL)
{
if (l2->val > l1->val)
{
tail->next = l1;
tail = tail->next;
l1 = l1->next;
}
else
{
tail->next = l2;
tail = tail->next;
l2 = l2->next;
}
}
//拷贝剩下的节点
if (l2 == NULL)
{
tail->next = l1;
}
else
{
tail->next = l2;
}
//释放哨兵空间
struct ListNode* tmp = guard->next;//记得是存放guard->next
free(guard);
guard = NULL;
return tmp;
}
6.链表分割
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:创建两个新链表,一个存放比x小的节点,另一个存放等于或大于x的节点,创建cur遍历原链表,填充好新链表后,将两个链表连接,再返回
注意一点:将两个链表链接完成后,将存放较大数字的链表尾部的next置空,否则有可能该next还指向其他节点,导致连接后的链表形成环
比如:
此时9的next还连着3,就会形成一个环
代码(只实现函数部分):
ListNode* partition(ListNode* pHead, int x) {
// write code here
ListNode* lesshead = (ListNode*)malloc(sizeof(ListNode));
ListNode* lesstail = lesshead;
lesshead->next = NULL;
ListNode* greaterhead = (ListNode*)malloc(sizeof(ListNode));
ListNode* greatertail = greaterhead;
greaterhead->next = NULL;
ListNode* cur = pHead;
while (cur != NULL)
{
if (cur->val < x)
{
lesstail->next = cur;
lesstail = cur;
cur = cur->next;
}
else
{
greatertail->next = cur;
greatertail = cur;
cur = cur->next;
}
}
lesstail->next = greaterhead->next;
greatertail->next = NULL;
pHead = lesshead->next;
free(lesshead);
free(greaterhead);
return pHead;
}
7.链表的回文结构
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
bool是c++的变量类型,只有true和false两种
思路一:创建一个数组int a[900],将链表的数据按顺序放入数组
这样可以,但是空间复杂度不符合O(1),就不多解释了
思路二:1.先找到中间节点; 2.再反转中间节点及之后的节点;3.从头节点处和中间节点处开始比较
以示例1为例:
再分析一个奇数位的链表:
到这里可能有人奇怪,3也要反转吗?
我们先来看代码,由于找中间节点与反转链表我们之前实现过,这里就不再写代码怎么实现了
typedef struct ListNode {
int val;
struct ListNode* next;
}ListNode;
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* quick = head;
struct ListNode* slow = head;
while (quick != NULL && quick->next != NULL)
{
slow = slow->next;
quick = quick->next->next;
}
return slow;//返回中间节点
}
//反转链表
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL || head->next == NULL)
{
return head;
}
struct ListNode* n1 = NULL;
struct <以上是关于链表12道经典OJ题——助你理解链表原理的主要内容,如果未能解决你的问题,请参考以下文章
八道经典的面试链表题--------快乐人的java巩固日记[1]