指定区间的链表反转问题|力扣leetcode92反转链表2(cpp、Java实现)

leetcode92 反转链表2

题目介绍:

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

题目来源:

力扣

反转的整体思想是,在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。

方法一:头插法

方法一的缺点是:如果 left 和 right 的区域很大,恰好是链表的头节点和尾节点时,找到 left 和 right 需要遍历一次,反转它们之间的链表还需要遍历一次,虽然总的时间复杂度为 O(N),但遍历了链表 2次

初始状态: pre_node -> curr_node -> next_node

反转后的状态: pre_node <- curr_node next_node

举个栗子🌰

举个例子:链表为12345 翻转234,每次循环只翻转一个

即:

第一次324

第二次432

翻转的大致逻辑是将cur后面的插入到pre的后面,然后将链接补好

在循环中cur和pre没有执行的元素没有变化,但是他们的位置发生了变化。

如图所示:

三角里面的数字是代表的是

next=cur->next;

cur->next=next->next;

next->next=pre->next;

pre->next=next;

这四步的顺序。

步骤

首先,我们创建一个名为 dummyNode 的 ListNode,将其值设为 -1,dummyNode.next 指向原始链表头节点 head,然后定义一个指针 pre 指向 dummyNode,用于找到要翻转的起点 left 前一个节点。设置起点 cur 为 pre.next。

然后,我们需要将第 left到第right个节点顺序翻转。我们使用一个循环,将 cur 节点后面的节点移动到它前面来,将其插入到 pre 节点的后面,如下:

  • 首先,定义一个指针 next,让其指向 cur 的下一个节点,备份一下 cur 的下一个节点,方便后面使用。
  • 然后更新 cur 节点的 next,使其指向 next 节点的 next。
  • 接着,让 next 节点的 next 指向 pre 的下一个节点,将 next 节点插入到 pre 节点和 cur 节点之间。
  • 最后,更新 pre 指针和 cur 指针,将它们指向下一个待翻转的节点位置。

反复执行循环,每次移动 cur 节点后面的节点,在 pre 节点和 cur 节点之间插入,直到将第 left 到第 right 个节点全部翻转完毕。函数执行完后,我们返回翻转后的链表头节点即 dummyNode.next。

为什么要使用虚拟头结点?

这里使用虚拟头结点的原因是因为可能翻转的区间是从头开始,那么可能就需要换头,而使用虚拟节点就可以不换头了。

代码

cpp代码如下:

