Skip to content
Advertisement

Space complexity in shallow vs deep copy

Consider the problem of merging two sorted linked lists whilst keeping the order. For instance if

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

and

L2: 2->3->5

Then the result should be 1->1->2->2->3->4->5. In Elements of Programming Interview, the author presents the following solution in Java, which consists of running through both lists and choosing the smallest value to add to a dummy head:

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 ; 
}

The author then states that this solution is O(1) in space since it is not creating any additional nodes.

I’ve implemented a very similar solution in Python, but I noticed that there is a problem with the following (same is applicable for p2):

current.next = p1
p1 = p1.next

I noticed that, in Python, in this case, the solution won’t work for updating the value of p1 in the second line will also change the value of current.next in the line above (i.e the first line is a shallow copy), thus making the resulting list be None (equivalent to Null pointer). It is possible to have a working solution in python by replacing the first line with

current.next = NewNode(p1.val,None)

Where NewNode creates a new node with the same value as p1 and a next “pointer” pointing to None. However, this clearly will utilise O(m+n) space where m and n are the lengths of both lists.

But this made me question que whole statement inside the if blocks in the Java code. When I do current.next = p1, am I not creating a (deep) copy of the list and therefore using extra space? How come the space complexity in this case is O(1)? I understand from a more algorithmic view where the O(1) space claim comes from, but I am failing to see this materialise in the code. If p1 = p1.next does not update the value of current, there must be a difference in them, so a deep copy must be occuring, but this would mean extra space. What am I missing?

Advertisement

Answer

I don’t know where you went astray in your python implementation, but it should work fine without the current.next = NewNode(p1.val,None) change you made which creates additional nodes (and thus uses extra space). No deep copies are performed nor necessary, because the algorithm is essentially just manipulating pointers. That is, I think the main thing you’re missing is that objects are implicitly references in Java and Python (which are implicitly pointers). This means current and p1 are just two different references to nodes (they aren’t the nodes themselves).

About the block of code in question:

current.next = p1
p1 = p1.next

When you execute current.next = p1, you’re setting the next field of current to be the value of p1. Because p1 is a node, i.e. an object, and objects are references, the value of p1 is a reference to a node. So, after the first statement executes, current.next is a reference to the same node p1 refers to (there’s no deep copy).

Then, with p1 = p1.next, you’re re-defining the variable p1, which is currently a reference to a node, to be the value of p1.next: either a reference to another node or None. This second statement doesn’t change the underlying node that p1 referred to, nor does it impact the value of current.next: you’re just making p1 refer to a different node than it did previously.

Because the algorithm is only updating next fields through references to the currently existing nodes in L1 and L2 rather than creating new nodes, it is O(1). You can see a working python implementation below:

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]
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement