2022-10-27 11:45阅读: 29评论: 0推荐: 0

力扣-92-反转链表Ⅱ

其实对链表的考察就是考察指针,不喜欢Java写算法题的一大原因就是Java没有指针

区间反转链表,相对于整体反转链表而言
回忆一下链表的整体反转,大概是两种做法

  1. 递归,从后往前处理
  2. 迭代,用三个指针(修改原指针的话只需要两个额外的)
    这里感觉用迭代更直观容易些,回顾一下迭代是怎么反转链表的
ListNode* pre = nullptr, * cur = head;// 初始化pre为空指针,因为头反转后指向为空
nxt = cur->next;// 先保存下一个节点
cur->next = pre;// 修改节点指针指向上一个节点
pre = cur;
cur = nxt;

不完全是那么简单,尽管只反转其中一段,但这一段前后的两个指针也需要改变

头节点反转不一定指向空
而如果是left=1或者是right=0,就没有

感觉如果用迭代的话,需要额外保存反转段的前一个节点和后一个节点,而且这两个节点可能为空
想要一趟趟遍历迭代难点就在于反转段前后指针的处理,这两个指针不一定存在
如果想要保存这两个指针,怎么初始化保存也很麻烦

瞄一眼题解了
第一次完成功能的实现

ListNode* reverse(ListNode* head) {
// 这里的head不能改,后面要用
// left可能==right,参数判断放到前面去
ListNode* pre = nullptr,*cur = head,*nxt;
while (cur) {
nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* dummyHead = new ListNode(-1);
dummyHead->next = head;// 使用虚拟头节点避免是否为空的分类讨论
// 但是这里不需要虚拟尾节点?
ListNode* preNode = dummyHead;
// 这里不需要额外的count遍历+while,可以控制for循环此时
for (int i = 0; i < left - 1; i++) preNode = preNode->next;
// 让preNode指向区间的前一个节点
ListNode* rightN = preNode;
for (int i = 0; i < right - left + 1; i++) rightN = rightN->next;
// 让rightN指向区间右侧的节点
// succNode当然可能为空,但是它只会被指向而不是指向别人,送一不会出翔空指针异常,所以不需要虚拟尾节点
ListNode* leftN = preNode->next, *succNode = rightN->next;// 这里保存succNode是因为rightN->next会被修改
rightN->next = nullptr;// 把区间右节点置空是为了区间反转的结束条件,也是保证正常反转
preNode->next = reverse(leftN);
leftN->next = succNode;// 反转后leftN变成了反转区间的最后一个节点
return dummyHead->next;// 这里不能是返回head,因为head可能是反转段的一部分,会变化
// 那为什么dummy->next可以?当head作为被反转区间的第一个节点,dummyHead就是preNode,所以dummyHead->next其实是被更新了的
}

所以我之前是被卡在了哪里?
首先就是关于反转段前后两个节点,可能为空和可能不空的情况讨论复杂想不明白,当然也想到了虚拟头,但是没想明白为什么不需要虚拟尾
另外就是多此一举了一个计数count,本可以通过for循环控制次数

但是它这里并不能算是顺序一次遍历,第一趟只遍历到right,后来反转反向遍历right-left+1
也就是总时间复杂度O(2right-left+1),当left=1,right=n,就相当于整个链表反转,复杂度会退化到O(2N)

再者就是区间反转时的指针操作,包括一个指针置空,以及反转子函数中不能修改原指针(不然原指针会变成空指针),当然这一块儿可能还有优化的空间

迭代写法应该是比递归更清晰的,下次就应该考虑递归的写法了,另外题解中还有一种插入的写法

2023.8.31

面试中遇到了这道题,一下子思路还不是很清晰,在面试官的提示下才一步步写出来

回顾上面的代码,思路是:

首先先前整体反转函数是可以沿用的,因为只要给出指定的头节点也就是起始位置节点就可以了。
但是,再调用前需要将结束位置的指针断开,防止后续节点被反转
但是这样因为局部反转后还要接回去,就要保存结束位置的后指针,当然还有起始位置的前指针
关于虚拟头节点……为什么需要它?因为如果起始位置就是链表头的话,它的前一个节点是不存在的,那么上述思路就不得不去考虑额外的情况,很麻烦
为什么不需要虚拟尾节点?因为头不可以是 nullptr 但是尾可以

ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode* dumpNode = new ListNode(0, head);
ListNode* preLeft = dumpNode, * start, * nxtRight;
for (int i = 0; i < left - 1; i++)preLeft = preLeft->next;
start = preLeft->next;
ListNode* end = preLeft;
for (int i = 0; i < right - left + 1; i++)end = end->next;
nxtRight = end->next;
end->next = nullptr;
preLeft->next = reverse(start);
start->next = nxtRight;
return dumpNode->next;
}

这四个指针都不能被省略

分别是:

  1. 反转段起始指针,也是反转后的尾指针:start
  2. 反转段起始指针的前一个指针 preLeft,它会指向反转后的头指针
  3. 反转段的后一个指针 nxtRight,用于被反转后的 start 指向
  4. 反转段的最后一个指针 end,需要在反转前断开(在这之前还要保存 nxtRight),确保反转段正常反转

本文作者:YaosGHC

本文链接:https://www.cnblogs.com/yaocy/p/16831050.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YaosGHC  阅读(29)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起