python练习:两个链表的第一个公共结点

题目

输入两个链表,找出它们的第一个公共结点。

分析

对于公共结点,解释应该是:如果两个单向链表存在公共节点,那么从第一个公共节点开始到最后一个节点都是公共节点。也就是说,相同的点,不仅值相同,后继节点也相同,那么同理公共结点后面的点也是不仅值相同,而且后继节点也相同。这样的话,就可以把两条链条看成 Y 字型了,某一个结点后面的点全部一样。

例如下图中,链表 6 - 7 就是两个链表的公共链表,而节点6就是第一个公共节点。

python练习:两个链表的第一个公共结点_第1张图片

方法一

最直观的当然就是暴力法——双重循环。

那么在第一链表上顺序遍历每个节点,每遍历到一个节点,就在第二个链表上顺序遍历每个节点。如果在第二个链表上有一个节点和第一个链表上的节点一样,则说明两个链表在这个节点上重合。返回第一个相等的节点值即可。这种方法就不做代码展示了。

假设第一个链表长度为m,第二个链表的长度为n。则这种方法的时间复杂度为 O(m∗n)。

方法二

我们可以借助快慢指针的思想。先分别遍历链表,计算出两个链表的长度差。然后先移动长链表的指针,使得长短链表的指针距离各自的末尾有相同的距离;再同时移动两个指针,直到出现两指针指向的节点相同为止,那么这个相同节点就是第一个公共节点。

具体来说,使用一个快指针指向长链表,一个慢指针指向短链表。快指针先走两个长度差的步数,然后慢指针再和快指针一起走,指向同一个节点的时候,就是两个链表的第一个公共节点

这种方法时间复杂度为 O(m + n)。

方法三

按照公共节点的概念,我们还可以想到:如果我们能从两个链表的末尾节点开始遍历,找到最后一个相同的节点,那么这个节点就是第一个公共节点。

然而单向链表并不支持反向遍历!在单链表中只能从头结点开始按顺序遍历,最后才能到达尾结点。最后到达的尾结点却要最先被比较,这是 “后进先出” 的特性。

因此我们可以利用的性质,维护两个辅助栈,分别把两个链表的节点放入两个栈里,这样两个链表的尾节点就位于两个栈的栈顶。然后依次比较这两个辅助栈的栈顶元素。如果相同,则把栈顶弹出接着比较下一个栈顶,直到找到最后一个相同的结点。

这种方法时间复杂度为 O(m + n),空间复杂度为 O(m + n)。

方法四

最少代码也最玄学:用两个指针遍历”两个链表“,最终两个指针到达 None 或者到达公共结点。其实思路和方法二是一样的。

具体分析两个链表的情况如下。

  • 第一种情况:相同长度有交点

        两个指针一起走,步长一致,碰到第一个相同的节点 p1 == p2,退出循环,return p1。

  • 第二种情况:相同长度无交点

        两个指针一起走,直到走到最后一个节点,p1.next 和 p2.next都为 None,满足相等的条件,退出循环,return p1。

  • 第三种情况:不同长度有交点

        两个指针一起走,当一个指针p1走到终点时,说明p1所在的链表比较短,让p1指向另一个链表的头结点开始走,直到p2走到终点;让p2指向短的链表的头结点,那么,接下来两个指针要走的长度就一样了,变成第一种情况。

  • 第四种情况:不同长度无交点

        两个指针一起走,当一个指针p1走到终点时,说明p1所在的链表比较短,让p1指向另一个链表的头结点开始走,直到p2走到终点;让p2指向短的链表的头结点,那么,接下来两个指针要走的长度就一样了,变成第二种情况。

 

代码

class ListNode:
    def __init__(self, data):
        self.data = data
        self.next = None


def FindLength(head):  # 计算链表长度
    if head is None:
        return 0
    length = 0
    while head:
        length += 1
        head = head.next
    return length


def FindFirstCommonNode1(head1, head2):
    len1 = FindLength(head1)
    len2 = FindLength(head2)
    lens = abs(len1 - len2)  # 两个链表长度的差值
    # 哪个链表更长,哪个就先走
    if len1 > len2:
        while lens:
            head1 = head1.next
            lens -= 1
    elif len1 < len2:
        while lens:
            head2 = head2.next
            lens -= 1
    # 再同时在两个链表上遍历
    while head1:
        if head1 == head2:  # 如果两个指针指向了同一个结点,就返回当前结点
            return head1.data
        head1 = head1.next
        head2 = head2.next


