【牛客】链表的反转(C语言)

通过反转链表,可以更加熟练掌握链表。再此基础之上,尝试反转链表的变式k个一组反转和指定区间的反转。


 

1. 反转链表 

1.1 题目

描述

给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。

数据范围: 0\leq n\leq10000≤n≤1000

要求:空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n) 。

如当输入链表{1,2,3}时,

经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。

以上转换过程如下图所示:

示例1

输入:

{1,2,3}

复制返回值:

{3,2,1}

复制

示例2

输入:

{}

复制返回值:

{}

复制说明:

空链表则输出空 

 

1.2 题解

如果我们要申请一个临时变量来交换两数a和b的话,有如下伪代码:

  • int temp;
  • temp = a;
  • a = b;
  • b = temp;

 链表的反转实际上就是结点间的反转,对于两结点间的反转可以参考两数的交换,有如下代码:

  • next = cur->next;
  • cur->next = pre;
  • pre = cur;
  • cur = next;

题解代码: 

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

/**
 * 
 * @param pHead ListNode类 
 * @return ListNode类
 */
struct ListNode* ReverseList(struct ListNode* pHead ) {
    // write code here
    struct ListNode *pre = NULL;
    struct ListNode *cur = pHead;
    struct ListNode *next = NULL;
    while(cur)
    {
        next = cur->next;
        cur->next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

2. 链表中的节点每k个一组翻转

2.1 题目

描述

将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。

数据范围: \ 0 \le n \le 2000 0≤n≤2000 , 1 \le k \le 20001≤k≤2000 ,链表中每个元素都满足 0 \le val \le 10000≤val≤1000
要求空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)

例如:

给定的链表是 1\to2\to3\to4\to51→2→3→4→5

对于 k = 2k=2 , 你应该返回 2\to 1\to 4\to 3\to 52→1→4→3→5

对于 k = 3k=3 , 你应该返回 3\to2 \to1 \to 4\to 53→2→1→4→5

示例1

输入:

{1,2,3,4,5},2

复制返回值:

{2,1,4,3,5}

复制

示例2

输入:

{},1

复制返回值:

{}

2.2 题解

思路:

我们要先分成一组一组,之后就是组内翻转,最后是将反转后的分组连接。

但是连接的时候遇到问题了:首先如果能够翻转,链表第一个元素一定是第一组,它翻转之后就跑到后面去了,而第一组的末尾元素才是新的链表首,我们要返回的也是这个元素,而原本的链表首要连接下一组翻转后的头部,即翻转前的尾部,如果不建立新的链表,看起来就会非常难。但是如果我们从最后的一个组开始翻转,得到了最后一个组的链表首,是不是可以直接连在倒数第二个组翻转后的尾(即翻转前的头)后面,这样从后往前是不是看起来就容易多了。

怎样从后往前呢?我们这时候可以用到自上而下再自下而上的递归或者说栈。接下来我们说说为什么能用递归?如果这个链表有nnn个分组可以反转,我们首先对第一个分组反转,那么是不是接下来将剩余n−1n-1n−1个分组反转后的结果接在第一组后面就行了,那这剩余的n−1n-1n−1组就是一个子问题。我们来看看递归的三段式模版:

  • 终止条件: 当进行到最后一个分组,即不足k次遍历到链表尾(0次也算),就将剩余的部分直接返回。
  • 返回值: 每一级要返回的就是翻转后的这一分组的头,以及连接好它后面所有翻转好的分组链表。
  • 本级任务: 对于每个子问题,先遍历k次,找到该组结尾在哪里,然后从这一组开头遍历到结尾,依次翻转,结尾就可以作为下一个分组的开头,而先前指向开头的元素已经跑到了这一分组的最后,可以用它来连接它后面的子问题,即后面分组的头。

具体做法:

  • step 1:每次从进入函数的头节点优先遍历链表k次,分出一组,若是后续不足k个节点,不用反转直接返回头。
  • step 2:从进入函数的头节点开始,依次反转接下来的一组链表。
  • step 3:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 
 * @param head ListNode类 
 * @param k int整型 
 * @return ListNode类
 */
struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
    // write code here
     //找到每次翻转的尾部
        struct ListNode* tail = head;
        //遍历k次到尾部
        for(int i = 0; i < k; i++){
            //如果不足k到了链表尾,直接返回,不翻转
            if(tail == NULL)
                return head;
            tail = tail->next;
        }
        //翻转时需要的前序和当前节点
        struct ListNode* pre = NULL;
        struct ListNode* cur = head;
        //在到达当前段尾节点前
        while(cur != tail){
            //翻转
            struct ListNode* temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }
        //当前尾指向下一段要翻转的链表
        head->next = reverseKGroup(tail, k);
        return pre;
}

