常见的单链表面试题

要做单链表的面试题,首先得要学会实现单链表,无头单向非循环链表的实现,有需要的童鞋还可以看一下带头双向循环链表的实现

  1. 从尾到头打印单链表

思路:采用递归的方法,当plist->next指向的节点为NULL时,递归停止,开始反向依次输出链表上的节点

void TailToHead(ListNode *plist)
{
	assert(plist);
	if(plist == NULL)
		return;
	TailToHead(plist->next);
	printf("%d->",plist->data);
}
  1. 删除一个无头单链表的非尾节点(不能遍历链表)

思路:

  • 将要删除的节点定义为cur,要删除节点的下一节点定义为next;
  • 将要删除节点的下一节点的data赋给要删除的节点;
  • 将要删除节点的下一节点的next赋给删除节点的下一节点的next;
  • free掉要删除节点的下一节点

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

void DeleteNoHead(ListNode* pos)
{
	assert(pos);
	if(pos == NULL)
		return;
	ListNode* cur = pos;
	ListNode* next = pos->next;
		cur->data = next->data;
		cur->next = next->next;
	free(next);
	next = NULL;
}

感悟:要求删除3,实际删除的是5而不是3,原因在于:在执行删除操作之前已经将5所在的节点赋给3,而所谓的删除不过是将要删除的节点用下一节点覆盖而已

  1. 在无头单链表的一个节点前插入一个节点(不能遍历链表)

思路:

  • 创建一个值为 pos节点的值 的新节点;
  • 把新节点插入到pos的后面;
  • 把要插入的节点的值赋给pos

在这里插入图片描述

void InsertNoHead(ListNode* pos, LDataType data)
{
	if (pos == NULL)
		return;
	ListNode* node = BuyListNode(pos->data);
	pos->next = node;
	node->next = pos->next;
	pos->data = data;
}
  1. 单链表实现约瑟夫环(JosephCircle)

首先看一下什么约瑟夫环问题?

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

思路:

  • 把头指针定义为cur,遍历链表
  • 设定k值,循环到k值时,记录当前位置的下一个节点为next
  • 把next节点非给cur节点,释放next节点

在这里插入图片描述

ListNode* JosephCircle(ListNode* head, LDataType k)
{
	ListNode* cur = head;
	while (cur->next)
	{
		while (k--)
		{
			cur = cur->next;
		}
		ListNode* next = cur->next;
		cur->data = next->data;
		cur->next = next->next;
		free(next);
	}
	return cur;
}
  1. 逆置/反转单链表

思路:

  • 定义三个节点,分别为当前节点,当前节点的下一个节点,逆置后新链表的头节点
  • 把头结点的 next 置空,再逐个头插入新链表

在这里插入图片描述

void Reverse(ListNode* head)
{
	ListNode* cur = head;
	ListNode *_next = cur->next;
	ListNode *newHead = NULL;
	cur->next = NULL;
	while (_next != NULL)
	{
		newHead = _next;
		newHead->next = cur->next;
		cur->next = newHead;
		_next = _next->next;
	}
}
  1. 单链表排序(冒泡排序)

思路:

  • 单链表排序和普通排序的思路是一致的,遍历链表,交换值
void BubbleSort(ListNode* head)
{
	ListNode* cur = head;
	ListNode* _next = cur->next;
	for (; cur != NULL; cur = _next)
	{
		size_t flag = 0;
		while (_next)
		{
			if (cur->data > _next->data)
         	{
         		size_t tmp = cur->data;
         		cur->data = _next->data;
         		_next->data = tmp;
				flag++;
			}
			_next = _next->next;
			cur = cur->next;
		}
		if (flag == 1)
			break;
	}
}
  1. 合并两个有序链表,合并后依然有序

思路:

  • 新建一个节点作为合并完的链表的头节点
  • 若其中一条链表为空,直接返回另一条链表
  • 找到两条链表头节点中较小的作为新链表的头节点
  • 遍历两条链表,按序将小节点连接在新链表上
  • 如果在遍历过程中,其中一条链表已经遍历完,则直接把另一条链表的剩余部分连接在新链表上
