单链表常见面试题

      数据结构和算法,是我们程序设计最重要的两大元素,可以说,我们的编程,都是在选择和设计合适的数据结构来存放数据,然后再用合适的算法来处理这些数据。

      在面试中,最经常被提及的就是链表,因为它简单,但又因为需要对指针进行操作,凡是涉及到指针的,都需要我们具有良好的编程基础才能确保代码没有任何错误。

      链表是一种动态的数据结构,因为在创建链表时,我们不需要知道链表的长度,当插入一个结点时,只需要为该结点分配内存,然后调整指针的指向来确保新结点被连接到链表中。所以,它不像数组,内存是一次性分配完毕的,而是每添加一个结点分配一次内存。正是因为这点,所以它没有闲置的内存,比起数组,空间效率更高。

下面列举几个面试中常见的单链表实现的面试题:

1、不遍历链表删除非尾节点

      通常我们做链表节点的删除时,都需要知道目标节点的前驱,让前驱的next指针指向目标节点的next,然后释放目标节点。但当我们不知道目标节点的前驱时,就不能通过这种方法来实现目标节点的删除,此时就需要用后一个节点覆盖目标节点,然后删除后一个节点的方法来实现目标节点的删除,即将后一个节点的data复制到目标节点中,让目标节点的next指向后一个节点的next,然后释放后一个节点,完成目标节点的删除。

代码如下:

void RemoveNodeNotTail(SListNode **ppFirst, SListNode *pos)
{
	SListNode *pDel = pos;
	SListNode *pNode = pDel->pNext;

	assert(pos);
	
	pos->data = pNode->data;
	pos->pNext = pNode->pNext;
	free(pNode);

}

2、遍历一次,找到中间节点

       只遍历一遍要找到中间节点,我们可以定义一快一慢两个指针pFast和pSlow,让他们同时从链表的头结点开始向后遍历,慢指针一次向后走一步,快指针一次向后走两步,当pFast == NULL时,pSlow所指向的位置即为链表的中间节点。

代码如下:

SListNode* FindMid(SListNode *pFirst)
{
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst->pNext;
	assert(pFirst);
	while(pFast)
	{
		pSlow = pSlow->pNext;
		pFast = pFast->pNext->pNext;
	}
	pSlow->pNext = NULL;

	return pSlow;
}

3、遍历一次,找到倒数第 k 个结点(k从1开始)

       遍历一次找到倒数第k个节点,原理同遍历一次找到中间节点相同,定义一快一慢两个指针pFast和pSlow,都指向链表的头结点,让pFast先向后遍历k-1步,然后让两个指针同时向后遍历,当pFast == NULL时,pSlow所指向的位置即为倒数第k个节点所在的位置。

代码如下:

SListNode* FindK(SListNode *pFirst, int k)
{
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst;
	assert(pFirst);
	while(k)
	{
		pFast = pFast->pNext;
		k--;
	}
	while(pFast)
	{
		pSlow = pSlow->pNext;
		pFast = pFast->pNext;
	}

	return pSlow;
}

4、遍历一次,删除倒数第 k 个结点(k从1开始),不能用替换删除法

上一道题我们已经能找到倒数第k个节点,但要删除它并且不能用替换法,那么需要我们在遍历时找到第k-1个节点,即k的前一个节点,然后将第k-1个节点的next指向第k个节点的next,删除第k个节点即可。

代码如下:

void RemoveK(SListNode *pFirst, int k)
{
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst;
	SListNode *pDel;

	while(k+1)
	{
		pFast = pFast->pNext;
		k--;
	}

	while(pFast)
	{
		pSlow = pSlow->pNext;
		pFast = pFast->pNext;
	}

	pDel = pSlow->pNext;
	pSlow->pNext = pDel->pNext;
	free(pDel);
}

5、约瑟夫环

       约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。 

这里我们以k = 3为例,大致过程如图所示:

《单链表常见面试题》

以红色箭头处开始计数,数到第三个时删除,以此类推,当只剩下一个元素时停止。

当我们用链表实现时,首先要将一个单链表构造成一个环,此时我们只需要遍历一次链表,找到链表的最后一个节点,让最后一个节点的next指向第一个节点即可。构成环后,从第一个节点开始向后遍历,没走三个节点,做一次删除,直至链表只剩下一个人节点时停止,返回最后一个节点。

代码如下:

