diff --git a/best-time-to-buy-and-sell-stock/Jeehay28.js b/best-time-to-buy-and-sell-stock/Jeehay28.js new file mode 100644 index 000000000..1964dd1e5 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/Jeehay28.js @@ -0,0 +1,41 @@ +/** + * @param {number[]} prices + * @return {number} + */ + +// TC : O(n) +// SC : O(1) + +var maxProfit = function (prices) { + if (prices.length === 1) { + return 0; + } + + // Two variables (profitMax and priceMin) are used to store the maximum profit and minimum price seen, which require O(1) space. + let profitMax = 0; + let priceMin = prices[0]; + + for (const price of prices) { + const profit = price - priceMin; + profitMax = Math.max(profit, profitMax); + priceMin = Math.min(price, priceMin); + } + + return profitMax; +}; + +// Why Constants Are Ignored in Big-O +// In Big-O notation, O(2) is simplified to O(1) because constants are irrelevant in asymptotic analysis. +// Big-O focuses on how resource usage scales with input size, not fixed values. + +// Using 2 variables: O(1) +// Using 10 variables: O(1) +// Using 100 variables: O(1) + +// What Space Complexity Looks Like for Larger Growth +// O(n): Memory grows linearly with the input size (e.g., storing an array of n elements). +// O(n^2): Memory grows quadratically (e.g., a 2D matrix with n*n elements). +// 𝑂(log 𝑛): Memory grows logarithmically (e.g., recursive calls in binary search). +// O(1): Fixed memory usage, regardless of input size (e.g., using a fixed number of variables). + + diff --git a/group-anagrams/Jeehay28.js b/group-anagrams/Jeehay28.js new file mode 100644 index 000000000..40310ddee --- /dev/null +++ b/group-anagrams/Jeehay28.js @@ -0,0 +1,88 @@ +// Guided approach +// TC : O(n*k), where n is the number of strings, and k is the average length of each string. +// SC : O(n*k) +// overal time complexity improved : from O(n * klogk) to O(n * k) + +/** + * Time Complexity Breakdown: + * + * Step | Time Complexity | Explanation + * --------------------------------------- | ------------------- | ---------------------------------------- + * Outer loop over strings (`for` loop) | O(n) | Iterate over each string in the input array `strs`. + * Create key (`createKey`) | O(k) per string | For each string, count character frequencies, with k being the length of the string. + * Map operations (`set` and `get`) | O(1) per string | Inserting and retrieving values from a Map. + * Result array | O(n * k) | Storing grouped anagrams in the result array. + * + * Overall Time Complexity: | O(n * k) | Total time complexity considering all steps. + * + * Space Complexity Breakdown: + * + * Step | Space Complexity | Explanation + * --------------------------------------- | ------------------- | ----------------------------------------- + * Map to store grouped anagrams | O(n * k) | Map stores n groups with each group having at most k characters. + * Auxiliary space for `createKey` | O(1) | The frequency array used to count characters (constant size of 26). + * Space for the result array | O(n * k) | Result array storing n groups of up to k elements. + * + * Overall Space Complexity: | O(n * k) | Total space complexity considering all storage. + */ + +/** + * @param {string[]} strs + * @return {string[][]} + */ + +var groupAnagrams = function (strs) { + const createKey = (str) => { + const arr = new Array(26).fill(0); + + for (const ch of str) { + const idx = ch.charCodeAt() - "a".charCodeAt(); + arr[idx] += 1; + } + + return arr.join("#"); + }; + + let map = new Map(); + + for (const str of strs) { + const key = createKey(str); + map.set(key, [...(map.get(key) || []), str]); + } + + return Array.from(map.values(map)); +}; + +// *My own approach + +// Time Complexity +// 1. Sorting Each String: +// Sorting a string takes O(k*logk), where k is the length of the string. +// Since we sort each string in the input array of size n, the total cost for sorting is O(n*klogk). + +// 2. Hash Map Operations: +// Inserting into the hash map is O(1) on average. Over n strings, the cost remains O(n). + +// Overall Time Complexity: +// O(n*klogk), where n is the number of strings and k is the average length of a string. + +// /** +// * @param {string[]} strs +// * @return {string[][]} +// */ + +// var groupAnagrams = function (strs) { +// // helper function +// const sorted = (str) => { +// return str.split("").sort().join(""); +// }; + +// let obj = {}; + +// for (const str of strs) { +// const key = sorted(str); +// obj[key] = [...(obj[key] || []), str]; +// } + +// return Object.values(obj); +// }; diff --git a/implement-trie-prefix-tree/Jeehay28.js b/implement-trie-prefix-tree/Jeehay28.js new file mode 100644 index 000000000..d1fa20af7 --- /dev/null +++ b/implement-trie-prefix-tree/Jeehay28.js @@ -0,0 +1,69 @@ +// Space complexity: O(n * m), where n is the number of words and m is the average length of the words stored in the trie. +var Trie = function () { + this.root = {}; // Initialize the trie with a root node +}; + +/** + * @param {string} word + * @return {void} + */ + +// Time Complexity: O(m), where m is the length of the word being inserted +Trie.prototype.insert = function (word) { + let currentNode = this.root; + for (any of word) { + // If the character doesn't exist, create a new node + if (!currentNode[any]) { + currentNode[any] = {}; + } + currentNode = currentNode[any]; // Move to the next node + } + currentNode.end = true; // Mark the end of the word +}; + +/** + * @param {string} word + * @return {boolean} + */ +// Time Complexity: O(m), where m is the length of the word being searched +Trie.prototype.search = function (word) { + let currentNode = this.root; + for (any of word) { + // If the character doesn't exist in the trie, return false + if (!currentNode[any]) { + return false; + } + currentNode = currentNode[any]; // Move to the next node + } + + return currentNode.end === true; +}; + +/** + * @param {string} prefix + * @return {boolean} + */ +// Time Complexity: O(m), where m is the length of the prefix +Trie.prototype.startsWith = function (prefix) { + let currentNode = this.root; + + for (any of prefix) { + // If the character doesn't exist, return false + if (!currentNode[any]) { + return false; + } + currentNode = currentNode[any]; // Move to the next node + } + + return true; // Return true if the prefix exists +}; + +/** + * Your Trie object will be instantiated and called as such: + * var obj = new Trie() + * obj.insert(word) + * var param_2 = obj.search(word) + * var param_3 = obj.startsWith(prefix) + */ + + diff --git a/word-break/Jeehay28.js b/word-break/Jeehay28.js new file mode 100644 index 000000000..a8a8ebddc --- /dev/null +++ b/word-break/Jeehay28.js @@ -0,0 +1,36 @@ +/** + * @param {string} s + * @param {string[]} wordDict + * @return {boolean} + */ + +// Time Complexity: O(n * w * m) +// - n is the length of the string s. +// - w is the number of words in the dictionary wordDict. +// - m is the average length of words in wordDict. + +// Space Complexity: O(n) +// - The dp array of size n + 1 is the primary contributor to space usage, where n is the length of the string s. +var wordBreak = function (s, wordDict) { + dp = new Array(s.length + 1).fill(false); + dp[0] = true; + + // O(n) + for (let i = 1; i <= s.length; i++) { + // O(w) + for (word of wordDict) { + if (i >= word.length && s.slice(i - word.length, i) === word) { + // s.slice(i - word.length, i), the slicing operation takes O(m), where m is the length of the word being checked + dp[i] = dp[i - word.length]; + } + + if (dp[i]) { + break; + } + } + } + + return dp[s.length]; +}; + +