目录
LeetCode第203题:移除链表元素
LeetCode第206题:反转链表
LeetCode第876题:链表的中点节点
牛客网:链表中倒数最后k个节点
LeetCode第21题:合并两个有序链表
牛客网:分割链表
牛客网:链表的回文结构
LeetCode第160题:相交链表
LeetCode第141题:环形链表
LeetCode第142题:环形链表II
LeetCode第138题:复制带随机指针的链表
本期博客我将对一些经典的链表题进行总结详解
题目链接:
203. 移除链表元素
对于此题我的想法是直接将链表中等于val值的节点从链表中移除就行:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* temp1 = head;
struct ListNode* temp2 = head;
int i = 0;
if (temp1 == NULL)
{
return head;
}
while (temp1)
{
if (i == 0)
{
if (head->val == val)
{
head = head->next;
temp1 = head;
temp2 = head;
}
else
i++;
}
else
{
if (temp1->val == val)
temp2->next = temp1->next;
else
temp2 = temp1;
temp1 = temp1->next;
}
}
return head;
}
成功通过:
题目链接:
206. 反转链表
本题我先想到了使用递归的方法,先用函数层层递归找到链表最后一个节点,再将每一层所传的节点用尾插法存入一个全局结构体指针变量temp中,最后返回temp这个指针就可以了:
void SListPushBack(struct ListNode** pplist, struct ListNode* node)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
struct ListNode* newplist = node;//创建一个新节点为插入链表做准备
newplist->next=NULL;
if (*pplist == NULL)//如果传入的是个空链表就直接插入一个新节点
{
*pplist = newplist;
return;
}
//传入的不是空链表就找到链表尾部
struct ListNode* tail = *pplist;
while (tail->next)
{
tail = tail->next;
}
tail->next = newplist;//在链表尾部插入数据
}
struct ListNode* temp = NULL;
void returnNode(struct ListNode* node)
{
if (node == NULL)
return;
returnNode(node->next);
SListPushBack(&temp, node);
}
struct ListNode* reverseList(struct ListNode* head) {
temp = NULL;
returnNode(head);
return temp;
}
使用递归的方法时间复杂度为O(n),空间复杂度为O(n)运行起来不是很理想:
接下来使用迭代法来解决它:
在遍历链表时,将当前节点的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL || head->next == NULL)
return head;
struct ListNode* temp = NULL, * new = NULL, * List = head->next;
int i = 0;
while (List->next)
{
if (i == 0)
{
new = head;
new->next = NULL;
i++;
}
else {
temp = List;
List = List->next;//在temp被修改之前要提前找到链表的下一个节点,否则在下一步temp->被修改时List也会被修改从而丢失链表的下一个节点
temp->next = new;
new = temp;
}
}
return new;
}
此方法只需要遍历一次链表时间复杂度为O(n),空间复杂度为O(1)的效果:
题目链接:
876. 链表的中间结点
此题我们使用快慢指针的方法可以快速的解决掉该题:
设置两个遍历链表的指针fast和slow指针,fast指针每次遍历链表的两个节点,slow每次遍历链表的一个节点。当fast指针遍历完链表之后slow指针才遍历完整个链表的一半。此时返回slow指针即为链表的中间节点。
struct ListNode* middleNode(struct ListNode* head){
struct ListNode*fast=head,*slow=head;
while(fast!=NULL&&fast->next!=NULL)
{
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
此方法只要遍历一次链表时间复杂度为O(n),空间复杂度为O(1):
题目地址:
链表中倒数最后k个结点
在此题中可以直接设置两个指针fast,slow。让fast指针先向前移动k-1个节点,再一起移动fast和slow指针。当fast遍历完链表之后slow指针指向的就是链表中倒数第k个节点(此题要注意传让的k值是否合理,不然会造成越界访问):
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k) {
if(pListHead==NULL)
return pListHead;
struct ListNode* fast=pListHead,*slow=pListHead;
for(k-1;k>0;k--)
{
if(fast==NULL)
return NULL;
fast=fast->next;
}
while(fast)
{
fast=fast->next;
slow=slow->next;
}
return slow;
}
此方法只要遍历一次链表时间复杂度为O(n),空间复杂度为O(1):
题目地址:
21. 合并两个有序链表
在此题中,我使用了两个指针temp1和temp2分别遍历List1和List2链表,另给一个指针new接收temp1和temp2指针比较后的节点,谁的val值小就将该节点赋给new形成一个新链表:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
if(list1==NULL&&list2==NULL)
return NULL;
else if(list1==NULL)
return list2;
else if(list2==NULL)
return list1;
struct ListNode* new=NULL,*temp1=list1,*temp2=list2;
if(temp1->val>temp2->val)
{
new=temp2;
temp2=temp2->next;
}
else
{
new=temp1;
temp1=temp1->next;
}
struct ListNode* head=new;
while(temp1&&temp2)
{
if(temp1->val>temp2->val)
{
new->next=temp2;
temp2=temp2->next;
new=new->next;
}
else
{
new->next=temp1;
temp1=temp1->next;
new=new->next;
}
}
if(temp1==NULL)
new->next=temp2;
else
new->next=temp1;
return head;
}
此方法只需要遍历每个链表一次,时间复杂度为O(n),空间复杂度为O(1):
题目地址:
链表分割_牛客题霸_牛客网
这道题目在原链表上进行操作将会异常复杂繁琐,所以我们的操作是创建两个带哨兵位的头节点的链表:greater 和 less。(选择创建带哨兵位头节点的链表是为了方便改变头节点下一个节点,否则修改新链表头节点时较麻烦。)greater后面跟大于x的结点,less后面跟小于x的结点。我们只需让less的尾结点接上greater的头结点即可。这样既实现了分割又保证原数据的顺序不被改变:
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
ListNode* greaterhead, *greatertail, *lesshead, *lesstail;
greatertail = greaterhead = (ListNode*)malloc(sizeof(ListNode));
lesstail = lesshead = (ListNode*)malloc(sizeof(ListNode));
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;
}
greatertail->next = NULL;
lesstail->next = greaterhead->next;
return lesshead->next;
}
};
该方法只需要遍历一次链表,时间复杂度为O(n),空间复杂度为O(1):
题目地址:
链表的回文结构_牛客题霸_牛客网
对于该题我先用快慢指针找到链表的中间节点将其赋予指针slow(在这里要判断链表节点的寄偶数),再将slow之前的节点用迭代法反转形成一个新的链表reverse,接着将reverse链表与slow链表相对应的节点数据值相比较(这里如果整个链表的节点数为奇数,slow要指向下一个节点),如果一一对应就是回文结构了:
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
if (A == NULL || A->next == NULL)
return false;
ListNode* slow = A, *fast = A, *reverse = NULL, *temp = A, *New = NULL;
int i = 0;
while (fast)
{
fast = fast->next;
if (fast == NULL)
{
i++;
break;
}
fast = fast->next;
slow = slow->next;
}
while (temp != slow)
{
New = temp;
temp = temp->next;
New->next = reverse;
reverse = New;
}
if (i != 0)
{
slow = slow->next;
}
while (slow) {
if (slow->val != reverse->val)
return false;
slow = slow->next;
reverse = reverse->next;
}
return true;
}
};
该方法需要遍历两次链表,时间复杂度为O(n),空间复杂度为O(1):
题目地址:
160. 相交链表
对于该题我们看图可知,如果两链表相交,它们到最后相同的节点数一定是一样的,那我们不妨计算出每个链表的节点总数,再相减其节点总个数算出差值,接着让节点数多的一条链表先遍历差值个数的节点数,最后两个链表再一起同时遍历,终有一个节点是它们第一次相交的节点:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if(headA==NULL||headB==NULL)
return NULL;
struct ListNode*h1=headA,*h2=headB;
int A=0,B=0;
while(h1)
{
A++;
h1=h1->next;
}
while(h2)
{
B++;
h2=h2->next;
}
int c=A>B?A-B:B-A;
if(A>B)
{
while(c--)
{
headA=headA->next;
}
}
else
{
while(c--)
{
headB=headB->next;
}
}
while(headA)
{
if(headA==headB)
return headA;
headA=headA->next;
headB=headB->next;
}
return NULL;
}
题目地址:
141. 环形链表
在判断链表结构是否为环形链表时,我们可以用快慢指针的方法。设定fast和slow指针,fast每次走两个节点,slow每次走一个节点,当链表为环形的时,slow和fast都进入了环形结构就变成了追击问题。它们之间的速度差始终为1,只要时间足够它们总能相遇(其他的速度差不能保证它们之间会相遇,具体要看环形结构,所以速度差为1是最保险的方法):
bool hasCycle(struct ListNode *head) {
struct ListNode*fast=head,*slow=head;
while(fast&&fast->next)
{
fast=fast->next;
if(fast==slow)
return true;
fast=fast->next;
if(fast==slow)
return true;
slow=slow->next;
}
return false;
}
题目地址:
142. 环形链表 II
该题在上一题的基础上增加了一个条件:返回链表开始入环的第一个节点。
对此我们可以故技重施,照上一题快慢指针的方法先判断链表是不是环状结构,如果是就返回fast和slow相遇的节点,如果不是则返回空。是环状结构呢,我们将fast和slow相遇的节点“剪断”,即将相遇的节点的next置空(但是这里要提前保存next指针指向的节点,方便我们找链表开始入环的第一个节点)。这样该链表就被“剪”成了一个相交链表,链表的尾节点就是fast和slow相遇的节点,而相交的起始节点就是链表开始入环的第一个节点:
现在我们就可以看成两个单链表的相交问题了,还是使用LeetCode第160题的方法找到它们相交的节点:
struct ListNode* returnnode(struct ListNode *head)
{
struct ListNode*fast=head,*slow=head;
while(fast&&fast->next)
{
fast=fast->next;
if(fast==slow)
return fast;
fast=fast->next;
if(fast==slow)
return fast;
slow=slow->next;
}
return NULL;
}
struct ListNode* findnode(struct ListNode *head,struct ListNode *nextnode)
{
struct ListNode*h1=head,*h2=nextnode;
int A=0,B=0;
while(h1)
{
A++;
h1=h1->next;
}
while(h2)
{
B++;
h2=h2->next;
}
int c=A>B?A-B:B-A;
if(A>B)
{
while(c--)
{
head=head->next;
}
}
else
{
while(c--)
{
nextnode=nextnode->next;
}
}
while(head)
{
if(head==nextnode)
return head;
head=head->next;
nextnode=nextnode->next;
}
return NULL;
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* cutnode=returnnode(head);
if(cutnode==NULL)
return NULL;
struct ListNode* nextnode=cutnode->next;
cutnode->next=NULL;
return findnode(head,nextnode);
}
该方法的时间复杂度为O(n),空间复杂度为O(1):
题目地址:
138. 复制带随机指针的链表
咋一眼看到这个题目感觉好复杂,直接暴力求解,先将整个链表拷贝一份,再慢慢一个一个来还原节点中的random指针:
struct Node* BuySListNode(int x) {
struct Node* p = (struct Node*)malloc(sizeof(struct Node));
p->random = NULL;
p->val = x;
p->next = NULL;
return p;
}
void CapyNode(struct Node** pplist, int x) {
struct Node* newplist = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newplist;
return;
}
struct Node* tail = *pplist;
while (tail->next) {
tail = tail->next;
}
tail->next = newplist;
}
void CapyRandom(struct Node** pplist, struct Node* Head, struct Node* node, struct Node* head)
{
if (node->random == NULL)
{
(*pplist)->random = NULL;
return;
}
while (head)
{
if (node->random == head)
{
(*pplist)->random = Head;
return;
}
head = head->next;
Head = Head->next;
}
}
struct Node* copyRandomList(struct Node* head) {
struct Node* Copy = NULL, * temp = head;
while (temp) {
CapyNode(&Copy, temp->val);
temp = temp->next;
}
temp = head;
struct Node* Head = Copy;
while (Copy)
{
CapyRandom(&Copy, Head, temp, head);
Copy = Copy->next;
temp = temp->next;
}
return Head;
}
这种方法时间复杂度为O(n^2),不太理想:
下面我们用另一种方法来将时间复杂度改善到O(n):
比如现在有一原链表:
每次遍历一个节点将复制开辟的节点链接在原节点的next指针上,再将复制的节点的next指针指向原链表节点的next节点(红色的为新开辟的复制节点):
接着(只要random不是空指针)在我们再一次遍历原链表时直接找到原节点的random指针,而在random指针指向的节点的next指针就是我们复制节点的random指针所要指向的节点了。如此一来我们就不用一个一个挨个的去找random指针的指向了:
代码如下:
struct Node* BuySListNode(int x) {
struct Node* p = (struct Node*)malloc(sizeof(struct Node));
p->random = NULL;
p->val = x;
p->next = NULL;
return p;
}
void CapyNode(struct Node** pplist) {
struct Node* newplist = BuySListNode((*pplist)->val);
newplist->next=(*pplist)->next;
(*pplist)->next=newplist;
}
void CapyRandom(struct Node*head)
{
struct Node*list=head;
while(list)
{
if(list->random)
list->next->random=list->random->next;
list=list->next->next;
}
}
struct Node* copyRandomList(struct Node* head) {
if(head==NULL)
return NULL;
struct Node*temp=head;
while (temp) {
CapyNode(&temp);
temp = temp->next->next;
}
CapyRandom(head);
struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
struct Node*new=copy;
while(head)
{
copy->next=head->next;
head=head->next->next;
copy=copy->next;
}
return new->next;
}
该方法需要遍历三次链表,时间复杂度为O(n):
本次的链表题目合集就到此为止了,希望可以帮得上各位~