算法题学习---链表内指定区间反转

250 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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<mnsize,链表中每个节点的值满足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

image.png

后面一轮的任务类似,是将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;
    }