diff --git a/data-structure-implementations/queue.js b/data-structure-implementations/queue.js deleted file mode 100644 index d784307..0000000 --- a/data-structure-implementations/queue.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - -QUEUE - -Abstract data type -FIFO - First in, first out -Collection of elements with enqueue and dequeue operations. -Note that there is a natural order. Elements are removed in the reverse order of their addition. - -DO NOT use an array and the native push/shift method in your implementation. Use an object as the underlying data structure. - - -*** Operations: - -myQueue.enqueue(value) -=> count of queue -add value to collection - -myQueue.dequeue() -=> oldest element added collection -Remove item so that it is no longer in collection - -myQueue.peek() -=> oldest element added collection -Similiar to dequeue, but do not remove element from collection - -myQueue.count() -=> number of elements in queue - - -*** Nightmare mode: - -Modify your queue to take a max capacity and return a string if you try to add an element when there's no more room: -myQueue.enqueue(value) -=> "Max capacity already reached. Remove element before adding a new one." - -Create a contains method to check if a value is in the queue: -myQueue.contains('findme') -=> true/false -What's the time complexity? - -Create an until method to get the number of dequeues until you get to a certain value: -queue values - (first)2-5-7-3-6-9(last) -myQueue.until(7) -=> 3 -What's the time complexity? - - */ - -function Queue(capacity) { - this._capacity = capacity; - this._storage = {}; - this._head = 0; - this._tail = 0; -} - -// O(1) -Queue.prototype.enqueue = function(value) { - if (this.count() < this._capacity) { - this._storage[this._tail++] = value; - return this.count(); - } - return 'Max capacity already reached. Remove element before adding a new one.'; -}; - -// O(1) -Queue.prototype.dequeue = function() { - var element = this._storage[this._head]; - delete this._storage[this._head]; - if (this._head < this._tail) this._head++; - return element; -}; - -// O(1) -Queue.prototype.peek = function() { - return this._storage[this._head]; -} - -// O(1) -Queue.prototype.count = function() { - return this._tail - this._head; -}; - - -var myQueue = new Queue(3); -console.log(myQueue.enqueue('a'), 'should be 1'); -console.log(myQueue.enqueue('b'), 'should be 2'); -console.log(myQueue.enqueue('c'), 'should be 3'); -console.log(myQueue.enqueue('d'), 'should be Max capacity reached'); -console.log(myQueue.dequeue(), 'should be a'); -console.log(myQueue.count(), 'should be 2'); -console.log(myQueue.peek(), 'should be b'); -console.log(myQueue.count(), 'should be 2'); diff --git a/data-structures/binarySearchTree.js b/data-structures/binarySearchTree.js new file mode 100644 index 0000000..aaa631b --- /dev/null +++ b/data-structures/binarySearchTree.js @@ -0,0 +1,189 @@ +/* + +BINARY SEARCH TREES + +Abstract data type + +A binary search tree is a tree with the additional constraints: +- each node has only two child nodes (node.left and node.right) +- all the values in the left subtree of a node are less than or equal to the value of the node +- all the values in the right subtree of a node are greater than the value of the node + + +*** Operations: +bsTree.insert(value) +=> bsTree (return for chaining purposes) +Insert value into correct position within tree + +bsTree.contains(value) +=> true/false +Return true if value is in tree, false if not + +bsTree.traverseDepthFirst_inOrder(callback) +=> undefined +Invoke the callback for every node in a depth-first in-order (visit left branch, then current node, than right branch) +Note: In-Order traversal is most common type for binary trees. For binary search tree, this visits the nodes in ascending order (hence the name). + +bsTree.traverseDepthFirst_preOrder(callback) +=> undefined +Invoke the callback for every node in a depth-first pre-order (visits current node before its child nodes) + +bsTree.traverseDepthFirst_postOrder(callback) +=> undefined +Invoke the callback for every node in a depth-first post-order (visit the current node after its child nodes) + +bsTree.traverseBreadthFirst(callback) +=> undefined +Invoke the callback for every node in a breadth-first order + +bsTree.checkIfFull() +=> true/false +A binary tree is full if every node has either zero or two children (no nodes have only one child) + +bsTree.checkIfBalanced() +=> true/false +For this exercise, let's say that a tree is balanced if the minimum height and the maximum height differ by no more than 1. The height for a branch is the number of levels below the root. + +*** Additional Exercises: +A binary search tree was created by iterating over an array and inserting each element into the tree. Given a binary search tree with no duplicates, how many different arrays would result in the creation of this tree. + +*/ + + +// Binary Search Tree +function BinarySearchTree (value) { + this.value = value; + this.left = null; + this.right = null; +} + +// O(log(n)) +BinarySearchTree.prototype.insert = function(value) { + if (value <= this.value) { + if (this.left) this.left.insert(value); + else this.left = new BinarySearchTree(value); + } + else { + if (this.right) this.right.insert(value); + else this.right = new BinarySearchTree(value); + } + return this; +}; + +// O(log(n)); +BinarySearchTree.prototype.contains = function(value) { + if (this.value === value) return true; + if (value < this.value) { + // if this.left doesn't exist return false + // if it does exist, check if its subtree contains the value + return !!this.left && this.left.contains(value); + } + if (value > this.value) { + // if this.right doesn't exist return false + // if it does exist, check if its subtree contains the value + return !!this.right && this.right.contains(value); + } + return false; +}; + +var bsTree = new BinarySearchTree(10); +bsTree.insert(5).insert(15).insert(8).insert(3).insert(7).insert(20).insert(17).insert(9).insert(14); + +// In-Order traversal is most common +// visit left branch, then current node, than right branch +// For binary search tree, this visits the nodes in ascending order (hence the name) +// O(n) +BinarySearchTree.prototype.traverseDepthFirst_inOrder = function(fn) { + if (!this.left && !this.right) return fn(this); + if (this.left) this.left.traverseDepthFirst_inOrder(fn); + fn(this); + if (this.right) this.right.traverseDepthFirst_inOrder(fn); +}; + +var result_traverseDepthFirst_inOrder = []; +bsTree.traverseDepthFirst_inOrder(function(node) { + result_traverseDepthFirst_inOrder.push(node.value); +}); +console.log(result_traverseDepthFirst_inOrder, 'should be [3,5,7,8,9,10,14,15,17,20]'); + +// Pre-Order traversal +// visits current node before its child nodes +// O(n) +BinarySearchTree.prototype.traverseDepthFirst_preOrder = function(fn) { + fn(this); + if (this.left) this.left.traverseDepthFirst_preOrder(fn); + if (this.right) this.right.traverseDepthFirst_preOrder(fn); +}; + +var result_traverseDepthFirst_preOrder = []; +bsTree.traverseDepthFirst_preOrder(function(node) { + result_traverseDepthFirst_preOrder.push(node.value); +}); +console.log(result_traverseDepthFirst_preOrder, 'should be [10,5,3,8,7,9,15,14,20,17]'); + +// Post-Order traversal +// visit the current node after its child nodes +// O(n) +BinarySearchTree.prototype.traverseDepthFirst_postOrder = function(fn) { + if (this.left) this.left.traverseDepthFirst_postOrder(fn); + if (this.right) this.right.traverseDepthFirst_postOrder(fn); + fn(this); +}; + +var result_traverseDepthFirst_postOrder = []; +bsTree.traverseDepthFirst_postOrder(function(node) { + result_traverseDepthFirst_postOrder.push(node.value); +}); +console.log(result_traverseDepthFirst_postOrder, 'should be [3,7,9,8,5,14,17,20,15,10]'); + +// O(n) +BinarySearchTree.prototype.traverseBreadthFirst = function(fn) { + var queue = [this]; + while (queue.length) { + var node = queue.shift(); + fn(node); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } +}; + +var result_traverseBreadthFirst = []; +bsTree.traverseBreadthFirst(function(node) { + result_traverseBreadthFirst.push(node.value); +}); +console.log(result_traverseBreadthFirst, 'should be [10,5,15,3,8,14,20,7,9,17]'); + +// O(n) +// A binary tree is full if every node has either zero or two children (no nodes have only one child) +BinarySearchTree.prototype.checkIfFull = function() { + var result = true; + this.traverseBreadthFirst(function(node) { + if (!node.left && node.right) result = false; + else if (node.left && !node.right) result = false; + }); + return result; +}; + +console.log(bsTree.checkIfFull(), 'should be false'); + +var fullBSTree = new BinarySearchTree(10); +fullBSTree.insert(5).insert(20).insert(15).insert(21).insert(16).insert(13); +console.log(fullBSTree.checkIfFull(), 'should be true'); + +// For this exercise, let's say that a tree is balanced if the minimum height and the maximum height differ by no more than 1. The height for a branch is the number of levels below the root. +// O(n) +BinarySearchTree.prototype.checkIfBalanced = function() { + var heights = []; + var recurse = function(node, height) { + if (!node.left && !node.right) return heights.push(height); + node.left && recurse(node.left, height+1); + node.right && recurse(node.right, height+1); + }; + recurse(this, 1); + var min = Math.min.apply(null, heights); + var max = Math.max.apply(null, heights); + return max-min <= 1; +}; + +console.log(bsTree.checkIfBalanced(), 'should be true'); +console.log(fullBSTree.checkIfBalanced(), 'should be false'); diff --git a/data-structure-implementations/doublyLinkedList.js b/data-structures/doublyLinkedList.js similarity index 94% rename from data-structure-implementations/doublyLinkedList.js rename to data-structures/doublyLinkedList.js index c5e1657..2d98697 100644 --- a/data-structure-implementations/doublyLinkedList.js +++ b/data-structures/doublyLinkedList.js @@ -17,19 +17,19 @@ myList.print() => string with all values in list (ex: '0, 1, 2, 3') myList.insertAfter(refNode, value) -=> value of new node +=> new node insert new node associated with value passed in after refNode myList.removeAfter(refNode) -=> value of removed node +=> removed node remove node after the refNode myList.insertHead(value) -=> value associated with new head +=> new head insert new head node at the beginning of the list with the value passed in myList.removeHead() -=> value of removed head node +=> removed head node remove the head node of the linked list myList.findNode(value) @@ -40,11 +40,11 @@ myList.findNode(value) Say we have a linked list that has 100 items and we want to add an item to the very end. How would you do that with your current implementation? How can you modify the data structure to add an item to the end in constant time? myList.appendToTail(value) -=> value of tail node +=> tail node add a new tail node at the end of the list with the associated value passed in myList.removeTail() -=> value of removed tail node +=> removed tail node remove the tail node from the list @@ -59,16 +59,16 @@ How can we modify our data structures (Node and Linked List classes) so that we Once you've come up with a plan, implement the following methods. myList.insertBefore(refNode, value) -=> value of new node inserted +=> new node inserted insert new node with associated value before refNode myList.removeBefore(refNode) -=> value of removed node +=> removed node remove node before the refNode passed in -*** Nightmare mode: +*** Additional Exercises: Implement a circularly linked list: https://en.wikipedia.org/wiki/Linked_list#Circularly_linked_list @@ -77,8 +77,6 @@ https://en.wikipedia.org/wiki/Linked_list#Circularly_linked_list */ -// 40 min - function Node(value) { this.value = value; this.next = null; @@ -122,7 +120,7 @@ LinkedList.prototype.insertAfter = function(node, value) { if (this.tail === node) this.tail = newNext; // set prev properties - return newNext.value; + return newNext; }; LinkedList.prototype.removeAfter = function(node) { @@ -144,7 +142,7 @@ LinkedList.prototype.removeAfter = function(node) { // if removedNode is tail, set tail to node if (removedNode === this.tail) this.tail = node; - return removedNode.value; + return removedNode; }; LinkedList.prototype.insertHead = function(value) { @@ -153,7 +151,7 @@ LinkedList.prototype.insertHead = function(value) { this.head = newHead; newHead.next = oldHead; oldHead.prev = newHead; - return this.head.value; + return this.head; }; LinkedList.prototype.removeHead = function() { @@ -162,7 +160,7 @@ LinkedList.prototype.removeHead = function() { this.head = newHead; newHead.prev = null; oldHead.next = null; - return oldHead.value; + return oldHead; } LinkedList.prototype.findNode = function(value) { @@ -191,7 +189,7 @@ LinkedList.prototype.appendToTail = function(value) { newTail.prev = oldTail; this.tail = newTail; - return newTail.value; + return newTail; }; LinkedList.prototype.insertBefore = function(node, value) { @@ -207,7 +205,7 @@ LinkedList.prototype.insertBefore = function(node, value) { // if node is head, set newPrev as head if (node === this.head) this.head = newPrev; - return newPrev.value; + return newPrev; }; LinkedList.prototype.removeBefore = function(node) { @@ -227,8 +225,7 @@ LinkedList.prototype.removeBefore = function(node) { removedNode.next = null; removedNode.prev = null; - - return removedNode.value; + return removedNode; }; var myList = new LinkedList(0); diff --git a/data-structures/graph.js b/data-structures/graph.js new file mode 100644 index 0000000..da6f766 --- /dev/null +++ b/data-structures/graph.js @@ -0,0 +1,202 @@ +/* +GRAPHS + +Abstract data type + +Basic Graph: +Stores nodes (represented by any primitive value) and the neighbors for each node. This implementation represents a graph as an adjacency list (https://en.wikipedia.org/wiki/Adjacency_list). + +Here's an example: +1---2---3 + \ / + 4 +graph = { + 1: [2, 4], + 2: [1, 3, 4], + 3: [2], + 4: [1, 2] +} + +Constraints: +This graph implementation is undirected and can have unconnected nodes. The nodes are represented by unique primitive values. + +*** Operations: +graph.addNode(value) // value must be a primitive +=> undefined +Add node to graph + +graph.removeNode(value) +=> undefined +Remove node from graph + +graph.contains(value) +=> true/false +Returns true if value is found in graph, false otherwise + +graph.addEdge(value1, value2) +=> undefined +Create connection between two nodes if they're both present in the graph + +graph.removeEdge(value1, value2) +=> undefined +Remove connection between two nodes + +graph.hasEdge(value1, value2) +=> true/false +Returns true if edge exists, false otherwise + +graph.forEach(callback) +=> undefined +Traverse the graph and invoke the passed callback once for each node. The callback function receives the following for each node: node value, node Neighbors, all nodes. + +*** Nightmare mode: + +Implement traversal methods for depth-first and breadth-first traversal. The methods take a starting node and a callback that gets invoked for each node. The callback should receive two arguments: the node value and the distance (number of edges that separate the node from the starting node). See example usage below. + +graph.traverseDepthFirst(value1, callback) +=> undefined +Starting at the node with the value passed in, traverse the graph and invoke the callback for each node in a depth-first fashion. + +graph.traverseBreadthFirst(value, callback) +=> undefined +Starting at the node with the value passed in, traverse the graph and invoke the callback for each node in a breadth-first fashion. + +Example Usage: +1---2---3---5 + \ / + 4 +{ + '1': [ 2, 4 ], + '2': [ 1, 3, 4 ], + '3': [ 2, 5 ], + '4': [ 1, 2 ], + '5': [ 3 ] +} + +var traverseDF = []; +graph.traverseDepthFirst(1, function(val, distance) { traverseDF.push([val, distance]) }); +traverseDF should be [ [ 1, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 4, 2 ] ] + +var traverseBF = []; +graph.traverseBreadthFirst(1, function(val, distance) { traverseBF.push([val, distance]) }); +traverseBF should be [ [ 1, 0 ], [ 2, 1 ], [ 4, 1 ], [ 3, 2 ], [ 5, 3 ] ] + + +*** Exercises: + +Given a directed graph and two nodes in the graph, write a function that indicates whether there is a route between the two nodes. Bonus: rather than returning a boolean, have your function return the shortest distance between the two nodes (the number of edges that separate them). + +*/ + +function Graph () { + this._nodes = {}; +} + +Graph.prototype.addNode = function(value) { + if (value === undefined) return; + this._nodes[value] = this._nodes[value] || []; +}; + +Graph.prototype.removeNode = function(value) { + this._nodes[value].forEach(function(neighbor) { + var neighborsNeighbors = this._nodes[neighbor]; + var index = neighborsNeighbors.indexOf(value); + neighborsNeighbors.splice(index, 1); + }) + delete this._nodes[value]; +}; + +Graph.prototype.contains = function(value) { + return this._nodes[value] !== undefined; +}; + +Graph.prototype.addEdge = function(value1, value2) { + if (!this._nodes[value1] || !this._nodes[value2]) return 'Invalid node value'; + this._nodes[value1].push(value2); + this._nodes[value2].push(value1); +}; + +Graph.prototype.removeEdge = function(value1, value2) { + if (!this._nodes[value1] || !this._nodes[value2]) return 'Invalid node value'; + var value1Neighbors = this._nodes[value1]; + value1Neighbors.splice(value1Neighbors.indexOf(value2), 1); + var value2Neighbors = this._nodes[value2]; + value2Neighbors.splice(value2Neighbors.indexOf(value1), 1); +}; + +Graph.prototype.hasEdge = function(value1, value2) { + return this._nodes[value1].indexOf(value2) > -1; +}; + +Graph.prototype.forEach = function(fn) { + for (var node in this._nodes) { + fn(node, this._nodes[node], this._nodes); + } +}; + +Graph.prototype.traverseDepthFirst = function(value, fn, visited, distance) { + if (!this._nodes[value] || typeof fn !== 'function') return 'Invalid value or function'; + visited = visited || {}; + distance = distance || 0; + fn(value, distance); + visited[value] = true; + this._nodes[value].forEach(function(neighbor) { + if (visited[neighbor]) return; + this.traverseDepthFirst(neighbor, fn, visited, distance+1); + }, this); +}; + +Graph.prototype.traverseBreadthFirst = function(value, fn) { + if (!this._nodes[value] || typeof fn !== 'function') return 'Invalid value or function'; + var visited = {}; + var queue = [value]; + visited[value] = 0; + while (queue.length) { + var node = queue.shift(); + fn(node, visited[node]); + var neighbors = this._nodes[node].filter(function(neighbor) { + if (visited[neighbor] === undefined) { + visited[neighbor] = visited[node]+1; + return true; + } + }); + queue = queue.concat(neighbors); + } +}; + + + +var graph = new Graph(); + +graph.addNode(1); +graph.addNode(2); +graph.addNode(3); +graph.addNode(4); +graph.addNode(5); +console.log(graph._nodes, 'should have 5'); +graph.removeNode(5); +console.log(graph._nodes, 'should NOT have 5'); +console.log(graph.contains(4), 'should be true'); +console.log(graph.contains(7), 'should be false'); +graph.addEdge(1,2); +graph.addEdge(1,4); +graph.addEdge(3,2); +graph.addEdge(2,4); +graph.addEdge(3,4); +console.log(graph._nodes); +graph.removeEdge(4,3); +console.log(graph._nodes); +console.log(graph.hasEdge(1,2), 'should be true'); +console.log(graph.hasEdge(1,3), 'should be false'); +graph.forEach(function(node, neighbors) { + console.log(node, 'has neighbors:', neighbors); +}); +graph.addNode(5); +graph.addEdge(3,5); +console.log(graph._nodes); +var traverseDF = []; +graph.traverseDepthFirst(1, function(val, dist) { traverseDF.push([val, dist]) }); +console.log(traverseDF, 'should be [ [ 1, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 4, 2 ] ]'); +var traverseBF = []; +graph.traverseBreadthFirst(1, function(val, dist) { traverseBF.push([val, dist]) }); +console.log(traverseBF, 'should be [ [ 1, 0 ], [ 2, 1 ], [ 4, 1 ], [ 3, 2 ], [ 5, 3 ] ]'); diff --git a/data-structure-implementations/hashTable.js b/data-structures/hashTable.js similarity index 81% rename from data-structure-implementations/hashTable.js rename to data-structures/hashTable.js index 66d688c..4f13f42 100644 --- a/data-structure-implementations/hashTable.js +++ b/data-structures/hashTable.js @@ -45,7 +45,7 @@ myMap.forEach(callbackFn) Invokes callback function once for each key-value pair in the hash table -*** Nightmare mode: +*** Exercises: Resize the hash table: - if the count becomes greater than 75% of the table size, double the table size and redistribute the key/value pairs @@ -93,6 +93,21 @@ HashTable.prototype.find = function(key) { }; }; +// O(n) +HashTable.prototype.resize = function(newSize) { + var oldStorage = this._storage; + this._size = newSize; + this._count = 0; + this._storage = []; + var that = this; + oldStorage.forEach(function(bucket) { + bucket.forEach(function(item) { + var key = Object.keys(item)[0]; + that.set(key, item[key]); + }); + }); +}; + // O(1) HashTable.prototype.set = function(key, value) { @@ -108,6 +123,9 @@ HashTable.prototype.set = function(key, value) { newItem[key] = value; this._count++; bucket.push(newItem); + if (this._count > 0.75*this._size) { + this.resize(2*this._size); + } } return this; }; @@ -115,6 +133,7 @@ HashTable.prototype.set = function(key, value) { var myMap = new HashTable(10); console.log(myMap.set('key', 'value'), 'should be HT object'); + // O(1) HashTable.prototype.get = function(key) { var match = this.find(key).match; @@ -140,10 +159,15 @@ console.log(myMap.has('foo'), 'should be false'); // O(1) HashTable.prototype.delete = function(key) { var match = this.find(key).match; - var bucket = this.find(key).bucket; - var matchIndex = this.find(key).matchIndex; - match && bucket.splice(matchIndex, 1); - if (this._count > 0) this._count--; + if (match) { + var bucket = this.find(key).bucket; + var matchIndex = this.find(key).matchIndex; + bucket.splice(matchIndex, 1); + this._count--; + if (this._count < 0.25*this._size) { + this.resize(0.5*this._size); + } + } return !!match; }; @@ -169,8 +193,20 @@ HashTable.prototype.forEach = function(callback) { callback(item); }); }); -} +}; +console.log('count', myMap._count, 'should be 0'); +console.log('size', myMap._size, 'should be 5'); myMap.set('foo', 'bar'); myMap.set('fooAgain', 'barAgain'); +myMap.set('a', 1); +myMap.set('b', 2); myMap.forEach(console.log); +console.log('count', myMap._count, 'should be 4'); +console.log('size', myMap._size, 'should be 10 (doubled)'); +myMap.delete('a'); +console.log('count', myMap._count); +console.log('size', myMap._size); +myMap.delete('b'); +console.log('count', myMap._count); +console.log('size', myMap._size, 'should be 5 (halved)'); diff --git a/data-structures/heap.js b/data-structures/heap.js new file mode 100644 index 0000000..236f470 --- /dev/null +++ b/data-structures/heap.js @@ -0,0 +1,123 @@ +/* +HEAPS + +Abstract data type + +A max heap is a special type of binary tree that satisfies two properties: + +1. Shape property – All nodes which are at the same depth of the tree from the root node are said to be on the same level. Each level of a max heap must be filled with nodes before any nodes can appear on the next level of the max heap. When nodes are added to the next level of the max heap they must be added from left to right. +2. Heap property – In a max heap all nodes are greater than or equal to each of its children nodes. + +Heaps are usually implemented in an array. The first element is the root, the next two are the children of the root, and the next four are their children. The children of the node at position n are at positions 2n+1 and 2n+2. + + +View visualization here: https://presentpath.github.io/heap-visualizer/ + +*** Operations: + +heap.insert(value) +=> undefined +Add value to heap according to the shape and heap property + +heap.removeMax() +=> max value +Remove the max value from the heap, reorder the heap, and return the max value + +*/ + + +var Heap = function() { + this.storage = []; +}; + +Heap.prototype.insert = function(value) { + // Push to storage array + this.storage.push(value); + + var that = this; + + // Recursive function to handle swaps, input index + var reheapify = function(index) { + + // Get parent index + var parentInd = Math.ceil(index/2-1); + // Base Case : value < parent or parent is null + if (parentInd < 0 || that.storage[index] <= that.storage[parentInd]) { + return 'value added to index '+index; + } + // Recursive Case: swap with parent and make recursive call + var temp = that.storage[index]; + that.storage[index] = that.storage[parentInd]; + that.storage[parentInd] = temp; + + return reheapify(parentInd); + }; + return reheapify(that.storage.length-1); +}; + +// Heap remove max method on prototype +// Remove the max value from a heap, reorder the heap, and return the max value +Heap.prototype.removeMax = function() { + // Check if heap is currently empty + if (this.storage.length === 0) { + // If nothing to remove then return null + return null; + } else if (this.storage.length === 1) { + // If heap only has one element in it then pop off the lone element in the storage array and return it + var removed = this.storage.pop(); + + return removed; + } + + // Handle all other cases where heap has more than one node + // Preserve the max value in order to return it + var maxValue = this.storage[0]; + // Replace the root node with the last node of the heap and remove the last node + this.storage[0] = this.storage.pop(); + + // Preserve context for inner recursive helper function + var that = this; + + // Recursive function to restore the heap property of the heap + var reheapify = function(index) { + // Set index of max value to current node's index + var maxIndex = index; + + // Check first child node's value against current node + if ((2*index + 1 < that.storage.length) && (that.storage[2*index + 1] > that.storage[index])) { + // If greater then set index of max value to first child node's index + maxIndex = 2*index + 1; + } + // Check second child node's value against current max node + if ((2*index + 2 < that.storage.length) && (that.storage[2*index + 2] > that.storage[maxIndex])) { + // If greater then set index of max value to second child node's index + maxIndex = 2*index + 2; + } + // If the index of the max value is not equal to the index of the current node + // Then swap the nodes and reheapify at the new index of the current node + if (maxIndex !== index) { + // Swap node values (here's a nifty way to do so "in place" using the XOR bitwise operator) + that.storage[index] = that.storage[index] ^ that.storage[maxIndex]; + that.storage[maxIndex] = that.storage[index] ^ that.storage[maxIndex]; + that.storage[index] = that.storage[index] ^ that.storage[maxIndex]; + + // Reheapify at new index of current node + reheapify(maxIndex); + } + }; + + // Recursively move the swapped node down the heap until it's greater than both of its children + reheapify(0); + + // Return the removed max value from the heap + return maxValue; +}; + +// Instantiate heap +var heap = new Heap(); + +heap.insert(1); +heap.insert(2); +heap.insert(3); +heap.insert(5); +console.log(heap.storage); diff --git a/data-structure-implementations/linkedList.js b/data-structures/linkedList.js similarity index 90% rename from data-structure-implementations/linkedList.js rename to data-structures/linkedList.js index f838b1f..638b3d5 100644 --- a/data-structure-implementations/linkedList.js +++ b/data-structures/linkedList.js @@ -16,35 +16,35 @@ invoke callback function with the value of each node myList.print() => string with all values in list (ex: '0, 1, 2, 3') -myList.insertAfter(refNode, value) -=> value of new node +myList.insertAfter(refNode, value) +=> new node insert new node associated with value passed in after refNode -myList.removeAfter(refNode) -=> value of removed node +myList.removeAfter(refNode) +=> removed node remove node after the refNode myList.insertHead(value) -=> value associated with new head +=> new head insert new head node at the beginning of the list with the value passed in myList.removeHead() -=> value of removed head node +=> removed head node remove the head node of the linked list myList.findNode(value) => first node that has a value matching what was passed in -* Optimization: +* Optimization: Say we have a linked list that has 100 items and we want to add an item to the very end. How would you do that with your current implementation? How can you modify the data structure to add an item to the end in constant time? myList.appendToTail(value) -=> value of tail node +=> tail node add a new tail node at the end of the list with the associated value passed in myList.removeTail() -=> value of removed tail node +=> removed tail node remove the tail node from the list @@ -52,20 +52,21 @@ remove the tail node from the list Now let's think about creating insertBefore and removeBefore methods for the nodes in our list. Can you think of an efficient way to do so? -Think about time complexity. What would it be for your current implementation of a linked list? +Think about time complexity. What would it be for your current implementation of a linked list? How can we modify our data structures (Node and Linked List classes) so that we can make these O(1) operations? Once you've come up with a plan, implement the following methods. -myList.insertBefore(refNode, value) -=> value of new node inserted +myList.insertBefore(refNode, value) +=> new node inserted insert new node with associated value before refNode -myList.removeBefore(refNode) -=> value of removed node +myList.removeBefore(refNode) +=> removed node remove node before the refNode passed in +*SOLUTION: See doublyLinkedList.js file* *** Extra Credit: @@ -110,7 +111,7 @@ LinkedList.prototype.print = function() { LinkedList.prototype.insertAfter = function(node, value) { // get reference to former next var oldNext = node.next; - // create new node + // create new node var newNext = new Node(value); // store it as the new next node.next = newNext; @@ -118,14 +119,14 @@ LinkedList.prototype.insertAfter = function(node, value) { newNext.next = oldNext; // if reference node is tail, set tail to newNext if (this.tail === node) this.tail = newNext; - return newNext.value; + return newNext; }; LinkedList.prototype.removeAfter = function(node) { - // if node is tail, then there's nothing to remove - if (!removedNode) return 'Nothing to remove'; // store reference to removed node var removedNode = node.next; + // if node is tail, then there's nothing to remove + if (!removedNode) return 'Nothing to remove'; // get reference to node after removed node var newNext = removedNode.next; // set newNext as the next node @@ -134,7 +135,7 @@ LinkedList.prototype.removeAfter = function(node) { removedNode.next = null; // if removedNode is tail, set tail to node if (removedNode === this.tail) this.tail = node; - return removedNode.value; + return removedNode; }; LinkedList.prototype.insertHead = function(value) { @@ -142,7 +143,7 @@ LinkedList.prototype.insertHead = function(value) { var oldHead = this.head; this.head = newHead; newHead.next = oldHead; - return this.head.value; + return this.head; }; LinkedList.prototype.removeHead = function() { @@ -150,7 +151,7 @@ LinkedList.prototype.removeHead = function() { var newHead = oldHead.next; this.head = newHead; oldHead.next = null; - return oldHead.value; + return oldHead; } LinkedList.prototype.findNode = function(value) { @@ -164,7 +165,7 @@ LinkedList.prototype.findNode = function(value) { LinkedList.prototype.appendToTail = function(value) { var newTail = new Node(value); - + // // without myList.tail property: O(n) // var node = this.head; // while(node.next) { @@ -176,7 +177,7 @@ LinkedList.prototype.appendToTail = function(value) { this.tail.next = newTail; this.tail = newTail; - return newTail.value; + return newTail; }; @@ -203,7 +204,3 @@ myList.insertAfter(myList.findNode(2), 2.5); console.log(myList.print(), 'should be 0, 2, 2.5, 3, 4'); myList.removeAfter(myList.findNode(2)); console.log(myList.print(), 'should be 0, 2, 3, 4'); - - - - diff --git a/data-structures/queue.js b/data-structures/queue.js new file mode 100644 index 0000000..52d9464 --- /dev/null +++ b/data-structures/queue.js @@ -0,0 +1,190 @@ +/* + +QUEUE + +Abstract data type +FIFO - First in, first out +Collection of elements with enqueue and dequeue operations. +Note that there is a natural order. Elements are removed in the reverse order of their addition. + +DO NOT use an array and the native push/shift method in your implementation. Use an object as the underlying data structure. + + +*** Operations: + +myQueue.enqueue(value) +=> count of queue +add value to collection + +myQueue.dequeue() +=> oldest element added collection +Remove item so that it is no longer in collection + +myQueue.peek() +=> oldest element added collection +Similiar to dequeue, but do not remove element from collection + +myQueue.count() +=> number of elements in queue + + +*** Exercises: + +Modify your queue to take a max capacity and return a string if you try to add an element when there's no more room: +myQueue.enqueue(value) +=> "Max capacity already reached. Remove element before adding a new one." + +Create a contains method to check if a value is in the queue: +myQueue.contains('findme') +=> true/false +What's the time complexity? + +Create an until method to get the number of dequeues until you get to a certain value: +queue values - (first)2-5-7-3-6-9(last) +myQueue.until(7) +=> 3 +What's the time complexity? + +Implement a queue using two stacks (include enqueue, dequeue, peek, and count). + +*/ + + +function Queue(capacity) { + this._capacity = capacity || Infinity; + this._storage = {}; + this._head = 0; + this._tail = 0; +} + +// O(1) +Queue.prototype.enqueue = function(value) { + if (this.count() < this._capacity) { + this._storage[this._tail++] = value; + return this.count(); + } + return 'Max capacity already reached. Remove element before adding a new one.'; +}; + +// O(1) +Queue.prototype.dequeue = function() { + var element = this._storage[this._head]; + delete this._storage[this._head]; + if (this._head < this._tail) this._head++; + return element; +}; + +// O(1) +Queue.prototype.peek = function() { + return this._storage[this._head]; +} + +// O(1) +Queue.prototype.count = function() { + return this._tail - this._head; +}; + +// O(n) +Queue.prototype.contains = function(value) { + for (var i = this._head; i < this._tail; i++) { + if (this._storage[i] === value) return true; + } + return false; +}; + +// O(n) +Queue.prototype.until = function(value) { + for (var i = this._head; i < this._tail; i++) { + if (this._storage[i] === value) return i-this._head+1; + } + return null; +}; + +// var myQueue = new Queue(3); +// console.log(myQueue.enqueue('a'), 'should be 1'); +// console.log(myQueue.enqueue('b'), 'should be 2'); +// console.log(myQueue.enqueue('c'), 'should be 3'); +// console.log(myQueue.enqueue('d'), 'should be Max capacity reached'); +// console.log(myQueue.dequeue(), 'should be a'); +// console.log(myQueue.count(), 'should be 2'); +// console.log(myQueue.peek(), 'should be b'); +// console.log(myQueue.count(), 'should be 2'); +// console.log(myQueue.contains('b'), 'should be true'); +// console.log(myQueue.contains('d'), 'should be false'); +// console.log(myQueue._storage, myQueue._head); +// console.log(myQueue.until('b'), 'should be 1'); +// console.log(myQueue.until('c'), 'should be 2'); +// console.log(myQueue.until('d'), 'should be null'); + +// ____________________________________________ +// EXERCISES +// Implement a queue using two stacks +function Stack(capacity) { + this._capacity = capacity || Infinity; + this._storage = {}; + this._count = 0; +} + +Stack.prototype.push = function(value) { + if (this._count < this._capacity) { + this._storage[this._count++] = value; + return this._count; + } + return 'Max capacity already reached. Remove element before adding a new one.'; +}; + +Stack.prototype.pop = function() { + var value = this._storage[--this._count]; + delete this._storage[this._count]; + if (this._count < 0) { + this._count = 0; + } + return value; +}; + +Stack.prototype.peek = function() { + return this._storage[this._count-1]; +} + +Stack.prototype.count = function() { + return this._count; +}; + +function Queue_TwoStacks() { + this._stackIn = new Stack(); + this._stackOut = new Stack(); +} + +Queue_TwoStacks.prototype.enqueue = function(val) { + this._stackIn.push(val); +}; + +Queue_TwoStacks.prototype._transferStacks = function() { + while (this._stackIn.count() > 0) { + this._stackOut.push(this._stackIn.pop()); + } +}; + +Queue_TwoStacks.prototype.dequeue = function() { + if (this._stackOut.count() === 0) this._transferStacks(); + return this._stackOut.pop(); +}; + +Queue_TwoStacks.prototype.count = function() { + return this._stackIn.count() + this._stackOut.count(); +}; + +Queue_TwoStacks.prototype.peek = function() { + if (this._stackOut.count() === 0) this._transferStacks(); + return this._stackOut.peek(); +}; + +// var myQueue_TwoStacks = new Queue(3); +// console.log(myQueue_TwoStacks.enqueue('a'), 'should be 1'); +// console.log(myQueue_TwoStacks.enqueue('b'), 'should be 2'); +// console.log(myQueue_TwoStacks.enqueue('c'), 'should be 3'); +// console.log(myQueue_TwoStacks.enqueue('d'), 'should be Max capacity reached'); +// console.log(myQueue_TwoStacks.dequeue(), 'should be a'); +// console.log(myQueue_TwoStacks.count(), 'should be 2'); +// console.log(myQueue_TwoStacks.peek(), 'should be b'); +// console.log(myQueue_TwoStacks.count(), 'should be 2'); diff --git a/data-structure-implementations/set.js b/data-structures/set.js similarity index 99% rename from data-structure-implementations/set.js rename to data-structures/set.js index 22ac80e..a147b5e 100644 --- a/data-structure-implementations/set.js +++ b/data-structures/set.js @@ -27,7 +27,7 @@ mySet.forEach(callbackFn) calls callbackFn once for each value in the set -*** Extra Credit: +*** Exercises: Modify your set to take a max capacity and return a string if you try to add an element when there's no more room mySet.add(value) diff --git a/data-structure-implementations/stack.js b/data-structures/stack.js similarity index 52% rename from data-structure-implementations/stack.js rename to data-structures/stack.js index a695a99..8c856cd 100644 --- a/data-structure-implementations/stack.js +++ b/data-structures/stack.js @@ -29,7 +29,7 @@ myStack.count() => number of elements in stack -*** Nightmare mode: +*** Exercises: Modify your stack to take a max capacity and return a string if you try to add an element when there's no more room: myStack.push(value) @@ -46,10 +46,12 @@ myStack.until(7) => 4 What's the time complexity? - */ +Implement a MinStack that has a min method which will return the minimum value in the stack in constant time. + +*/ function Stack(capacity) { - this._capacity = capacity; + this._capacity = capacity || Infinity; this._storage = {}; this._count = 0; } @@ -65,6 +67,10 @@ Stack.prototype.push = function(value) { // O(1) Stack.prototype.pop = function() { + if (this._count === 0) { + return 'No element inside the stack. Add element before poping.' + } + var value = this._storage[--this._count]; delete this._storage[this._count]; if (this._count < 0) { @@ -84,13 +90,61 @@ Stack.prototype.count = function() { }; -var myStack = new Stack(3); -console.log(myStack.push('a'), 'should be 1'); -console.log(myStack.push('b'), 'should be 2'); -console.log(myStack.push('c'), 'should be 3'); -console.log(myStack.push('d'), 'should be Max capacity reached'); -console.log(myStack.pop(), 'should be c'); -console.log(myStack.count(), 'should be 2'); -console.log(myStack.peek(), 'should be b'); -console.log(myStack.count(), 'should be 2'); +// var myStack = new Stack(3); +// console.log(myStack.push('a'), 'should be 1'); +// console.log(myStack.push('b'), 'should be 2'); +// console.log(myStack.push('c'), 'should be 3'); +// console.log(myStack.push('d'), 'should be Max capacity reached'); +// console.log(myStack.pop(), 'should be c'); +// console.log(myStack.count(), 'should be 2'); +// console.log(myStack.peek(), 'should be b'); +// console.log(myStack.count(), 'should be 2'); + +//____________________________________________ +// Implement a min stack +function MinStack(capacity) { + this._capacity = capacity; + this._storage = {}; + this._count = 0; + this._min = new Stack(); +} + +// O(1) +MinStack.prototype.push = function(value) { + if (this._count < this._capacity) { + if (this._min.peek() < value) { + this._min.push(this._min.peek()); + } else { + this._min.push(value); + } + this._storage[this._count++] = value; + return this._count; + } + return 'Max capacity already reached. Remove element before adding a new one.'; +}; +// O(1) +MinStack.prototype.pop = function() { + this._min.pop(); + var value = this._storage[--this._count]; + delete this._storage[this._count]; + if (this._count < 0) { + this._count = 0; + } + return value; +}; + +// O(1) +MinStack.prototype.peek = function() { + return this._storage[this._count-1]; +}; + +// O(1) +MinStack.prototype.count = function() { + return this._count; +}; + +// O(1) +MinStack.prototype.min = function() { + return this._min.peek(); +}; diff --git a/data-structures/tree.js b/data-structures/tree.js new file mode 100644 index 0000000..a6a2a1e --- /dev/null +++ b/data-structures/tree.js @@ -0,0 +1,106 @@ +/* + +TREES + +Abstract data type + +General Tree: +A tree has a root node. +The root node has 0 or more children. +Each child node has 0 or more children. +(each node in the tree can be seen as a subtree) + +Constraints: +A child has only one parent and the root node has no parent. +Note: A tree is a special type of graph. A tree is a graph without cycles. + + +*** Operations: + +tree.addChild(value) +=> child node (new tree) +add child to tree/subtree and return child node (which should be a tree instance) + +tree.contains(value) +=> true/false +Return true if value is in tree, false if not + +tree.traverseDepthFirst(callback) +=> undefined +Invoke the callback for every node in a depth-first order + +tree.traverseBreadthFirst(callback) +=> undefined +Invoke the callback for every node in a breadth-first order + + +*** Exercises: +Given treeA and treeB, check if treeB is a subtree of treeA (meaning that there exists a node n in treeA such that the subtree of n is identical to treeB). + +Given a dictionary, create a prefix tree (commonly known as a trie) +https://en.wikipedia.org/wiki/Trie + +*/ + +// N-ary Tree (any number of children) +function Tree (value) { + this.value = value; + this.children = []; +} + +// Adds child to tree or subtree bound to this keyword +// O(1) +Tree.prototype.addChild = function(value) { + var child = new Tree(value); + this.children.push(child); + return child; +}; + +var tree = new Tree(1); +var branch1 = tree.addChild(2); +var branch2 = tree.addChild(3); +var branch3 = tree.addChild(4); +branch1.addChild(5); +branch1.addChild(6); +branch3.addChild(7).addChild(8); + +// O(n) +Tree.prototype.contains = function(value) { + if (this.value === value) return true; + for (var i=0; i 5*4*3*2*1 => 120 +*/ + +function factorial(n) { + if (n === 1) return 1; + return n*factorial(n-1); +} + +console.log(factorial(5)); diff --git a/recursion/fibonacci.js b/recursion/fibonacci.js new file mode 100644 index 0000000..4d271f6 --- /dev/null +++ b/recursion/fibonacci.js @@ -0,0 +1,33 @@ +/* + +Write a function that outputs the nth Fibonnaci number. A number in this sequence is found by adding up the two numbers before it. + +Fibonnaci's sequence: +input 0 1 2 3 4 5 6 7 8 9 ... +output 0 1 1 2 3 5 8 13 21 34 ... + +What is the time complexity? Can you think of optimizing your solution? (Hint: look up dynamic programming) + +*/ + +// O(2^n) implementation +function fibonnaci (n) { + if (n === 0 || n === 1) return n; + return fibonnaci(n-1) + fibonnaci(n-2); +} + + +// O(n) implementation using dynamic programming +function fibonnaciDP (n) { + var memo = { + 0: 0, + 1: 1 + }; + function recurse(m) { + if (memo[m] === undefined) { + memo[m] = recurse(m-1) + recurse(m-2); + } + return memo[m]; + } + return recurse(n); +} diff --git a/recursion/flatten.js b/recursion/flatten.js new file mode 100644 index 0000000..5b6124b --- /dev/null +++ b/recursion/flatten.js @@ -0,0 +1,21 @@ +/* +Implement a function that flattens a nested array. + +flatten([1,[2],[3, [[4]]]]); +=> [1,2,3,4] +*/ + +function flatten(arr) { + var result = []; + arr.forEach(function(element) { + if (!Array.isArray(element)) { + result.push(element); + } else { + result = result.concat(flatten(element)); + } + }); + return result; +} + + +console.log(flatten([1,[2],[3, [[4]]]])); diff --git a/recursion/greatestCommonDivisor.js b/recursion/greatestCommonDivisor.js new file mode 100644 index 0000000..470110c --- /dev/null +++ b/recursion/greatestCommonDivisor.js @@ -0,0 +1,20 @@ +/* + +Write a function that takes two numbers and returns the greatest common divisor. + +*/ + +// Euclid's algorithm +function gcd(num1, num2) { + var min = Math.min(num1, num2); + var max = Math.max(num1, num2); + if (max % min === 0) return min; + else return gcd(min, max % min); +} + +// Dijkstra's algorithm +function gcd(num1, num2) { + if (num1 === num2) return num1; + else if (num1 > num2) return gcd(num1-num2, num2); + else return gcd(num1, num2-num1); +} diff --git a/recursion/paintFill.js b/recursion/paintFill.js new file mode 100644 index 0000000..df0c72e --- /dev/null +++ b/recursion/paintFill.js @@ -0,0 +1,28 @@ +/* + +Implement a function that takes in a two-dimensional array of colors that represents a screen, a point in the array, and a color. The function will change the original color of the point to the new color and will fill the surrounding area with the original color in the same fashion. + +*/ + +function paintFill(screen, point, newColor) { + var originalColor = screen[point.row][point.column]; + function recurse(row, column) { + screen[row][column] = newColor; + if (screen[row-1] && screen[row-1][column] === originalColor) recurse(row-1, column); + if (screen[row+1] && screen[row+1][column] === originalColor) recurse(row+1, column); + if (screen[row][column-1] === originalColor) recurse(row, column-1); + if (screen[row][column+1] === originalColor) recurse(row, column+1); + } + recurse(point.row, point.column); + return screen; +} + +var screen = [ + [1,1,1,1,1,1,1], + [1,2,2,2,2,1,1], + [3,3,3,2,2,2,1], + [1,1,2,2,2,3,3], + [1,1,1,1,3,3,3] +]; + +console.log(paintFill(screen, {row: 4, column: 4}, 5)); diff --git a/recursion/pathCount.js b/recursion/pathCount.js new file mode 100644 index 0000000..ab278a7 --- /dev/null +++ b/recursion/pathCount.js @@ -0,0 +1,65 @@ +/* + +Version 1: +Given the size of a grid (X rows and Y columns), write a function that returns the number of possible paths one can take starting at the top left of the grid and ending at the bottom right, assuming you can only move to the right and down. + +Version 2: +Now, imagine that the you can move up, down, left, or right but cannot visit a spot that has already been visited. How many unique paths can the you take? +Hint: it may be useful to create a grid class and use it to keep track of the state as the you traverses the grid. What useful methods can you put on your grid class? Can you write an implementation that only uses a single grid? + +*/ + +// Version 1 +function pathCountV1 (rowCount, columnCount) { + var pathCount = 0; + function recurse(i, j) { + if (i === rowCount-1 && j === columnCount-1) pathCount++; + else { + if (i= rowCount || j >= colCount) { return; } + if (grid.hasBeenVisited(i, j)) { return; } + grid.togglePiece(i, j); + recurse(grid, i+1, j); + recurse(grid, i, j+1); + recurse(grid, i-1, j); + recurse(grid, i, j-1); + grid.togglePiece(i, j); + } + recurse(grid, 0, 0); + return count; +} diff --git a/recursion/recursionIntro.js b/recursion/recursionIntro.js new file mode 100644 index 0000000..ccfc00f --- /dev/null +++ b/recursion/recursionIntro.js @@ -0,0 +1,73 @@ +//1. Write a function that loops through the numbers n down to 0. If you haven't done so try using a while loop to do this. + +var countDown = function(n) { + while (n > 0) { + console.log(n); + n--; + } +} + +//2. Next, try looping just like above except using recursion + +var recursiveCountDown = function(n) { + while (n > 0) { + console.log(n); + return recursiveCountDown(--n); + } +} + +//3.Write a function 'exponent' that takes two arguments base, and expo, uses a while loop to return the exponenet value of the base. + +var exponent = function(base, expo) { + var val = base; + + while (expo > 1) { + val *= base + expo--; + } + + return val; +} + +//4. Write a function 'RecursiveExponent' that takes two arguments base, and expo, recursively returns exponent value of the base. + +var recursiveExponent = function(base, expo) { + if (expo === 1) { + return base; + } + + return base * recursiveExponent(base, --expo) +} + +//5. Write a function 'recursiveMultiplier' that takes two arguments, 'arr and num', and multiplies each arr value into by num and returns an array of the values. + +var recursiveMultiplier = function(arr, num) { + if(arr.length === 0){ + return arr; + } + + var last = arr.pop(); + + recursiveMultiplier(arr, num); + + arr.push(last * num); + + return arr; +} + +//6. Write a function 'recursiveReverse' that takes an array and uses recursion to return its contents in reverse + +var recursiveReverse = function(arr) { + var reversedArr = []; + var addItems = function(orderedArr) { + if (orderedArr.length > 0) { + reversedArr.push(orderedArr.pop()); + addItems(orderedArr); + } + return; + } + + addItems(arr); + + return reversedArr; +} diff --git a/recursion/reverse.js b/recursion/reverse.js new file mode 100644 index 0000000..cb11bb7 --- /dev/null +++ b/recursion/reverse.js @@ -0,0 +1,13 @@ +/* +Implement a function that will reverse a string recursively. + +reverse('abcdefg') +=> 'gfedcba' +*/ + +function reverse(str) { + if (str.length === 0) return ''; + return str[str.length-1] + reverse(str.substr(0,str.length-1)); +} + +console.log(reverse('abcdefg')); diff --git a/recursion/stringPermutations.js b/recursion/stringPermutations.js new file mode 100644 index 0000000..7ba7517 --- /dev/null +++ b/recursion/stringPermutations.js @@ -0,0 +1,19 @@ +/* + +Write a function that takes a string and returns all permutations of the string. Ensure that there are no duplicates in the output. + +*/ + +function permutations(str) { + var results = {}; + function recurse(word, remainder) { + if (remainder.length === 0) { + return results[word] = true; + } + for (var i=0; i hi) return null; + var mid = Math.floor((hi-lo)/2) + lo; + if (target === array[mid]) return mid; + else if (target < array[mid]) return recurse(lo, mid-1); + else return recurse(mid+1, hi); + })(0, array.length-1); +} + +var arr = [0,1,2,3,4,5]; +console.log(binarySearch(arr, 0), 'should be', 0); +console.log(binarySearch(arr, 1), 'should be', 1); +console.log(binarySearch(arr, 2), 'should be', 2); +console.log(binarySearch(arr, 3), 'should be', 3); +console.log(binarySearch(arr, 4), 'should be', 4); +console.log(binarySearch(arr, 5), 'should be', 5); +console.log(binarySearch(arr, 8), 'should be', null); diff --git a/sorting-algorithms/src/bubble.js b/sorting-algorithms/bubble.js similarity index 75% rename from sorting-algorithms/src/bubble.js rename to sorting-algorithms/bubble.js index b890b5e..9f09e6d 100644 --- a/sorting-algorithms/src/bubble.js +++ b/sorting-algorithms/bubble.js @@ -9,6 +9,7 @@ Iterate over array, comparing adjacent items and swap if in incorrect order. Lar - Implement bubble sort - Identify time complexity +- Identify space complexity Optimizations: - Make algorithm adaptive (if at any point array is already sorted, exit function early). After doing this, what is time complexity for nearly sorted arrays? @@ -20,6 +21,18 @@ Variants: */ +/* +Properties: +O(1) extra space +Time complexity: +- worst: O(n2) comparisons and swaps +- best: O(n) when nearly sorted +not stable +adaptive - O(n) time when nearly sorted + +Use cases: +Similar to insertion sort (many properties are the same for insertion and bubble sort) - when the data is nearly sorted (since it's adaptive) or when the problem size is small (because it has low memory overhead) +*/ var bubbleSort = function(array) { // while wall > 0 diff --git a/sorting-algorithms/heap.js b/sorting-algorithms/heap.js new file mode 100644 index 0000000..f710ad7 --- /dev/null +++ b/sorting-algorithms/heap.js @@ -0,0 +1,146 @@ +/* +HEAP SORT + +*** Description + +Heap sort consists of two parts: +1) Build a heap out of the data from the input array +2) Create sorted array by iteratively removing the largest element from the heap and inserting it into the sorted array + +*** Exercise + +Implement heap sort using the Heap constructor provided. + +*** Extra Credit + +Now try building heapsort from scratch, without using the Heap constructor. See if you can do everything in place. +(for hints see https://en.wikipedia.org/wiki/Heapsort#Algorithm) + +Example: +Note that the array is separated into two portions - + heap|sorted +[ 7 2 5 1|8 9 10] +swap first value (max) with element at end of heap +[ 1 2 5 7|8 9 10] +heap size has decreased and sorted portion has now grown +[ 1 2 5|7 8 9 10] +heap portion is re-heapified so that the first value is the new max +[ 5 1 2|7 8 9 10] + heap|sorted +repeat... + +*/ + +var Heap = function() { + this.storage = []; +}; + +Heap.prototype.insert = function(value) { + // Push to storage array + this.storage.push(value); + + var that = this; + + // Recursive function to handle swaps, input index + var reheapify = function(index) { + + // Get parent index + var parentInd = Math.ceil(index/2-1); + // Base Case : value < parent or parent is null + if (parentInd < 0 || that.storage[index] <= that.storage[parentInd]) { + return 'value added to index '+index; + } + // Recursive Case: swap with parent and make recursive call + var temp = that.storage[index]; + that.storage[index] = that.storage[parentInd]; + that.storage[parentInd] = temp; + + return reheapify(parentInd); + }; + return reheapify(that.storage.length-1); +}; + +// Heap remove max method on prototype +// Remove the max value from a heap, reorder the heap, and return the max value +Heap.prototype.removeMax = function() { + // Check if heap is currently empty + if (this.storage.length === 0) { + // If nothing to remove then return null + return null; + } else if (this.storage.length === 1) { + // If heap only has one element in it then pop off the lone element in the storage array and return it + var removed = this.storage.pop(); + + return removed; + } + + // Handle all other cases where heap has more than one node + // Preserve the max value in order to return it + var maxValue = this.storage[0]; + // Replace the root node with the last node of the heap and remove the last node + this.storage[0] = this.storage.pop(); + + // Preserve context for inner recursive helper function + var that = this; + + // Recursive function to restore the heap property of the heap + var reheapify = function(index) { + // Set index of max value to current node's index + var maxIndex = index; + + // Check first child node's value against current node + if ((2*index + 1 < that.storage.length) && (that.storage[2*index + 1] > that.storage[index])) { + // If greater then set index of max value to first child node's index + maxIndex = 2*index + 1; + } + // Check second child node's value against current max node + if ((2*index + 2 < that.storage.length) && (that.storage[2*index + 2] > that.storage[maxIndex])) { + // If greater then set index of max value to second child node's index + maxIndex = 2*index + 2; + } + // If the index of the max value is not equal to the index of the current node + // Then swap the nodes and reheapify at the new index of the current node + if (maxIndex !== index) { + // Swap node values (here's a nifty way to do so "in place" using the XOR bitwise operator) + that.storage[index] = that.storage[index] ^ that.storage[maxIndex]; + that.storage[maxIndex] = that.storage[index] ^ that.storage[maxIndex]; + that.storage[index] = that.storage[index] ^ that.storage[maxIndex]; + + // Reheapify at new index of current node + reheapify(maxIndex); + } + }; + + // Recursively move the swapped node down the heap until it's greater than both of its children + reheapify(0); + + // Return the removed max value from the heap + return maxValue; +}; + +// ******* HEAPSORT IMPLEMENTATION ******* +function heapSort(arr) { + // create a heap + var heap = new Heap(); + // iterate over input array and add values to heap + arr.forEach(heap.insert.bind(heap)); + // remove max value from heap until empty + var result = []; + while (heap.storage.length) { + result.push(heap.removeMax()); + } + // reverse the result array to sort from smallest to largest + return result.reduceRight(function(acc, val) { + acc.push(val); + return acc; + }, []); +} +console.log(heapSort([1,2,5,9,4,1,6,3])); + +/* +Properties: +O(1) extra space if done in place +O(nlog(n)) time +not stable +not adaptive +*/ diff --git a/sorting-algorithms/src/insertion.js b/sorting-algorithms/insertion.js similarity index 82% rename from sorting-algorithms/src/insertion.js rename to sorting-algorithms/insertion.js index 4af8af8..afdec0b 100644 --- a/sorting-algorithms/src/insertion.js +++ b/sorting-algorithms/insertion.js @@ -20,6 +20,7 @@ now repeat for next unsorted element - Implement insertion sort for array of numbers - Identify time complexity +- Identify space complexity - Modify function to take comparator function. specify default if not provided (check out native Array.sort comparator function for reference) - Use your comparator function to verify that your sort is stable by taking input: [{value: 15}, {value: 10, order: 1}, {value: 10, order: 2}] @@ -30,6 +31,20 @@ now repeat for next unsorted element */ + +/* +Properties: +O(1) extra space +Time complexity: +- worst: O(n^2) comparisons and swaps +- best: O(n) when nearly sorted +stable +adaptive - O(n) time when nearly sorted + +Use cases: +When the data is nearly sorted (since it's adaptive) or when the problem size is small (because it has low memory overhead) +*/ + var insertionSort = function(array, comparator) { comparator = comparator || defaultComparator; diff --git a/sorting-algorithms/src/merge.js b/sorting-algorithms/merge.js similarity index 86% rename from sorting-algorithms/src/merge.js rename to sorting-algorithms/merge.js index e79493d..fc659aa 100644 --- a/sorting-algorithms/src/merge.js +++ b/sorting-algorithms/merge.js @@ -16,6 +16,7 @@ Split array into sublists of size 1, merge adjacent sublists into sorted lists, - Implement recursive merge sort (you might want to write a helper function to handle the merge step) - Implement iterative merge sort - Identify time complexity +- Identify space complexity - Modify function to take comparator function. specify default if not provided (check out native Array.sort comparator function for reference) - Use your comparator function to verify that your sort is stable by taking input: [{value: 15}, {value: 10, order: 1}, {value: 10, order: 2}] @@ -29,7 +30,18 @@ subarrays for natural merge sort: [ [1,2], [4,5], [9] ] */ -var mergeSortRecursive = function(array) { + +/* +Properties: +O(n) extra space for iterative solution +O(n·log(n)) time (for worst and best) +stable - the only stable O(n·log(n)) sorting algorithm +not adaptive + +Use cases: +If stabilty is a requirement and using extra space is no concern, merge sort is great because it's simple to implement, it's the only stable O(nlog(n)) sorting algorithm. +*/ +function mergeSortRecursive (array) { // base case if (array.length <= 1) return array; @@ -43,7 +55,7 @@ var mergeSortRecursive = function(array) { return merge(leftSorted, rightSorted); }; -var mergeSortIterative = function(array) { +function mergeSortIterative (array) { // create array of subarrays with each element var splitArr = array.map(function(element) { return [element]; }); diff --git a/sorting-algorithms/src/quick.js b/sorting-algorithms/quick.js similarity index 80% rename from sorting-algorithms/src/quick.js rename to sorting-algorithms/quick.js index 76b22b5..d580ba6 100644 --- a/sorting-algorithms/src/quick.js +++ b/sorting-algorithms/quick.js @@ -10,8 +10,10 @@ It has a partitioning step, in which you pick an element (called a pivot) and pa *** Exercises - Write a partition helper function. For choice of pivot, for a basic implementation, we recommend choosing either the first or last element in the subarray. If you need hints, look up the Lumoto partiton scheme. Test this out before moving forward! -- Implement quicksort +- Implement quicksort iteratively +- Implement quicksort recursively - Identify time complexity +- Identify space complexity - Consider implications for choice of pivot (https://en.wikipedia.org/wiki/Quicksort#Choice_of_pivot) @@ -22,13 +24,25 @@ Variants: */ + + +/* +Properties: +O(n) extra space +O(n^2) time (for few unique keys), but typically O(n·log(n)) if recursion is balanced +not stable +not adaptive + +Use cases: +Quicksort is in place and has low overhead. If a stable sort is not necessary. It has a higher worstcase time complexity than merge sort (if pivot is not in center of array) +*/ function quicksort(array, lo, hi) { if (lo === undefined) lo = 0; if (hi === undefined) hi = array.length-1; if (lo < hi) { // partition array - p = partition(array, lo, hi); + var p = partition(array, lo, hi); console.log('partitioning from', lo, 'to', hi, '=> partition:', p); // sort subarrays quicksort(array, lo, p-1); diff --git a/sorting-algorithms/src/selection.js b/sorting-algorithms/selection.js similarity index 87% rename from sorting-algorithms/src/selection.js rename to sorting-algorithms/selection.js index 686b970..2db7b5c 100644 --- a/sorting-algorithms/src/selection.js +++ b/sorting-algorithms/selection.js @@ -19,6 +19,7 @@ sorted portion has now grown: - Implement selection sort - Identify time complexity +- Identify space complexity Stable Variant - Implement as a stable sort - rather than swapping, the minimum value is inserted into the first position and all other items are shifted one to the right. How does this impact performance? @@ -29,6 +30,21 @@ Stable Variant */ + +/* +Properties: +O(1) extra space +O(n^2) comparisons (for worst and best) +O(n) swaps +not stable +not adaptive + +Use cases: +Since selection sort minimizes the number of swaps, it's useful when the cost of swapping items is high. + +Comparison to other algorithms: + +*/ var selectionSort = function (array, comparator) { comparator = comparator || defaultComparator; array.forEach(function(element, index) {