单链表面试题汇总

本文罗列出单链表在面试中常见的考题,一开始都是《剑指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;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值