def FindFirstCommonNode2(head1, head2):
    stack1 = []
    stack2 = []
    while head1:  # 将第一个链表的节点依次放入第一个栈中
        stack1.append(head1)
        head1 = head1.next
    while head2:  # 将第二个链表的节点依次放入第二个栈中
        stack2.append(head2)
        head2 = head2.next
    node = ListNode(None)
    # 从两个栈顶开始,依次比较,找到最后一个相等的节点
    while stack1 and stack2 and stack1[-1] is stack2[-1]:
        node = stack1.pop()
        stack2.pop()
    return node.data  # 返回公共节点的值



def FindFirstCommonNode3(head1, head2):
    if not head1 or not head2:
        return None
    p1, p2 = head1, head2
    while p1 != p2:
        # 如果当前结点为None,就指向另一个链表的头结点开始第二次遍历
        p1 = head2 if not p1 else p1.next
        p2 = head1 if not p2 else p2.next
    return p1.data if p1 else None


def printLinkedList(head):
    while head:
        print(head.data, end=" ")
        head = head.next
    print()


if __name__ == '__main__':
    # 测试用例1
    head1 = ListNode(0)
    node1, node2, node3, node4, node5 = ListNode(1), ListNode(4), ListNode(6), ListNode(7), ListNode(8)
    head1.next = node1
    node1.next = node2
    node2.next = node3
    node3.next = node4
    node4.next = node5
    printLinkedList(head1.next)

    head2 = ListNode(0)
    node6, node7 = ListNode(2), ListNode(3)
    head2.next = node6
    node6.next = node7
    node7.next = node4
    printLinkedList(head2.next)

    print(FindFirstCommonNode1(head1.next, head2.next))
    print(FindFirstCommonNode2(head1.next, head2.next))
    print(FindFirstCommonNode3(head1.next, head2.next))
    
    # 测试用例2
    head2 = ListNode(0)
    node6, node7, node8, node9 = ListNode(2), ListNode(3), ListNode(7), ListNode(8)
    head2.next = node6
    node6.next = node7
    node7.next = node8
    node8.next = node9

    print(FindFirstCommonNode1(head1.next, head2.next))
    print(FindFirstCommonNode2(head1.next, head2.next))
    print(FindFirstCommonNode3(head1.next, head2.next))

 

结果

上面的运行结果为:

1 4 6 7 8 
2 3 7 8 
7
7
7
None
None
None

到这里,我们的三个方法代码就成功了。

 

但是如果我把第二个方法的代码写成这样呢:

def FindFirstCommonNode2(head1, head2):
    stack1 = []
    stack2 = []
    while head1:  # 将第一个链表的值依次放入第一个栈中
        stack1.append(head1.data)
        head1 = head1.next
    while head2:  # 将第二个链表的值依次放入第二个栈中
        stack2.append(head2.data)
        head2 = head2.next
    # 从两个栈顶开始,依次比较,找到最后一个相等的值
    node = None
    while stack1 and stack2 and stack1[-1] is stack2[-1]:
        node = stack1.pop()
        stack2.pop()
    return node

这时候的运行结果就会变成:

1 4 6 7 8 
2 3 7 8 
7
7
7
None
7
None

我们可以看到,在第二个测试用例中,方法二的输出结果仍然为7,但另外两种方法返回却是None。为什么会这样?

看一下我使用的两个测试用例,其实值都是相同的。不同在于,第二个测试用例中,第二个链表的节点7、8是创建了新节点,没有使用第一个链表中的节点。

第一个测试用例中的两个链表:python练习:两个链表的第一个公共结点_第2张图片

第二个测试用例中的两个链表:python练习:两个链表的第一个公共结点_第3张图片

所以此时的方法二中只是把链表中节点的值入栈,依次出栈比较的时候,也只是比较了节点的值是否相同。根本就无法确定它们的后续指针是否指向的同一个地方,那么该方法只找到了值相同的 “公共节点” 而已。

在之前的方法二代码中,我们是把链表上的节点放入栈里;比较栈顶元素是否相等的时候,也是比较的节点是否相等,而不是说仅仅比较节点的值是否相同,这样就能实现了 包含值和后继指针的这整体节点是否是相同的,即真正的公共节点。

而其他方法一直是在链表的基础上从前往后移动比较的,所以后继指针也包含在了比较中,那么就找到了公共结点值和后继指针完全相同的。

 

也就是说:并不是两个节点的值相同就是公共节点,一定要考虑清楚细节哟。

你可能感兴趣的:(算法设计分析,Python)