【问题标题】:Space complexity in shallow vs deep copy浅拷贝与深拷贝的空间复杂度
【发布时间】:2022-01-25 16:53:30
【问题描述】:

考虑在保持顺序的同时合并两个已排序的链表的问题。例如如果

L1: 1->1->2->4

L2: 2->3->5

那么结果应该是1->1->2->2->3->4->5。在 Elements of Programming Interview 中,作者提出了以下 Java 解决方案,包括遍历两个列表并选择最小的值添加到虚拟头中:

public static ListNode <Integer> mergeTwoSortedList s (ListNode <Integer> LI,
ListNode <Integer> L2) {  // Creates a placeholder for the result.
   ListNode <Integer> dummyHead = new ListNode<>(® , null);
   ListNode <Integer> current = dummyHead;
   ListNode <Integer> p1 = L1 , p2 = L2 ;
   while (p1 != null && p2 != null) {
        if (p1.data <= p2.data) {
           current.next = p1;
           p1 = p1.next ;
        } else {
           current.next = p2 ;
           p2 = p2.next ;
        }
        current = current.next ;
        }
    // Appends the remaining nodes of p1 or p2.
        current.next = p1 != null ? p1 : p2 ;
        return dummy head.next ; 
}

然后作者指出,这个解决方案在空间上是 O(1),因为它没有创建任何额外的节点。

我在 Python 中实现了一个非常相似的解决方案,但我注意到以下问题存在(同样适用于 p2):

current.next = p1
p1 = p1.next

我注意到,在 Python 中,在这种情况下,解决方案无法更新第二行中 p1 的值,同时也会更改上面一行中 current.next 的值(即第一行是浅拷贝),从而使结果列表为无(相当于空指针)。通过将第一行替换为

,可以在 python 中找到一个可行的解决方案
current.next = NewNode(p1.val,None)

NewNode 创建一个与 p1 具有相同值的新节点和一个指向 None 的下一个“指针”。但是,这显然会利用 O(m+n) 空间,其中 m 和 n 是两个列表的长度。

但这让我质疑 Java 代码中 if 块中的整个语句。当我执行 current.next = p1 时,我是否没有创建列表的(深层)副本并因此使用额外的空间?在这种情况下,空间复杂度怎么会是 O(1)?我从更算法的角度理解 O(1) 空间声明的来源,但我没有看到这在代码中实现。如果 p1 = p1.next 不更新 current 的值,则它们之间肯定存在差异,因此必须进行深层复制,但这意味着额外的空间。我错过了什么?

【问题讨论】:

    标签: python java linked-list complexity-theory space-complexity


    【解决方案1】:

    我不知道您在 python 实现中误入歧途,但如果没有您所做的 current.next = NewNode(p1.val,None) 更改,它应该可以正常工作,这会创建额外的节点(因此会使用额外的空间)。不需要执行深度复制,因为该算法本质上只是在操作指针。也就是说,我认为您缺少的主要内容是对象是 Java 和 Python 中的隐式引用(它们是隐式指针)。这意味着currentp1 只是对节点的两个不同引用(它们本身不是节点)。

    关于有问题的代码块:

    current.next = p1
    p1 = p1.next
    

    当您执行current.next = p1 时,您将current 的下一个字段设置为p1 的值。因为p1是一个节点,即一个对象,而对象是引用,所以p1的值是一个节点的引用。所以,在第一条语句执行后,current.next 是对同一节点的引用 p1 引用(没有深拷贝)。

    然后,使用p1 = p1.next,您将重新定义变量p1,它当前是一个节点的引用,作为p1.next 的值:要么是对另一个节点的引用,要么是None .第二条语句不会更改p1 引用的底层节点,也不会影响current.next 的值:您只是让p1 引用与之前不同的节点。

    因为该算法只是通过引用 L1 和 L2 中当前存在的节点来更新下一个字段,而不是创建新节点,所以它是 O(1)。您可以在下面看到一个有效的 python 实现:

    class ListNode:
        def __init__(self, data, next):
            self.data = data
            self.next = next
    
        def print(self):
            def _print(node, seq):
                seq.append(node.data)
                _print(node.next, seq) if node.next else print(seq)
            _print(self, [])
    
    
    def mergeLists(L1, L2):
        dummyHead = ListNode(None, None)
        current = dummyHead
        p1, p2 = L1, L2
        while p1 and p2:
            if p1.data <= p2.data:
                current.next = p1
                p1 = p1.next
            else:
                current.next = p2
                p2 = p2.next
            current = current.next
        current.next = p1 if p1 else p2
        return dummyHead.next
    
    
    L1 = ListNode(1, ListNode(2, ListNode(3, ListNode(4, ListNode(5, None)))))
    L1.print()  # [1, 2, 3, 4, 5]
    
    L2 = ListNode(3, ListNode(4, ListNode(6, None)))
    L2.print()  # [3, 4, 6]
    
    merged = mergeLists(L1, L2)
    merged.print()  # [1, 2, 3, 3, 4, 4, 5, 6]
    

    【讨论】:

    • 确实,我的 Python 代码有一些错误,正如你提到的,它工作正常,但我仍然对参考资料有疑问,你的回答现在更清楚了。
    猜你喜欢
    • 2011-02-09
    • 2017-10-14
    • 2012-04-12
    • 2022-01-15
    • 2015-01-13
    • 2011-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多