ListNode* ListMerge(ListNode* list1, ListNode* list2)
{
	ListNode* list = NULL;//新链表的头节点
	assert(list1);
	assert(list2);
	//如果其中一条链表为空,直接返回另一条链表
	if (list1 == NULL)
		return list2;
	if (list2 == NULL)
		return list1;
	//为合并后的新链表找到一个头节点
	if (list1->data > list2->data)
	{
		list = list2;
		list2 = list2->next;
	}
	else
	{
		list = list1;
		list1 = list1->next;
	}
	ListNode* cur = list;
	//找到两条链表中 较小的节点连接在新链表上
	while (list1 && list2)
	{
		if (list1->data > list2->data)
		{
			cur->next = list2;
			cur = cur->next;
			list2 = list2->next;
		}
		else
		{
			cur->next = list1;
			cur = cur->next;
			list1 = list1->next;
		}
	}
	//如果在遍历过程中,其中一条链表已经遍历完,
	//则直接把另一条链表的剩余部分连接在新链表上
	if (list1->next == NULL)
		cur->next = list2;
	if (list2->next == NULL)
		cur->next = list2;
	return list;
}
  1. 查找单链表的中间节点,要求只能遍历一次链表

思路:

  • 定义两个节点,一个快节点一个慢节点
  • 快的每次走两步,慢的走一步
  • 当快节点的next为空时,返回慢节点

在这里插入图片描述

ListNode* FindMidNode(ListNode* head)
{
	ListNode* slow = head;
	ListNode* fast = head;
	assert(head);
	while (fast && fast->next && fast->next->next)
	{
		fast = fast->next->next;
		slow = slow->next;
	}
	return slow;
}
  1. 查找单链表的倒数第K个节点,要求只能遍历一次链表

思路:

  • 定义两个节点,一个快节点一个慢节点
  • 先让快节点走K步,然后两个节点一起走
  • 当快节点的next为空时,返回慢节点

在这里插入图片描述

ListNode* FindKNode(ListNode* head, LDataType k)
{
	ListNode* fast = head;
	ListNode* slow = head;
	if (head == NULL)
		return NULL;
	while (fast && fast->next)
	{
		if(k--)
		{
			fast = fast->next;
		}
		fast = fast->next;
		slow = slow->next;
	}
	return slow;
}
  1. 删除链表的倒数第K个节点

思路:

  • 首先找到倒数第K个节点
  • 遍历链表找到倒数第K个节点的前一个节点为cur
  • 把cur->next指向倒数第K个节点的下一个节点,释放倒数第K个节点

在这里插入图片描述

void DeleteKNode(ListNode* head, LDataType k)
{
	ListNode* kNode = FindKNode(head, k);
	ListNode* cur = head;
	while (cur->next != kNode)
	{
		cur = cur->next;
	}
	cur->next = kNode->next;
	free(kNode);
}
  1. 判断单链表是否带环

思路:

  • 定义快慢指针,快指针走两步,慢指针走一步
  • 当快指针等于慢指针时,说明链表带环,返回当前位置的节点
  • 如果链表不带环,快指针先到头,此时结束循环,返回空

在这里插入图片描述

ListNode* IsCycle(ListNode* head)
{
	ListNode* fast = head;
	ListNode* slow = head;
	//链表为空 | 链表中只有一个节点
	if (head == NULL || head->next == NULL)
		return NULL;
	//快节点到头
	while (fast && fast->next)
	{
		fast = fast->next->next;
		slow = slow->next;
		if (fast == slow)
		{
			return slow;
		}
	}
	return NULL;
}

若带环,求入口点

思路:
从链表头结点到入口节点的长度 = 相遇节点到入口节点的长度

推导过程:

设环的长度为:C
从链表头结点到入口节点的长度为:L
入口节点到相遇节点的长度为:X
快指针在环中走的圈数:n
快指针走的步数是:L+nC+X
慢指针走的步数是:L+X
根据快指针走的步数是慢指针的两倍可得关系:L+nC+X = 2(L+X)
化简后可得:nC = L+X (n=1,2,3…)
令n = 1,则有C = L + X,即从链表头结点到入口节点的长度 = 相遇节点到入口节点的长度

在这里插入图片描述

