通过反转链表,可以更加熟练掌握链表。再此基础之上,尝试反转链表的变式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;
}
};
参考文献:牛客网