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

leetcode92 反转链表2

题目介绍:

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

题目来源:

leetcode.cn/problems/re...

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

方法一:头插法

方法一的缺点是:如果 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代码如下:

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代码如下:

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代码:

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代码:

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,即将要反转部分的末尾节点的下一个节点,将反转后的部分的末尾节点与剩余部分连接起来,实现链表的拼接。

方法总结

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

  1. 头插法
  • 头插法是一种利用辅助节点将每个节点插入到链表头部的方法。具体来说,我们先创建一个辅助节点,然后依次遍历链表中的每个节点,将其插入到辅助节点的后面,随后将辅助节点指向该节点。最后,我们将辅助节点后面的节点全部取出来,即为反转后的链表。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 这种方法思路简单,代码实现也直接。
  1. 迭代法:
  • 以链表的指针作为遍历的方式,依次反转指定区间内的节点。每遍历到一个节点就将其反转,直到遍历完指定区间内的所有节点。具体来说,我们需要用三个指针,分别指向当前节点、前一个节点和后一个节点。每遍历一个节点,就将当前节点的指针指向前一个节点,并将当前节点的指针移动到后一个节点,直到遍历完整个链表为止。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 迭代法将原链表的节点一个个取下来再插入到新链表中,需要定义新链表和指向当前节点的指针,操作比较繁琐;而头插法通过在原链表头节点前插入一个虚拟节点,避免了对新链表的定义,并且只需要一个指向虚拟节点的指针,操作简单直观。
  1. 递归法:
  • 将指定区间内的节点分为两部分,第一部分是需要反转的,第二部分是不需要反转的。先递归反转第二部分,再反转第一部分,并将第一部分的尾节点连接到后面反转后的头节点。具体来说,我们调用一个函数,该函数会先递归到链表的最后一个节点,然后依次将每个节点的指针指向前一个节点,从而实现整个链表的反转。
  • 该方法的时间复杂度为O(n),空间复杂度为O(n)。
  • 这种方法使用递归实现,相对来说比较难以理解。
  1. 双指针法:
  • 使用两个指针 start 和 end 分别指向需要反转的区间的开始节点和结束节点,同时使用一个指针 prev 保存 start 前面的节点,使用一个指针 succ保存 end 后面的节点。然后将这个区间中的节点依次反转,直到反转到 end 节点位置。最后将反转的链表与原链表连接起来即可。
  • 该方法的时间复杂度为O(n),空间复杂度为O(1)。
  • 这种方法思路清晰,易于理解。

欢迎关注我喵~持续更新ing

相关推荐
京东云开发者几秒前
Java的SPI机制详解
java
超级小忍22 分钟前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
程序无bug26 分钟前
Spring IoC注解式开发无敌详细(细节丰富)
java·后端
小莫分享29 分钟前
Java Lombok 入门
java
程序无bug29 分钟前
Spring 对于事务上的应用的详细说明
java·后端
食亨技术团队30 分钟前
被忽略的 SAAS 生命线:操作日志有多重要
java·后端
苦学编程的谢1 小时前
Maven
java·maven·intellij-idea
考虑考虑1 小时前
Maven 依赖范围(Scope)
java·后端·maven
chao_7891 小时前
回溯题解——子集【LeetCode】二进制枚举法
开发语言·数据结构·python·算法·leetcode
张小洛1 小时前
Spring AOP 设计解密:代理对象生成、拦截器链调度与注解适配全流程源码解析
java·后端·spring·spring aop·aop