SListNode * JocephCircle(SListNode *pFirst, int k)
{
	SListNode *pLast = pFirst;
	SListNode *pNode = pFirst;
	SListNode *pKill;

	for(pLast; pLast->pNext != NULL; pLast=pLast->pNext)
	{
		;
	}
	pLast->pNext = pFirst;  //将单链表构造成环

	while(pNode->pNext != pNode)
	{
		int i = 0;
		for(pNode; pNode->pNext != NULL; )
		{
			pNode=pNode->pNext;
			i++;
			if(i == k-2)
			{
				break;
			}
		}
		pKill = pNode->pNext;
		pNode->pNext = pKill->pNext;
		pNode = pNode->pNext;
		free(pKill);
	}

	pNode->pNext = NULL;
	return pNode;

}

6、冒泡排序

冒泡排序(Bubble Sort)是最经典也是最简单的排序算法之一,步骤为:

1)、比较相邻的元素。如果第一个比第二个大,就交换他们两个。

2)、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

3)、针对所有的元素重复以上的步骤,除了最后一个。

4)、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

单链表实现冒泡排序代码如下:

void BubbleSort(SListNode *pFirst)
{
	SListNode *pNode = pFirst;
	SListNode *pCpr = pNode->pNext;
	SListNode *pCheck ;
	SListNode *pEnd = (SListNode*)malloc(sizeof(SListNode));
	int set; 
	int flag = 1;
	pEnd = NULL;
	while(flag)
	{
		pNode = pFirst;
		pCpr = pNode->pNext;
		pCheck = pNode;
		while(pCpr != pEnd)
		{
			if(pNode->data <= pCpr->data)
			{
				pNode = pCpr;
				pCpr = pNode->pNext;
			}
			else
			{
				set = pNode->data;
				pNode->data = pCpr->data;
				pCpr->data = set;
				pNode = pCpr;
				pCpr = pNode->pNext;
			}
		}
		pEnd = pNode;
		for(pCheck; pCheck->pNext != NULL; pCheck=pCheck->pNext)  //检查是否所有元素都不需要交换
		{
			if(pCheck->data > pCheck->pNext->data)
			{
				flag = 1;
				break;
			}
			flag = 0;
		}
	}
}

7、合并两个有序链表

假设有两个有序的链表,先需要将两个链表合并成一个链表,并使合并后的链表依然有序。

这时我们可以重新定义一个新的链表,然后分别从两个需要合并的链表中拿出第一节点,比较他们的大小,将较小的节点拿出尾插入新链表,然后重复此步骤,直至有一个链表为空,最后将不为空的链表的左右节点尾插入新链表即可。

代码如下:

SListNode* MergeOrderedList(SListNode *p1First, SListNode *p2First)
{
	SListNode *pFirst;
	SListNode *pNode = NULL;
	while(p1First && p2First)   //当两个链表都不为空时,比较两个链表第一个节点元素的大小
	{
		if(pNode == NULL)
		{
			if(p1First->data <= p2First->data)
			{
				pNode = p1First;
				p1First = p1First->pNext;
			}
			else
			{
				pNode = p2First;
				p2First = p2First->pNext;
			}
			pFirst = pNode;
		}
		if((pNode != NULL)&&(p1First->data <= p2First->data))
		{
			pNode->pNext = p1First;
			p1First = p1First->pNext;
			pNode = pNode->pNext;
		}
		else if((pNode != NULL)&&(p1First->data > p2First->data))
		{
			pNode->pNext = p2First;
			p2First = p2First->pNext;
			pNode = pNode->pNext;
		}
	}
	if(p1First)  //当第二个链表为空,第一个链表不为空时,将第一个链表的剩余节点尾插入新链表
	{
		for(p1First; p1First != NULL; p1First=p1First->pNext)
		{
			pNode->pNext = p1First;
			pNode = pNode->pNext;
		}
	}
	else  //当第一个链表为空,第二个链表不为空时,将地二个链表的剩余节点元素尾插入新链表
	{
		for(p2First; p2First != NULL; p2First=p2First->pNext)
		{
			pNode->pNext = p2First;
			pNode = pNode->pNext;
		}
	}

	return pFirst;
}

8、判断链表是否带环;若带环,求环的长度和入口点

要判断一个链表是否带环,只需要定义一快一慢两个指针pFast和pSlow,让他们同时从链表的头结点开始向后遍历,慢指针一次向后走一步,快指针一次向后走两步,若两个指针最终相遇,则链表带环;若快指针遍历到NULL,则表明链表不带环。

要求环的长度,首先要确定环的入口点:

《单链表常见面试题》

当pSlow和pFast第一次相遇时,pSlow走过的路程为l + a,pFast走过的路程为l + 2*a + c,又因为我们知道pFast的速度为pSlow的两倍,因此可得出l = c,此时我们只需要定义一个pMeet指针从头节点出发开始遍历,pSlow从相遇点出发继续遍历,当两个指针相遇时,所指向的节点即为环的入口节点。

知道了环的入口节点,那么环的长度就十分好求了。用pMeet指针记录环的入口节点,pSlow从入口节点出发向后遍历,当两个指针再次相遇时,pSlow所走过的路径即为环的长度。

