diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0a3855..4bd0eb14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.17.0](https://github.com/amejiarosario/dsa.js/compare/1.16.0...1.17.0) (2020-09-04) + + +### Features + +* **book/set:** add questions and solutions ([f40dc63](https://github.com/amejiarosario/dsa.js/commit/f40dc6314a14e1750146a19163b3b70c30f09d70)) + # [1.16.0](https://github.com/amejiarosario/dsa.js/compare/1.15.0...1.16.0) (2020-09-03) diff --git a/book/D-interview-questions-solutions.asc b/book/D-interview-questions-solutions.asc index 71e65e48..180a6222 100644 --- a/book/D-interview-questions-solutions.asc +++ b/book/D-interview-questions-solutions.asc @@ -577,35 +577,81 @@ The sum is 1, however `sum - k` is `0`. If it doesn't exist on the map, we will - Time: `O(n)`. We visit every number once. - Space: `O(n)`. The map size will be the same as the original array. -// :leveloffset: +1 -// === Solutions for TOPIC Questions -// (((Interview Questions Solutions, TOPIC))) -// :leveloffset: -1 +:leveloffset: +1 + +=== Solutions for Set Questions +(((Interview Questions Solutions, Set))) + +:leveloffset: -1 + + +[#set-q-most-common-word] +include::content/part03/set.asc[tag=set-q-most-common-word] + +This problem requires multiple steps. We can use a `Set` for quickly looking up banned words. For getting the count of each word, we used a `Map`. + +*Algorithm*: + +- Convert text to lowercase. +- Remove any special characters `!?',;.`. +- Convert the paragraph into words array. +- Count how many times words occur. +- Exclude banned words from counts. +- Return the word (or first one) that is the most repeated. + +*Implementation*: + +[source, javascript] +---- +include::interview-questions/most-common-word.js[tags=description;solution] +---- + +Here are heavily relying on Regular Expressions: + +- `\W+` would match all non-words. +- `\s+` catches all whitespace. + +The line that is mapping words to count seems very busy. Here's another version of the same code a little bit more explicit: + +[source, javascript] +---- +include::interview-questions/most-common-word.js[tags=explicit] +---- + +*Complexity Analysis*: + +- Time: `O(m + n)`, where `n` is paragraph length and `m` is the number of banned words. If we were NOT using a `Set` for prohibited words, then the runtime would have been `O(mn)`. +- Space: `O(m + n)`. The extra space complexity is given by the size of the `Map` and `Set`. + -// [#TOPIC-q-FILENAME] -// include::content/part03/TOPIC_FILE.asc[tag=TOPIC-q-FILENAME] -// RESTATE REQUIREMENTS AND DESCRIPTIONS -// *Algorithm*: -// - STEP 1 -// - STEP 2 -// - STEP 2.1 -// - STEP 2.2 +[#set-q-longest-substring-without-repeating-characters] +include::content/part03/set.asc[tag=set-q-longest-substring-without-repeating-characters] -// *Implementation*: +One of the most efficient ways to find repeating characters is using a `Map` or `Set`. Use a `Map` when you need to keep track of the count/index (e.g., string -> count) and use a `Set` when you only need to know if there are repeated characters or not. -// [source, javascript] -// ---- -// include::interview-questions/FILENAME.js[tags=description;solution] -// ---- +*Algorithm*: + +* Visit each letter. +** Insert the letter on a Set. +** Keep track of the maximum size of the Set in `max`. +** If the letter has been seen before, delete until there's no duplicate. +* Return max. -// IMPLEMENTATION NOTES +*Implementation*: -// *Complexity Analysis*: +[source, javascript] +---- +include::interview-questions/longest-substring-without-repeating-characters.js[tags=description;solution] +---- + +We could also have used a Map and keep track of the indexes, but that's not necessary. In this case, the `Set` is all we need. + +*Complexity Analysis*: -// - Time: `O(?)`. WHY? -// - Space: `O(?)`. WHY? +- Time: `O(n)`. We visit each letter once. +- Space: `O(W)`, where `W` is the max length of non-repeating characters. The maximum size of the Set gives the space complexity. In the worst-case scenario, all letters are unique (`W = n`), so our space complexity would be `O(n)`. In the avg. case where there are one or more duplicates, it uses less space than `n`, because `W < n`. diff --git a/book/content/part03/set.asc b/book/content/part03/set.asc index 4f02fa61..7b64690e 100644 --- a/book/content/part03/set.asc +++ b/book/content/part03/set.asc @@ -7,9 +7,9 @@ endif::[] === Set (((Set))) (((Data Structures, Non-Linear, Set))) -A set is a data structure where duplicated entries are not allowed. Set is like an array with unique values. +A set is a data structure where duplicated entries are not allowed. A Set is like an array with only unique values. -NOTE: JavaScript has already a built-in Set data structure. +NOTE: JavaScript already has a built-in Set data structure. Take a look at the following example: @@ -38,15 +38,15 @@ TIP: A hint... it should perform all operations in *O(1)** or at most *O(log n)* If we use a `map`, we can accomplish this. However, maps use a key/value pair. If we only use the keys, we can avoid duplicates. Since in a `map` you can only have one key at a time. -As you might remember from the <> chapter, there are two ways of implementing a `map` and both can be used to create a `set`. Let's explore the difference between the two implementations are. +As you might remember from the <> chapter, there are two ways of implementing a `map`, and both can be used to create a `set`. Let's explore the difference between the two implementations are. ==== HashSet vs TreeSet -We can implement a `map` using a *balanced BST* and using a *hash function*. If we use them to implement a `Set`, then we would have a `HashSet` and `TreeSet` respectively. +We can implement a `map` using a *balanced BST* or a *hash function*. If we use them to implement a `Set`, we would have a `HashSet` and `TreeSet`. * `TreeSet`, would return the values sorted in ascending order. * `HashSet`, would return the values in insertion order. -* Operations on a `HashSet` would take on average O(1) and in the worst case (rehash is due), it would take O(n). +* Operations on a `HashSet` would take on average O(1), and in the worst case (rehash is due), it would take O(n). * Operation on a `TreeSet` is always O(log n). Let’s implement both! @@ -65,7 +65,7 @@ include::{codedir}/data-structures/sets/tree-set.js[tag=constructor] ---- <1> Converts an array or any iterable data structure to a set. -A common use case for Sets is to remove duplicated values from an array. We can do that by passing them in the constructor as follows: +An everyday use case for Sets is to remove duplicated values from an array. We can do that bypassing them in the constructor as follows: .Removing duplicates from an Array using a Set [source, javascript] @@ -115,7 +115,7 @@ Voilà! That’s it! ===== Converting TreeSet to Array -A common use case for a Set is to convert it to an array or use in an iterator (for loops, forEach, …). Let’s provide the method for that: +Another use case for a Set is to convert it to an array or use an iterator (for loops, forEach, …). Let’s provide the method for that: .TreeSet's iterator [source, javascript] @@ -151,7 +151,7 @@ No more duplicates in our array! Check out our https://github.com/amejiarosario/dsa.js/blob/f69b744a1bddd3d99243ca64b3ad46f3f2dd7342/src/data-structures/sets/tree-set.js#L12[GitHub repo for the full TreeSet implementation]. -Let’s now, implement a `HashSet`. +Let’s now implement a `HashSet`. [[hashset]] ==== HashSet @@ -172,7 +172,7 @@ This constructor is useful for converting an array to set and initializing the ` ===== Inserting values to a HashSet -To insert items in a HashSet we use the `set` method of the `HashMap`: +To insert items in a HashSet, we use the `set` method of the `HashMap`: .HashSet's `add` method [source, javascript] @@ -181,7 +181,7 @@ include::{codedir}/data-structures/sets/hash-set.js[tag=add, indent=0] } ---- -`HashMap` stores key/value pairs, but for this, we only need the key, and we ignore the value. +`HashMap` stores key/value pairs, but we only need the keys for Set, so we ignore the value. ===== Finding values in a HashSet @@ -198,7 +198,7 @@ true, and if it’s empty, it will be false. ===== Deleting values from a HashSet -For deleting a value from a hashSet we use the HashMap’s delete method: +For deleting a value from a hashSet, we use the HashMap’s delete method: .HashSet's `delete` method [source, javascript] @@ -210,7 +210,7 @@ This method has an average runtime of *O(1)*. ==== HashSet vs HashMap Time Complexity -We can say that `HashMap` in on average more performant O(1) vs. O(log n). However, if a +We can say that `HashMap` in on average, more performant O(1) vs. O(log n). However, if a rehash happens, it will take *O(n)* instead of *O(1)*. A `TreeSet` is always *O(log n)*. (((Tables, Non-Linear DS, HashSet/TreeSet complexities))) @@ -236,3 +236,80 @@ difference besides runtime is that: .TreeSet vs HashSet * HashSet keeps data in insertion order * TreeSet keeps data sorted in ascending order. + + +==== Practice Questions +(((Interview Questions, Set))) + +// tag::set-q-most-common-word[] +===== Most common word + +*ST-1*) _Given a text and a list of banned words. +Find the most common word that is not on the banned list. +You might need to sanitize the text and strip out punctuation `?!,'.`_ +// end::set-q-most-common-word[] + +// _Seen in interviews at: Amazon._ + +Examples: + +[source, javascript] +---- +mostCommonWord( + `How much wood, would a Woodchuck chuck, + if a woodchuck could chuck?`, + ['a'], +); // woodchuck or chuck (both show up twice) + +mostCommonWord( +`It's a blue ball and its shade... Very BLUE!`, +['and']); // blue (it show up twice, "it" and "its" once) +---- + +Starter code: + +[source, javascript] +---- +include::../../interview-questions/most-common-word.js[tags=description;placeholder] +---- + + +_Solution: <>_ + + + + + + + + + + + +// tag::set-q-longest-substring-without-repeating-characters[] +===== Longest Without Repeating + +*ST-2*) _Find the length of the longest substring without repeating characters._ + +// end::set-q-longest-substring-without-repeating-characters[] + +// _Seen in interviews at: Amazon, Facebook, Bloomberg._ + +Examples: + +[source, javascript] +---- +lenLongestSubstring('aaaaa'); // 1 ('a') +lenLongestSubstring('abccdefg'); // 5 ('cdefg') +lenLongestSubstring('abc'); // 3 ('abc') +---- + +Starter code: + +[source, javascript] +---- +include::../../interview-questions/longest-substring-without-repeating-characters.js[tags=description;placeholder] +---- + + +_Solution: <>_ diff --git a/book/interview-questions/binary-tree-right-side-view.js b/book/interview-questions/binary-tree-right-side-view.js index 52b8c3e2..2e489077 100644 --- a/book/interview-questions/binary-tree-right-side-view.js +++ b/book/interview-questions/binary-tree-right-side-view.js @@ -4,7 +4,7 @@ const { Queue } = require('../../src/index'); /** * Find the rightmost nodes by level. * - * @example + * @example rightSideView(bt([1,2,3,4])); // [1, 3, 4] * 1 <- 1 * / \ * 2 3 <- 3 diff --git a/book/interview-questions/buy-sell-stock.js b/book/interview-questions/buy-sell-stock.js index 47422ff0..b12c11d4 100644 --- a/book/interview-questions/buy-sell-stock.js +++ b/book/interview-questions/buy-sell-stock.js @@ -6,6 +6,7 @@ * maxProfit([1, 2, 3]); // => 2 * maxProfit([3, 2, 1]); // => 0 * @param {number[]} prices - Array with daily stock prices + * @returns {number} - Max profit */ function maxProfit(prices) { // end::description[] diff --git a/book/interview-questions/daily-temperatures.js b/book/interview-questions/daily-temperatures.js index c46cf702..82f01cf5 100644 --- a/book/interview-questions/daily-temperatures.js +++ b/book/interview-questions/daily-temperatures.js @@ -9,6 +9,7 @@ * dailyTemperatures([73, 69, 72, 76, 73]); // [3, 1, 1, 0, 0] * * @param {number[]} t - Daily temperatures + * @returns {number[]} - Array with count of days to warmer temp. */ function dailyTemperatures(t) { // end::description[] diff --git a/book/interview-questions/linkedlist-same-data.js b/book/interview-questions/linkedlist-same-data.js index fd303d36..ca99badc 100644 --- a/book/interview-questions/linkedlist-same-data.js +++ b/book/interview-questions/linkedlist-same-data.js @@ -9,8 +9,9 @@ * hasSameData(['he', 'll', 'o'], ['hel', 'lo']); // true * hasSameData(['hel', 'lo'], ['hi']); // false * - * @param {ListNode} l1 - The root node of list 1 - * @param {ListNode} l2 - The root node of list 2 + * @param {ListNode} l1 - The root node of list 1. + * @param {ListNode} l2 - The root node of list 2. + * @returns {boolean} - true if has same data, false otherwise. */ function hasSameData(l1, l2) { // end::description[] diff --git a/book/interview-questions/longest-substring-without-repeating-characters.js b/book/interview-questions/longest-substring-without-repeating-characters.js index 3da6aac9..3c3606a6 100644 --- a/book/interview-questions/longest-substring-without-repeating-characters.js +++ b/book/interview-questions/longest-substring-without-repeating-characters.js @@ -1,6 +1,16 @@ -// https://leetcode.com/problems/longest-substring-without-repeating-characters/submissions/ - -function lengthOfLongestSubstring(s: string): number { +// tag::description[] +/** + * Find the length of the longest substring without duplicates. + * @example lenLongestSubstring('abccxyz'); // => 4 (cxyz) + * @param {string} s - The string. + * @returns {number} - The length of the longest unique substring. + */ +function lenLongestSubstring(s) { + // end::description[] + // tag::placeholder[] + // write your code here... + // end::placeholder[] + // tag::solution[] let max = 0; const set = new Set(); @@ -11,4 +21,9 @@ function lengthOfLongestSubstring(s: string): number { } return max; -}; + // end::solution[] + // tag::description[] +} +// end::description[] + +module.exports = { lenLongestSubstring }; diff --git a/book/interview-questions/longest-substring-without-repeating-characters.spec.js b/book/interview-questions/longest-substring-without-repeating-characters.spec.js index c56ff203..7011e0d4 100644 --- a/book/interview-questions/longest-substring-without-repeating-characters.spec.js +++ b/book/interview-questions/longest-substring-without-repeating-characters.spec.js @@ -1,5 +1,24 @@ -describe('', () => { - it('', () => { +const { lenLongestSubstring } = require('./longest-substring-without-repeating-characters'); +// const { } = require('../../src/index'); +[lenLongestSubstring].forEach((fn) => { + describe(`Set: ${fn.name}`, () => { + it('should work with null/empty', () => { + const actual = ''; + const expected = 0; + expect(fn(actual)).toEqual(expected); + }); + + it('should work with small case', () => { + const actual = 'abc'; + const expected = 3; + expect(fn(actual)).toEqual(expected); + }); + + it('should work with other case', () => { + const actual = 'abccdefg'; + const expected = 5; + expect(fn(actual)).toEqual(expected); + }); }); }); diff --git a/book/interview-questions/max-subarray.js b/book/interview-questions/max-subarray.js index a1217635..6fb62d5d 100644 --- a/book/interview-questions/max-subarray.js +++ b/book/interview-questions/max-subarray.js @@ -7,6 +7,7 @@ * maxSubArray([-3,4,-1,2,1,-5]); // => 6 * * @param {number[]} a - Array + * @returns {number} - max sum */ function maxSubArray(a) { // end::description[] diff --git a/book/interview-questions/merge-lists.js b/book/interview-questions/merge-lists.js index ad26c326..b50f7f33 100644 --- a/book/interview-questions/merge-lists.js +++ b/book/interview-questions/merge-lists.js @@ -9,6 +9,7 @@ const ListNode = require('../../src/data-structures/linked-lists/node'); * * @param {ListNode} l1 - The root node of list 1 * @param {ListNode} l2 - The root node of list 2 + * @returns {ListNode} - The root of the merged list. */ function mergeTwoLists(l1, l2) { // end::description[] diff --git a/book/interview-questions/most-common-word.js b/book/interview-questions/most-common-word.js new file mode 100644 index 00000000..b2c3f58f --- /dev/null +++ b/book/interview-questions/most-common-word.js @@ -0,0 +1,52 @@ +// tag::description[] +/** + * Find the most common word that is not banned. + * @example mostCommonWord("It's blue and it's round", ['and']) // it + * @param {string} paragraph - The text to sanitize and search on. + * @param {string[]} banned - List of banned words (lowercase) + * @returns {string} - The first word that is the most repeated. + */ +function mostCommonWord(paragraph, banned) { + // end::description[] + // tag::placeholder[] + // write your code here... + // end::placeholder[] + // tag::solution[] + const words = paragraph.toLowerCase().replace(/\W+/g, ' ').split(/\s+/); + const b = new Set(banned); + const map = words.reduce((m, w) => (b.has(w) ? m : m.set(w, 1 + (m.get(w) || 0))), new Map()); + const max = Math.max(...map.values()); + for (const [w, c] of map.entries()) if (c === max) return w; + return ''; + // end::solution[] + // tag::description[] +} +// end::description[] + + +// tag::explicit[] +function mostCommonWordExplicit(paragraph, banned) { + const words = paragraph + .toLowerCase() + .replace(/\W+/g, ' ') + .split(/\s+/); + const exclude = new Set(banned); + + const wordsCount = words.reduce((map, word) => { + if (exclude.has(word)) return map; + const count = map.get(word) || 0; + return map.set(word, 1 + count); + }, new Map()); + + const max = Math.max(...wordsCount.values()); + + for (const [word, count] of wordsCount.entries()) { + if (count === max) { + return word; + } + } + return ''; +} +// end::explicit[] + +module.exports = { mostCommonWord, mostCommonWordExplicit }; diff --git a/book/interview-questions/most-common-word.spec.js b/book/interview-questions/most-common-word.spec.js new file mode 100644 index 00000000..a0a501de --- /dev/null +++ b/book/interview-questions/most-common-word.spec.js @@ -0,0 +1,41 @@ +const { mostCommonWord, mostCommonWordExplicit } = require('./most-common-word'); +// const { } = require('../../src/index'); + +[mostCommonWord, mostCommonWordExplicit].forEach((fn) => { + describe(`Set: ${fn.name}`, () => { + it('should work with null/empty', () => { + const actual = ''; + const expected = ''; + expect(fn(actual, [])).toEqual(expected); + }); + + it('should work with small case', () => { + const actual = 'a'; + const expected = ''; + expect(fn(actual, ['a'])).toEqual(expected); + }); + + it('should work with small case 2', () => { + const actual = 'a'; + const expected = 'a'; + expect(fn(actual, ['b'])).toEqual(expected); + }); + + it('should work with other case', () => { + expect(fn( + `How much wood, would a Woodchuck chuck, + if a woodchuck could chuck?`, + ['a'], + )).toEqual({ + asymmetricMatch: (actual) => ['woodchuck', 'chuck'].includes(actual), + }); + }); + + it('should work with \' case', () => { + expect(fn( + 'It\'s a blue ball and its shade... Very BLUE!', + ['and'], + )).toEqual('blue'); + }); + }); +}); diff --git a/book/interview-questions/recent-counter.js b/book/interview-questions/recent-counter.js index 052e27f6..79eada7c 100644 --- a/book/interview-questions/recent-counter.js +++ b/book/interview-questions/recent-counter.js @@ -5,8 +5,8 @@ const { Queue } = require('../../src/index'); * Counts the most recent requests within a time window. * Each request has its timestamp. * If the time window is 2 seconds, - * any requests that happened more than 2 seconds before the most recent request - * should not count. + * any requests that happened more than 2 seconds before the most + * recent request should not count. * * @example - The time window is 3 sec. (3000 ms) * const counter = new RecentCounter(3000); diff --git a/book/interview-questions/subarray-sum-equals-k.js b/book/interview-questions/subarray-sum-equals-k.js index 42db8545..61aa74f0 100644 --- a/book/interview-questions/subarray-sum-equals-k.js +++ b/book/interview-questions/subarray-sum-equals-k.js @@ -1,4 +1,11 @@ // tag::description[] +/** + * Find the number of subarrays that add up to k. + * @example subarraySum([1, -1, 1], 0); // 3 ([1,-1,1], [1], [1]) + * @param {number[]} nums - Array of integers. + * @param {number} k - The target sum. + * @returns {number} - The number of solutions. + */ function subarraySum(nums, k) { // end::description[] // tag::placeholder[] diff --git a/book/interview-questions/two-sum.js b/book/interview-questions/two-sum.js index 985585ec..37d75204 100644 --- a/book/interview-questions/two-sum.js +++ b/book/interview-questions/two-sum.js @@ -1,4 +1,12 @@ // tag::description[] +/** + * Find two numbers that add up to the target value. + * Return empty array if not found. + * @example twoSum([19, 7, 3], 10) // => [1, 2] + * @param {number[]} nums - Array of integers + * @param {number} target - The target sum. + * @returns {[number, number]} - Array with index 1 and index 2 + */ function twoSum(nums, target) { // end::description[] // tag::placeholder[] diff --git a/book/interview-questions/valid-parentheses.js b/book/interview-questions/valid-parentheses.js index a822a214..2e097cc0 100644 --- a/book/interview-questions/valid-parentheses.js +++ b/book/interview-questions/valid-parentheses.js @@ -2,12 +2,13 @@ /** * Validate if the parentheses are opened and closed in the right order. * - * @example + * @examples * isParenthesesValid('(){}[]'); // true * isParenthesesValid('([{}])'); // true * isParenthesesValid('([{)}]'); // false * * @param {string} string - The string + * @returns {boolean} - True if valid, false otherwise. */ function isParenthesesValid(string) { // end::description[] diff --git a/package-lock.json b/package-lock.json index b3cde99f..7d5d9509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dsa.js", - "version": "1.16.0", + "version": "1.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3b49557f..feaa51db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsa.js", - "version": "1.16.0", + "version": "1.17.0", "description": "Data Structures & Algorithms in JS", "author": "Adrian Mejia (https://adrianmejia.com)", "homepage": "https://github.com/amejiarosario/dsa.js",