1.移除链表元素
思路1:
直接在原链表中改动,创建两个标记指针一个为当前指针(cur)另一个为前指针(pre),将head的值赋值给cur。
遍历链表。
当链表中的值为要求移除的值。
先判断pre是否为空,因为头指针要移除的方式和别的不同,将head的next赋值给cur,将头指针free掉,再将cur的值赋值给head。
如果pre不为空,且cur的值又等于要删除的值时,将前指针pre的next指向cur的下一个指针,将当前指针释放,再将前指针的next赋值给cur。
如果cur的值不等于要求删除的值则将当前指针cur赋值给前指针pre,再将cur的next赋值给cur。
代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//直接在原链表中改动
struct ListNode* removeElements(struct ListNode* head, int val){
if(head==NULL)
{
return NULL;
}
struct ListNode* pre=NULL;
struct ListNode* cur=head;
while(cur)
{
if(cur->val==val)
{
if(pre)
{
pre->next=cur->next;
free(cur);
cur=pre->next;
}
else
{
cur=head->next;
free(head);
head=cur;
}
}
else
{
pre=cur;
cur=cur->next;
}
}
return head;
}
思路2:
创建一个哨兵位相较于思路1少了一步判断前指针是否为空。创建一个哨兵位,并将其赋值给两个新建标记指针newhead和tail,再创建一个当前指针标记cur,用于遍历链表。
当cur的值不等于要求删除的值时,将尾指针指tail指向当前指针cur即tail->next=cur,再将尾指针tial移至tail的下一个节点即tail=tail->next。同时将cur移至链表下一个节点。
如果cur的值等于要删除的值时,则创建一个要删除的指针标记del,先将cur移至下一个节点再将del释放,顺序不能乱,如果先释放del就找不到cur的下一个节点了。
最后当遍历完成后,将tail的next置空代表新链表结束,并将newhead的next赋值给head,将newhead的空间释放并将head返回即可。
代码如下:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* newhead=(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail=newhead;
struct ListNode* cur=head;
while(cur)
{
if(cur->val!=val)
{
tail->next=cur;
tail=tail->next;
cur=cur->next;
}
else
{
struct ListNode* del=cur;
cur=cur->next;
free(del);
}
}
tail->next=NULL;
head=newhead->next;
free(newhead);
return head;
}
2.反转链表
思路:将原链表的所有节点的指向逆置,创建三个标记指针,前指针pre,当前指针cur,下一个指针next,遍历原链表,将前一个指针pre赋值给当前指针cur的next,如此循环直至遍历结束。需要注意的是,当前指针cur会移动到空指针区域,所以最后返回的指针是前指针pre。
代码如下:
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL)
{
return NULL;
}
struct ListNode* pre=NULL;
struct ListNode* cur=head;
struct ListNode* next=head->next;
while(cur)
{
cur->next=pre;
pre=cur;
cur=next;
if(next)
{
next=next->next;
}
}
return pre;
}
3.链表中间节点
思路:创建两个标记指针,一个快指针一个慢指针,慢指针一次走一步,快指针一次走两步。当fast或者fast的next其中一个为空时,slow刚好走到中间节点处。
循环条件为什么是fast&&fast->next不为空呢?
因为当节点个数为奇数时,如果条件只是fast不为空的话,fast走到最后一个节点满足条件,再走两步会出现NULL->next的问题。
代码如下:
struct ListNode* middleNode(struct ListNode* head){
struct ListNode* slow=head;
struct ListNode* fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
4.链表中倒数第k个结点
思路:创建两个标记指针,一个slow指针,一个fast指针。
要求你输出倒数第k个节点时,fast先走k步,然后再进入循环,slow和fast都是每次走一步,当fast为空时则结束循环,此时的slow刚好是倒数第k个节点。
需要注意的是,可能k的值会超出链表长度或者k为0的情况。
先分析k超出链表长度的情况,在实现fast先走k步时就可以加入判断,如果fast为空时,还进入了循环,那么可以判断k的值超出链表长度,返回NULL即可。
第二种特殊情况,k为0时,则fast先走0步,循环结束后slow和fast都为NULL,则其返回的slow为NULL。
代码如下:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
struct ListNode* slow=pListHead;
struct ListNode* fast=pListHead;
for(int i=0;i<k;i++)
{
if(fast==NULL)
{
return NULL;
}
fast=fast->next;
}
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
5.合并两个有序链表
思路:
首先可能存在三种特殊情况可以直接返回,根据代码可以看出。
非特殊情况时,创建哨兵位newhead和标记指针tail,两个初始化的值均为哨兵位的地址,在创建一个标记指针head置空。这里的主要思想是判断两个链表当前节点的大小进行尾插。
循环条件为当list1或者list2其中一个链表结束即其中一个为空时结束循环,需要注意的是循环的条件是当其中一个链表遍历完时就结束了,不能保证另一个链表也遍历完了,所以需要在最后判断一下,如果不为空则直接将未结束的链表接在tail->next处。
代码如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
if(list1==NULL&&list2!=NULL)
{
return list2;
}
if(list1!=NULL&&list2==NULL)
{
return list1;
}
if(list1==NULL&&list2==NULL)
{
return NULL;
}
struct ListNode* newhead=(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail=newhead;
struct ListNode* head=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!=NULL)
{
tail->next=list1;
}
if(list2!=NULL)
{
tail->next=list2;
}
head=newhead->next;
free(newhead);
return head;
}
6.链表分割
思路:创建两个哨兵位,一个哨兵位是小于x的节点尾插至lesslist链表中,另一个哨兵位是大于x的节点尾插至greaterlist链表中,再将lesstial的next与greaterhead的next连接。需要注意的是要将greatertail的next置空,如果不置空会出现环状链表。
代码如下:
ListNode* partition(ListNode* pHead, int x) {
// write code here
if(pHead==NULL)
{
return NULL;
}
struct ListNode* lesslist=(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* greaterlist=(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* lesshead,*lesstail,*greaterhead,*greatertail;
lesshead=lesstail=lesslist;
greaterhead=greatertail=greaterlist;
struct ListNode* cur=pHead;
while(cur)
{
if(cur->val<x)
{
lesstail->next=cur;
lesstail=lesstail->next;
}
else
{
greatertail->next=cur;
greatertail=greatertail->next;
}
cur=cur->next;
}
lesstail->next=greaterhead->next;
greatertail->next=NULL;
pHead=lesshead->next;
free(lesshead);
free(greaterhead);
return pHead;
}
7.链表的回文结构
思路:
将中间节点之前的节点指向都逆置,根据链表节点的个数是奇数还是偶数,向两个方向遍历并判断值是否相等和结束标志是否一致。
设置标记指针slow,fast,前指针pre,当前指针cur,下一个指针next。
快指针一次走两步,慢指针一次走一步,先将中间节点之前的节点指向逆置。
判断fast是否为空指针,如果是空指针那么链表的节点个数为偶数个,先将当前指针cur的next改回指向next,然后判断pre和cur指向的值是否相等,如果不相等则返回false。因为循环条件是其中一个标记为空就结束,并不知道另一个节点是否为空,所以在循环结束后要判断一下两个指针是否都为空,如果有一个不为空就返回false。
如果fast部位空指针,那么链表节点个数为奇数个,那么判断的标记指针与节点个数为偶数个的标记指针不同,判断的是pre和next的值是否相等,注意的点同上。
当都满足条件时则返回true。
代码如下:
bool chkPalindrome(ListNode* A) {
if(A==NULL)
{
return false;
}
// write code here
struct ListNode* slow=A;
struct ListNode* fast=A;
struct ListNode* pre=NULL;
struct ListNode* cur=A;
struct ListNode* next=A->next;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
cur->next=pre;
pre=cur;
cur=next;
next=next->next;
}
if(fast==NULL)
{
cur->next=next;
while(pre&&cur)
{
if(pre->val!=cur->val)
{
return false;
}
pre=pre->next;
cur=cur->next;
}
if(pre!=NULL||cur!=NULL)
{
return false;
}
}
else
{
while(pre&&next)
{
if(pre->val!=next->val)
{
return false;
}
next=next->next;
pre=pre->next;
}
if(pre!=NULL||next!=NULL)
{
return false;
}
}
return true;
}
8.相交链表
思路:计算两个链表的长度,然后计算他们的差,让长的链表先走他们的差的步数,再一起走,当两个链表的当前节点指针相等时,返回任意其中一个节点的地址。
代码如下:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* cur1=headA;
struct ListNode* cur2=headB;
int lena=1;
int lenb=1;
while(cur1)
{
lena++;
cur1=cur1->next;
}
while(cur2)
{
lenb++;
cur2=cur2->next;
}
int len=lena>lenb?lena-lenb:lenb-lena;
if(lena>lenb)
{
for(int i=0;i<len;i++)
{
headA=headA->next;
}
}
else
{
for(int i=0;i<len;i++)
{
headB=headB->next;
}
}
while(headA!=headB)
{
headA=headA->next;
headB=headB->next;
}
return headA;
}
9.环形链表
思路:
创建两个标记指针,slow和fast,slow一次走一步,fast一次走两步。
循环当fast或者fast->next为空时,则说明该链表不是环形链表。
如果不为空时,则继续循环,当fast和slow再次相等的话那么该链表为环形链表。
代码如下:
bool hasCycle(struct ListNode *head) {
struct ListNode* slow=head;
struct ListNode* fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
return true;
}
}
return false;
}
10.返回环形链表入环的第一个节点
思路1:
创建两个标记指针,分别为快慢指针,慢指针一次走一步,快指针一次走两步,先找到相遇点。
然后一个从头节点开始走,一个从相遇点开始走,当head等于meet时则该点为环的入口节点。
为什么?
根据图进行分析
假设链表头到环的入口点的距离为:L
环入口点到相遇点的距离为:X
环的长度为:C
slow走的路程为:L+X
fast走的路程为:L+n*C+X
为什么时n*C?因为可能在slow进环前,fast已经绕了很多了圈。
2*(L+X)=L+n*C+X
L=n*C-X
由此得出一个从相遇点开始走,另一个从头节点开始走,相遇时就是环的入口点。
代码如下:
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* slow=head;
struct ListNode* fast=head;
struct ListNode* meet=NULL;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
meet=fast;
while(meet!=head)
{
head=head->next;
meet=meet->next;
}
return meet;
}
}
return NULL;
}
11.总结以上题型思路
在移除链表元素中用到尾插的思想;
在反转链表时可以通过逆置各个结点指向或者时利用头插法实现;
求链表中间节点可以通过快慢指针的方式去解决;
倒数第k个节点则是让快指针先走k步然后再一起走的方法去求得结果;
合并两个有序链表也是通过尾插思路解决;
链表分割也是采用了尾插思想解决问题;
链表得回文结构则是通过快慢指针先逆置中间节点之前得结点指向,然后从中间开始往两个方向遍历判断是否是回文结构;
相交链表先求出两个链表的长度,让长的链表先走两链表长度之差的步数,再一起走,当走到同一个节点时则返回该节点;
判断是否为环形链表利用快慢指针解决;
求出环形链表的入口点有两种思路:第一种思路是先判断是否为环形链表,如果是就找到相遇点,分别设置两个标记指针一个从相遇点开始走,另一个从头开始走,当他们走到同一个节点时则返回该节点;
第二种思路是将相遇点的下一个节点作为一个新的头节点,相遇点作为一个新的头节点,变成相交链表问题解决。
综上所述:链表基础增加节点的方法头插法,尾插法可以先尝试思考能否解决问题。除此之外创建哨兵位可以不用为处理头节点问题做特殊处理。快慢指针的思路也是一种常见的解题方式。