前言
在学习了单链表这种数据结构后,便可以尝试去练习一些题目来巩固对这块知识点。练习这些题目还可以让我们提升代码能力和对指针有新的理解。
1. 移除链表元素
方法一
思路
通过两个指针变量来遍历整个链表。cur指针一开始指向链表的头结点,prev指针一开始为空,若cur指针指向的链表的值不等于val,那么将prev指针指向cur,cur指针指向下一个结点。倘若cur指针指向的结点的值等于val,那么需要判断prev的指向是否为空,若为空表示这是头删结点。需要将head指指针指向下一个结点。
代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
if(head == NULL)
return NULL;
struct ListNode* prev = NULL;
struct ListNode* cur = head;
while(cur)
{
if(cur->val == val)
{
if(prev == NULL)
{
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;
}
该算法的优点明显,时间复杂度为O(N),空间复杂度仅为O(1)。需要注意的对于cur指针释放后野指针的问题,当然只要能理解单链表的基本实现的话这题还是比较容易掌握的。因为本质还是在玩删除,删除就是改变next指针变量的指向并释放掉删除的结点的空间即可。
方法二
思路
方法二的思路就是开辟一个新的头指针用于指向新链表的头结点,同时创建一个变量来记录链表当前的尾部。然后遍历原链表,将不需要删除的结点依次尾插到新链表中即可。相比较第一种方法这种方法比较容易写和使用。
代码
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* newhead,*cur,*tail;
newhead = tail = NULL;
cur = head;
while(cur)
{
if(cur->val != val)
{
if(newhead == NULL)
{
newhead = tail = cur;
}
else
{
tail->next = cur;
tail = tail->next;
}
cur = cur->next;
}
else
{
struct ListNode*next = cur->next;//保存下一结点
free(cur);//删除结点
cur = next;
}
}
if(tail)
tail->next = NULL;
return newhead;
}
2. 链表的中间结点
方法一
思路
先遍历链表求出长度l,然后将长度l除以2后,让指针偏移l/2步后便是中间结点。
代码
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* cur, *mid;
cur = mid = head;
int l = 0;
while(cur)
{
++l;
cur = cur->next;
}
int k = l/2;
while(k--)
{
mid = mid->next;
}
return mid;
}
方法二
思路
快慢指针法,定义两个指针变量,快指针一次走两步,慢指针一次走一步。当快指针走到尾结点或者尾结点的下一个结点即(NULL)时,slow的位置就是中间结点(即偶数长度结点的第二个中间结点)。
代码
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;
}
快慢指针法的效率更高,但是需要注意的是要考虑链表长度奇偶性的问题。
3. 链表中倒数第k个结点
方法一
思路
本题也可以使用快慢针法来做,但是这里的快慢指针不是速度上的快慢,而是顺序上的快慢。找倒数第k个节点,可以先让fast走k步。然后fast和slow一起走,直到fast走到NULL。返回slow即可。需要考虑特殊情况,特殊情况一为空链表找结点情况,特殊情况二位k的值小于1或大于链表长度。
代码
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
if(pListHead == NULL)
return NULL;
struct ListNode *fast,*slow;
slow = fast = pListHead;
while(k--)
{
if(fast == NULL)
return NULL;
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
3. 反转链表
方法一
思路
三指针反转法。定义一个指针prev,并初始化成空,定义一个cur指针指向原链表的头结点,定义一个next指针指向cur指针的后一个结点。将cur的next指向prev指针,以达到反转的效果。然后再将三个指针迭代向后走,直到cur遍历完整个链表。此时,prev指针指向原链表最后一个结点,返回prev指针即可。
代码
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL)
return NULL;
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next = cur->next;
while(cur)
{
//反转
cur->next = prev;
//迭代
prev = cur;
cur = next;
if(next)
next = next->next;
}
return prev;
}
方法二
思路
取原链表结点头插到新链表。
代码
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode*newhead ,*cur;
cur = head;
newhead = NULL;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
这题需要注意的是需要提前保存下一个结点,先改变了cur指向就难以找到下一个结点了。所以,本题需要理解用next保存改变指向前的cur下一个结点的地址。
4. 合并两个有序链表
方法一
思路
遍历两个链表,将较小值尾插到新的链表中。需要注意的是一开始尾插到新链表需要赋值,两个链表为空分别的处理,其中一个链表遍历完了,应该将另一个链表整体尾插到新链表中。
代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
struct ListNode* cur1 = list1, *cur2 = list2;
struct ListNode* head, *tail;
head = tail = NULL;
while(cur1 && cur2)
{
if(cur1->val > cur2->val)
{
if(head == NULL)
{
head = tail = cur2;
}
else
{
tail->next = cur2;
tail = tail->next;
}
cur2 = cur2->next;
}
else
{
if(head == NULL)
{
head = tail = cur1;
}
else
{
tail->next = cur1;
tail = tail->next;
}
cur1 = cur1->next;
}
}
//将未走完的链表直接尾插到tail后
if(cur1)
tail->next = cur1;
if(cur2)
tail->next = cur2;
return head;
}
方法二
思路
可以在方法一的基础上添加哨兵位头结点来简化代码量。有了哨兵位头结点插入数据就不需要考虑空指针赋值问题,可以直接插入数据。但是,需要避免内存泄露方面的问题。
代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* cur1 = list1, *cur2 = list2;
struct ListNode* guard=NULL, *tail=NULL;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while(cur1 && cur2)
{
if(cur1->val > cur2->val)
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
else
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
}
//将未走完的链表直接尾插到tail后
if(cur1)
tail->next = cur1;
if(cur2)
tail->next = cur2;
struct ListNode* newhead = guard->next;
free(guard);//释放避免内存泄露
return newhead;
}
5. 链表分割
方法一
思路
定义两个哨兵位头结点来指向两个链表,将小于X的结点尾插到一个链表中,将大于等于X的结点尾插到另一个链表中。将大于等于X的链表尾插到小于X的链表中。
代码
ListNode* partition(ListNode* pHead, int x)
{
struct ListNode* gGuard,*gTail,*lTail,*lGuard,*cur;
lGuard = lTail = (struct ListNode*)malloc(sizeof(struct ListNode));
gGuard = gTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lTail->next = gTail->next = NULL;
cur = pHead;
while(cur)
{
if(cur->val >= x)
{
gTail->next = cur;
gTail = gTail->next;
}
else
{
lTail->next = cur;
lTail = lTail->next;
}
cur = cur->next;
}
lTail->next = gGuard->next;
gTail->next = NULL;
pHead = lGuard->next;
free(gGuard);
free(lGuard);
return pHead;
}
本题定义两个哨兵位头结点可以大大减少我们对于特殊情况下尾插数据的处理和对于空表情况的处理。当然使用了自己开辟的哨兵位头结点,就要手动去释放吊动态申请的空间。
6. 链表的回文结构
方法一
思路
按题意要求,空间复杂度为O(1),时间复杂度为O(N)。先找到链表的中间结点,然后逆置后半部分链表,判断前半段链表和后半段的结点,若完全相同就返回true,否则返回false。
代码
struct ListNode* findmid(struct ListNode* head)
{
struct ListNode* fast,*slow;
fast = slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
struct ListNode* reverse(struct ListNode* head)
{
struct ListNode*cur = head,*prev = NULL;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
bool chkPalindrome(ListNode* A)
{
struct ListNode* mid = findmid(A);
struct ListNode* rmid = reverse(mid);
while(A && rmid)
{
if(A->val != rmid->val)
return false;
A = A->next;
rmid = rmid->next;
}
return true;
}
7. 相交链表
方法一
思路
先遍历两个链表记录链表长度,判断其尾结点是否相等,若不相等直接返回NULL,若相等则求出两个链表长度的差距。然后让长点链表先走差距步,最后两个链表一起走,返回第一个相同的地址,若找不到相同的地址返回NULL。
代码
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode *list1 = headA, *list2= headB;
int len1 = 1;
int len2 = 1;
while(list1->next)
{
++len1;
list1=list1->next;
}
while(list2->next)
{
++len2;
list2=list2->next;
}
if(list1 != list2)
return NULL;
int gap = abs(len1-len2);
struct ListNode *longlist= headA,*shortlist= headB;
if(len1<len2)
{
longlist = headB;
shortlist = headA;
}
while(gap--)
{
longlist = longlist->next;
}
while(longlist && shortlist)
{
shortlist = shortlist->next;
longlist = longlist->next;
}
return longlist;
}
8. 环形链表
方法一
思路
通过快慢指针法,让快指针一次走两步,慢指针一次走一步。若快指针走到NULL,则表示链表无换。若快指针等于慢指针则表示链表有环。
代码
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;
}
快慢指针问题的证明
关于判断链表是否带环,上述的快慢指针法中,若链表带环为什么快指针一定能追上慢指针?我将举例证明。假设链表长度为C,当慢指针入环时,此时快指针距离慢指针的距离为N。那么每次快慢指针间的距离-1。故快慢指针间的距离一定会为0,即快指针一定追上慢指针。
倘若进环时快慢指针间的距离大于等于2呢?也就是例如fast一次走3步,slow一次走1步。那么此时我们不仅需要考虑slow进环时,与fast间的距离N是否能够被2整除,还需要考虑当N为奇数时,环的周长C是否为奇数的问题。所以当进环时快慢指针间的距离大于2时,这是有可能fast是永远追不上slow的。
9. 环形链表 II
方法一
思路
这里可以通过快慢指针遍历链表。若快指针走到NULL,则表示该链表无环。若快慢指针相遇,记录相遇点地址。然后让相遇点地址meet,和起始地址head一起向走。它们会在入口点相遇
证明
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* fast, *slow;
fast =slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
struct ListNode *meet = slow;
struct ListNode* start = head;
while(meet != start)
{
meet = meet->next;
start = start->next;
}
return meet;
}
}
return NULL;
}
方法二
思路
这里可以通过快慢指针遍历链表。若快指针走到NULL,则表示该链表无环。若快慢指针相遇,记录相遇结点,让相遇结点的next置空。然后转换成求两个链表交点问题。
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* listA = headA,*listB = headB;
int lenA = 1;
int lenB = 1;
while(listA->next)
{
++lenA;
listA=listA->next;
}
while(listB->next)
{
++lenB;
listB=listB->next;
}
if(listA != listB)
return NULL;
int gap = abs(lenA-lenB);
struct ListNode* longlist = headA,*shortlist = headB;
if(lenB>lenA)
{
shortlist = headA;
longlist = headB;
}
while(gap--)
{
longlist = longlist->next;
}
while(longlist && shortlist)
{
if(shortlist == longlist)
return longlist;
longlist = longlist->next;
shortlist = shortlist->next;
}
return NULL;
}
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *fast, *slow;
slow = fast = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
struct ListNode *meet = slow;
struct ListNode *L1 = meet->next;
struct ListNode *L2 = head;
meet->next = NULL;
return getIntersectionNode(L1,L2);
}
}
return NULL;
}
10. 复制带随机指针的链表
方法一
思路
本题复制复杂链表的难点在于处理随机指针random。本题思路有很多种这里我就介绍一下一种比较巧妙的思路。首先,现在原链表的每个节点后面复制一个节点,并将其和原链表链接起来。然后,通过链接起来的链表我们可以发现一个规律,那就是复制结点的random指针指向为原链表random指针的next指向。最后,将复制链表解下来,尾插到新链表中,恢复原链表的指向关系。
代码
struct Node* copyRandomList(struct Node* head)
{
struct Node* cur = head;
//将复制节点拷贝到原结点后
while(cur)
{
//开辟复制件点
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
struct Node* next = cur->next;
//拷贝
copy->val = cur->val;
copy->next = next;
cur->next = copy;
//迭代
cur = next;
}
//处理复制结点的random指针
cur = head;
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;//最关键的一步
}
cur = next;
}
//解下复制结点,尾插到新链表,恢复原链表指向
cur = head;
struct Node* copyHead = NULL, *copyTail = NULL;
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
//尾插
if(copyHead == NULL)
{
copyTail = copyHead = copy;
}
else
{
copyTail->next = copy;
copyTail = copyTail->next;
}
//恢复原链表
cur->next = next;
cur = next;
}
return copyHead;
}
总结
通过练习以上链表经典题目,可以使我们对于链表和指针的理解跟上一个层次。希望本篇文章能够使你有所收获,如有错误请指出,谢谢。