1.题目描述
2.解题思路(Python版)
首先分析题目,对于两个单向链表来说,如果从某一个节点开始这两个链表出现了公共节点,那么这两个链表的后续节点都是相同的,不可能再出现分叉,因此就像题目描述里所绘制的图形一样,两个有公共节点而部分重合的链表,其拓扑结构只可能是Y型而不是X型。
首先想到的解决问题的方法就是直接遍历,每当遍历到链表1中的一个结点时,就将链表2整个遍历一遍,如果在链表2中发现与链表1中的当前节点一样的节点,也就找到了两个链表的公共节点,如果没有的话,就继续移动到链表1的下一个节点,直到遍历完整个链表1。这种方法的优势是逻辑比较简单,也不耗费额外的空间,但其时间复杂度较高。
为了改进上述方法,进一步想到如果两个链表有公共节点,那么其尾部一定是重合的,如果可以从两个链表的尾部开始往前比较,那么最后一个相同的节点就是我们要找的第一个公共节点,对于单向链表来说,要想先对尾结点作比较,可以借助栈LIFO的特点:将两个链表分别存储到两个辅助栈中,然后分别比较两个栈顶的节点是否相同,如果相同就取出,比较下一个栈顶,直到找到最后一对相同的栈顶,这就是原始两个链表的第一个公共节点。借助栈的方法比起最开始的直接遍历法来说,其时间效率有所提升,但空间消耗增大。
为了进一步均衡时间和空间效率,可以采用遍历两次的方法,思路如下:借助栈来解决上述问题之所以方便,是因为其可以同时遍历到达两个链表的尾结点,当两个链表的长度不一致时,我们同时从头开始遍历就无法同时到达尾节点。因此,可以采用遍历两次的方法:第一次遍历分别得到两个链表的长度,然后计算出长链表比断链表多出来的长度L;第二次遍历先在较长的链表上走L步,然后两者同时遍历,直到找到一个相同的节点。
假设两个链表的长度分别为m和n,上述三种方法的时间复杂度和空间复杂度分别如下表所示:
时间复杂度 | 空间复杂度 | |
直接遍历 | O(mn) | O(1) |
借助栈 | O(m+n) | O(m+n) |
遍历两次 | O(m+n) | O(1) |
比较后发现只有第三种方法满足题目中的复杂度要求,因此采用第三种方法解题。
此外还可以使用哈希表或双指针的方法解题,待学习后补充。
方法一:遍历两次
思路:
1.编写一个计算链表长度的函数,并使用该函数计算链表1、链表2的长度;
2.得到两个链表的长度差,先在长链表上走L步,然后再同时开始遍历;
3.遍历的循环条件是:长链表当前节点不为空+短链表当前节点不为空+两个链表当前节点不相同,若不满足上述条件则跳出循环,跳出循环的可能有以下两种:一是发现公共节点,二是两个链表均循环到结尾,最后将跳出循环前两个链表的最后一个节点输出(要么是第一个公共节点开始的后续链表,要么是一个空链表)
参考代码:
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
#
# @param pHead1 ListNode类
# @param pHead2 ListNode类
# @return ListNode类
#
class Solution:
def FindFirstCommonNode(self , pHead1 , pHead2 ):
# write code here
#得到两个链表的长度
Length1 = self.GetListLength(pHead1)
Length2 = self.GetListLength(pHead2)
if Length1 >= Length2: #第一次遍历,比较两者长度
pLongList = pHead1
pShortList = pHead2
LengthDif = Length1 - Length2
else:
pLongList = pHead2
pShortList = pHead1
LengthDif = Length2 - Length1
for i in range(LengthDif):
pLongList = pLongList.next #先在较长的链表上走几步,保证两个链表剩余部分长度相同
while pLongList and pShortList and pLongList != pShortList:
pLongList = pLongList.next
pShortList = pShortList.next
pFirstCommonNode = pLongList
return pFirstCommonNode
#定义计算链表长度函数
def GetListLength(self , pHead):
Length = 0
pNode = pHead
while pNode:
Length = Length + 1
pNode = pNode.next
return Length
该方法需要注意的是循环条件的设置,以及对特殊情况的分析,如两个链表中至少有一个空链表,或者遍历到尾节点都没有发现公共节点等。
复杂度:
时间复杂度O(N):N为两个链表的长度之和,分别对两个链表做了两次遍历;
空间复杂度O(1):在运行过程中没有借助额外的辅助空间。
为提升解决其他问题的能力,现将前两种方法的解题方法阐述如下:
方法二:蛮力法(直接遍历)
思路:
1.从链表1的第一个节点开始顺序遍历,每遍历一个节点,就将链表2中所有节点与其比较一遍,如果发现与当前节点相同的节点,则输出该节点作为首个公共节点,如没有相同的节点,移动到链表1的下一个节点,重复上述流程;
2.若整个链表1遍历完都未发现相同节点,则说明两个链表无公共节点。
参考代码:
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
#
# @param pHead1 ListNode类
# @param pHead2 ListNode类
# @return ListNode类
#
class Solution:
def FindFirstCommonNode(self , pHead1 , pHead2 ):
# write code here
if pHead1 is None or pHead2 is None:
return None
cur1 = pHead1 #创建两个指针,分别指向两个链表的头节点
while cur1:
cur2 = pHead2
while cur2: #固定链表1节点,顺序遍历链表2中的节点
if cur1 == cur2:
return cur1
cur2 = cur2.next
cur1 = cur1.next
return None
复杂度:
时间复杂度O(N²):N为两个链表的长度之和,当链表较长时花费的时间很多。
空间复杂度O(1):在运行过程中没有借助额外的辅助空间。
方法三:借助栈
思路:
将两个链表分别存储到两个辅助栈中,分别比较两个栈顶的节点是否相同,如果相同就取出,比较下一个栈顶,直到找到最后一对相同的栈顶,这就是原始两个链表的第一个公共节点。
参考代码:
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
#
#
# @param pHead1 ListNode类
# @param pHead2 ListNode类
# @return ListNode类
#
class Solution:
def FindFirstCommonNode(self , pHead1 , pHead2 ):
# write code here
if pHead1 is None or pHead2 is None:
return None
stack1 = [] #创建两个辅助栈
stack2 = []
while pHead1:
stack1.append(pHead1)
pHead1 = pHead1.next
while pHead2:
stack2.append(pHead2)
pHead2 = pHead2.next
pCommonNode = None
while stack1 and stack2: #比较两个栈顶的节点
pNode1 = stack1.pop()
pNode2 = stack2.pop()
if pNode1 is pNode2:
pCommonNode = pNode1
else:
break
return pCommonNode
复杂度:
时间复杂度O(N):N为两个链表的长度之和,遍历两个链表所花费的时间。
空间复杂度O(N):N为两个链表的长度之和,也是两个辅助栈所花费的空间。
3.相关知识点
(1)用哈希表、双指针法解题;
(2)两个存在公共节点的链表的拓扑形状和一棵树的形状非常相似,只是这里的指针是从叶节点指向根节点的。两个链表的第一个公共节点正好就是二叉树中两个叶节点的最低公共祖先。