1. 反转链表
题目来源:反转链表
题目的链表结构说明:其头节点是有值的,可以理解为头结点就是首元节点,如下图:
链表反转的思路如下:
prior
指向当前节点的上一个节点
head
指向当前节点
next
指向当前节点的下一个节点
其中prior
指针的初始值一定为NULL
!因为第1次反转的节点成为了最后1个节点,最后1个节点的指针域为NULL
。
//链表节点包含的内容
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* ReverseList(struct ListNode* head ) {
struct ListNode *next = NULL; //记忆下一个节点的位置,初值任意
struct ListNode *prior = NULL; //记忆前一个节点的位置,初值必为NULL
while (head!=NULL) //链表是否结束
{
next = head->next; //记忆下一个节点位置
head->next = prior; //反转
prior = head; //记忆前一个节点位置
head = next; //移到到下一个节点
}
return prior; //prior此时指向反转前链表的最后一个节点
}
2. 链表内指定区间反转
题目来源:链表内指定区间反转
2.1 基本解法
解题思路:
- 反转局部链表,可以将区间的部分当作完整链表进行反转
- 再将已经反转好的局部链表与其他节点建立连接,重构链表
- 使用虚拟头节点的技巧,可以避免对头节点复杂的分类考虑,简化操作
start
指向翻转链表区间外部前一个节点
end
指向翻转链表区间外部后一个节点
left
指向翻转链表区间内部的第一个节点
right
指向翻转链表区间内部的最后一个节点注:不太清楚的可看下面的示意图,红框框出来的部分表示要反转的区间。
prior
指向当前节点的上一个节点
head
指向当前节点
next
指向当前节点的下一个节点上面这3个指针在下面的示意图没有体现出来,因为在上面的反转链表部分已经讲过了。
//链表节点包含的内容
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
struct ListNode phead; //创建虚拟的头结点
phead.next = head; //虚拟头结点指向链表的头结点
struct ListNode *next = NULL; //反转的下一个节点
struct ListNode *prior = NULL; //反转的前一个节点,初始值一定为NULL
struct ListNode *start = &phead; //反转链表区间外部的前一个节点
struct ListNode *end = NULL; //反转链表区间外部的后一个节点
struct ListNode *left = NULL; //反转链表区间内部的第一个节点
struct ListNode *right = NULL; //反转链表区间内部的最后一个节点
int i = 0;
for (i=0; i<m-1; ++i) //找到链表反转的前一个节点
{
start = start->next; //start的起始值:并不是整个链表的第一个节点
}
head = right = left = start->next; //链表反转的第一个节点
for (i=m; i<n; ++i) //找到链表反转的最后一个节点
{
right = right->next;
}
end = right->next; //反转链表结束后的节点
right->next = NULL; //将反转部分断开,不然下面的判断条件head!=NULL会出现问题
while (head!=NULL)
{
next = head->next; //存储下一个节点
head->next = prior; //反转
prior = head; //存储前一个节点
head = next; //移到下一个节点
}
start->next = right; //将断开的链表接上
left->next = end; //将断开的链表接上
return phead.next;
}
创建虚拟头节点的错误方法:
struct ListNode *phead = (struct ListNode*)malloc(sizeof(struct ListNode)); //虚拟头结点
phead->next = head;
按照上述方法创建,需要释放掉malloc
在堆上开辟的空间,函数内部无法通过free
函数释放。因为该函数返回的是开辟节点的指针域,返回前如果释放掉开辟节点,会导致函数返回失败。
如果在函数外部释放,因为返回的是开辟节点的指针域,即链表的头结点的地址,没有返回开辟节点的地址,找不到虚拟头节点的地址,因此函数外部无法释放。
创建虚拟头节点时应该创建局部变量,这样函数结束后会自动销毁:
struct ListNode phead; //创建虚拟的头结点
phead.next = head; //虚拟头结点指向链表的头结点
注意:start
指针的初始值一定为虚拟的头节点,因为可能从链表的第1个节点就开始反转。start
指向的是反转区间外部的前一个节点。
这里prior
的起始值可以不为NULL
,因为后面的代码left->next = end;
会起到相同的作用。
2.2 进阶解法 - 头插法简易版(推荐)
因为在 m 位置到 n 位置区间进行反转,故在 m-1 结点后进行头插法,依次将翻转区间内的结点插入到 m-1 结点后。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
//定义虚拟头结点, 以防从第1个结点就开始翻转
struct ListNode Head;
Head.next = head;
struct ListNode* pHead = &Head; //pHead指向第 m-1 个结点
struct ListNode* next = NULL;
struct ListNode* first = NULL;
// 找到第 m-1 个结点
for (int i=1; i<m; ++i)
{
pHead = pHead->next;
}
// 记录第 1 个翻转结点的位置
first = head = pHead->next;
// m~n 个结点进行反转
for (int i=m; i<=n; ++i)
{
next = head->next;
head->next = pHead->next;
pHead->next = head;
head = next;
}
// 连上断开的链表
first->next = head;
return Head.next;
}
2.3 进阶解法 - 头插法
在上一种方法中,需要先找到end
指针要指向的结点,再重新进行翻转,需要遍历2次。
头插法法只需要遍历1次即可,一边遍历一边翻转。
pstart
指向翻转链表区间外部前一个节点
head
指向翻转的当前节点
next
指向翻转的下一个节点
核心:将next
指针指向的节点作为插入的节点,接在pstart
指针指向的节点后面。
下图为区间2-4的翻转示意图:
翻转时顺序:
head
指向结点的指针域设置为next
指向结点的下一个结点next
指向结点的指针域设置为pstart
指向结点的下一个结点pstart
指向结点的指针域设置为next
指向结点
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
struct ListNode phead; //创建虚拟的头结点
phead.next = head; //虚拟头结点指向链表的头结点
struct ListNode* pstart = &phead; //要指向翻转区间的前一个结点
struct ListNode* next = NULL;
int i = 0;
//从虚拟头结点开始寻找,以防第1个结点属于翻转区间
for (i=0; i<m-1; ++i)
{
pstart = pstart->next;
}
head = pstart->next; //第1个要翻转的结点
//循环体中的顺序不能更换!
for (i=m; i<n; ++i)
{
next = head->next;
head->next = next->next;
next->next = pstart->next;
pstart->next = next;
}
return phead.next;
}
3. 链表中的节点每k个一组翻转
题目来源:链表中的节点每k个一组翻转
解题思路:
- 先求出链表长度
length
,length/k
为整个链表要反转的次数 - 将区间的部分当作完整链表进行反转
- 再将已经反转好的局部链表与其他节点建立连接,重构链表
- 在下一次反转前,要先设置好对应的指针
第2, 3步的处理上面已经解决过了,主要是剩下的第1步和第4步该如何处理
下面代码中的指针功能同第2题:链表内指定区间反转。
下图主要列出了一些主要的指针,其他不重要的指针可由主要的指针所推出来。
上图中prior
指针一开始指向哪不重要,针对每次翻转的第1个节点,后面的代码left->next = end;
会起到相同的作用。
每次翻转的第1个节点会被
left
指针所指;left->next = end;
会连接上断开的链表。
实际翻转过程会在第2步和第3步不断循环。
所以每次翻转后,指针的设置非常重要!
struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
struct ListNode pHead; //创建虚拟的头结点
pHead.next = head; //虚拟头结点指向链表的头结点
struct ListNode* start = &pHead; //初值只能是这个,不可更改
struct ListNode* end = NULL;
struct ListNode* prior = NULL;
struct ListNode* next = &pHead;
struct ListNode* left = head; //初值只能是这个,不可更改
struct ListNode* right = &pHead; //初值只能是这个,不可更改
int i = 0;
int j = 0;
int length = 0; //链表长度
//求链表长度。不创建新的指针,借助next指针来求取长度
for (length=0; next->next!=NULL; ++length)
{
next = next->next;
}
//链表的反转次数为length/k -> 为整数
for (i=0; i<(length/k); ++i)
{
//------------------以下代码同第2题:链表内指定区间反转
for (j=0; j<k; ++j)
{
right = right->next;
}
end = right->next;
right->next = NULL;
while(head!=NULL)
{
next = head->next;
head->next = prior;
prior = head;
head = next;
}
start->next = right;
left->next = end;
//------------------以上代码同第2题:链表内指定区间反转
//在下一次反转前预先设置好对应的指针
start = right = left;
head = left = left->next; //下一次反转的第1个节点位置
}
return pHead.next;
}