链表操作题:链表逆置、K 个一组翻转链表

算法题集 专栏收录该内容
32 篇文章 0 订阅

链表结构:

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;		/* 返回不带头的第一个数据结点 */
}
思路二:原地逆置

我们如果想在原链表的基础上进行逆置,那必定避免不了要断开链表结点之间的“链”,而对于单链表来说,结点之间的“链”一旦断开,我们就无法通过正常途径访问到后续链表的结点。

如下面的 23 之间的线索断开了,pCur指针就无法继续访问2之后的结点。

在这里插入图片描述
当然解决的方案也不是没有,如果我们在 23 断开前,先行保存好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;
}

  • 3
    点赞
  • 2
    评论
  • 4
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 2 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

我叫RT

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值