diff --git a/DesignHashMap Linear Chaining Question b/DesignHashMap Linear Chaining Question new file mode 100644 index 00000000..6308c825 --- /dev/null +++ b/DesignHashMap Linear Chaining Question @@ -0,0 +1,36 @@ +706. Design HashMap + +Design a HashMap without using any built-in hash table libraries. + +Implement the MyHashMap class: + +MyHashMap() initializes the object with an empty map. +void put(int key, int value) inserts a (key, value) pair into the HashMap. If the key already exists in the map, update the corresponding value. +int get(int key) returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key. +void remove(key) removes the key and its corresponding value if the map contains the mapping for the key. + + +Example 1: + +Input +["MyHashMap", "put", "put", "get", "get", "put", "get", "remove", "get"] +[[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]] +Output +[null, null, null, 1, -1, null, 1, null, -1] + +Explanation +MyHashMap myHashMap = new MyHashMap(); +myHashMap.put(1, 1); // The map is now [[1,1]] +myHashMap.put(2, 2); // The map is now [[1,1], [2,2]] +myHashMap.get(1); // return 1, The map is now [[1,1], [2,2]] +myHashMap.get(3); // return -1 (i.e., not found), The map is now [[1,1], [2,2]] +myHashMap.put(2, 1); // The map is now [[1,1], [2,1]] (i.e., update the existing value) +myHashMap.get(2); // return 1, The map is now [[1,1], [2,1]] +myHashMap.remove(2); // remove the mapping for 2, The map is now [[1,1]] +myHashMap.get(2); // return -1 (i.e., not found), The map is now [[1,1]] + + +Constraints: + +0 <= key, value <= 106 +At most 104 calls will be made to put, get, and remove. \ No newline at end of file diff --git a/HashMapDoubleHashing.java b/HashMapDoubleHashing.java new file mode 100644 index 00000000..214a7f2d --- /dev/null +++ b/HashMapDoubleHashing.java @@ -0,0 +1,64 @@ +/* +HashSet using Double Hashing is already done in Design 1 Assignment + +Time Complexity: O(1) for put, get, and remove. +Space Complexity: O(1) for operations; O(n) for storage due to the 2D array. + */ +public class HashMapDoubleHashing { + + int primaryArraySize; + int secondaryArraySize; + int[][] storage; + + public HashMapDoubleHashing() { + this.primaryArraySize = 1001; + this.secondaryArraySize = 1000; + // Using -1 to indicate an empty slot since key and value are non-negative integers + this.storage = new int[primaryArraySize][]; + } + + private int getPrimaryHashValue(int key) { + return key / primaryArraySize; + } + + private int getSecondaryHashValue(int key) { + return key % secondaryArraySize; + } + + public void put(int key, int value) { + int primaryArrayIndex = getPrimaryHashValue(key); + + if (storage[primaryArrayIndex] == null) { + // Initialize a new array if it doesn't exist, using -1 as the default value + this.storage[primaryArrayIndex] = new int[secondaryArraySize]; + for (int i = 0; i < secondaryArraySize; i++) { + this.storage[primaryArrayIndex][i] = -1; + } + } + + int secondaryArrayIndex = getSecondaryHashValue(key); + storage[primaryArrayIndex][secondaryArrayIndex] = value; + } + + public int get(int key) { + int primaryArrayIndex = getPrimaryHashValue(key); + + if (storage[primaryArrayIndex] == null) { + return -1; // Key doesn't exist + } + + int secondaryArrayIndex = getSecondaryHashValue(key); + return storage[primaryArrayIndex][secondaryArrayIndex]; + } + + public void remove(int key) { + int primaryArrayIndex = getPrimaryHashValue(key); + + if (storage[primaryArrayIndex] == null) { + return; // Nothing to remove as value doesn't exist + } + + int secondaryArrayIndex = getSecondaryHashValue(key); + storage[primaryArrayIndex][secondaryArrayIndex] = -1; // Set to -1 to indicate removal + } +} diff --git a/HashMapLinearChainingHW.java b/HashMapLinearChainingHW.java new file mode 100644 index 00000000..2fbf9489 --- /dev/null +++ b/HashMapLinearChainingHW.java @@ -0,0 +1,150 @@ +/* + Time Complexity : {Time Complexity is analyzed based on => Whenever we are receiving some input, with the increase in our input, how our + algorithm performs} + In order to optimize performance we are increasing the primary data structure size and decreasing the secondary data + structure size to have fewer collisions. If our time complexity is not increasing after a particular point,and that point + is a very small number, we can tell our average time complexity is O(1), here the small number for us is 100 in + secondary data structure. + + Note: If interviewer is asking that 100 size of secondary bucket is also large number, and it is affecting performance, then the answer can + be I can check in production and based on performance if time complexity is not O(1) or more, I can increase the size of primary data structure. + + Note: It is also means increase of size of primary data structure and decrease of secondary data structure is trade off between + performance and space. + + Space Complexity : For user oriented functions, all the functions put(), get(), remove() will be having amortized complexity O(1). For constructor, + we are creating storage so it will have O(n) but that is not what we be looking at. We only look at what we created for user. + + Did this code successfully run on Leetcode : Yes + + HASH MAP USING LINEAR CHAINING {HOMEWORK} + + Here the primary dataset will be a Node Array and secondary will be linkedlist, in java hash map by default uses Linear chaining to + handle collisions + + null, __ , null, null {primary buckets} + ↓ + -1, -1 {linked list} + ↓ + 2001, 3 + ↓ + 3001, 5 + + Here, like in previous homework Design 1, we choose 1000 for primary bucket and 1000 for secondary bucket, we followed this formula + where we took square root of 10^6 so we got 1000 Primary bucket and 1000 secondary bucket, but here if we use that size, we will have 1000 + collisions. + */ +class HashMapLinearChainingHW { + + class Node + { + int key; + int value; + Node next; + + public Node(int key, int value) { + this.key = key; + this.value = value; + } + } + + Node[] storage; //Primary array + int primaryBucketSize; + + public HashMapLinearChainingHW() { + /* + As the bucket size is 10000, there will be 100 collisions, due to less collisions, after 100 elements, the time to access + the element, basically search will become constant i.e. O(1) + */ + this.primaryBucketSize = 10000; + this.storage = new Node[primaryBucketSize]; + } + + private int getPrimaryHash(int key) { + return key%primaryBucketSize; + } + + private Node getPrevNode(Node head, int key) { + Node prev = null; + Node curr = head; + + //Traversing through the nodes to find previous + while(curr != null && curr.key!=key) + { + prev = curr; + curr = curr.next; + } + + return prev; + } + + public void put(int key, int value) { + int primaryIndex = getPrimaryHash(key); + if(storage[primaryIndex] == null){ + storage[primaryIndex] = new Node(-1, -1); + } + /* + There will be two scenarios: + 1) If the node exists, replace the value. + 2) If he node doesn't exist, we will reach the end of linked list and the last node will point to null, so point the last node + to point to new node and new node will point to null. + + Note: We will not need a tail pointer, as anyway due to point 1, we have to traverse through complete linked list to check if the + value exists, it doesn't then only we will add a new node in end, so tail pointer is of no use. + */ + + Node prev = getPrevNode(storage[primaryIndex], key); + if(prev.next == null) //Point 2 + { + prev.next = new Node(key, value); + } + else { + prev.next.value = value; //Point 1 + } + } + + public int get(int key) { + + int primaryIndex = getPrimaryHash(key); + if(storage[primaryIndex] == null) { //Primary index location in array itself is null + return -1; + } + + Node prev = getPrevNode(storage[primaryIndex], key); + if(prev.next == null) { //Point 2, element not found after traversal + return -1; + } + + return prev.next.value; + } + + public void remove(int key) { + int primaryIndex = getPrimaryHash(key); + if(storage[primaryIndex] == null) { //Primary index location in array itself is null + return; + } + + Node prev = getPrevNode(storage[primaryIndex], key); + if(prev.next == null) { //Point 2, element not found after traversal + return; + } + + /* + eg: remove 3 hashmap {100,2 ; 101,3 ; 102,5} + prev = {100} + prev.next = curr.next => old value of prev.next is 101, new value is 102 + curr.next = null meaning 101 is made to point to null + */ + Node curr = prev.next; + prev.next = curr.next; + curr.next = null; + } +} + +/** + * Your MyHashMap object will be instantiated and called as such: + * MyHashMap obj = new MyHashMap(); + * obj.put(key,value); + * int param_2 = obj.get(key); + * obj.remove(key); + */ \ No newline at end of file diff --git a/HashSetLinearChaining.java b/HashSetLinearChaining.java new file mode 100644 index 00000000..f15b84ef --- /dev/null +++ b/HashSetLinearChaining.java @@ -0,0 +1,90 @@ +/* + Time Complexity : This implementation maintains the O(1) average time complexity for operations + due to the hashing, similar to hash map. + Space Complexity : Amortized Complexity for User Functions (add(), remove(), contains()): O(1) + Worst-Case Complexity for User Functions: O(m), where m is the number of elements in the set. + */ +class HashSetLinearChaining { + + class Node { + int key; + Node next; + + public Node(int key) { + this.key = key; + } + } + + Node[] storage; // Primary array + int primaryBucketSize; + + public HashSetLinearChaining() { + // Initialize with the same primary bucket size as the hash map + this.primaryBucketSize = 10000; + this.storage = new Node[primaryBucketSize]; + } + + private int getPrimaryHash(int key) { + return key % primaryBucketSize; + } + + private Node getPrevNode(Node head, int key) { + Node prev = null; + Node curr = head; + + // Traverse the nodes to find the previous node + while (curr != null && curr.key != key) { + prev = curr; + curr = curr.next; + } + + return prev; + } + + public void add(int key) { + int primaryIndex = getPrimaryHash(key); + if (storage[primaryIndex] == null) { + // Initialize the linked list with a dummy head node + storage[primaryIndex] = new Node(-1); + } + + Node prev = getPrevNode(storage[primaryIndex], key); + + // If the key does not exist, add it to the end of the list + if (prev.next == null) { + prev.next = new Node(key); + } + } + + public void remove(int key) { + int primaryIndex = getPrimaryHash(key); + if (storage[primaryIndex] == null) { + // If the bucket is empty, do nothing + return; + } + + Node prev = getPrevNode(storage[primaryIndex], key); + if (prev.next == null) { + // Key not found in the set + return; + } + + // Remove the node + Node curr = prev.next; + prev.next = curr.next; + curr.next = null; + } + + public boolean contains(int key) { + int primaryIndex = getPrimaryHash(key); + if (storage[primaryIndex] == null) { + // If the bucket is empty, the key is not present + return false; + } + + Node prev = getPrevNode(storage[primaryIndex], key); + return prev.next != null; // Return true if the key is found + } +} + + diff --git a/QueueUsingStack Question b/QueueUsingStack Question new file mode 100644 index 00000000..75e95369 --- /dev/null +++ b/QueueUsingStack Question @@ -0,0 +1,42 @@ +232. Implement Queue using Stacks {Easy} + +Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty). + +Implement the MyQueue class: + +void push(int x) Pushes element x to the back of the queue. +int pop() Removes the element from the front of the queue and returns it. +int peek() Returns the element at the front of the queue. +boolean empty() Returns true if the queue is empty, false otherwise. +Notes: + +You must use only standard operations of a stack, which means only push to top, peek/pop from top, size, and is empty operations are valid. +Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations. + + +Example 1: + +Input +["MyQueue", "push", "push", "peek", "pop", "empty"] +[[], [1], [2], [], [], []] +Output +[null, null, null, 1, 1, false] + +Explanation +MyQueue myQueue = new MyQueue(); +myQueue.push(1); // queue is: [1] +myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) +myQueue.peek(); // return 1 +myQueue.pop(); // return 1, queue is [2] +myQueue.empty(); // return false + + +Constraints: + +1 <= x <= 9 +At most 100 calls will be made to push, pop, peek, and empty. +All the calls to pop and peek are valid. + + +Follow-up: Can you implement the queue such that each operation is amortized O(1) time complexity? In other words, performing n operations will take overall O(n) time even if one of those operations may take longer. + diff --git a/QueueUsingStacks.java b/QueueUsingStacks.java new file mode 100644 index 00000000..2be333a0 --- /dev/null +++ b/QueueUsingStacks.java @@ -0,0 +1,140 @@ +/* + Time Complexity : For user oriented functions, push, pop, peep, empty time complexity is O(1). For pop, only once in a blue moon we will have + time complexity as O(n) because we have to move all elements to out stack from in stack once out stack is empty. But since it + is so rare, the average time complexity will be considered which is O(1) for selecting data structures. + + Space Complexity : The space complexity of queue implementation with two stacks remains O(n) because the storage needed grows linearly with + the number of elements in the queue. + + Did this code successfully run on Leetcode : Yes + +Property of Queue: FIFO +1) Two stacks will be used to implement queue , as asked in the problem statement +9,5,6,7,2 + + | 2 | | | + | 7 | | | + | 6 | | | + | 5 | | | + | 9 | | | + + IN STACK OUT STACK + +2) POP,8,POP,10 + +Now, we need to transfer all elements in Out stack before Pop to get the First element + + | | | 9 | + | | | 5 | + | | | 6 | + | | | 7 | + | | | 2 | + + POP + + | | | | + | | | 5 | + | | | 6 | + | | | 7 | + | | | 2 | + + Insert 8 + | | | | + | | | 5 | + | | | 6 | + | | | 7 | + | 8 | | 2 | + + POP + + | | | | + | | | | + | | | 6 | + | | | 7 | + | 8 | | 2 | + + Insert 10 + | | | | + | | | | + | | | 6 | + | 10 | | 7 | + | 8 | | 2 | + + 3) POP, POP, POP + | | | | + | | | | + | | | | + | 10 | | | + | 8 | | | + +4) When Out array is empty transfer elements from In to Out + | | | | + | | | | + | | | | + | | | 8 | + | | | 10 | + + 5) Insert 11 + | | | | + | | | | + | | | | + | | | 8 | + | 11 | | 10 | + + */ + +import java.util.Stack; + +class QueueUsingStacks { + + Stack inStack; + Stack outStack; + + public QueueUsingStacks() { + this.inStack = new Stack<>(); + this.outStack = new Stack<>(); + } + + public void push(int x) { + inStack.push(x); + } + + public int pop() { + + /* + Edge case we don't need, but still writing it => we can't pop on an empty stack but in problem statement + it is mentioned All the calls to pop and peek are valid. + */ + if(empty()) + return -1; + + peek(); + + return outStack.pop(); + } + + public int peek() { + if(outStack.isEmpty()){ + + while(!inStack.isEmpty()) { + outStack.push(inStack.pop()); + } + } + + return outStack.peek(); + } + + public boolean empty() { + //If both the stacks are empty then queue is empty + return inStack.isEmpty() && outStack.isEmpty(); + } +} + +/** + * Your MyQueue object will be instantiated and called as such: + * MyQueue obj = new MyQueue(); + * obj.push(x); + * int param_2 = obj.pop(); + * int param_3 = obj.peek(); + * boolean param_4 = obj.empty(); + */ \ No newline at end of file