嵌入式面试热点:链表反转——四种单链表反转方法(C语言)

链表是面试里面经常涉及到的考点,因为链表的结构相比于 HashmapHashtableConcurrenthashmap 或者图等数据结构简单许多,对于后者更多面试的侧重点在于其底层实现。比如 HashmapEntry<k,v> 等操作、如何扩容、容量的设定等。链表的考察更侧重于代码的书写和思路的形成。虽然说,链表的结构简单,但是涉及到指针的操作,容易引申出一些挑战性的考题,其中也牵涉到诸多小的细结的考虑,更能看出代码书写的能力和功底。

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

在这里插入图片描述

链表的存取从头指针(Head)开始,头指针表示链表中第一个结点的存储位置。同时由于最后一个结点没有后续数据,则线性链表中最后一个结点的指针为“空”(NULL)。

在这里插入图片描述

反转链表,又可以称为翻转或逆置链表,它们表达的是同一个意思。把上图所示的有序链表反转后,得到新的有序链表。

在这里插入图片描述

常用的实现链表反转的方案有四种,这里分别将它们称为迭代反转法、递归反转法、头插法和原地逆置法。

关于链表的增删改查问题,我在另一篇博客《【面试分享】嵌入式面试题常考难点之关于单链表的增删改查》中做了详细的讨论,欢迎有需要的小伙伴查阅。


一、迭代反转法

从当前链表的第一个结点开始遍历整个链表,期间逐个改变所遍历的结点的指针域,令其指向前一个结点。具体实现方法需要借助三个结点指针,如下图,分别把三个指针命名为 begmidend

在这里插入图片描述

遍历链表的过程就是三个指针一起向链表结尾逐步偏移,直到 end 指针指向 NULL 为止。这个过程中,由mid指针改变它所指向的结点的指针域,令其指向beg所指向的结点。如下图:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

最后只需改变Head头指针的指向,令其和mid同向,就实现了链表的反转。

代码实现如下:

//Demo1.c

#include <stdio.h>

typedef struct Node
{
	int data;
	struct Node *next;
}NODE;

void printLink(NODE *phead)
{
	while (phead != NULL) {
		printf("%d ", phead->data);
		phead = phead->next;
	}
	printf("\n");
}

NODE *iteration(NODE *phead)
{
	if (phead == NULL || phead->next == NULL)       //判断头指针是否指向链表头结点,或者链表是否存在
		return phead;

	NODE *beg = NULL;
	NODE *mid = phead;
	NODE *end = phead->next;

	while (1) {
		mid->next = beg;	//修改mid指向的结点的指针域,令其指向beg所指的结点
		if (end == NULL)	//判断end是否为空,如果为空表示链表已经遍历完毕
			break;
		beg = mid;			//整体偏移三个指针
		mid = end;
		end = end->next;
	}
	phead = mid;			//遍历结束后,链表完成反转,修改头指针指向新的链表头结点

	return phead;
}

int main()
{
	NODE t5 = {5, NULL};	//只为做验证,用了静态创建链表的方式,实际编程中不会用这种方式
	NODE t4 = {4, &t5};
	NODE t3 = {3, &t4};
	NODE t2 = {2, &t3};
	NODE t1 = {1, &t2};

	NODE *head = &t1;	//创建头指针,用此方法反转链表可以不需要用到头指针,后面的函数的形参可以直接使用&t1,结果是一样的

	printf("链表反转前:\n");
	printLink(head);
	printf("链表反转后:\n");
	head = iteration(head);
	printLink(head);

	return 0;
}

运行结果:

在这里插入图片描述

二、递归反转法

递归反转法的实现思想是从链表的尾结点开始,依次向前遍历,遍历过程依次改变各结点的指向,即令其指向前一个结点。这种方法一般会在函数中建立一个新的头指针,通过层层递进的方式找到链表尾结点,然后将新的头指针指向尾结点,再层层返回把每个遍历后的结点都指向上一个结点,最后令原先的头结点指向 NULL,使其成为链表反转后,新链表的尾结点,并返回新的头指针。在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码实现如下:

// Demo2.c
// 代码与 Demo1.c 重复,这里只展示代码片断,替换到 Demo1.c 效果一致。

NODE *iteration(NODE *phead)
{
	if (phead == NULL || phead->next == NULL) {
		return phead;
	} else {
		/*一直递归,直到找到链表中最后一个结点
		*当逐层退出时,new_head一直指向原链表中最后一个结点
		*而函数中phead指针在每层退出后,都指向上一个结点
		*/
		NODE *new_head = iteration(phead->next);
		phead->next->next = phead;				//每退出一层,都需要改变phead->next结点指针域的指向
		phead->next = NULL;						//同时令phead所指结点的指针域为NULL
		return new_head;
	}
}

三、头插法

头插法一般用于链表的创建,这里用的头插法大致相同,就是将原链表的结点从链表头逐个取下,并用头插法创建链表的方式重新建立一个反向排序的链表,已达到反转链表的效果。具体做法如下图:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

代码实现如下:

// Demo3.c

NODE *iteration(NODE *phead)
{
	NODE *new_head = NULL;		//创建一个新的头指针
	NODE *temp = NULL;			//临时的结点指针,用于转接

	if (phead == NULL || phead->next == NULL)
		return phead;

	while (phead != NULL) {
		temp = phead;			//旧链表头指针指向的结点放进temp
		phead = phead->next;	//头指针指向下一个结点,相当于上个结点已经剔除于链表之外,
		temp->next = new_head;	//被剔除的结点从新链表头部插入
		new_head = temp;		//新链表的头指针指向新加入的结点			
	}

	return new_head;
}

四、原地逆置法

这个方法和头插法类似,区别在于头插法是通过摘除旧的结点重新排列成新的链表,而原地逆置法则是直接对原链表做修改。这就需要借助两个结点指针进行链表结点的标记,再通过遍历链表逐个逆置结点。具体过程如下图:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

代码实现如下:

// Demo4.c

NODE *iteration(NODE *phead)
{
	NODE *beg = NULL;
	NODE *end = NULL;
	
	if (phead == NULL || phead->next == NULL)
        	return phead;

	beg = phead;
	end = phead->next;
	while (end != NULL) {
		beg->next = end->next; 		//将end所指向的结点拿出
		end->next = phead;			//将end所指向的结点放在链表头
		phead = end;				//头指针指向现在的链表头
		end = beg->next;			//调整end的指向,指向下一个要拿出的结点
	}

	return phead;
}

总结

  1. 使用迭代反转法实现时,初始状态忽略头结点(直接将 mid 指向首元结点),仅需在最后一步将头结点的 next 改为和 mid 同向即可;
  2. 使用头插法或者原地逆置法实现时,仅需将要插入的结点插入到头结点之前即可;
  3. 递归法并不适用反转有头结点的链表(但并非不能实现),该方法更适用于反转无头结点的链表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Grayson Zheng

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

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

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

打赏作者

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

抵扣说明:

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

余额充值