链表面试题详解

前言

之前我们学习过C语言中的数据结构——链表,我们这一节做几个面试题来巩固一下链表的知识,那么废话不多说,我们正式进入今天的学习

面试题1:返回倒数第K个节点

题目:

思路一:

我们可以新建一个数组,把每个节点里面的内容全部存入数组中去,然后在数组中查找倒数第K个数据;

这种方法的优点是:很容易想到,实现起来没有什么难度

这种方法的缺点是:空间复杂度很高

所以我们不采取这个方法

思路二:

我们可以把链表遍历两遍:

第一次遍历用于查找数组中有几个节点;

第二次遍历用于找出倒数第K个节点的值;

该方法完全可行,但是仍然不够完美,所以我们暂时也不采用

思路三:

使用快慢指针解决问题:

1.我们先定义一个慢指针slow和一个快指针fast

2.我们先让快指针提前走K位,然后两个指针同时向后走,在走的期间两指针之间始终保持K位

3.当fast指针走完整个链表的时候,slow指针所在的节点里面的内容就是倒数第K个节点的值

int kthToLast(struct ListNode* head, int k) 
{
	struct ListNode* fast = head, * slow = head;
	//快指针先走K
	while (k--)
	{
		fast = fast->next;
	}
	//同时走
	while (fast)
	{
		slow = slow->next;
		fast = fast->next;
	}
	return slow->val;
}

代码的实现很简单,故我们不做细节讲解

面试题2:链表的回文结构

题目:

题解

我们知道:回文结构有两种情况:

1.节点个数为奇数

12321

2.节点个数为偶数

1221

该题目因为对时间复杂度和空间复杂度都有要求,所以我们并不好想出很多种解题思路,所以只给出一种解题思路:

1.我们先去查找中间节点;

                                                                       (⊙o⊙)

12321

2.我们接着把中间节点之后的所有元素全部逆置一下

12123

3.我们创建两个指针变量,其中一个指针变量从头开始向后遍历,另外一个指针变量从逆置后的第一个元素开始向后遍历判断两个指针变量每次指向的元素是否相等

                     (⊙o⊙)  ——>                        (⊙o⊙)——>

12123

4.我们分析一下奇数偶数的情况:

(1):当链表里面的节点数为偶数个的时候,如果后面的指针走到空的时候,此时前一个指针指向的值刚好走到中间节点,此时链表全部遍历完成,我们只需要判断两指针指向节点的内容是否相等就行了

(2):当链表里面的节点数为奇数个的时候,我们逆置链表以后中间节点会出现在最后一位,因为中间节点的取值是单独的且中间节点只有一个,所以没有其他的元素和它匹配

但是我们再次分析一下代码,虽然我们把节点逆置了,但是中间节点的前一个节点的next指针还是指向中间节点,所以当后面的指针变量指向了被逆置的中间节点的时候,此时前面的指针变量经过next指针也找到了中间节点,此时两个指针变量都是指向中间节点,故也相同。我们只需要判断指针变量的next指针是否指向NULL,所以无论是奇数还是偶数都不影响代码的功能

知道了这个题目的解题思路我们就可以开始编写代码了:

1.我们先来编写一个代码用于找到中间节点:

struct ListNode* middleNode(struct ListNode* head)
{
	struct ListNode* slow = head, * fast = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;
	}
	return slow;
}

2.我们再来编写一个代码用于逆置

struct ListNode* reverseList(struct ListNode* head)
{
	struct ListNode* cur = head;
	struct ListNode* newhead = NULL;
	while (cur)
	{
		struct ListNode* next = cur->next;
		//头插
		cur->next = newhead;
		newhead = cur;
		cur = next;
	}
}

3.我们先来查找一下中间节点,如果节点个数是奇数个的话就返回最中间的节点;如果节点个数是偶数个的话就返回中间两个节点的第二个节点

	struct ListNode* mid = middleNode(A);

4.我们现在需要逆置中间节点以后的元素

	struct ListNode* rmid = reverseList(mid);

5.然后我们按照刚才所讲解的思路进行编写,rmid或者A只要有一个指向了NULL就结束循环

	while (rmid && A)
	{
		if (rmid->val != A->val)
		{
			return false;
		}
		
		rmid = rmid->next;
		A = A->next;
	}

此时我们的代码就编写成功了,我们把代码整合起来:

struct ListNode* middleNode(struct ListNode* head)
{
	struct ListNode* slow = head, * fast = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;
	}
	return slow;
}


