持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情
描述
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n),空间复杂度 O(1)。 例如: 给出的链表为NULL1→2→3→4→5→NULL, m=2,n=4m=2,n=4, 返回1→4→3→2→5→NULL.
数据范围: 链表长度 0<size≤1000,0<m≤n≤size,链表中每个节点的值满足0∣val∣≤1000
要求:时间复杂度 O(n) ,空间复杂度 O(n)
进阶:时间复杂度 O(n),空间复杂度 O(1)
示例1
输入:
{1,2,3,4,5},2,4
返回值:
{1,4,3,2,5}
示例2
输入:
{5},1,1
返回值:
{5}
分析
考虑特殊情况
在进行问题分析过程前,我们先考虑一下特殊情况,那么在这道问题中,有哪些情况是特殊的呢?
不难发现,如下两种情况:
- 空链表,即头指针为NULL,这种情况我们直接返回头指针即可;
-
链表只有一个节点,即pHead->next为NULL,这种情况我们也直接返回头指针即可。
考虑一般情况
实际上我们可以将问题分解为几步:
- 找到第m个节点,记录前驱以及m节点;
- 将m到n之间视为子链表,进行链表反转,并记录最后的n节点及其后继节点;
- 最后将子链表与整体链表进行拼接:m的前驱的next等于n,m的next等于n的后继
注意,这里还有一些细节问题,由于我们不确定头结点是否需要反转,我们可以申请一个虚拟节点,它的next指向头结点,这样,我们在返回信息时,只需要返回该虚拟节点的next节点即可,省去了许多问题分类判断。
代码演示
版本一
这里的实现方式就是上面的分析方式,注意在初始化时,我们将pPre = pReturn;这点也是很重要的。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
class Solution {
public:
/**
*
* @param head ListNode类
* @param m int整型
* @param n int整型
* @return ListNode类
*/
ListNode* reverseBetween(ListNode* head, int m, int n) {
// write code here
if (nullptr == head || nullptr == head->next) {
return head;
}
int curNodeIndex = 1;
ListNode* pBeforeStart = nullptr;
ListNode* pStart = nullptr;
ListNode* pEnd = nullptr;
ListNode* pAfterEnd = nullptr;
ListNode* pPre = nullptr;
ListNode* pCur = head;
ListNode* pNext = nullptr;
ListNode* pReturn = static_cast<ListNode*>(malloc(sizeof(ListNode)));
pReturn->next = head;
pPre = pReturn;
while (curNodeIndex < m) {
pPre = pCur;
pCur = pCur->next;
curNodeIndex++;
}
pBeforeStart = pPre;
pStart = pCur;
while (curNodeIndex <= n) {
pNext = pCur->next;
pCur->next = pPre;
pPre = pCur;
pCur = pNext;
curNodeIndex++;
}
pEnd = pPre;
pAfterEnd = pCur;
// link the reverse
pBeforeStart->next = pEnd;
pStart->next = pAfterEnd;
return pReturn->next;
}
};
版本二
实际上我们只需要关注处理区间内的节点,在遍历处理区间内节点时,保持如下原则将next位置上面的节点不断移到最前面的位置即可
- 在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。
-
- pCur:指向待反转区域的当前处理节点;
- pNext:永远指向 pCur 的下一个节点,循环过程中,pCur变化以后pNext 会变化;
- pPre:永远指向待反转区域的第一个节点的前驱,在循环过程中不变。
首先,我们通过遍历的方法找到了m位置的前一个节点pPre,这里我们用例子中的{1,2,3,4,5},2,4来举例,如下:
每次循环的任务都是将pNext位置的节点移到pPre后面
当前链表:pReturn-->1(pPre)-->2(pCur)-->3-->4-->5-->NULL
-
第一轮循环的任务是将pNext位置的3移到pPre后面
- 首先对pNext赋值为pCur->next,
- 因为要移动pNext,所以我们要先让pCur指向pNext的下一个位置,pCur->next = pNext->next
- 然后断开pNext,此时pNext需要保存pPre的后续,pNext->next = pPre->next
- 最后将pPre的后继更新为pNext。一次循环结束
- 此时链表变为pReturn-->1(pPre)-->3(pNext)-->2(pCur)-->4-->5-->NULL
后面一轮的任务类似,是将pNext(4)这个节点放到pPre的后面
ListNode* reverseBetween(ListNode* head, int m, int n) {
// write code here
if (nullptr == head || nullptr == head->next) {
return head;
}
ListNode* pPre = nullptr;
ListNode* pCur = nullptr;
ListNode* pNext = nullptr;
ListNode* pReturn = static_cast<ListNode*>(malloc(sizeof(ListNode)));
pReturn->val = -1;
pReturn->next = head;
pPre = pReturn;
for(int i = 0; i < m - 1; ++i){
pPre = pPre->next;
}
pCur = pPre->next;
for(int i=0;i<n-m;++i){
pNext = pCur->next;
pCur->next = pNext->next;
pNext->next = pPre->next;
pPre->next = pNext;
}
return pReturn->next;
}