/**
 * 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* reverseBetween(ListNode* head, int left, int right) {
        ListNode *dummy=new ListNode(-1);
        dummy->next=head;
        ListNode *pre=dummy;
        for(int i=0;i<left-1;i++){
            pre=pre->next;
        }
        ListNode *cur=pre->next;
        ListNode *next;
        for(int i=0;i<right-left;i++){
            next=cur->next;
            cur->next=next->next;
            next->next=pre->next;
            pre->next=next;
        }
        return dummy->next;
    }
};

java代码如下:

public ListNode reverseBetween(ListNode head, int left, int right) {
    // 设置 dummyNode 是这一类问题的一般做法
    ListNode dummyNode = new ListNode(-1);
    dummyNode.next = head;
    ListNode pre = dummyNode;
    for (int i = 0; i < left - 1; i++) {
        pre = pre.next;
    }
    ListNode cur = pre.next;
    ListNode next;
    for (int i = 0; i < right - left; i++) {
        next = cur.next;
        cur.next = next.next;
        next.next = pre.next;
        pre.next = next;
    }
    return dummyNode.next;
}

思考

  • 为什么 i < left - 1 ?

这个循环的目的是将 pre 指针移动到需要反转的起始位置之前的节点。因为 pre 最初指向 dummyNode 的头节点,所以我们需要移动 left - 1 次才能到达目标位置。

  • 为什么 i < right - left ?

这个循环的目的是进行链表的反转操作。我们需要将从起始位置到终止位置的一段链表进行反转。因为我们已经移动了 left-1 步,所以这里我们只需反转 right - left 次。

  • 为什么是ListNode *cur=pre->next;而非ListNode *cur=pre;?

因为在反转链表的过程中,我们需要将 cur 作为当前节点,然后将 next 作为下一个节点,并进行指针的调整。所以我们需要让 cur 指向 pre 的下一个节点,即 pre->next。这样才能保证链表可以正确地反转。

如果我们让 cur 等于 pre,那么实际上就是没有移动到下一个节点,而是一直停留在当前节点,导致链表无法反转。

方法二:双指针法(也可称为穿针引线法)

穿针引线的来历:先确定好需要反转的部分,也就是left 到 right 之间,然后再将三段链表拼接起来。这种方式类似裁缝一样,找准位置减下来,再缝回去。故形象地称之为穿针引线。

这种方法较之第一种实现难度较高,但复用性强。

步骤

  1. 定义两个指针:pre指向要反转区域的前一个节点,cur指向要反转的第一个节点。
  2. 将pre的next指针指向要反转区域的后一个节点,这样可以在反转完后将反转部分与原链表连接起来。
  3. 使用双指针法反转区域内的链表,直到cur指向要反转区域的后一个节点为止。
  4. 将反转的链表与原链表连接起来。即把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的 next 指针指向 succ。(pre为前驱,succ为后继)

画个图方便理解:

总结:将链表划分成三部分,先将待反转的区域反转,然后将反转的链表与原链表连接起来。

代码

以下是cpp代码:

/**
* 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* reverseBetween(ListNode* head, int left, int right) {
    ListNode *dummy=new ListNode(-1);
    dummy->next=head;
    ListNode *pre=dummy;
    for(int i=0;i<left-1;i++){
        pre=pre->next;
    }
    ListNode *leftNode=pre->next;
    ListNode *rightNode=pre;
    for(int i=0;i<right-left+1;i++){
        rightNode=rightNode->next;
    }
    ListNode *succ=rightNode->next;
    rightNode->next=nullptr;
    reverseList(leftNode);
    pre->next=rightNode;
    leftNode->next=succ;
    return dummy->next;
}
ListNode* reverseList(ListNode* head) {
    ListNode *currNode=head,*preNode=nullptr;
    while(currNode!=nullptr){
        ListNode *next=currNode->next;
        currNode->next=preNode;
        preNode=currNode;
        currNode=next;           
    }
    return preNode;
}
};

如果把leftNode->next=succ;反过来,写成succ=leftNode->next;则会报错。

错误代码为:runtime error: member access within null pointer of type 'ListNode' (solution.cpp)

错误原因为:试图使用空指针

在写代码时,要尽量避免该情况的出现,仔细检查时表示把空指针赋给了某个值。

以下是Java代码:

public ListNode reverseBetween(ListNode head, int left, int right) {
        // 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论,在这一点上,方法一和方法二一样
    
        ListNode dummyNode = new ListNode(-1);//定义一个虚拟结点
        dummyNode.next = head;
        ListNode pre = dummyNode;//pre为前驱
    
        // step1:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
        //写在 for 循环里,可以使语义清晰
        for (int i = 0; i < left - 1; i++) {
            pre = pre.next;
        }
        // step2:从 pre 再走 right - left + 1 步,来到 right 节点
        ListNode rightNode = pre;
        for (int i = 0; i < right - left + 1; i++) {
            rightNode = rightNode.next;
        }
        // step3:切出一个子链表
        ListNode leftNode = pre.next;
        ListNode succ = rightNode.next;//succ为后继
    
        rightNode.next = null;

        // step4:同第 206 题,反转链表的子区间
        reverseLinkedList(leftNode);
        // step5:接回到原来的链表中
        pre.next = rightNode;
        leftNode.next = succ;
        return dummyNode.next;
    }
    private void reverseLinkedList(ListNode head) {
        // 反转链表;也可以使用递归法反转
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
    }

思考

  • 如果rightNode.next = null;这里不设置next为null会怎么样?

如果没有 `rightNode->next=nullptr;` 则会导致整个链表没有断开,那么链表会成为一个环形,即从第 right+1 个结点到第 left-1 个结点会变成一个环,这样程序就会进入一个死循环并最终崩溃。

所以 `rightNode->next=nullptr;` 的作用是将第 right 个结点的 next 设为空指针,使得整个链表变成了两个子链表,从头节点到 left-1 个结点一个,从left个结点到 right 个结点一个,从 right+1 个结点以后的再一个。

  • pre.next = rightNode;这里为什么可以用rightNode

在代码中,我们需要将左边界节点的前一个节点(即pre)的next指针指向右边界节点(rightNode)。而rightNode是在遍历到右边界节点的前一个节点时获得的,所以可以直接使用rightNode来指代右边界节点。也就是说,pre->next=rightNode这一步的作用是将左边界节点的前一个节点的next指针指向右边界节点。

  • 为什么要让leftNode.next = succ;

在给定的代码中,leftNode表示要反转部分的起始节点,succ表示要反转部分的末尾节点的下一个节点。leftNode->next=succ;的目的是将反转后的部分与剩余部分连接起来,使整个链表保持完整。通过将leftNode的next指针指向succ,即将要反转部分的末尾节点的下一个节点,将反转后的部分的末尾节点与剩余部分连接起来,实现链表的拼接。

方法总结

反转指定区间链表可以使用以下四种方法:

头插法:

  • 头插法是一种利用辅助节点将每个节点插入到链表头部的方法。具体来说,我们先创建一个辅助节点,然后依次遍历链表中的每个节点,将其插入到辅助节点的后面,随后将辅助节点指向该节点。最后,我们将辅助节点后面的节点全部取出来,即为反转后的链表。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 这种方法思路简单,代码实现也直接。

迭代法:

  • 以链表的指针作为遍历的方式,依次反转指定区间内的节点。每遍历到一个节点就将其反转,直到遍历完指定区间内的所有节点。具体来说,我们需要用三个指针,分别指向当前节点、前一个节点和后一个节点。每遍历一个节点,就将当前节点的指针指向前一个节点,并将当前节点的指针移动到后一个节点,直到遍历完整个链表为止。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 迭代法将原链表的节点一个个取下来再插入到新链表中,需要定义新链表和指向当前节点的指针,操作比较繁琐;而头插法通过在原链表头节点前插入一个虚拟节点,避免了对新链表的定义,并且只需要一个指向虚拟节点的指针,操作简单直观。

递归法:

  • 将指定区间内的节点分为两部分,第一部分是需要反转的,第二部分是不需要反转的。先递归反转第二部分,再反转第一部分,并将第一部分的尾节点连接到后面反转后的头节点。具体来说,我们调用一个函数,该函数会先递归到链表的最后一个节点,然后依次将每个节点的指针指向前一个节点,从而实现整个链表的反转。
  • 该方法的时间复杂度为O(n),空间复杂度为O(n)。
  • 这种方法使用递归实现,相对来说比较难以理解。

双指针法:

  • 使用两个指针 start 和 end 分别指向需要反转的区间的开始节点和结束节点,同时使用一个指针 prev 保存 start 前面的节点,使用一个指针 succ保存 end 后面的节点。然后将这个区间中的节点依次反转,直到反转到 end 节点位置。最后将反转的链表与原链表连接起来即可。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 这种方法思路清晰,易于理解。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值