力扣每日一题__92. 反转链表 II
题目链接: LC 92.反转链表
这道题是对链表的局部进行翻转的, 我们先来看一个普通版本的链表翻转。
题目链接 : Acwing 35. 反转链表
acwing上这道题目的意思就是让你反转整个链表, 这里我们分为迭代和递归两种不同的版本
首先分析这个过程 , 1号节点的next指向的是2号节点, 如果我们要进行翻转链表的操作,我们应该将1号节点的next指向nullptr, 然后2号节点的next指向1号节点, 3号节点的next指向2号节点 ...... , 即将每一个节点的next指针指向其上一个节点。 1号节点我们只需让它指向nullptr即可。 故明白了基本的思路 ,接下来就是代码实现, 在代码实现中讨论具体的细节.
迭代版本C++的实现
struct ListNode // 节点的数据结构, 包含一个数据和指针
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
ListNode* reverseList(ListNode* head)
{
// 若此时的头节点为空, 或者此时头节点的下一个为空(说明此时只有一个元素) 不用处理,直接返回头节点
if(!head || !head->next) return head;
ListNode *pre = nullptr;
// pre指针表示即当前节点的前一节点,由于我们的操作是将当前的节点的next更新为上一节点,
//而第一个节点的next 应更新为nullptr, 故pre初始化为nullptr;
auto cur = head; // cur表示当前遍历的节点
while(cur) // 遍历整个链表,更新每个节点的next的值
{
auto next = cur -> next; // 提前保存当前节点的next节点, 因为会被更新为上一节点
cur -> next = pre; // 将当前节点的next更新为上一节点
pre = cur; // 同时要遍历下一节点时,应该同时更新pre为下一节点的上一节点,即更新为本次节点
cur = next; // cur更新为下一遍历节点
}
return pre; // 最后的pre为原本链表的最后一个节点,经过遍历后其成为头节点
}
递归版本C++的实现
我们的迭代版本是分析了翻转链表的过程就是将每个节点的next更新为上一节点这样的思路而写出的代码, 而递归版本也同样也是这个思路,只不过递归函数每次实现的是一个局部. 例如上图的例子中,当目前遍历的节点不是空且其下一个节点也不为空时,继续递归遍历,直到遇到一个节点其下一个节点为空,即当遍历到4号节点的时候发现其下一节点为空,4说明号节点已经是最后一个节点,其将作为翻转链表后的头节点,故这里直接返回4号节点, 返回调用他的上一个节点中,然后将4号节点的下一个节点更新为3号节点,即 3号节点 -> next -> next = 3号节点。 解释一下, 3号节点 -> next 就是4号节点, 然后4号节点 -> next = 3号节点, 不就是将4号节点的下一个节点更新为3号节点. 故继续返回从2号节点到3号节点之间更新, 故代码的思路为:
ListNode* reverseList(ListNode* head)
{
if(!head || !head->next) return head; // 判断本次节点是否为空,或者是否是最后一个节点
auto tail = reverseList(head->next); // 递归,因为本次节点并不是最后一个节点,故继续递归直到找到最后一个节点
head->next->next = head; // 将当前节点的下一个节点的下一个节点更新为当前节点
head->next = nullptr; // 当前节点的下一个更新为nullptr
return tail; // 返回翻转链表后的头节点 。
}
故现在再回过头来看力扣这道题目,故这道题目在局部上翻转的策略可以采用我们的迭代版本的思路, 即将left 和 right之间的的部分进行翻转,但这里还是要注意问题,看下面图片
可以看到 (left + 1, right)的部分就是将每一个结点的下一个结点更新为上原本的上一节点, 但注意的是这里有两个跟全部翻转不同的地方,就是left - 1这个位置的next结点的更新和left结点的next结点的更新。而且这里我们注意到,执行局部更新头节点有可能变(即left就是头节点的时候),也可能不变(left节点在头节点右边的情况下) , 故针对这种头节点变或者不变的情况,我们可以统一用一个虚拟头节点来处理,即将虚拟头节点的next指向头节点,故不管left的位置在哪, 虚拟头节点是不会变的。故接下来看代码实现
ListNode* reverseBetween(ListNode* head, int left, int right)
{
ListNode* dummy = new ListNode(-1); // 给虚拟头节点dummy申请内存
dummy->next = head; // 令虚拟头节点的next指向头节点head
auto a = dummy; // 由于left - 1这个位置的节点更新方式特殊,故我们定义一个指针遍历到 left - 1这个位置的节点
for(int i = 0; i < left - 1; ++i) a = a->next; // a指针遍历到 left - 1这个位置的节点
auto b = a->next, c = b->next; // 此时b为left位置的节点, c为left + 1位置的节点
for(int i = 1; i <= right - left; ++i) // 通过上图分析我们知道我们是从left + 1这个位置开始一直到right才是我们上面两道简单的题目的更新逻辑
{
auto d = c->next; // 保存当前更新节点的下一节点
c->next = b; // 将当前节点的next更新为上一节点
b = c; // 更新下一节点的上一节点是哪一个
c = d; // 将当前节点更新为下一节点
}
// 最后更新两个特殊的位置 left - 1和left
a->next->next = c; // left位置的更新
a->next = b; // left - 1位置的更新
// 接下来的话我们其实可以直接返回 dummy -> next; 但对于C++来说会内存泄漏故进行处理
auto res = dummy->next;
delete dummy;
return res;
}
解释得很详细,赞👍