# SE103, Week 1 HackerRank Solutions ### Multiple Choice Question 1: Minimum Bytes Per Node Question: On a 64-bit machine, what is the minimum number of bytes per node needed to implement a Singly Linked List, assuming that each node stores a reference to its value? **Answer**: 16 ### Multiple Choice Question 2: List Operations Question: Given the list `1->2`, what would the result look like after the following operations are applied sequentially? Insert(3) Insert(4) Delete(1) What about after setting `head.next.next.val = 5`? **Answer**: 4->3->2 4->3->5 ### Multiple Choice Question 3: Time and Space Complexity What is the space and time complexity of the following algorithm for reversing a linked list? ```python= def get_last(head): if not head or not head.next: return head return get_last(head.next) def reverse(head): if not head or not head.next: return head r = reverse(head.next) l = get_last(r) head.next = None l.next = head return r ``` **Answer**: Time Complexity: O(n^2) Space Complexity: O(n^2) ### Multiple Choice Question 4: Execution by Hand What is the output of running the following code with the input `head = 1 → 2 → 3 → 4 → 5, k = 3`? ```python= def do_what(head, k): if not head: return head e = head ne = head i = 0 while i < k: e = e.next if not e: return head i += 1 while e.next: ne = ne.next e = e.next d = Node("d") d.next = ne.next ne.next = None e.next = head return d.next ``` **Answer**: 3->4->5->1->2 ### Multiple Choice Question 5: Algorithm Space Complexity What is the space complexity of the following algorithm for splitting a linked list into parts? ```python= def splitListToParts(root, k): if k < 2: return [root] len_l = 0 c = root while c: len_l += 1 c = c.next binlen = int(len_l / k) olen = len_l - binlen * k blens = [binlen for i in range(k)] for i in range(olen): blens[i] += 1 ds = [ListNode("dummy") for _ in range(k)] c = root t = 0 b = 0 cd = ds[0] while c: if t == blens[b]: b += 1 t = 0 cd = ds[b] cd.next = c c = c.next cd = cd.next cd.next = None t += 1 return [d.next for d in ds] ``` **Answer**: O(k) ### Coding Question 1: Compute Length Compute the length of the list A. ```java= static int getLength(SinglyLinkedListNode A) { int length = 0; while (A != null) { A = A.next; length += 1; } return length; } ``` ```python= # Iterate over the list until there are no more elements, incrementing a # counter at each iteration. def getLength(A): length = 0 while A: A = A.next length += 1 return length ``` ### Coding Question 2: Palindrome Linked List Given a singly linked list, determine if it is a palindrome. ```java= class Solution { static SinglyLinkedListNode reverseList(SinglyLinkedListNode head) { if (head == null || head.next == null) return head; SinglyLinkedListNode cur = head; SinglyLinkedListNode next = head.next; cur.next = null; while (next != null) { SinglyLinkedListNode temp = next.next; next.next = cur; cur = next; next = temp; } return cur; } static int len(SinglyLinkedListNode A) { int length = 0; while (A != null) { A = A.next; length++; } return length; } static boolean isPalindrome(SinglyLinkedListNode A) { int length = len(A); if (length == 0 || length == 1) return true; int half = (length + 1) / 2; SinglyLinkedListNode cur = A; for (int i = 0; i < half; i++) cur = cur.next; SinglyLinkedListNode secondHalfReversed = reverseList(cur); cur = secondHalfReversed; while (cur != null && A != null) { if (cur.data != A.data) return false; cur = cur.next; A = A.next; } return true; } }; ``` ```python= # The key insight is that we want to iterate from both ends of the list # simultaneously to see whether they match, but a singly linked list only # supports fast iteration in one direction. Therefore, we need to reverse the # right half of the list. # Thus, the algorithm first cuts the list in half and reverses the second half. # Then, iterate over the two halves together, and return False if one of their # values does not match. If all values match, then the original list was a # palindrome. Finding the halfway point of the list, reversing the list, and # iterating over the two halves each take linear time, so the algorithm takes # linear time overall. def isPalindrome(A): def getLength(A): length = 0 while A: A = A.next length += 1 return length def reverseList(head): previous = None while head: next_head = head.next head.next = previous previous = head head = next_head return previous length = getLength(A) if length == 0 or length == 1: return True half = (length + 1) / 2 cur = A for i in xrange(half): cur = cur.next secondHalfReversed = reverseList(cur) cur = secondHalfReversed while cur and A: if cur.data != A.data: return False cur = cur.next A = A.next return True ``` ### Coding Question 3: Plus One Linked List Given a non-negative integer represented as a non-empty singly linked list of digits, add one to the integer. You may assume the integer do not contain any leading zero, except the number 0 itself. The digits are stored such that the most significant digit is at the head of the list. ```java= class Solution { static SinglyLinkedListNode reverseList(SinglyLinkedListNode head) { if (head == null || head.next == null) return head; SinglyLinkedListNode cur = head; SinglyLinkedListNode next = head.next; cur.next = null; while (next != null) { SinglyLinkedListNode temp = next.next; next.next = cur; cur = next; next = temp; } return cur; } static SinglyLinkedListNode addOne(SinglyLinkedListNode A) { A = reverseList(A); SinglyLinkedListNode cur = A; cur.data++; while (cur.data == 10) { cur.data = 0; if (cur.next != null) { cur = cur.next; cur.data++; } else { cur.next = new SinglyLinkedListNode(1); break; } } return reverseList(A); } } ``` ```python= # The key insight is that adding one to a number represented as a linked list # may result in the need to propagate an increment from the least sigificant # digit upwards to the most significant digit. Since it is expensive to iterate # a linked list backwards, the algorithm below first reverses the linked list. # It then increments the least sigificant digit and performs any carrying # operation that is needed, and returns the revese of the resulting list. The # two linked list reversals and the propagation each take linear time, so the # overall running time is O(n). def addOne(A): def reverseList(head): previous = None while head: next_head = head.next head.next = previous previous = head head = next_head return previous A = reverseList(A) cur = A cur.data += 1 while cur.data == 10: cur.data = 0 if cur.next: cur = cur.next cur.data += 1 else: cur.next = SinglyLinkedListNode(1) break return reverseList(A) ``` ### Coding Question 4: LRU Cache Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put. get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1. put(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item. An optimal can do both operations in O(1) time complexity. Feel free to implement or use any data structures available in the standard library, unless you find a pre-built LRU Cache in the standard library. Here is an example usage. ```java= LRUCache cache = new LRUCache( 2 /* capacity */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // returns 1 cache.put(3, 3); // evicts key 2 cache.get(2); // returns -1 (not found) cache.put(4, 4); // evicts key 1 cache.get(1); // returns -1 (not found) cache.get(3); // returns 3 cache.get(4); // returns 4 ``` **Java** ```java= import java.util.*; static class LRUCache { class Node{ public Node(int key, int value) { this.key = key; this.value = value; } public int key; public int value; public Node prev; public Node next; } int capacity; Node evictionQueueHead; Node evictionQueueTail; Map<Integer, Node> nodes; public LRUCache(int capacity) { this.capacity = capacity; nodes = new HashMap<Integer, Node>(); } private void evict(Node node) { if (node.prev != null) { node.prev.next = node.next; } if (node.next != null) { node.next.prev = node.prev; } if (node == evictionQueueTail) { evictionQueueTail = evictionQueueTail.prev; } if (node == evictionQueueHead) { evictionQueueHead = evictionQueueHead.next; } } private void addToEvictionQueue(Node node) { if (evictionQueueTail == null) { evictionQueueTail = evictionQueueHead = node; return; } evictionQueueTail.next = node; node.prev = evictionQueueTail; node.next = null; evictionQueueTail = evictionQueueTail.next; } public int get(int key) { if (!nodes.containsKey(key)) { return -1; } Node node = nodes.get(key); evict(node); addToEvictionQueue(node); return node.value; } public void put(int key, int value) { if (!nodes.containsKey(key) && nodes.size() >= capacity) { // Need to evict head of queue first. nodes.remove(evictionQueueHead.key); evict(evictionQueueHead); } Node node = null; if (nodes.containsKey(key)) { evict(nodes.get(key)); node = nodes.get(key); node.value = value; } else { node = new Node(key, value); nodes.put(key, node); } addToEvictionQueue(node); } } ``` **Python** ```python= # The LRU cache implementation below represents the user's data as a doubly # linked list and a map from keys to nodes in the list. Each node contains a # key-value pair, and nodes are stored from least recently used to most # recently used. Lookups for both get() and put() use the map to find the # target node, and lookups cause the node that was accessed to move to the end # of the linked list. If a put() occurs on a new key and the queue is full, the # node at the beginning of the list is evicted by removing its key from the map # and removing it from the list. # Removing an element from a doubly linked list is O(1). # Adding an element to the end of a doubly linked list when one maintains a # tail pointer is O(1). # Looking up a key in a map is O(1). # These are the only operations used by this implementation, so it supports # both get and put in O(1) time, class LRUCache(object): # This class represents a node in a doubly linked list which is used as an # eviction queue for the LRU Cache. class Node: def __init__(self, key, value): self.key = key self.value = value self.prev = None self.next = None def __init__(self, capacity): # Store the capacity so that we will know when we must evict. self.capacity = capacity self.evictionQueueHead = None self.evictionQueueTail = None # Map keys to nodes in the doubly linked list. The nodes store the # actual values. self.nodes = {} # Remove an element from the eviction queue. def evict(self, node): if node.prev: node.prev.next = node.next if node.next: node.next.prev = node.prev if node == self.evictionQueueTail: self.evictionQueueTail = self.evictionQueueTail.prev if node == self.evictionQueueHead: self.evictionQueueHead = self.evictionQueueHead.next # Add an element to the end of the eviction queue. This method assumes that # the node being added is not already in the eviction queue def addToEvictionQueue(self, node): if self.evictionQueueTail == None: self.evictionQueueTail = self.evictionQueueHead = node return self.evictionQueueTail.next = node node.prev = self.evictionQueueTail # This node may have been attached elsewhere before. node.next = None self.evictionQueueTail = self.evictionQueueTail.next def get(self, key): """ :type key: int :rtype: int """ if key not in self.nodes: return -1 node = self.nodes[key] self.evict(node) self.addToEvictionQueue(node) return node.value def put(self, key, value): """ :type key: int :type value: int :rtype: void """ if key not in self.nodes and len(self.nodes) >= self.capacity: # Need to evict head of queue first. del self.nodes[self.evictionQueueHead.key] self.evict(self.evictionQueueHead) node = None if key in self.nodes: self.evict(self.nodes[key]) node = self.nodes[key] node.value = value else: node = self.Node(key, value) self.nodes[key] = node self.addToEvictionQueue(node) ```