一些链表OJ思路
快慢指针法
1.链表的中间节点
通过上图分析,当链表为奇数个时slow指针指向的位置恰好是链表中间位置。为偶数个时当fast为空时,slow指针指向的位置恰好是第二个中间节点。注意while循环成立的条件是fast和fast->next指针不为空
2.链表中倒数第k个节点
这串代码k–是fast先走k步的情况,设链表数为n,那么倒数第k个节点其实是正数第n-k+1个节点,fast先走k步后用while循环判断当fast等于空时停止链表的移动并返回slow,fast移动到空时就相当于slow移动了n-k+1中的+1次。
同理fast先走k-1次,倒数第k个节点相当于正数第n-(k-1)个节点,用while循环当fast->next不等于空时链表依次向后移动,最后返回slow。
反转链表
法一:设置三个指针,先将n1置空,由于n2的指针指向发生改变,所以需要n3记录原链表n2的下一个节点,然后依次向后挪到一位,直到n2为空时,n1已经是反转后链表中最后一个节点,返回n1。法二:取节点头插到新链表
设置三个指针,因为头插后cur的指针指向改变,所以需用next提前记录原链表中cur的下一个节点,每次头插完后更新newhead和cur位置,当cur不为空时,用next记录cur下一个节点,循环结束后返回newhead,比法一好在不需要单独判断头指针为空的情况。
合并两个有序链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//判断头指针为空的情况
if(list1==NULL)
{
return list2;
}
if(list2==NULL)
{
return list1;
}
struct ListNode*n1=list1,*n2=list2,*head=NULL,*tail=NULL;
while(n1&&n2)
{
if(n1->val<n2->val)
{
if(head==NULL)
{
head=tail=n1;
}
else
{
tail->next=n1;
tail=tail->next;
}
n1=n1->next;//不管是否为头节点,n1都需要后移一位
}
else
{
if(head==NULL)
{
head=tail=n2;
}
else
{
tail->next=n2;
tail=tail->next;
}
n2=n2->next;
}
}
//判断有指针还未完全链接的情况
if(n1)
{
tail->next=n1;//不需要在移动tail,节点已链接完
}
if(n2)
{
tail->next=n2;
}
return head;
}
法一
思路:通过n1、n2两指针代替参数指针依次遍历比较,取小的尾插。需注意判断参数指针为空的情况,以及最后剩余的一个指针的链接问题。
法二(哨兵节点)
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
struct ListNode*n1=list1,*n2=list2,*guard=NULL,*tail=NULL;
guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
tail->next=NULL;
while(n1&&n2)
{
if(n1->val<n2->val)
{
tail->next=n1;
tail=tail->next;
n1=n1->next;
}
else
{
tail->next=n2;
tail=tail->next;
n2=n2->next;
}
}
//判断有指针还未完全链接的情况
if(n1)
{
tail->next=n1;//不需要在移动tail,节点已链接完
}
if(n2)
{
tail->next=n2;
}
struct ListNode*head=guard->next;
free(guard);
return head;
}
用malloc为guard和tail指针开辟结构体大小的空间,二者指向同一块空间,都将他们的next节点置空,防止参数指针为空时guard->next造成非法访问,guard->next作为新头节点返回,tail通过遍历完成合并操作。
优势在于尾插时不需要给头指针赋值,直接插入就行,并且不需要单独考虑头指针为空情况。
链表分割
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=NULL;//前缀g代表great,接受大的节点,l代表less,反之
ltail->next=NULL;
struct ListNode*cur=pHead;
while(cur)
{
if(cur->val<x)
{
ltail->next=cur;
ltail=ltail->next;
}
else
{
gtail->next=cur;
gtail=gtail->next;
}
cur=cur->next;
}
ltail->next=gguard->next;//链接两个重新排列后的链表
gtail->next=NULL;//尾插不会改变下一个节点,手动置空
pHead=lguard->next;//重置头指针
free(gguard);
free(lguard);
return pHead;
}
};
由于编译器没有c语言的模式,用的cpp框架,代码是用c语言实现。
思路:小于参数值的尾插到一个链表,大于等于的尾插到另一个链表,最后将两个链表链接起来。运用哨兵位节点可有效避免多种插入时空指针的讨论情况需要注意一点,尾插时是不改变下一个节点指向的,所以在链接两个链表之后,gtail作为新链表中其实是指向原链表中它的下一个节点,也就是说肯定指向新链表中某一个节点,所以必须将它手动置空,否则会造成循环问题。
链表的回文结构
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
//找到中间节点
struct ListNode*middleNode(struct ListNode*head)
{
struct ListNode*slow,*fast;
slow=fast=head;
while(fast&&fast->next)//判断节点数为奇数或偶数
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
//对中间节点往后开始逆置
struct ListNode*reverseList(struct ListNode*head)
{
struct ListNode*cur=head,*newhead=NULL;
while(cur)
{
struct ListNode*next=cur->next;
cur->next=newhead;//取原链表中节点头插
newhead=cur;
cur=next;
}
return newhead;
}
class PalindromeList {
public:
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=abs(lenA-lenB);//计算绝对值
struct ListNode*longlist=headA,*shortlist=headB;
if(lenA<lenB)//用ifelse语句一样,判断链表长度
{
longlist=headB;
shortlist=headA;
}
while(gap--)
{
longlist=longlist->next;
}
//两个指针同时走,第一个地址相同的节点就是交点
while(longlist!=shortlist)
{
longlist=longlist->next;
shortlist=shortlist->next;
}
return longlist;
}
思路:先分别求两个链表的长度,再让长的链表走差距步(相差的节点数),最后两个链表统一出发,第一个地址相同的节点就是交点。
需注意第一步遍历完两个链表后,要对两个链表的尾指针进行判断,相等代表有交点,不相等直接返回空结束。这种思路比较容易想到
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *pA = headA, *pB = headB;
while (pA != pB) {
pA = pA == NULL ? headB : pA->next;
pB = pB == NULL ? headA : pB->next;
}
return pA;
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/intersection-of-two-linked-lists/solutions/811625/xiang-jiao-lian-biao-by-leetcode-solutio-a8jn/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这是一种非常巧妙的思路,大家可以想明白吗,我就不过多赘述,可以上官网看看。相比上一种较难想到
环形链表问题
概念:尾结点指针不指向空,指向该链表中的某一个节点。
先来一道简单题引入
1.环形链表证明
bool hasCycle(struct ListNode *head) {
struct ListNode *fast,*slow;
fast=slow=head;
while(fast&&fast->next)//快指针走两步,所以断言两次
{
slow=slow->next;
fast=fast->next->next;
//判断环中追击相遇问题
if(slow==fast)
{
return true;
}
}
return false;
}
快慢指针思想,快指针先进环,待得慢指针进环后转化为追击问题。
- 思考:追击过程中能否相遇
主要看快指针每次比慢指针多走多少位移,也就是每一次移动的缩小距离,再就是看他们所在的环结构的长度是奇数还是偶数,若是每次缩小距离的奇偶性和环结构的长度的奇偶性相同(奇数可能还要考虑是否为某些数的倍数,不过顶多循环几次,是有限的),就能相遇。
2.返回入口点
做这个题首先需要了解并证明一个结论:一个指针从相遇点走,一个指针从起始点走,最终会在环形链表的入口点相遇。
先假设各个距离参数如图所示,环长为c,0<=X<=C-1(X等于0代表slow刚进环时fast恰好绕环一圈在入口点相遇,X等于C-1代表slow刚进环时fast恰好绕一圈并多走一步,是他们环中最大相差距离)。
相遇时slow走的距离为L+X,slow进环前fast已经走了n圈(n>=1),所以fast走得距离为L+n*C+X。由以下证明结合上图可得。
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(fast==slow)//变成追击相遇问题
{
//运用结论,一个指针从相遇点走,另一个起始点走
//最终会在入口点相遇
struct ListNode *meet=slow;//fast也行
struct ListNode *start=head;
while(start!=meet)
{
start=start->next;
meet=meet->next;
}
return meet;//start也行
}
}
return NULL;
}
该题还有另一种思路,思路难度小,代码量大
两个链表,一个从起始点到相遇点,一个从相遇点next到相遇点,可以将相交链表中的代码作为该题函数调用的定义。
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(fast==slow)//变成追击相遇问题
{
//转化成list1和list2求交点
struct ListNode *list1=head;
struct ListNode *meet=fast;//slow也行
struct ListNode *list2=meet->next;
meet->next=NULL;//将环分割成两个链表求交点
return getIntersectionNode(list1, list2);
}
}
return NULL;
}
结语
通过以上例题发现除环形链表外的大部分题目,其实都在换种方式用头插、尾插等方式解决问题,一般少用具体查找和删除(涉及改变前一个和后一个节点的指向,需要分类讨论稍复杂),要熟悉哨兵位头节点以及各种置空条件的判断,而且这些题目都是单向的,要对单链表增删查改的掌握程度有一定要求。
- 希望对大家有所帮助,感谢支持