From 16005f3bf39597759918e34b39d27eb25a5755e7 Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Sun, 22 Mar 2020 13:55:10 -0400 Subject: [PATCH 1/3] feat(trie): remove method --- src/data-structures/trees/trie-1.js | 48 ++++++++++++++++++++++ src/data-structures/trees/trie.js | 34 ++++++++++++++++ src/data-structures/trees/trie.spec.js | 55 ++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/src/data-structures/trees/trie-1.js b/src/data-structures/trees/trie-1.js index 80ede3b0..ce328b5a 100644 --- a/src/data-structures/trees/trie-1.js +++ b/src/data-structures/trees/trie-1.js @@ -67,6 +67,54 @@ class Trie { return words; } + + /** + * Return true if found the word to be removed, otherwise false. + * Iterative approach + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove(word) { + const stack = []; + let curr = this; + + for (const char of word) { + if (!curr.children[char]) { return false; } + stack.push(curr); + curr = curr.children[char]; + } + + if (!curr.isWord) { return false; } + let node = stack.pop(); + + do { + node.children = {}; + node = stack.pop(); + } while (node && !node.isWord); + + return true; + } + + /** + * Return true if found the word to be removed, otherwise false. + * recursive approach + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove2(word, i = 0, parent = this) { + if (i === word.length - 1) { + return true; + } + const child = parent.children[word.charAt(i)]; + if (!child) return false; + + const found = this.remove(word, i + 1, child); + + if (found) { + delete parent.children[word.charAt(i)]; + } + return true; + } } // Aliases diff --git a/src/data-structures/trees/trie.js b/src/data-structures/trees/trie.js index b634443b..1a03c241 100644 --- a/src/data-structures/trees/trie.js +++ b/src/data-structures/trees/trie.js @@ -21,6 +21,40 @@ class Trie { curr.isWord = true; } + /** + * Return true if found the word to be removed, otherwise false. + * @param {string} word - The word to remove + * @returns {boolean} + */ + remove(word) { + return this.removeHelper(word); + } + + /** + * Remove word from trie, return true if found, otherwise false. + * @param {string} word - The word to remove. + * @param {Trie} parent - The parent node. + * @param {number} index - The index. + * @param {number} meta.stop - Keeps track of the last letter that won't be removed. + * @returns {boolean} + */ + removeHelper(word, parent = this, index = 0, meta = { stop: 0 }) { + if (index === word.length) { + parent.isWord = false; + if (Object.keys(parent.children)) { meta.stop = index; } + return true; + } + const child = parent.children[word.charAt(index)]; + if (!child) { return false; } + if (parent.isWord) { meta.stop = index; } + const found = this.removeHelper(word, child, index + 1, meta); + // deletes all the nodes beyond `meta.stop`. + if (found && index >= meta.stop) { + delete parent.children[word.charAt(index)]; + } + return found; + } + /** * Retun last node that matches word or prefix or false if not found. * @param {string} word - Word to search. diff --git a/src/data-structures/trees/trie.spec.js b/src/data-structures/trees/trie.spec.js index f72f7adf..fca6588e 100644 --- a/src/data-structures/trees/trie.spec.js +++ b/src/data-structures/trees/trie.spec.js @@ -69,6 +69,13 @@ describe('Trie', () => { expect(trie.startsWith('do')).toEqual(true); }); + it('should match full words if partial is set', () => { + expect(trie.search('dogs', { + partial: true, + })).toEqual(true); + expect(trie.startsWith('dogs')).toEqual(true); + }); + it('should not match non existing words', () => { expect(trie.search('doors')).toEqual(false); }); @@ -129,5 +136,53 @@ describe('Trie', () => { expect(words).toEqual([]); }); }); + + describe('remove', () => { + it('should remove a word', () => { + trie = new Trie(); + trie.insert('a'); + expect(trie.remove('a')).toEqual(true); + expect(trie.getAllWords()).toEqual([]); + }); + + it('should remove word and keep other words', () => { + trie = new Trie(); + trie.insert('a'); + trie.insert('ab'); + expect(trie.remove('a')).toEqual(true); + expect(trie.getAllWords()).toEqual(['ab']); + }); + + it('should remove surrounding word', () => { + trie = new Trie(); + trie.insert('a'); + trie.insert('ab'); + expect(trie.remove('ab')).toEqual(true); + expect(trie.getAllWords()).toEqual(['a']); + }); + + it('should return false when word is not found', () => { + expect(trie.remove('not there')).toBe(false); + }); + + it('should remove words in between and still match', () => { + expect(trie.remove('dog')).toBe(true); + expect(trie.search('dogs')).toBe(true); + expect(trie.startsWith('dog')).toBe(true); + expect(trie.getAllWords()).toEqual([ + 'dogs', 'door', 'day', 'cat', + ]); + }); + + it('should remove word and no longer match partials', () => { + expect(trie.remove('dogs')).toBe(true); + expect(trie.search('dogs')).toBe(false); + expect(trie.search('dog')).toBe(true); + expect(trie.startsWith('dog')).toBe(true); + expect(trie.getAllWords()).toEqual([ + 'dog', 'door', 'day', 'cat', + ]); + }); + }); }); }); From eac045a6bedd0223a2e8b12225f87c428e6fb66f Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Thu, 26 Mar 2020 21:26:28 -0400 Subject: [PATCH 2/3] feat(linkedList): remove by node --- .../linked-lists/linked-list.js | 19 +++++++++++++ .../linked-lists/linked-list.spec.js | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/data-structures/linked-lists/linked-list.js b/src/data-structures/linked-lists/linked-list.js index 191ef164..5da9e56f 100644 --- a/src/data-structures/linked-lists/linked-list.js +++ b/src/data-structures/linked-lists/linked-list.js @@ -265,6 +265,25 @@ class LinkedList { return false; } + /** + * Remove element by Node + * O(1) + */ + removeByNode(node) { + if (!node) { return null; } + if (node === this.first) { + return this.removeFirst(); + } + if (node === this.last) { + return this.removeLast(); + } + node.previous.next = node.next; + node.next.previous = node.previous; + this.size -= 1; + + return node.value; + } + /** * Iterate through the list yield on each node * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#User-defined_iterables diff --git a/src/data-structures/linked-lists/linked-list.spec.js b/src/data-structures/linked-lists/linked-list.spec.js index 288412c1..9026db8e 100644 --- a/src/data-structures/linked-lists/linked-list.spec.js +++ b/src/data-structures/linked-lists/linked-list.spec.js @@ -223,6 +223,33 @@ describe('LinkedList Test', () => { expect(linkedList.length).toBe(1); }); }); + + describe('#removeByNode', () => { + it('should remove first node', () => { + const node = linkedList.first; + linkedList.removeByNode(node); + expect(linkedList.first.value).toEqual('found'); + expect(linkedList.first.previous).toEqual(null); + expect(linkedList.size).toEqual(1); + }); + + it('should remove last node', () => { + const node = linkedList.last; + linkedList.removeByNode(node); + expect(linkedList.first.value).toEqual(0); + expect(linkedList.first.next).toEqual(null); + expect(linkedList.size).toEqual(1); + }); + + it('should remove from the middle', () => { + const node = linkedList.first; + linkedList.addLast('last'); + linkedList.removeByNode(node); + expect(linkedList.first.next).toEqual(linkedList.last); + expect(linkedList.last.previous).toEqual(linkedList.first); + expect(linkedList.size).toEqual(2); + }); + }); }); describe('Doubly Linked List and aliases', () => { From 3e787c6f9ba9c094272be5ac05b997ce44a359d3 Mon Sep 17 00:00:00 2001 From: Adrian Mejia Date: Thu, 26 Mar 2020 21:27:09 -0400 Subject: [PATCH 3/3] feat(lru-cache): add new implementations --- .../01-arrays/longest-unique-characters.js | 72 ++++++++++++ src/data-structures/custom/lru-cache-1.js | 29 +---- src/data-structures/custom/lru-cache-2.js | 72 ++++++++++++ src/data-structures/custom/lru-cache-3.js | 51 +++++++++ src/data-structures/custom/lru-cache.js | 39 ++----- src/data-structures/custom/lru-cache.spec.js | 108 ++++++++++++++++++ 6 files changed, 314 insertions(+), 57 deletions(-) create mode 100644 lab/exercises/01-arrays/longest-unique-characters.js create mode 100644 src/data-structures/custom/lru-cache-2.js create mode 100644 src/data-structures/custom/lru-cache-3.js create mode 100644 src/data-structures/custom/lru-cache.spec.js diff --git a/lab/exercises/01-arrays/longest-unique-characters.js b/lab/exercises/01-arrays/longest-unique-characters.js new file mode 100644 index 00000000..895e4e91 --- /dev/null +++ b/lab/exercises/01-arrays/longest-unique-characters.js @@ -0,0 +1,72 @@ +/** + * @param {string} s + * @return {number} + */ +function lengthOfLongestSubstring(s) { + let max = 0; + let start = 0; + const map = {}; + + for (let i = 0; i < s.length; i++) { + const char = s[i]; + + if (map[char]) { + start = map[char] + 1; + } + + map[char] = i; + max = Math.max(1 + i - start, max); + } + + return max; +} + +const assert = require('assert'); + +const testCases = { abcabcbb: 3 }; + +for (const [string, unique] of Object.entries(testCases)) { + assert.equal(lengthOfLongestSubstring(string), unique); +} + +/* + Longest string without duplicate chars. + + "abcabcbb" + 3 (abc) + + i=6 + c=b + s=5 + h={a:3,b:4,c:2} + m=3 + + --- + a + 1 + + aa + 1 + + ab + 2 + + abc + 3 + + aab + 2 + + aabca + 3 + + "dvdf" + 3 (vdf) + + "abcabcbb" + 3 (abc) + --- + + map: O(n) + backtracking +*/ diff --git a/src/data-structures/custom/lru-cache-1.js b/src/data-structures/custom/lru-cache-1.js index d6453b5d..521559a6 100644 --- a/src/data-structures/custom/lru-cache-1.js +++ b/src/data-structures/custom/lru-cache-1.js @@ -1,30 +1,7 @@ - /** - * 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. - - Follow up: - Could you do both operations in O(1) time complexity? - - Example: - - LRUCache cache = new LRUCache( 2); - - 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 - - * https://leetcode.com/problems/lru-cache/description/ - * - * @param {number} capacity + * Least Recently Used (LRU) cache. + * Map + Array: O(n) + * @param {number} capacity - Number of items to hold. */ const LRUCache = function (capacity) { this.map = new Map(); diff --git a/src/data-structures/custom/lru-cache-2.js b/src/data-structures/custom/lru-cache-2.js new file mode 100644 index 00000000..b07a4b6b --- /dev/null +++ b/src/data-structures/custom/lru-cache-2.js @@ -0,0 +1,72 @@ +/** + * Least Recently Used (LRU) cache. + * Map + (Hash)Set: O(1) + * @param {number} capacity - Number of items to hold. + */ +var LRUCache = function(capacity) { + this.capacity = capacity || 2; + this.map = new Map(); + this.set = new Set(); + this.size = 0; +}; + +/** + * @param {number} key + * @return {number} + */ +LRUCache.prototype.get = function(key) { + if (!this.map.has(key)) return -1; + // move to top + this.set.delete(key); + this.set.add(key); + + return this.map.get(key); +}; + +/** + * @param {number} key + * @param {number} value + * @return {void} + */ +LRUCache.prototype.put = function(key, value) { + this.map.set(key, value); + // move to top + this.set.delete(key); + this.set.add(key); + + if (this.set.size > this.capacity) { + const leastUsedKey = this.set.values().next().value; + this.map.delete(leastUsedKey); + this.set.delete(leastUsedKey); + } + + this.size = this.map.size; +}; + +/** + * Your LRUCache object will be instantiated and called as such: + * var obj = new LRUCache(capacity) + * var param_1 = obj.get(key) + * obj.put(key,value) + */ + + +/* + Implement a hashMap cache with a given capacity that once reach deletes the least used element and store the new one. + + --- + + c = new LRUCache(2); + c.put(1,1); + c.put(2,2); + c.put(3,3); // deletes key 1 + + c = new LRUCache(2); + c.put(1,1); + c.put(2,2); + c.get(1); + c.put(3,3); // deletes key 2 + +*/ + +module.exports = LRUCache; diff --git a/src/data-structures/custom/lru-cache-3.js b/src/data-structures/custom/lru-cache-3.js new file mode 100644 index 00000000..506ffcce --- /dev/null +++ b/src/data-structures/custom/lru-cache-3.js @@ -0,0 +1,51 @@ +const DLinkedList = require('../linked-lists/linked-list'); +/** + * Least Recently Used (LRU) cache. + * Map + Double LinkedList: O(1) + * @param {number} capacity - Number of items to hold. + */ +class LRUCache extends Map { + constructor(capacity) { + super(); // initialize map + this.capacity = capacity; + this.list = new DLinkedList(); + } + + get(key) { + if (!super.has(key)) { return -1; } + + // console.log('get', {key}); + const node = super.get(key); + this.moveToHead(key, node); + + return node.value.value; + } + + put(key, value) { + // console.log('put', {key, value}); + let node; + if (super.has(key)) { + node = super.get(key); + node.value.value = value; + } else { + node = this.list.addLast({key, value}); + } + this.moveToHead(key, node); + + if (this.list.size > this.capacity) { + const firstNode = this.list.removeFirst(); + super.delete(firstNode.key); + } + } + + moveToHead(key, node) { + // remove node and put it in front + this.list.removeByNode(node); + const newNode = this.list.addLast(node.value); + super.set(key, newNode); + // console.log('\tlist', Array.from(this.list).map(l => l.node.value)); + // console.log('\tlist', Array.from(this.list).map(l => l.node.value.key)); + } +} + +module.exports = LRUCache; diff --git a/src/data-structures/custom/lru-cache.js b/src/data-structures/custom/lru-cache.js index 13638b6c..2907f8e6 100644 --- a/src/data-structures/custom/lru-cache.js +++ b/src/data-structures/custom/lru-cache.js @@ -1,34 +1,7 @@ /** - * 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. - - Follow up: - Could you do both operations in O(1) time complexity? - - Example: - - LRUCache cache = new LRUCache( 2); - - 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 - - * https://leetcode.com/problems/lru-cache/description/ - * https://leetcode.com/submissions/detail/178329173/ - * - * @param {number} capacity + * Least Recently Used (LRU) cache. + * (ordered) Map: O(1) + * @param {number} capacity - Number of items to hold. */ class LRUCache { constructor(capacity) { @@ -53,7 +26,7 @@ class LRUCache { rotate(key) { this.moveToTop(key); while (this.map.size > this.capacity) { - const it = this.map.keys(); + const it = this.map.keys(); // keys are in insertion order. this.map.delete(it.next().value); } } @@ -65,6 +38,10 @@ class LRUCache { this.map.set(key, value); } } + + get size() { + return this.map.size; + } } module.exports = LRUCache; diff --git a/src/data-structures/custom/lru-cache.spec.js b/src/data-structures/custom/lru-cache.spec.js new file mode 100644 index 00000000..73368627 --- /dev/null +++ b/src/data-structures/custom/lru-cache.spec.js @@ -0,0 +1,108 @@ +const LRUCache = require('./lru-cache-3'); + +describe('LRU Cache', () => { + let c; + + describe('#constructor', () => { + it('should initialize', () => { + c = new LRUCache(); + expect(c).toBeDefined(); + }); + + it('should initialize', () => { + c = new LRUCache(7); + expect(c.capacity).toEqual(7); + }); + }); + + describe('when initialized', () => { + beforeEach(() => { + c = new LRUCache(2); + }); + + describe('#put', () => { + it('should insert new elements', () => { + c.put(1, 1); + expect(c.size).toEqual(1); + }); + + it('should update existing element', () => { + c.put(1, 1); + c.put(1, 2); + expect(c.size).toEqual(1); + }); + }); + + describe('#get', () => { + it('should get element', () => { + c.put(1, 1); + expect(c.get(1)).toEqual(1); + }); + + it('should return -1 for non-existing elements', () => { + expect(c.get(1)).toEqual(-1); + }); + + it('should not add non-existing number to the top of the list', () => { + c.put(1, 1); + expect(c.get(8)).toEqual(-1); + c.put(2, 2); + expect(c.get(9)).toEqual(-1); + expect(c.get(1)).toEqual(1); + expect(c.get(2)).toEqual(2); + }); + + it('should return -1 for removed elements', () => { + c.put(1, 1); + c.put(2, 2); + c.put(3, 3); + expect(c.get(1)).toEqual(-1); + }); + + it('should not remove value if accessed recently', () => { + c.put(1, 1); + c.put(2, 2); + expect(c.get(1)).toEqual(1); + c.put(3, 3); + expect(c.get(1)).toEqual(1); + expect(c.get(2)).toEqual(-1); + }); + + it('should update a value', () => { + c.put(1, 1); + c.put(1, 2); + expect(c.get(1)).toEqual(2); + }); + }); + + it('should work with size 10', () => { + c = new LRUCache(10); + + c.put(10, 13); + c.put(3, 17); + c.put(6, 11); + c.put(10, 5); + c.put(9, 10); + expect(c.get(13)).toEqual(-1); + c.put(2, 19); + expect(c.get(2)).toEqual(19); + expect(c.get(3)).toEqual(17); + c.put(5, 25); + expect(c.get(8)).toEqual(-1); + c.put(9, 22); + c.put(5, 5); + c.put(1, 30); + expect(c.get(11)).toEqual(-1); + c.put(9, 12); + expect(c.get(7)).toEqual(-1); + expect(c.get(5)).toEqual(5); + expect(c.get(8)).toEqual(-1); + expect(c.get(9)).toEqual(12); + c.put(4, 30); + c.put(9, 3); + expect(c.get(9)).toEqual(3); + expect(c.get(10)).toEqual(5); + expect(c.get(10)).toEqual(5); + }); + }); +});