struct ListNode* reverseList(struct ListNode* head)
{
	struct ListNode* cur = head;
	struct ListNode* newhead = NULL;
	while (cur)
	{
		struct ListNode* next = cur->next;
		//头插
		cur->next = newhead;
		newhead = cur;
		cur = next;
	}
	return newhead;
}

bool chkPalindrome(ListNode* A) 
{
	struct ListNode* mid = middleNode(A);
	struct ListNode* rmid = reverseList(mid);
	while (rmid && A)
	{
		if (rmid->val != A->val)
		{
			return false;
		}
		
		rmid = rmid->next;
		A = A->next;
	}
	return true;
}

面试题3:相交链表

题目:

题解:

我们先来分析一下会不会存在如下情况:

因为单链表的一个节点里面只有一个next,所以不会存在以上的结构

所以我们可以知道解体的步骤如下:

1.我们首先需要判断两个链表是否相交

若要判断两个链表是否相交,我们首先需要找到两个链表的尾指针如果尾指针相等的话,两个链表才有可能相交,不能用节点里面的值来判断两链表是否相交

2.如果两个链表相交,我们求出两个链表的交点

我每次事先定义两个变量curA、curB。变量curA用于遍历第一个链表,变量curB用于变量第二个链表;

思路一:

我们先让curA的单个节点与curB里面的所有节点进行比较,要是curA当前位置的地址与curB里面所有节点的地址都不相等的话,就让curA向后移动一位,并且继续和curB里面所有节点的地址比较。当我们找到了curA里面的某个地址与B链表里面的某个地址相同时,那么找到的这个就是交点;如果两个链表中没有节点的地址是相同的,则说明没有交点

此时最坏的情况就是两个链表没有交点,所以该算法的时间复杂度是O(N^2)

思路二:

如图,我们知道L1=L2,所以此时我们有更好的处理方法

1.我们先算出相对较短的链表的长度,再算出相对较长的链表的长度

2.我们用长的链表的长度减去短的链表的长度得到一个x值,此时长的链表就可以从第x个节点开始遍历,因为两个链表长度是不相等的,x节点之前的节点一定不是交点

3.我们定义两个变量curA、curB。此时就可以让两个变量同时开始向后遍历,直到找到交点

此时最坏的情况是最后一个节点是交点,此时的时间复杂度为O(N)

根据以上的思路我们可以写出代码如下:

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) 
{
	struct ListNode* curA = headA, * curB = headB;
	int lenA = 0, lenB = 0;
	while (curA->next)
	{
		curA = curA->next;
		++lenA;
	}
	while (curB->next)
	{
		curB = curB->next;
		++lenB;
	}
	if (curA != curB)
	{
		return NULL;
	}
	//len少算1,但是不影响结果
	//长的先走差距的步数,再同时走,第一个相等的点就是交点
	//假设法
	int gap = abs(lenA - lenB);
	struct ListNode* longList = headA, * shortList = headB;
	if (lenB > lenA)
	{
		longList = headB;
		shortList = headA;
	}
	while (gap--)
	{
		longList = longList->next;
	}
	while (longList != shortList)
	{
		longList = longList->next;
		shortList = shortList->next;
	}
	return shortList;
}

我们在代码中选用了假设法,因为若是直接用if……else结构对长链表和短链表进行判断的话就会造成代码重复,显得冗杂(abs是取绝对值

面试题3:环形链表

题目:

题解:

我们之前学习链表的时候,知道了链表分为很多种类型:带头节点的链表和不带头节点的链表、循环链表和不循环链表、单链表和双向链表

我们之前学习循环链表的时候,考虑的是尾节点的next指针指向头节点的情况,其实循环链表不仅仅只有这些情况。尾节点的next指针可以指向任意一个节点,甚至可以指向自己

那么我们又该如何判断一个链表是否是环形链表呢?

我们来分析一下:

首先这个题目的难点是:如果我们一直使用一个指针变量向下遍历,因为链表是一个环形链表,所以代码会一直执行,造成死循环

该题目的解决需要用到快慢指针:

1.我们先定义两个变量,一个是fast指针一个是slow指针。fast指针一次走两步,slow指针一次走一步

2.若是链表是一个带环的链表。那么随着fast指针和slow指针的遍历,它们之间迟早会相遇;若是链表不是一个带环的链表,fast和slow指针就不会相遇

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode* head) 
{
	struct ListNode* slow = head, * fast = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;
		if (slow == fast)
			return true;
	}
	return false;
}

该代码在编写的角度来看非常简单,但是思路较难想到