代码如下:

void IsLoopSList(SListNode *pFirst)
{
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst->pNext->pNext;
	SListNode *pEnter = (SListNode*)malloc(sizeof(SListNode));
	int flag = 1;
	int LoopLen = 0;
	while((pSlow != pFast) && (pFast != NULL))
	{
		if(flag)
		{
			pSlow = pSlow->pNext;
			flag = 0;
			continue;
		}
		pSlow = pSlow->pNext;
		pFast = pFast->pNext->pNext;
	}
	
	if(pFast != NULL)
	{
		SListNode *pMeet = pSlow;
		SListNode *pNode = pFirst;
		while(pNode != pMeet)
		{
			pNode = pNode->pNext;
			pMeet = pMeet->pNext;
		}
		pEnter->data = pMeet->data;
		pEnter->pNext = NULL;
		printf("该链表带环,环的入口点为:> ");
		PrintSList(pEnter);

		pFast = pFast->pNext->pNext;
		while(pSlow != pFast)
		{
			if(LoopLen == 0)
			{
				pSlow = pSlow->pNext;
				LoopLen++;
				continue;
			}
			pSlow = pSlow->pNext;
			pFast = pFast->pNext->pNext;
			LoopLen++;
		}
		printf("环的长度为:> %d\n", LoopLen);
		return;
	}

	printf("该链表不带环\n");

}

9、判断两个链表是否相交;相交则求交点(链表不带环)

若两个不带环的链表相交,则他们的尾节点必相同;若要求交点,则需要比较两个链表的长度,让较长的链表先向后遍历至和较短的链表长度相等,然后两个链表同时向后遍历,并比较节点是否相同,当遇到第一个相同的节点时,则为两个链表的交点。

代码如下:

int SListLen(SListNode *pFirst)  //求链表长度
{
	int len = 0;
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst->pNext->pNext;
	SListNode *pEnter = (SListNode*)malloc(sizeof(SListNode));
	int flag = 1;
	pEnter = NULL;
	while((pSlow != pFast) && (pFast != NULL))
	{
		if(flag)
		{
			pSlow = pSlow->pNext;
			flag = 0;
			continue;
		}
		if(pFast->pNext == NULL)
		{
			pFast = NULL;
			break;
		}
		pSlow = pSlow->pNext;
		pFast = pFast->pNext->pNext;
	}
	
	if(pFast != NULL)
	{
		SListNode *pMeet = pSlow;
		SListNode *pNode = pFirst;
		while(pNode != pMeet)
		{
			pNode = pNode->pNext;
			pMeet = pMeet->pNext;
		}
	pEnter->data = pMeet->data;
	}

	while(pFirst  != NULL || pFirst != pEnter)
	{
		len++;
		pFirst = pFirst->pNext;
	}
	return len;
}

void IsCross(SListNode *p1First, SListNode *p2First)  //判断链表是否相交并求交点
{
	SListNode *p1Node = p1First;
	SListNode *p2Node = p2First;
	SListNode *pCross = (SListNode*)malloc(sizeof(SListNode));
	int len1 = SListLen(p1First);
	int len2 = SListLen(p2First);

	if(len1 > len2)
	{
		int k = len1 - len2;
		while(k)
		{
			p1Node = p1Node->pNext;
			k--;
		}
	}
	if(len2 > len1)
	{
		int k = len2- len1;
		while(k)
		{
			p2Node = p2Node->pNext;
			k--;
		}
	}

	while(p1Node != p2Node && p1Node != NULL && p2Node != NULL)
	{
		p1Node = p1Node->pNext;
		p2Node = p2Node->pNext;
	}
	if(p1Node != NULL && p2Node != NULL)
	{
		pCross->data = p1Node->data;
		pCross->pNext = NULL;
		printf("两个链表相交,交点为:> ");
		PrintSList(pCross);
		return;
	}

	printf("两个链表不相交\n");
	return;
}

10、判断两个链表是否相交,若相交则求交点(链表可能带环)

若有两个链表,则他们的带环情况有以下三种可能:

(1)两个链表都不带环

(2)一个链表带环一个链表不带环

(3)两个链表都带环

若出现(1)情况,则只需调用我们刚写的不带环链表判断相交的函数即可;若出现(2)情况,则两个链表必定不相交;若出现(3)情况,则较为复杂,我们在这里重点讨论第(3)种情况下的判断。

当两个链表都带环时,可能有以下三种情况:

《单链表常见面试题》

当出现①情况时,两个链表不相交。

当出现②情况时,两个链表的交点在环外,那么我们可以转化为不带环链表判断相交即可。

