两两交换链表中的节点
-
题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
-
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
-
-
力扣24题
双指针法
-
建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。
-
接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* swapPairs(ListNode* head) { ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作 ListNode* cur = dummyHead; // 令cur指向虚拟头结点,注意必须让cur指向要进行操作的前一个结点才能操作后要交换的两个结点 while (cur->next != nullptr &&cur->next->next != nullptr) { // 链表长度为奇数的话结束条件是cur->next->next!=nullptr // 链表长度为偶数的话结束条件是cur->next!=nullptr ListNode* temp = cur->next; // 记录临时节点,记录交换元素的第一个结点 ListNode* temp1 =cur->next->next->next; // 记录交换元素的第二结点所要指向的结点 cur->next = cur->next->next; // 步骤一 cur->next->next = temp; // 步骤二 temp->next = temp1; // 步骤三 cur = cur->next->next; // cur移动两位,准备下一轮交换 } ListNode* result = dummyHead->next; // 记录首节点 delete dummyHead; // 释放头结点内存空间 return result; } };
-
时间复杂度:O(n)
-
空间复杂度:O(1)
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ //递归版本 struct ListNode* swapPairs(struct ListNode* head){ //递归结束条件:头节点不存在或头节点的下一个节点不存在。此时不需要交换,直接返回head if(!head || !head->next) return head; //创建一个节点指针类型保存头结点下一个节点 struct ListNode *newHead = head->next; //更改头结点+2位节点后的值,并将头结点的next指针指向这个更改过的list head->next = swapPairs(newHead->next); //将新的头结点的next指针指向老的头节点 newHead->next = head; return newHead; }
删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2:
输入:head = [1], n = 1 输出:[] 示例 3:
输入:head = [1,2], n = 1 输出:[1]
思路一
-
计算出链表的长度,然后用长度减去n得到删除元素的前一个元素进行删除,不建议使用,链表过长的话,此方法执行起来很麻烦
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { //先求出表长,在进行删除,同样找到前一个元素进行删除 ListNode* dummyHead = new ListNode(0); dummyHead->next = head; ListNode *cur=dummyHead; int count = 0,sum=0; while(cur->next!=nullptr){ cur=cur->next; sum++; } cur=dummyHead; while(cur->next!=nullptr&&count<sum-n){ cur=cur->next; count++; } ListNode *temp=cur->next; cur->next=temp->next; delete temp; ListNode *result=dummyHead->next; delete dummyHead; return result; } };
快慢指针法
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
思路是这样的,但要注意一些细节。
分为如下几步:
-
首先这里我推荐大家使用虚拟头结点,这样方便处理删除实际头结点的逻辑
-
定义fast指针和slow指针,初始值为虚拟头结点
-
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作)
-
fast和slow同时移动,直到fast指向末尾
-
删除slow指向的下一个节点
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode* dummyHead = new ListNode(0); dummyHead->next = head; ListNode *fast= dummyHead; ListNode *slow = dummyHead; n++;//目的是让快指针多走一步,以至于后续慢指针与快指针同时移动时慢指针能指向要删除元素的前一个位置 while(n--&&fast!=nullptr){ fast=fast->next; } //注意,也不能在while后面写fast=fast->next;来多移动一位,原因是可能对空指针进行操作 while(fast!=nullptr){ //fast和slow同时移动 fast=fast->next; slow=slow->next; } ListNode *temp=slow->next; //记录被删元素,以便于释放空间 slow->next=temp->next; delete temp; ListNode *result=dummyHead->next; delete dummyHead; //释放头结点空间 return result; } };
-
时间复杂度: O(n)
-
空间复杂度: O(1)
链表相交
-
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
示例 2:
示例 3:
-
面试题02.07
思路
-
简单来说,就是求两个链表交点节点的指针。 但是要注意,交点不是数值相等,而是指针相等。
-
而且原本给的链表中就有相交的,而且相交部分统一都在后面,我只需要找到交点并返回就行
-
为了方便举例,假设节点元素数值相等,则节点指针相等。
-
看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:
-
我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:
-
-
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { ListNode *curA = headA; ListNode *curB = headB; int lenA=0,lenB=0; //先求出链表的长度,以便于找到同一起点上的共同部分(此部分的从A.B表后面开始相等,直到结束) while(curA!=NULL){ curA=curA->next; lenA++; } while(curB!=NULL){ curB=curB->next; lenB++; } curA=headA; //在还原curA和curB curB=headB; // 让curA为最长链表的头,lenA为其长度 if(lenA<lenB){ swap(lenA,lenB); swap(curA,curB); } // 求长度差 int gap= lenA-lenB; // 让curA和curB在同一起点上(末尾位置对齐) while(gap--){ curA=curA->next; } // 遍历curA 和 curB,遇到相同则直接返回,,两表原先就有共有部分,所以只需要让节点相同 while(curA!=NULL){ if(curA==curB){ return curA; } curA=curA->next; curB=curB->next; } return NULL; } };
-
时间复杂度:O(n + m)
-
空间复杂度:O(1)
环形链表
-
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
-
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
-
力扣142
快慢指针法
-
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针
-
fast和slow各自再走一步, fast和slow就相遇了
-
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合
如果有环,如何找到这个环的入口
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
-
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
-
那么相遇时: slow指针走过的节点数为:
x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y):
x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
-
所以要求x ,将x单独放在左面:
x = n (y + z) - y
,再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为
x = z
,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
-
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点
-
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *detectCycle(ListNode *head) { ListNode *fast = head; //快指针每次比慢指针多走两个 ListNode *slow = head; while(fast!=NULL&&fast->next!=NULL){ //快指针快,所以只需要判断快指针 fast=fast->next->next; slow=slow->next; // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 if(fast==slow){ //相遇的话说明有环 ListNode *index1=fast; //从相遇位置处到环口处的长度等于从head到环口的长度 ListNode *index2=head; while(index1!=index2){ //进行循环判断,找到环入口,内循环结束 index1=index1->next; index2=index2->next; } return index1; //index1==index2 } } return NULL; //没找到返回null } };
-
时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
-
空间复杂度: O(1)
补充
在推理过程中,大家可能有一个疑问就是:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
即文章链表:环找到了,那入口呢?中如下的地方:
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了。
这说明什么呢?
在slow开始走的那一环已经和fast相遇了。
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去。
好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对链表:环找到了,那入口呢?的补充。