3. 链表内指定区间反转

3. 1 题目

描述

将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。
例如:
给出的链表为 1\to 2 \to 3 \to 4 \to 5 \to NULL1→2→3→4→5→NULL, m=2,n=4m=2,n=4,
返回 1\to 4\to 3\to 2\to 5\to NULL1→4→3→2→5→NULL.
 

数据范围: 链表长度 0 < size \le 10000<size≤1000,0 < m \le n \le size0<m≤n≤size,链表中每个节点的值满足 |val| \le 1000∣val∣≤1000

要求:时间复杂度 O(n)O(n) ,空间复杂度 O(n)O(n)

进阶:时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)

示例1

输入:

{1,2,3,4,5},2,4

复制返回值:

{1,4,3,2,5}

复制

示例2

输入:

{5},1,1

复制返回值:

{5}

3.2 题解

3.2.1 方法一逆转链表

  • step 1:我们可以在链表前加一个表头,后续返回时去掉就好了,因为如果要从链表头的位置开始反转,在多了一个表头的情况下就能保证第一个节点永远不会反转,不会到后面去。
  • step 2:使用两个指针,一个指向当前节点,一个指向前序节点。
  • step 3:依次遍历链表,到第m个的位置。
  • step 4:对于从m到n这些个位置的节点,依次断掉指向后续的指针,反转指针方向。
  • step 5:返回时去掉我们添加的表头。
/**
 * struct ListNode {
 *  int val;
 *  struct ListNode *next;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param head ListNode类
 * @param m int整型
 * @param n int整型
 * @return ListNode类
 */
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    // write code here
    if (!head || m == n) return head;
    struct ListNode* cur = malloc(sizeof(struct ListNode));
    cur->val = -1;
    cur->next = head;
    struct ListNode* res = cur;
    for (int i = 0; i < m - 1; i++) {
        res = res->next;
    }
    struct ListNode* node = res->next;
    struct ListNode* temp;
    for (int i = 0; i < n - m; i++) {
        temp = node->next;
        node->next = temp->next;
        temp->next = res->next;
        res->next = temp;
    }
    return cur->next;
}

 

3.2.2方法二递归 

思路:

我们来看看另一种分析:如果m == 1,就相当于反转链表的前nnn元素;如果 m != 1,我们把 head 的索引视为 1,那么我们是想从第 mmm 个元素开始反转,如果把 head.next 的索引视为1,那相对于 head.next的反转的区间应该是从第 m−1m - 1m−1 个元素开始的,以此类推,反转区间的起点往后就是一个子问题,我们可以使用递归处理:

  • 终止条件: 当m == 1,就可以直接反转前n个元素。
  • 返回值: 将已经反转后的子问题头节点返回给上一级。
  • 本级任务: 递归地缩短区间,拼接本级节点与子问题已经反转的部分。

1

2

//从头开始往后去掉前面不反转的部分

ListNode node = reverseBetween(head.next, m - 1, n - 1)

而每次反转,如果n == 1,相当于只颠倒第一个节点,如果不是,则进入后续节点(子问题),因此反转过程也可以使用递归:

  • 终止条件: 当n == 1时,只反转当前头节点即可。
  • 返回值: 将子问题反转后的节点头返回。
  • 本级任务: 缩短nnn进入子问题反转,等子问题回到本级再反转当前节点与后续节点的连接。

1

2

//颠倒后续的节点,直到n=1为最后一个

ListNode node = reverse(head.next, n - 1)

具体做法:

  • step 1:准备全局变量temp,最初等于null,找到递归到第nnn个节点时,指向其后一个位置,要将反转部分的起点(即反转后的尾)连接到这个指针。
  • step 2:按照第一个递归的思路缩短子问题找到反转区间的起点,将反转后的部分拼接到前面正常的后面。
  • step 3:按照第二个递归的思路缩短终点的子问题,从第nnn个位置开始反转,反转过程中每个子问题作为反转后的尾,都要指向temp。

下面给出C++代码: 

class Solution {
public:
    ListNode* temp = NULL;
    ListNode* reverse(ListNode* head, int n){
        //只颠倒第一个节点,后续不管
        if(n == 1){ 
            temp = head->next;
            return head;
        }
        //进入子问题
        ListNode* node = reverse(head->next, n - 1); 
        //反转
        head->next->next = head;
        //每个子问题反转后的尾拼接第n个位置后的节点
        head->next = temp;
        return node;
    }
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        //从第一个节点开始
        if(m == 1) 
            return reverse(head, n);
        //缩减子问题
        ListNode* node = reverseBetween(head->next, m - 1, n - 1); 
        //拼接已翻转
        head->next = node;
        return head;
    }
};

 


参考文献:牛客网

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ོ栖落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值