回顾:

我们再来想两个问题:

1.为什么两个指针一定会相遇,有没有可能永远错过,追不上?请证明一下

2.要是slow一次走一步,fast一次走3、4、5、6、n步,可不可行?

1:假设slow进入环中的时候,fast和slow的距离差是N。当执行一次“slow一次走一步,fast一次走两步”的操作时,slow和fast的距离差变成了N-1。所以每次执行一次“slow一次走一步,fast一次走两步”的操作都可以让slow和fast之间的距离差-1,当执行N次这个操作的时候,两个指针就会相遇

2:我们逐步分

走三步的情况:

我们依旧假设slow进入环中的时候,fast和slow的距离差是N。当执行一次“slow一次走一步,fast一次走三步”的操作时,slow和fast的距离差此时就会变成N-2.

所以当N是一个偶数的时候两个指针就会相遇;

N为奇数的时候两个指针第一次追击,因为fast指针追过头了,此时就错过了,两个指针进行新一轮的追击,此时两指针的距离变成C-1(C为环的长度)

此时就要考虑两种情况:

1.C-1是偶数——N是奇数:此时就可以追上

2.C-1是奇数——N是偶数:此时就无法追上

小结:

1.如果N是偶数,第一轮就可以追上

2.如果N是奇数,第一轮就不能追上,此时会错过,距离变成C-1

(1).如果C-1是一个偶数,下一轮追击就可以追上

(2).如果C-1是一个奇数,下一轮追击就无法追上

分析:

我们来分析一下:

因为fast走的距离是slow的3倍,所以:(L是进入环之前的长度)

3L = L + x*C + C - N

2L = (x+1) * C - N

2L = (x+1) * C - N :偶数 = (x+1) * 偶数 - 奇数,这个情况永远不会存在;

所以N是奇数的时候,C也一定是奇数;N是偶数的时候,C也一定是偶数

总结:

一定可以追上;

如果N是偶数,那么第一轮追击就可以追上

如果N是奇数,那么第一轮追击就无法追上,但是因为C-1是一个偶数,所以第二轮追击可以追上

走n步的情况:

我们假设slow进入环中的时候,fast和slow的距离差是N。当执行一次“slow一次走一步,fast一次走n步”的操作时,slow和fast的距离差此时就会变成N-(n-1).

所以当N是(n-1)的倍数的时候两个指针就会相遇

当N不是(n-1)的倍数的时候,此时我们就要按以上的方法进行讨论,只是需要讨论很多很多次,这里就不做详细讲解了

面试题4:环形链表2

题目:

题解:

 方法一:(数学思想)

此题目与上一个题目紧密相关

1.我们先按上一个题目的思想,定义两个指针变量fast和slow,其中fast一次走两步,slow一次走一步,接着让两个指针相遇

2.我们创建一个指针变量meet记录下相遇的节点,然后创建一个头节点headhead指针和meet指针每次都向后走一步

3.当meet和head指针相遇的时候的节点就是环的第一个节点

我们先来计算一下相遇的时候slow和fast两个指针走的路程:

(设入环的距离是L,环的长度是C,slow入环时fast走的距离是N)

slow走的路程:L + N

fast走的路程:L + x*C + N

故:2*(L + N) = L + x*C +N

                    L = x*C - N

假设x = 1的时候:L = C - N

因为 head = L,meet = C - N

所以 head = meet 的时候两指针相遇在环的第一个节点

假设 x != 1 的时候:我们把表达式化为:L = (x - 1)*C + C - N

因为 (x - 1)*C 代表的是走的圈数,产生的效果是相同的,所以无论x取何值(x>=1),都能满足题意

所以无论x取何值(x>=1),head = meet 的时候两指针相遇在环的第一个节点

所以我们就可以很简单的写出代码:

struct ListNode* detectCycle(struct ListNode* head) 
{
	struct ListNode* slow = head, * fast = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;
		if (slow == fast)
		{
			struct ListNode* meet = slow;
			while (meet != head)
			{
				meet = meet->next;
				head = head->next;
			}
			return meet;
		}
	}
	return NULL;
}

方法二:

方法一的代码实现虽然很简单,但是这个方法很难想到,那么我们就再来讲解一个通俗易懂一点的代码吧

1.我们还是要先找到相遇的节点,我们记相遇的节点为meet

2.我们创建一个指针变量newhead,在newhead中存入meet的下一个节点的地址

3.我们把meet节点里面的next指针置为空,此时环形链表就从meet处截断了