当出现③情况时,两个链表的交点在环内,那么我们可以遍历其中一个链表的环,若在环内与另一个链表环的入口点相交,则两个链表相交,相遇点即为两个链表的交点。

要判断为情况②还是情况③,只需判断两个链表环的入口点是否相同即可。

代码如下:

int SListLen(SListNode *pFirst)  //计算链表节点数
{
	int len = 0;
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst->pNext->pNext;
	SListNode *pEnter = (SListNode*)malloc(sizeof(SListNode));
	int flag = 1;
	pEnter = NULL;
	while((pSlow != pFast) && (pFast != NULL))
	{
		if(flag)
		{
			pSlow = pSlow->pNext;
			flag = 0;
			continue;
		}
		if(pFast->pNext == NULL)
		{
			pFast = NULL;
			break;
		}
		pSlow = pSlow->pNext;
		pFast = pFast->pNext->pNext;
	}
	
	if(pFast != NULL)
	{
		SListNode *pMeet = pSlow;
		SListNode *pNode = pFirst;
		while(pNode != pMeet)
		{
			pNode = pNode->pNext;
			pMeet = pMeet->pNext;
		}
	pEnter->data = pMeet->data;
	}

	while(pFirst  != NULL || pFirst != pEnter)
	{
		len++;
		pFirst = pFirst->pNext;
	}
	return len;
}

SListNode* GetEnterSList(SListNode *pFirst)  //求一个带环链表的入口节点
{
	SListNode *pSlow = pFirst;
	SListNode *pFast = pFirst->pNext->pNext;
	SListNode *pEnter;
	SListNode *pMeet;
	SListNode *pNode;
	int flag = 1;
	int LoopLen = 0;
	while(pSlow != pFast)
	{
		if(flag)
		{
			pSlow = pSlow->pNext;
			flag = 0;
			continue;
		}
		pSlow = pSlow->pNext;
		pFast = pFast->pNext->pNext;
	}
	
	pMeet = pSlow;
	pNode = pFirst;
	while(pNode != pMeet)
	{
		pNode = pNode->pNext;
		pMeet = pMeet->pNext;
	}
	pEnter = pMeet;

	return pEnter;
}

void CrossSListWithCycle(SListNode *p1First, SListNode *p2First)
{
	//遍历两个链表
	SListNode *p1Slow = p1First; 
	SListNode *p1Fast = p1First->pNext->pNext;
	SListNode *p2Slow = p2First;
	SListNode *p2Fast = p2First->pNext->pNext;
	SListNode *p1Enter;
	SListNode *p2Enter;
	SListNode *pCycle;
	SListNode *pCross = (SListNode*)malloc(sizeof(SListNode));
	int flag1 = 1;
	int flag2 = 1;

	while(p1Slow != p1Fast && p1Fast != NULL)
	{
		if(flag1)
		{
			p1Slow = p1Slow->pNext;
			flag1 = 0;
			continue;
		}
		p1Slow = p1Slow->pNext;
		p1Fast = p1Fast->pNext->pNext;
	}
	while(p2Slow != p2Fast && p2Fast != NULL)
	{
		if(flag2)
		{
			p2Slow = p2Slow->pNext;
			flag2 = 0;
			continue;
		}
		p2Slow = p2Slow->pNext;
		p2Fast = p2Fast->pNext->pNext;
	}

	//1.两个链表都不带环,调用不带环链表相交判断函数
	if(p1Fast == NULL && p2Fast == NULL)
	{
		IsCross(p1First, p2First);
		return;
	}

	//2.一个带环一个不带环,两个链表不相交
	if((p1Slow == p1Fast && p2Fast == NULL)||(p2Slow == p2Fast && p1Fast == NULL))
	{
		printf("两个链表不相交\n");
		return;
	}

	//3.两个链表都带环
	//  (1).交点在环外
	p1Enter = GetEnterSList(p1First);
	p2Enter = GetEnterSList(p2First);
	if(p1Enter == p2Enter)
	{
		pCycle = p1Enter->pNext;
		p1Enter->pNext = NULL;
		IsCross(p1First, p2First);
		p1Enter->pNext = pCycle;
		return;
	}

	//  (2).交点在环内
	if(p1Enter != p2Enter)
	{
		pCycle = p1Enter->pNext;
		while(pCycle != p1Enter)
		{
			if(pCycle == p2Enter)
			{
				printf("两个链表相交,交点为:>");
				pCross->data = pCycle->data;
				pCross->pNext = NULL;
				PrintSList(pCross);
				return;
			}
			pCycle = pCycle->pNext;
		}

		//  (3).不相交
		printf("两个链表不相交\n");
	}

}

 

    原文作者:Dxx_xx4
    原文地址: https://blog.csdn.net/Dxx_xx4/article/details/80961905
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