链表结构:
struct ListNode {
int val;
ListNode* next;
ListNode(int x = 0) : val(x), next(NULL) {}
};
1.单链表的逆置, 注意是不带头节点的单链表。
给定单链表1→2→3→4→5
返回被反转后的链表 5→4→3→2→1;
- 对于逆置单链表来说,如果题目没有做特殊说明,我们可以将链表中的数据进行拷贝,然后将顺序倒置后的数据重新赋值到原链表中,这样就实现了一种伪单链表逆置操作,但此题的考察目的显示不是让我们这样操作的。
那么,从常规思路上出发就有两种方式进行操作:
-
思路一:构建一个辅助用的链表,在不改变原链表的基础上返回一个与原链表相逆置的链表。
具体操作为,使用头插的方式构建新链表,返回新链表的表头地址。
缺点:使用了辅助的链表,增加了空间复杂度。 -
思路二:在原有链表的基础上进行逆置,这里需要用到3个指针进行操作,最后通过指针操作将原链表原地逆置。
思路一:辅助链表逆置
我们按照头插的方式构建新链表,即可将原链表的数据逆置。
图示如下:
现有一链表: 1 → → →2 → → →3 → → →4 → → →5 → → →NULL 。
分别将链表中的元素按照头插的方式依次插入到新的链表中,最后得到的新链表即为逆置后的链表。
代码如下:
// 头插产生新链表,返回新链表的表头
ListNode* reverseList_1(ListNode* head) {
ListNode newList(0), /* 头结点不带数据的新链表表头 */
*pCur = head, /* 遍历原链表结点 */
*p = nullptr; /* 构建新链表结点 */
while (nullptr != pCur)
{
p = new ListNode(pCur->val); /* 新结点 */
p->next = newList.next; /* 头插 */
newList.next = p;
pCur = pCur->next; /* 指针移动,遍历原链表 */
}
return newList.next; /* 返回不带头的第一个数据结点 */
}
思路二:原地逆置
我们如果想在原链表的基础上进行逆置,那必定避免不了要断开链表结点之间的“链”,而对于单链表来说,结点之间的“链”一旦断开,我们就无法通过正常途径访问到后续链表的结点。
如下面的 2 与 3 之间的线索断开了,pCur指针就无法继续访问2之后的结点。
当然解决的方案也不是没有,如果我们在 2 、 3 断开前,先行保存好3的地址,链表在断开后依然可以通过我们提前保存好的地址进行访问。
比如,我们使用 pnext 保存 pCur 的下一个结点地址,在pCur将当前结点断开后,我们通过 pnext 即可找到断开之前原本在pCur之后的结点。
如此,我们便可以进行单链表的原地逆置操作了。
下面是详细步骤:
首先,我们需要三个指针,pCurr、pnext、ptail。
/*
* pCur 遍历链表结点
* pnext 保存链表断开处的下一结点,方便pCur遍历
* ptail 最初指向链表尾,操作完成后成为新链表的头
*/
- 最初状态下,我们pCur应该是指向链表头,而ptali直线链表尾部。 pCur准备向后遍历结点并逐个拆分,而ptail准备接收被pCur拆分的结点。
当pCur准备拆分结点1时,我们用pnext先保存好pCur的后继结点2的位置。
然后就可以拆分了。将1的指向改变,使之指向ptail所指向的结点。
接下来我们需要把ptail指针移动的1的位置上,以便于我们在ptail的位置插入新结点的时候是一种头插的状态。顺便把pCur指针移动到pnext的位置上,这样我们pCur又可以正常的遍历链表了。
接下来我们只需要继续重复以上操作即可,pnext继续保存着pCur的下一结点,为pCur的遍历提供帮助,ptail的位置总是不断的插入新的结点(被pCur断开的结点),直至pCur为NULL。
可以看到,当我们遍历值pCur为NULL时,此时的结点已经逆置完成,代码参考如下:
// 原地逆置
// 原地逆置会改变传入参数的指向,故这里传入的是指针类型的引用
void reverseList_2(ListNode* &head)
{
ListNode *pCur = head,
*pNext = nullptr,
*pTail = nullptr;
while (nullptr != pCur)
{
pNext = pCur->next; /* 保存下一结点 */
pCur->next = pTail; /* 断开当前结点,插pTail所在链表 */
pTail = pCur; /* pTail更新指向至链表的头 */
pCur = pNext; /* 从链表断裂出,继续遍历 */
}
head = pTail; /* 将head指向更新至逆置后的表头位置 */
}
相关题目链接可以前往LeetCode:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/ 。注:这里给出的函数是 void 无返回值,LeetCode给出的函数有返回值,如果将上述代码进行提交需在最后一行添加一句 return pTail;
。
2.k个一组反转链表递归和非递归实现
将给出的链表中的节点每k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。
要求空间复杂度 O(1)
例如:
给定的链表是1→2→3→4→5
对于 k=2, 你应该返回 2→1→4→3→5
对于 k=3, 你应该返回 3→2→1→4→5
因为之前刚实现了链表的逆置函数,因此我们可以将链表拆分成 长为k 的n个子链表,分别调用逆置函数,随后将各子链表拼接。
除此之外,我们也可以实现一个区间范围内的逆置函数,然后将链表分为以k个为一组的区间,调用区间逆置函数即可。
这里我们也可用两种思路解题,其一便是利用循环,其二便是通过递归的方式解决。
思路一:非递归分组逆置
首先,可以参照链表原地逆置的代码,实现区间范围内的逆置函数。参考代码如下:
// 区间逆置 [head, tail) ,注意这里的区间是'左闭右开'的。
// 功能:将区间内链表逆置,逆置后首地址由函数返回值带回,逆置后的尾地址指向tail
ListNode* reverseRangeList(ListNode* head, ListNode* tail = nullptr)
{
ListNode* pCur = head,
* pNext = nullptr,
* pTail = tail;
while (pCur != tail)
{
pNext = pCur->next; /* 保存下一结点 */
pCur->next = pTail; /* 断开当前结点,插pTail所在链表 */
pTail = pCur; /* pTail更新指向至链表的头 */
pCur = pNext; /* 从链表断裂出,继续遍历 */
}
return pTail;
}
/*
* 伪代码演示:
* 链表:head => [1->2->3->4->5->null]
* 指针:tail => list[4]
* 调用:res = reverseRangeList(head,tail)
* 结果:res => [3->2->1->4->5]
*/
此函数将会把 [head,tail) 内的链表进行逆置,并由函数返回逆置后的链表头地址。注意,这里的区间范围是不含tail的。如下图所示:
区间范围为 [1, 4] ,逆置后新的头结点3将由函数返回值带出。
注:需要注意的是,reverseRangeList( head, tail)
逆置函数只是区间范围内的逆置,区间外的“链”是没有影响的。
比如以下链表中,以K值为2,进行逆置。那么调用逆置函数后第一步逆置后的效果如下所示:
可以看到区间范围内确实逆置了,但逆置后的链表头与上一个结点(或头指针)之间的指向关系还需要处理一下。
对于函数调用 res = reverseRangeList(n1, n3);
的结果,res 接收了逆置后的头2,因此我们只需要进行 head = res;
即可修正区间头部与头指针之间的指向关系。而参数 n1 在函数调用之后指向了1位置,参数n3始终在结点3位置没有被修改。
而对于两个‘组’之间,我们使用一个 tail 指针保存上一段的结尾,这样在每次完成区间逆置后,就可以及时的修正各段之间的指向关系。
至此,我们已经模拟了函数所需要处理的全部过程,接下来就只需要把他们转换为代码即可,参考代码如下:
// 区间内逆置
ListNode* reverseRangeList(ListNode* head, ListNode* tail)
{
ListNode* pCur = head,
* pNext = nullptr,
* pTail = tail;
while (pCur != tail)
{
pNext = pCur->next; /* 保存下一结点 */
pCur->next = pTail; /* 断开当前结点,插pTail所在链表 */
pTail = pCur; /* pTail更新指向至链表的头 */
pCur = pNext; /* 从链表断裂出,继续遍历 */
}
return pTail;
}
// 以K为一组进行逆置
void reverseKGroup(ListNode* &head, int k)
{
int i = 0; /* 计数器,计算遍历的链表长度是否满足k */
ListNode* phead = head, /* phead保存子链表头部 */
* pCur = head, /* 遍历链表,在区间[phead,pCur)内逆置 */
* ptmp = nullptr, /* 拆分链表时,临时用于标记子链表的结束 */
* tail = nullptr;
while (nullptr != pCur)
{
i++;
pCur = pCur->next;
if (i == k) /* 可以进行拆分逆置了 */
{
ptmp = reverseRangeList(phead, pCur); /* 逆置[phead,。..,pCur) => ptmp->[...phead],pCur */
if (phead == head) head = ptmp; /* 第一次逆置,需要确定逆置后的头 */
else tail->next = ptmp; /* 把上一子链表的尾,与该子链表的头进行拼接 */
tail = phead; /* 记录该次逆置后的尾结点 */
phead = pCur;// phead=phead->next; /* phead更新至下一个子链表的头 */
i = 0; /* 计数器重新计数 */
}
}
}
相关题目链接可以前往LeetCode:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/submissions/ 。注:这里给出的函数是 void 无返回值,LeetCode给出的函数有返回值,如果将上述代码进行提交需在最后一行添加一句 return head;
。
思路二:递归分组逆置
递归的方法也是采用分组,将每一组进行一个逆置的思路进行的。这里就不再详细复述了,参考代码如下:
// 功能:将head的前k个结点逆置,逆置后首地址由函数返回,尾地址指向tail
// 几种函数返回NULL的情况:
// 当n大于head的最大长度时,返回NULL
// 当head为NULL时, 返回NULL
// 当head等于tail是, 返回NULL
ListNode* reverseList_r(ListNode* head, int k, ListNode* tail = nullptr)
{
if (nullptr != head)
{
ListNode* pnext = head->next;
head->next = tail; // 当前结点指向上一个结点
if (k != 1) // k==1时,是最后一个结点
{
return reverseList_r(pnext, k - 1, head);
}
}
return head;
}
ListNode* reverseKGroup_r(ListNode* head, int k)
{ /* pCur -遍历,试探是否足够k个结点逆置;
* ptail-接收分组后,下一组的头地址
*/
ListNode* pCur = head,* ptail = nullptr;
int i = 0;
for ( ; nullptr != pCur && i < k; pCur = pCur->next, i++); // 找到第k个结点
if (nullptr == pCur && i < k) return head; // 剩余结点不足以完成一次逆置
ptail = reverseKGroup_r(pCur,k); // 当前的尾,指向下段的首
return reverseList_r(head, k, ptail);
}
3.完整代码及测试用例
#include <iostream>
#include <vector>
using namespace std;
/*
* 链表结构
*/
struct ListNode {
int val;
ListNode* next;
ListNode(int x = 0) : val(x), next(NULL) {}
};
// 通过vector<int> 创建链表
ListNode* CreateListNode(vector<int>& list)
{
ListNode* tmpHead = new ListNode(0);/* 创建临时头 */
ListNode* ptr = tmpHead;
for (int item : list)
{
ptr->next = new ListNode(item); // 尾插
ptr = ptr->next;
}
ptr = tmpHead->next;
delete tmpHead; /* 销毁临时头 */
return ptr;
}
// 通过vector<int> 创建链表
void FreeList(ListNode* head)
{
/*
* head 遍历链表销毁每一个结点
* pCur 保存当前结点的下一个结点,方便head遍历
*/
ListNode* pCur = nullptr;
while (nullptr != head)
{
pCur = head->next; /* pCur保存下一结点 */
delete head; /* 每次销毁当前结点 */
head = pCur; /* 找到原 下一结点 */
}
}
// 打印链表
void ShowList(ListNode* head)
{
ListNode* pCur = head;
while (nullptr != pCur)
{
cout << pCur->val << "->";
pCur = pCur->next;
}
cout << "\b\b " << endl; /* 退两格 */
}
/*------------------------------------------------------------------
* 题目1:
* 给定单链表1->2->3->4->5
* 返回被反转后的链表 5->4->3->2->1;
*
*-------------------------------------------------------------------
*/
// 法一:头插产生新链表,返回新链表
// 说明,这里产生了新的链表,需要额外进行delete
ListNode* reverseList_1(ListNode* head) {
ListNode newList(0), /* 头结点不带数据的新链表表头 */
* pCur = head, /* 遍历原链表结点 */
* p = nullptr; /* 构建新链表结点 */
while (nullptr != pCur)
{
p = new ListNode(pCur->val); /* 新结点 */
p->next = newList.next; /* 头插 */
newList.next = p;
pCur = pCur->next; /* 指针移动,遍历原链表 */
}
return newList.next; /* 返回不带头的第一个数据结点 */
}
// 法二: 原地逆置
// 说明:逆置后,返回值指向头结点
ListNode* reverseList_2(ListNode * head)
{
ListNode* pCur = head,
* pNext = nullptr,
* pTail = nullptr;
while (nullptr != pCur)
{
pNext = pCur->next; /* 保存下一结点 */
pCur->next = pTail; /* 断开当前结点,插pTail所在链表 */
pTail = pCur; /* pTail更新指向至链表的头 */
pCur = pNext; /* 从链表断裂出,继续遍历 */
}
head = pTail; /* 将head指向更新至逆置后的表头位置 */
return head;
}
// 法三:递归
// 说明:将head所在链表逆置,头结点由函数返回值带出,尾结点指向tail
ListNode* reverseList_3(ListNode* head, ListNode* tail = nullptr)
{
if (nullptr != head)
{
ListNode* pnext = head->next;
head->next = tail; // 当前结点指向上一个结点
return reverseList_3(pnext, head);
}
return tail;
}
/*------------------------------------------------------------------
* 题目2:
* k个一组反转链表
* eg:
* 给定链表 1->2->3->4->5
* 对于 k=2, 返回 2→1→4→3→5
* 对于 k=3, 返回 3→2→1→4→5
*
*-------------------------------------------------------------------
*/
// 区间逆置,返回新链表头
/*
* 说明:head所在的链表在区间[head,tail)内被逆置。不包含tail
* 头结点由返回值带出,尾结点指向tail
*/
ListNode* reverseRangeList(ListNode* head, ListNode* tail = nullptr)
{
ListNode* pCur = head,
* pNext = nullptr,
* pTail = tail;
while (pCur != tail)
{
pNext = pCur->next; /* 保存下一结点 */
pCur->next = pTail; /* 断开当前结点,插pTail所在链表 */
pTail = pCur; /* pTail更新指向至链表的头 */
pCur = pNext; /* 从链表断裂出,继续遍历 */
}
return pTail;
}
// 非递归实现
ListNode* reverseKGroup(ListNode* head, int k)
{
int i = 0; /* 计数器,计算遍历的链表长度是否满足k */
ListNode* phead = head, /* phead保存子链表头部 */
* pCur = head, /* 遍历链表,在区间[phead,pCur)内逆置 */
* ptmp = nullptr, /* 拆分链表时,临时用于标记子链表的结束 */
* tail = nullptr;
while (nullptr != pCur)
{
i++;
pCur = pCur->next;
if (i == k) /* 可以进行拆分逆置了 */
{
ptmp = reverseRangeList(phead, pCur); /* 逆置[phead,。..,pCur) => ptmp->[...phead],pCur */
if (phead == head) head = ptmp; /* 第一次逆置,保存头结点的位置 */
else tail->next = ptmp; /* 把上一子链表的尾,与该子链表的头进行拼接 */
tail = phead; /* 记录该次逆置后的尾结点 */
phead = pCur;// phead=phead->next; /* phead更新至下一个子链表的头 */
i = 0; /* 计数器重新计数 */
}
}
return head;
}
// 思路二:递归实现
/*---------------------------------------------------------------------
// 功能:将head的前k个结点逆置,逆置后首地址由函数返回,尾地址指向tail
// 几种函数返回NULL的情况:
// 当n大于head的最大长度时,返回NULL
// 当head为NULL时, 返回NULL
// 当head等于tail是, 返回NULL
*/
ListNode* reverseList_r(ListNode* head, int k, ListNode* tail = nullptr)
{
if (nullptr != head)
{
ListNode* pnext = head->next;
head->next = tail; // 当前结点指向上一个结点
if (k != 1) // k==1时,是最后一个结点
{
return reverseList_r(pnext, k - 1, head);
}
}
return head;
}
// 递归实现k个一组翻转链表
ListNode* reverseKGroup_r(ListNode* head, int k)
{ /* pCur -遍历,试探是否足够k个结点逆置;
* ptail-接收分组后,下一组的头地址
*/
ListNode* pCur = head,* ptail = nullptr;
int i = 0;
for ( ; nullptr != pCur && i < k; pCur = pCur->next, i++); // 找到第k个结点
if (nullptr == pCur && i < k) return head; // 剩余结点不足以完成一次逆置
ptail = reverseKGroup_r(pCur,k); // 当前的尾,指向下段的首
return reverseList_r(head, k, ptail);
}
int main()
{
/*
* 测试:题目一,链表逆置
*/
vector<int> list = { 1,2,3,4,5 };
ListNode* head = CreateListNode(list); // 创建链表
auto res1 = reverseList_1(head); // head:1,2,3,4,5 res1:5,4,3,2,1
ShowList(res1);
auto res2 = reverseList_2(head); // head:1,2,3,4,5 => res:5,4,3,2,1
ShowList(res2);
head = res2; // head重新指向头结点
auto res3 = reverseList_1(head); // head:5,4,3,2,1 => res3:1,2,3,4,5
ShowList(res3);
FreeList(res1);
FreeList(res3);
/*
* 测试:题目二,K个为一组翻转链表
*/
// 非递归版本测试
vector<int> list2 = { 1,2,3,4,5 };
ListNode* head2 = CreateListNode(list2); // 创建链表
auto ret = reverseKGroup(head2, 2); // 2个一组翻转,=> 2,1,4,3,5
ShowList(ret);
reverseKGroup(ret, 2); // 恢复
auto ret2 = reverseKGroup(head2, 3); // 3个一组翻转,=> 3,2,1,4,5
ShowList(ret2);
reverseKGroup(ret2, 3); // 恢复
reverseKGroup(head2, 100); // 测试:若K大于链表长度,则不进行翻转
FreeList(head2);
// 递归版本测试
vector<int> list22 = { 1,2,3,4,5,6,7,8,9,10 };
ListNode* head22 = CreateListNode(list22); // 创建链表
auto rer2_1 = reverseKGroup_r(head22, 3); // 3个一组翻转
ShowList(rer2_1);
reverseKGroup_r(rer2_1, 3); // 恢复
auto ret2_2 = reverseKGroup_r(head22, 4); // 4个一组翻转
ShowList(ret2_2);
reverseKGroup_r(ret2_2, 4); // 恢复
reverseKGroup_r(head22, 100); // 测试:若K大于链表长度,则不进行翻转
FreeList(head22);
return 0;
}