前言
之前我们学习过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.节点个数为奇数:
1 | 2 | 3 | 2 | 1 |
2.节点个数为偶数:
1 | 2 | 2 | 1 |
该题目因为对时间复杂度和空间复杂度都有要求,所以我们并不好想出很多种解题思路,所以只给出一种解题思路:
1.我们先去查找中间节点;
(⊙o⊙)
1 | 2 | 3 | 2 | 1 |
2.我们接着把中间节点之后的所有元素全部逆置一下;
1 | 2 | 1 | 2 | 3 |
3.我们创建两个指针变量,其中一个指针变量从头开始向后遍历,另外一个指针变量从逆置后的第一个元素开始向后遍历,判断两个指针变量每次指向的元素是否相等;
(⊙o⊙) ——> (⊙o⊙)——>
1 | 2 | 1 | 2 | 3 |
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记录下相遇的节点,然后创建一个头节点head,head指针和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;
}
结尾
如果能够理解并且独立写出以上几个问题,那么说明您有关链表的知识就已经掌握得很牢固了,希望这一节的内容可以给你带来帮助,谢谢您的浏览!!!