文章目录
单链表习题
反转链表
思路1
取结点头插
定义一个新的头指针为NULL,然后按照单链表头插的思路取结点头插,最后返回新的头
参考代码
struct ListNode* reverseList(struct ListNode* head){
struct ListNode*cur=head;
struct ListNode*rhead=NULL; //新的头
while(cur!=NULL)
{
struct ListNode*next=cur->next;
//头插
cur->next=rhead;
rhead=cur;
cur=next;
}
return rhead;
}
思路2
改变指针的指向
定义3个结点,遍历一遍链表通过三个结点改变链表中指针的指向,从而反转链表
参考代码
struct ListNode* reverseList(struct ListNode* head){
//空链表
if(head==NULL)
return head;
struct ListNode*n1=NULL;
struct ListNode*n2=head;
struct ListNode*n3=n2->next;
while(n2)
{
n2->next=n1;
n1=n2;
n2=n3;
if(n3)
n3=n3->next;
}
return n1;
}
移除链表元素
思路1
找一个删一个
结点的val等于val,释放掉此结点,继续遍历;否则直接遍历
缺点: 效率低,要考虑多种情况
参考代码
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode*cur=head;
struct ListNode*prev=head; //cur的前一个结点
while(cur)
{
if(cur->val == val)
{
struct ListNode*next=cur->next;
if(cur == head)
{
head=cur->next;
}
else
{
prev->next=cur->next; //cur的上一个结点链接到下一个
}
free(cur);
cur=next;
}
else
{
prev=cur;
cur=cur->next;
}
}
return head;
}
思路2
取结点尾插
按照单链表尾插的思路,不等于val尾插,等于val释放
参考代码
struct ListNode* removeElements(struct ListNode* head, int val){
//空链表
if(head==NULL)
return head;
struct ListNode*cur=head;
struct ListNode*tail,*newhead;
tail=newhead = NULL;
while(cur)
{
if(cur->val != val)
{
if(tail==NULL)
{
tail=newhead=cur;
}
else
{
tail->next=cur;
tail=cur;
}
cur=cur->next;
}
else
{
struct ListNode*next=cur->next;
free(cur);
cur=next;
}
}
//检查tail->next是否指向NULL
if(tail)
tail->next=NULL;
return newhead;
}
思路3
取结点尾插+带哨兵位头结点
哨兵位头结点不存储有效数据
整体思路还是思路2, 定义一个哨兵位头结点(不用多余处理头结点的val等于val的情况)直接尾插到它的后面
最后释放掉哨兵位,返回哨兵位的下一个,就是链表的头
参考代码
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode*cur=head;
struct ListNode*tail, *guard; //定义哨兵位的头结点
tail=guard=(struct ListNode*)malloc(sizeof(struct ListNode));
guard->next=NULL;
while(cur)
{
if(cur->val != val)
{
tail->next=cur;
tail=cur;
cur=cur->next;
}
else
{
struct ListNode*next=cur->next;
free(cur);
cur=next;
}
}
//将tail->next指向NULL
tail->next=NULL;
struct ListNode*newhead=guard->next; //保存链表真正的头结点
free(guard); //释放掉哨兵位头结点
return newhead;
}
合并两个有序链表
总结: 此题思路可以参考双指针博客中的合并两个有序数组(链接: 链接 )
简单来说就是归并的思路, 解法就是取小的尾插
思路1
取小的尾插
缺点: 要考虑多种特殊情况:
- list1为空
- list2为空
- 链表为NULL,第一次尾插时
参考代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
//list1为NULL,返回list2
if(list1 == NULL)
return list2;
//list2为NULL,返回list1
if(list2 == NULL)
return list1;
struct ListNode*head,*tail;
head=tail=NULL;
//取小的尾插
while(list1 && list2)
{
if(list1->val < list2->val)
{
if(tail==NULL) //链表为空,第一次尾插
{
tail=head=list1;
}
else
{
tail->next=list1;
tail=list1;
}
list1=list1->next;
}
else
{
if(tail==NULL)
{
tail=head=list2;
}
else
{
tail->next=list2;
tail=list2;
}
list2=list2->next;
}
}
//list1没链接完
if(list1)
tail->next=list1;
//list2没链接完
if(list2)
tail->next=list2;
return head;
}
思路2
取小的尾插+带哨兵位的头结点
优点: 直接尾插, 不用考虑特殊情况
参考代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
struct ListNode*guard,*tail;
guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
guard->next=NULL;
//取小的尾插
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next=list1;
tail=list1;
list1=list1->next;
}
else
{
tail->next=list2;
tail=list2;
list2=list2->next;
}
}
//list1没链接完
if(list1)
tail->next=list1;
//list2没链接完
if(list2)
tail->next=list2;
struct ListNode*newhead=guard->next;
free(guard);
return newhead;
}
链表分割
思路
取小的尾插+构造2个链表(创建2个哨兵位)
原链表中小于x的尾插到第一个链表, 其余尾插到第二个链表, 将两个链表链接起来, 释放掉2个哨兵位, 返回新链表的头结点即可
参考代码
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
struct ListNode*cur = pHead;
//申请2个哨兵位, 创建2个链表
struct ListNode*lessHead, *lessTail, *greaterHead, *greaterTail;
lessHead= lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
while(cur)
{
if(cur ->val <x)
{
lessTail->next =cur;
lessTail=cur;
}
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;
}
};
链表的回文结构
思路
找链表中间结点+反转链表
参考前几题解法, 先找到链表的中间结点后反转链表, 最后再遍历比较原链表和反转后的链表结点处的值
参考代码
//获取链表中间结点
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode*fast,*slow;
fast=slow=head;
//结点的个数有奇偶两种情况
while(fast && fast->next)
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
bool chkPalindrome(ListNode* A)
{
struct ListNode*mid=middleNode(A);
struct ListNode*rhead= reverseList(mid);
while(A && rhead)
{
if(A->val != rhead->val)
return false;
A=A->next;
rhead=rhead->next;
}
return true;
}
};
相交链表
思路
找到相同结点+双指针
- 遍历找两链表的尾结点并计算两链表的长度
- 尾结点的地址 不同直接返回false, 两链表不相交
- 尾结点的地址相同, 计算两链表的差距步
- 定义并找出长短链表, 长链表先走差距步, 后长短链表一起走遍历找到相交的结点
参考代码
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int lenA=0 , lenB =0;
//1. 判断两个链表尾结点地址是否相同并计算两个链表的长度
while(curA)
{
lenA++;
curA=curA->next;
}
while(curB)
{
lenB++;
curB=curB->next;
}
//两个链表尾结点地址不同,就不相交
if(curA != curB)
return NULL;
//2. 计算差距步, 长链表先走差距步, 短链表后走 ----> 目的是让长短链表同频率走
int gap=abs(lenA-lenB);
//找出长链表和短链表
struct ListNode* Longlist=headA;
struct ListNode* Shortlist=headB;
if(lenA < lenB)
{
Longlist=headB;
Shortlist=headA;
}
//长链表先走差距步
while(gap--)
{
Longlist=Longlist->next;
}
//找到相交结点停止返回, 否则继续走
while(Longlist != Shortlist)
{
Longlist=Longlist->next;
Shortlist=Shortlist->next;
}
return Longlist; //返回两链表中任意一个即可
}
链表的中间结点
思路
双指针
定义一个快指针和一个慢指针, 每次循环快指针走2步, 慢指针走1步
快指针所走路程是慢指针所走路程的2倍, 循环停止时返回慢指针的位置就是链表的中间结点
注意循环停止的条件: 链表结点个数可能为奇数, 也可能为偶数,要分2种情况讨论
本质: 距离差 , 一个路程是另一个路程2倍, 一个先走完停下来时, 另一个刚好走到中间
扩展: 当链表结点为偶数个时, 想返回链表两个中间结点的第一个, 需要记录一个prev结点(如下图), 返回prev
参考代码
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode*fast,*slow;
fast=slow=head;
//结点的个数有奇偶两种情况
while(fast && fast->next)
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
链表的倒数第k个结点
思路
双指针
定义一个快指针和一个慢指针, 快指针先走k步, 走完后快慢指针一起走, 当快指针走到NULL时返回慢指针即可
本质: 距离差 , 慢指针与快指针之间距离为k, 当快指针走到NULL时,取慢指针就是倒数第k个
参考代码
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode*fast,*slow;
fast = slow = pListHead;
//fast先走k步
while(k--)
{
//链表可能没有k步那么长
if(fast==NULL)
return NULL;
fast=fast->next;
}
while(fast)
{
fast=fast->next;
slow=slow->next;
}
return slow;
}
环形链表 I
思路
双指针 + 链表有环的条件
定义一个快指针和一个慢指针, 快指针每次走2步, 慢指针每次走1步, 快慢指针能够相遇说明链表有环。
深入讨论(需要理解)
1. 为什么快指针每次走两步,慢指针走一步一定能相遇?
2. 快指针一次走3步,走4步,…n步行吗?
参考代码
bool hasCycle(struct ListNode *head) {
struct ListNode*fast,*slow;
fast = slow=head;
while(fast && fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast == slow)
return true;
}
return false;
}
环形链表 II
思路1
双指针 + 两指针在环中相遇结论
参考环形链表 I 思路, 定义一个快指针和一个慢指针, 找到两指针在环中相遇的点后, 一个指针从相遇点开始走, 一个指针从起始点开始走, 当两者相等时就找到了进环点
结论
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。
参考代码
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode*fast, *slow;
fast=slow=head;
while(fast && fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow == fast)
{
struct ListNode* meet= slow;
while(meet != head)
{
meet =meet->next;
head=head->next;
}
return meet;
}
}
return NULL;
}
思路2
带环问题 + 找两个链表公共结点
参照环形链表I的思路, 定义两个快慢指针找到环中快慢指针交点后, 由此将链表分成两部分, 参照找相交链表的思路, 一头是head, 另一头是快慢指针交点的下一个结点, 由这两个结点起始遍历找到两链表交点就是入环点。
参考代码
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int lenA=0 , lenB =0;
//1. 判断两个链表尾结点地址是否相同并计算两个链表的长度
while(curA)
{
lenA++;
curA=curA->next;
}
while(curB)
{
lenB++;
curB=curB->next;
}
//两个链表尾结点地址不同,就不相交
if(curA != curB)
return NULL;
//2. 计算差距步, 长链表先走差距步, 短链表后走 ----> 目的是让长短链表同频率走
int gap=abs(lenA-lenB);
//找出长链表和短链表
struct ListNode* Longlist=headA;
struct ListNode* Shortlist=headB;
if(lenA < lenB)
{
Longlist=headB;
Shortlist=headA;
}
//长链表先走差距步
while(gap--)
{
Longlist=Longlist->next;
}
//找到相交结点停止返回, 否则继续走
while(Longlist != Shortlist)
{
Longlist=Longlist->next;
Shortlist=Shortlist->next;
}
return Longlist; //返回两链表中任意一个即可
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode*fast, *slow;
fast=slow=head;
while(fast && fast->next)
{
slow=slow->next;
fast=fast->next->next;
//从相遇点分开成2个链表, 转化成找两个链表的相交结点
if(slow == fast)
{
struct ListNode* meet= slow;
struct ListNode* otherhead= meet->next;
meet->next=NULL;
return getIntersectionNode(head, otherhead);
}
}
return NULL;
}
带环问题总结
带环问题解决一上来可能并没有思路, 先要学习思路后自己写, 最重要的是记住带环问题的结论, 并且理解熟悉结论的证明过程
解决带环问题的核心: 快慢双指针 + 结论
快慢指针问题总结
从链表的中间结点到环形链表解题都用到了快慢双指针, 快慢指针问题核心是让速度不同的两指针在遍历的过程中形成距离差, 来解决问题。
复制带随机指针的链表
思路
一种简单的思路:
- 遍历将原链表中的结点拷贝并链接到原结点的后面
- 设置拷贝结点的random
- 将拷贝结点解下来,链接组成拷贝链表, 最好将原链表复原。
参考代码
struct Node* copyRandomList(struct Node* head) {
struct Node*cur=head;
//1. 拷贝结点链接在原结点后面
while(cur)
{
struct Node*next=cur->next;
struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
//插入链接
cur->next=copy;
copy->next=next;
cur=next;
}
//2. 设置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. 解下来链接在一起
cur=head;
struct Node*copyHead=NULL, *copyTail=NULL;
while(cur)
{
struct Node*copy=cur->next;
struct Node*next=copy->next;
cur->next=next; //复原原链表
//尾插
if(copyTail==NULL)
{
copyHead=copyTail=copy;
}
else
{
copyTail->next=copy;
copyTail=copy;
}
cur=next;
}
return copyHead;
}