Space 浅拷贝与深拷贝的复杂性
Space complexity in shallow vs deep copy
考虑在保持顺序的情况下合并两个排序链表的问题。例如如果
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 ;
}
作者随后指出此解决方案在 space 中的复杂度为 O(1),因为它没有创建任何额外的节点。
我在 Python 中实现了一个非常相似的解决方案,但我注意到存在以下问题(同样适用于 p2):
current.next = p1
p1 = p1.next
我注意到,在 Python 中,在这种情况下,解决方案将无法更新第二行中 p1 的值,同时也会更改该行中 current.next 的值上面(即第一行是浅拷贝),从而使结果列表为 None (相当于空指针)。通过将第一行替换为
可以在 python 中得到一个可行的解决方案
current.next = NewNode(p1.val,None)
其中 NewNode 创建一个新节点,其值与 p1 相同,下一个“指针”指向 None。但是,这显然将利用 O(m+n) space,其中 m 和 n 是两个列表的长度。
但这让我质疑 Java 代码中 if 块内的整个语句。当我执行 current.next = p1 时,我不是在创建列表的(深)副本并因此使用额外的 space 吗?为什么在这种情况下 space 复杂度是 O(1)?我从更算法的角度理解 O(1) space 声明的来源,但我没有在代码中看到这一点。如果 p1 = p1.next 没有更新 current 的值,那么它们之间肯定存在差异,因此一定会发生深拷贝,但这将意味着额外的 space。我错过了什么?
我不知道你在 python 实现中哪里误入歧途了,但如果没有你所做的 current.next = NewNode(p1.val,None)
更改创建额外的节点(因此使用额外的 space).没有执行也没有必要执行深度复制,因为该算法本质上只是操纵指针。也就是说,我认为您缺少的主要内容是对象是 Java 和 Python 中的隐式引用(它们是隐式指针)。这意味着 current
和 p1
只是对节点的两个不同引用(它们本身不是节点)。
关于有问题的代码块:
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]
考虑在保持顺序的情况下合并两个排序链表的问题。例如如果
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 ;
}
作者随后指出此解决方案在 space 中的复杂度为 O(1),因为它没有创建任何额外的节点。
我在 Python 中实现了一个非常相似的解决方案,但我注意到存在以下问题(同样适用于 p2):
current.next = p1
p1 = p1.next
我注意到,在 Python 中,在这种情况下,解决方案将无法更新第二行中 p1 的值,同时也会更改该行中 current.next 的值上面(即第一行是浅拷贝),从而使结果列表为 None (相当于空指针)。通过将第一行替换为
可以在 python 中得到一个可行的解决方案current.next = NewNode(p1.val,None)
其中 NewNode 创建一个新节点,其值与 p1 相同,下一个“指针”指向 None。但是,这显然将利用 O(m+n) space,其中 m 和 n 是两个列表的长度。
但这让我质疑 Java 代码中 if 块内的整个语句。当我执行 current.next = p1 时,我不是在创建列表的(深)副本并因此使用额外的 space 吗?为什么在这种情况下 space 复杂度是 O(1)?我从更算法的角度理解 O(1) space 声明的来源,但我没有在代码中看到这一点。如果 p1 = p1.next 没有更新 current 的值,那么它们之间肯定存在差异,因此一定会发生深拷贝,但这将意味着额外的 space。我错过了什么?
我不知道你在 python 实现中哪里误入歧途了,但如果没有你所做的 current.next = NewNode(p1.val,None)
更改创建额外的节点(因此使用额外的 space).没有执行也没有必要执行深度复制,因为该算法本质上只是操纵指针。也就是说,我认为您缺少的主要内容是对象是 Java 和 Python 中的隐式引用(它们是隐式指针)。这意味着 current
和 p1
只是对节点的两个不同引用(它们本身不是节点)。
关于有问题的代码块:
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]