本文罗列出单链表在面试中常见的考题,一开始都是《剑指offer》上总结的题型,后面在遇到会更新到这里。
定义部分:
typedef struct Node
{
int data;
struct Node* next;
}Node,*List;
2020年3月12日:
链表逆置(反转链表):
解决这个题最大的问题就是对操作节点的前驱节点的保存,所以大致思路是:定义三个节点,一个是当前节点p_Node,一个是当前节点的后继节点p_Next,还有一个是当前节点的前驱节点p_Prev,然后可以从最后往前指,也可以从头往后反转,我这里使用从头往后反转,更快一点点。
Node* ReverseList(List plist)
{
if(plist == NULL || plist->next == NULL)
{
return NULL;
}
Node* p_Node = plist->next;
Node* tmp = NULL;
Node* p_Prev = NULL;
while(p_Node != NULL)
{
Node* p_Next = p_Node->next;
if(p_Next == NULL)
tmp = p_Node;
p_Node -> next = p_Prev;
p_Prev = p_Node;
p_Node = p_Next;
return tmp;
}
链表的倒数第K个节点:
输入一个链表,输出这个链表的倒数第K个节点,比如最后一个节点就是倒数第一个节点。
这里最简单的思路就是,倒数第K个节点就是正数第n-k+1个节点,遍历一遍链表,找出第n-k+1个节点就可以了。
基于这种思路我们很快就能写出代码:
Node* FindEndNode1(List plist,int k)
{
if(plist == NULL || k == 0)
{
return NULL;
}
Node* p;
int n = 1;
for(p = plist->next;p!=NULL;p=p->next)
{
n++;
}
p = plist -> next;
for(int i = 1;i <= n-k+1 ;i++)
{
p = p->next;
}
return p;
}
但其实这样的话时间复杂度达到了O(n^2),所以我们要换一种思路。
我们定义两个指针,第一个指针从头结点开始走k-1步,然后定义第二个节点从头结点开始和第一个节点一起走,当第一个节点到尾结点的时候,第二个节点指向的就是倒数第k个节点,其实也很好理解,第二个节点相当于走了n-k+1步。
Node* FindEndNode2(List plist,int k)
{
if(plist == NULL || k == 0)
{
return NULL;
}
Node* p_First = plist;
for(int i = 0;i < k-1;i++)
{
if(p_First->next != NULL)
{
p_First = p_First->next;
}
else
{
return NULL;//如果k的值大于链表长度,就返回NULL
}
}
Node* p_Second = plist;
while(p_First != NULL)
{
p_First = p_First->next;
p_Second = p_Second->next;
}
return p_Second;
}
链表中环的入口节点
如果一个链表中有一个环,如何能找到这个环的入口节点呢?输入链表,返回链表内环的入口节点。
这个题之前就遇到过,还是有一些印象的,基于上一个题的思路,我们也可以定义两个指针,我们称之为快慢指针。
首先要注意一个特点,单链表只有一个next指针,所以链表的环一定是在链表的尾部,简单的说不可能出现这样的情况:
所以出现一个环,常规情况下应该是这样的:
然后定义两个指针,第一个指针一次走两步,第二个指针一次走一步,这样下去两个指针迟早会相遇,而且相遇时两个指针肯定在环内,然后其中一个指针在走一次,走到相遇的位置,就可以确定环的长度n,这样准备工作就做好了,我们从头结点开始,第一个指针先走n步,然后第二个指针开始和第一个一起走,然后两个指针相差一个环的长度,当两个指针再相遇时,就是环的头结点。
(图随便画的,能看就好嘿嘿)
基于这个思路我们开始写代码:
先是判断是否是环,并且计算环的长度
Node* FindLoopNode(List plist)
{
if(plist == NULL)
return NULL;
Node *p=plist;
Node *q=plist;
while(p != q)
{
if(p->next == NULL)//如果链表到了尾结点,说明没有环
{
return NULL;//直接返回NULL
}
p=p->next->next;
q=q->next;
}
int n = 1;
p = p->next;
while(p!=q)
{
p = p->next;
n++;
}
if(n == 1)
{
return q;
}
else
{
return FindLoopFirst(plist,n);
}
}
然后准备工作做好了,就寻找入口节点:
Node* FindLoopFirst(List plist,int n)
{
Node *p = plist;
Node *q = plist;
for(int i = 0;i < n;i++)
{
p = p->next;
}
while(p!=q)
{
p=p->next;
q=q->next;
}
return q;
}
合并两个排序的链表:
要求合并的结果也是排序的。
看到这个题,最常规的思路有两种,一种是第二个链表中的节点一个一个插入第一个链表中,这样的时间复杂度是O(N^2),而且并没有用到排序链表的特性;第二种思路是直接合并链表,然后用排序算法进行排序,因为是链表,某些排序算法是无法使用的,或者说使用会更麻烦,这种思路虽然时间复杂度能快一点,但是也没有用到排序链表的特性。
虽然我们一开始可能想不到最好的解题思路,但是有思路总比没思路好,然后根据面试官的提醒再解出真正的解法也是可以的。
那么如何利用两个链表都排序这个特点呢,首先两个链表的头结点进行比较,链表一的头结点小于链表二的头结点,那么链表一的头结点就是新链表的头结点,反过来也一样,然后链表一的下一个节点就是新的头结点,然后用新的头结点和链表二的头结点进行比较,谁小谁就是新链表的尾结点,当然后面的过程和前面都一样了,
思路:
然后基于思路,我们开始书写代码
Node* MergeList(Node* plist1,Node* plist2)
{
if(plist1 == NULL)
return plist2;
else if(plist2 == NULL)
return plist1;
Node* NewList = NULL;
if(plist1->data < plist2->data)
{
NewList = plist1;
NewList->next = MergeList(plist1->next , plist2);
}
else
{
NewList = plist2;
NewList->next = MergeList(plist1 , plist2->next);
}
return NewList;
}
目前我遇到的链表的面试题也就这些了,如果再遇到新的有趣的单链表面试题还在这篇博客更新的。
2020年3月14日更:
判断两个单链表是否相交
给定两个单链表,检测其是否相交,如果相交返回相交的第一个节点。
这个题和找环的问题是差不多的,找环的问题更加复杂一点,我们把思路简化一下就可以方便的求解出来了。
首先我们肯定要判断是否有交点,在这里我们要知道一个前提,两个单链表有交点的话,后续节点内容都是相同的,如图:
所以判断两个链表相交,我们直接走到最后一个节点,两个链表的最后一个节点相同的话,必相交,并且我们在走到尾结点的时候记录链表的长度len1、len2,如果相交的话,我们让长的链表先走len1-len2个长度,然后两个链表一起走,节点相同时就是相交的第一个节点。
根据这个思路,我们可以写出代码:
Node* Is_SmilarNode(List head1,List head2)
{
if(head1 == NULL || head2 == NULL)
{
return NULL;
}
Node* p1 = head1->next;
Node* p2 = head2->netx;
int len1 = 1;
int len2 = 1;
while(p1->next != NULL)
{
p1 = p1->next;
len1++;
}
while(p2->next != NULL)
{
p2 = p2->next;
len2++;
}
if(p1 != p2)
{
return NULL;
}
else
{
p1=head1->next;
p2=head2->next;
if(len1>len2)
{
for(int i = 0;i<len1-len2;i++)
{
p1 = p1->next;
}
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
else if(len1<len2)
{
for(int i = 0;i<len2-len1;i++)
{
p2 = p2->next;
}
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
else
{
while(p1!=p2)
{
p1=p1->next;
p2=p2->next;
}
}
}
return p1;
}
我去查了下还有使用别的办法,比如两个栈,用哈希表的,使用呢些倒也是一种不错的思路,有兴趣的可以去了解一下。
删除某个节点P
给定单链表的某个节点p指针,p并非最后一个节点,即p->next != NULL,删除该节点
这个问题就是在于你无法找到要删除节点的前驱,这样删除你就会断开链表,所以这个问题我们要把链表看成数组来看,删除一个节点,把后面的节点往前移动一格就好了,然后再free最后一个节点,就实现了删除p节点。
bool Delete_p(Node* p)
{
if(p->next == NULL)
{
return false;
}
while(p->next->next != NULL)
{
p->data = p->next->data;
p->next = p->next->next;
p=p->next;
}
p->data= p->next->data;
free(p->next);
p->next = NULL;
return true;
}
只给定单链表中某个非空节点p,在p节点之前插入一个新节点。
这个问题的思路和上一个相似,我们无法找到非空节点p的前驱节点,但是我们可以向数组那样把节点p向后移,这里方便的时候,我们只需要插入一个新节点在p的next,然后把p节点的数据域给新节点,把要插入的数据替换给p,就相当于实现了在p前面插入新节点数据。
void Insert_p(Node* p,int val)
{
Node* q = (Node*)malloc(sizeof(Node));
q->next = p->next;
p->next = q;
q->data = p->data;
p->data = val;
}