4.此时问题就变成了我们之前写的寻找两个链表的交点的问题了

我们写出代码:

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{
	struct ListNode* curA = headA, * curB = headB;
	int lenA = 0, lenB = 0;
	while (curA->next)
	{
		curA = curA->next;
		++lenA;
	}
	while (curB->next)
	{
		curB = curB->next;
		++lenB;
	}
	if (curA != curB)
	{
		return NULL;
	}
	//len少算1,但是不影响结果
	//长的先走差距的步数,再同时走,第一个相等的点就是交点
	//假设法
	int gap = abs(lenA - lenB);
	struct ListNode* longList = headA, * shortList = headB;
	if (lenB > lenA)
	{
		longList = headB;
		shortList = headA;
	}
	while (gap--)
	{
		longList = longList->next;
	}
	while (longList != shortList)
	{
		longList = longList->next;
		shortList = shortList->next;
	}
	return shortList;
}

struct ListNode* detectCycle(struct ListNode* head) 
{
	struct ListNode* slow = head, * fast = head;
	while (fast && fast->next)
	{
		slow = slow->next;
		fast = fast->next->next;
		if (slow == fast)
		{
			struct ListNode* meet = slow;
			struct ListNode* newhead = meet->next;
			meet->next = NULL;
			return getIntersectionNode(head, newhead);
		}
	}
	return NULL;
}

面试题5:随机链表的复制

题目

我们先来理解一下题目的意思:

深拷贝:拷贝一个值和指针的指向跟当前链表一模一样的链表

题解:

1.我们先要完成链表的是深拷贝,链表的深拷贝相对而言很容易实现,我们只需要malloc申请空间,再把原链表里面的内容拷贝至malloc开辟的空间里面去,并且把每个节点尾插连接起来就好了

2.我们需要让random找到对应的节点。需要注意的是,我们不能通过查找节点里面的取值的方式来找到random对应的节点,像例子中所示,有两个节点里面有7,如果是查找7的话就会存在有两个节点里面的取值都是7,这样random就不知道该指向这两个节点中的哪一个节点了

方案一:

我们查找的是各个节点在链表中的相对位置

但是这样处理的话时间复杂度就是是O(N^2),效率就会比较低下。

方案二:

1.我们先拷贝链表的所有节点把拷贝的节点连接到原链表被拷贝的节点后面,此时每个拷贝节点就与原节点之间就建立了联系

	//控制random
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;
		if (cur->random == NULL)
		{
			copy->random = NULL;
		}
		else
		{
			copy->random = cur->random->next;
		}

		cur = copy->next;
	}

2.因为每个拷贝的节点都在原节点的后面,所以每个拷贝节点中的random也是对应的

	while (cur)
	{
		struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
		copy->val = cur->val;
		copy->next = cur->next;
		cur->next = copy;
		cur = copy->next;
	}

3.我们需要把拷贝后的链表整体拿下来并且尾插连接起来,再恢复原链表

	struct Node* copyhead = NULL;
	struct Node* copytail = NULL;
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;
		struct Node* next = copy->next;
		if (copytail == NULL)
		{
			copyhead = copytail = copy;
		}
		else
		{
			copytail->next = copy;
			copytail = copytail->next;
		}
		cur->next = next;
		cur = next;
	}

我们把代码整合起来就完成任务了

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

struct Node* copyRandomList(struct Node* head) 
{
	struct Node* cur = head;
	//拷贝节点插入到原节点的后面
	while (cur)
	{
		struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
		copy->val = cur->val;
		copy->next = cur->next;
		cur->next = copy;
		cur = copy->next;
	}
	//控制random
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;
		if (cur->random == NULL)
		{
			copy->random = NULL;
		}
		else
		{
			copy->random = cur->random->next;
		}
		cur = copy->next;
	}
	//把拷贝的节点取下来尾插成新链表,然后恢复原链表
	struct Node* copyhead = NULL;
	struct Node* copytail = NULL;
	cur = head;
	while (cur)
	{
		struct Node* copy = cur->next;
		struct Node* next = copy->next;
		if (copytail == NULL)
		{
			copyhead = copytail = copy;
		}
		else
		{
			copytail->next = copy;
			copytail = copytail->next;
		}
		cur->next = next;
		cur = next;
	}
	return copyhead;
}

结尾

如果能够理解并且独立写出以上几个问题,那么说明您有关链表的知识就已经掌握得很牢固了,希望这一节的内容可以给你带来帮助,谢谢您的浏览!!!

  • 44
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值