ListNode* GetEntry(ListNode* head, ListNode* MeetNode)
{
	while (head != MeetNode)
	{
		head = head->next;
		MeetNode = MeetNode->next;
	}
	return MeetNode;
}

求环的长度

思路:
从相遇节点的下一节点cur开始,遍历环,只要cur不等于MeetNode,长度 + 1

LDataType CycleLength(ListNode* MeetNode)
{
	ListNode* cur = MeetNode->next;
	LDataType length = 1;
	while (cur != MeetNode)
	{
		cur = cur->next;
		++length;
	}
	return length;
}
  1. 判断两个链表是否相交(链表不带环)

思路:
遍历两个链表到各自结束,当最后一个节点相等时,表示两个链表相交
在这里插入图片描述

LDataType IsCross_NoCycle(ListNode* list1, ListNode* list2)
{
	if (list1 == NULL || list2 == NULL)
		return -1;
	while(list1->next)
	{
		list1 = list1->next;
	}
	while(list2->next)
	{
		list2 = list2->next;
	}
	if (list1 == list2)
		return 1;
}

若相交,求交点

思路:

  • 先求出两个链表各自的长度
  • 求出两个链表长度的差值为gap
  • 长的那个,先走gap步,然后两个一起走
  • 当两者相等时,返回相等的这个节点,即交点
ListNode* GetNode_NoCycle(ListNode* list1, ListNode* list2)
{
	ListNode* Llist = list1;
	ListNode* Slist = list2;
	LDataType length1 = 0;
	LDataType length2 = 0;
	//求两个链表各自的长度
	while (Llist->next)
	{
		length1++;
	}
	while (Slist->next)
	{
		length2++;
	}
	//找到较长的那个链表
	if (length1 > length2)
	{
		Llist = list1;
		Slist = list2;
	}
	//两个链表长度的差值
	LDataType gap = abs(length1 - length2);
	//长的先走gap步
	while (gap--)
	{
		Llist = Llist->next;
	}
	//两个一起走,相等时退出循环,此时的节点即交点
	while (Llist != Slist)
	{
		Llist = Llist->next;
		Slist = Slist->next;
	}
	return Slist;
}
  1. 判断两个链表是否相交,若相交,求交点(链表可能带环)

我们知道,如果两个链表相交,则意味着它们最后的NULL指针指向的是同一个地方,即末节点必然相交,所以当其中一个链表有环时,这两个链表必然不相交。因为当链表有环时,环必须是相交的,否则不能满足链表相交的前提(尾节点相交)。

下面看一下两个链表相交的几种情况:
在这里插入图片描述

思路:

  • 先判断链表是否带环
  • 有一个不带环,则不相交
  • 两个都带环,入口点相同时,先把环的部分切掉,就变成了两个不带环链表求交点的问题
  • 两个都带环,入口点不同时,从第一条链表快慢指针相遇的节点开始,遍历环;如果找到第二条链表快慢指针相遇的节点,表示共用环,返回任一入口点即可(事实上,此时这个环上的任一节点都可作为相交点)
ListNode* IsCross_Cycle(ListNode* list1, ListNode* list2)
{
	//判断链表是否带环
	ListNode* MeetNode1 = IsCycle(list1);
	ListNode* MeetNode2 = IsCycle(list2);
	//其中一个不带环,不相交
	if (MeetNode1 == NULL || MeetNode2 == NULL)
	{
		return NULL;
	}

	//两个都带环,求出各自的入口点
	ListNode* entry1 = GetEntry(list1, MeetNode1);
	ListNode* entry2 = GetEntry(list2, MeetNode2);
	//入口点相同
	if (entry1 == entry2)
	{
		//先把环切掉,直接求出交点
		entry1->next = NULL;
		entry2->next = NULL;
		return GetNodeCross(list1, list2);
	}
	
	//入口点不同,从MeetNode1开始遍历链表,如果找到和MeetNode2相等的节点,
	//则证明共用环,即两个链表相交,此时返回任一入口点即可
	ListNode* cur = MeetNode1->next;
	while(cur!=MeetNode1 && cur != MeetNode2)
	{
		cur = cur->next;
	}
	if (cur = MeetNode2)
		return entry1;

	return NULL;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值