Last active
May 31, 2022 00:15
-
-
Save guilsa/fe5628c23a28c7e698361cedfd555ff9 to your computer and use it in GitHub Desktop.
Algorithm Problems
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Climbing Stairs (5/13) | |
// - can be modeled as a graph problem | |
// - starting from 0 we can take 1 step and get to 1 or take 2 steps and get to 2 | |
// - then from 1 we can take 1 step and get to 2 or take 2 steps and get to 3, and so on | |
// - if we get to n from a branch, we've found one way | |
// - recursion can be used to count number of unique paths that can count to n | |
// - to avoid repeated work, we can cache | |
// - if we don't cache, it's 2^n time because we must branch out two ways, n times | |
// - if we cache, it's linear time/space | |
// - finally, we can reduce to constant space by computing total distinct ways | |
// on the fly from n stairs down to 0 stairs given the observation that | |
// there is 1 way to get to n, 2 ways to get to n-1 and 3 ways to get to n-2 | |
// which I believe can be summarized in f(n - 1) + f(n - 2) | |
// time o(n) | space o(1) | |
function climbStairs(n) { | |
if (n === 2) return 2; | |
if (n === 1) return 1; | |
if (n < 1) return 0; | |
let nextPlusOne = 1; | |
let next = 2; | |
let distinctWays = 0; | |
for (let i = n - 3; i >= 0; i--) { | |
distinctWays = next + nextPlusOne; | |
nextPlusOne = next; | |
next = distinctWays; | |
} | |
return distinctWays; | |
}; | |
// time o(n) | space o(n) | |
function climbStairs(n, cache = {1: 1, 2: 2}) { | |
if (n in cache) { | |
return cache[n]; | |
} | |
cache[n] = climbStairs(n - 1, cache) + climbStairs(n - 2, cache); | |
return cache[n]; | |
}; | |
// Min Path Sum (5/9) | |
// DP makes it o(n) | |
// without it its more expensive, 2^n | |
function minPathSum(grid) { | |
const seen = {}; | |
return dfs(grid, 0, 0, seen); | |
}; | |
function dfs(grid, row, col, seen) { | |
if (seen[`${row}_${col}`]) return seen[`${row}_${col}`]; | |
if ( | |
row < 0 || | |
col < 0 || | |
row > grid.length - 1 || | |
col > grid[row].length - 1 | |
) { | |
return Number.POSITIVE_INFINITY; | |
} | |
if (row === grid.length - 1 && col === grid[row].length - 1) { | |
return grid[row][col]; | |
} | |
seen[`${row}_${col}`] = | |
grid[row][col] + | |
Math.min(dfs(grid, row, col + 1, seen), dfs(grid, row + 1, col, seen)); | |
return seen[`${row}_${col}`]; | |
} | |
// Add and Search Words (5/5) | |
// time - o(M) for well defined words, worse case o(M.26^N) | |
// space - O(1) for well defined words, worst case o(M) | |
class WordDictionary { | |
constructor() { | |
this.trie = new Trie(); | |
} | |
addWord(word) { | |
this.trie.populate(word); | |
} | |
search(word) { | |
return this.trie.contains(word); | |
} | |
} | |
class Trie { | |
constructor(string) { | |
this.root = {}; | |
} | |
populate(string) { | |
let node = this.root; | |
for (const char of string) { | |
if (!(char in node)) node[char] = {}; | |
node = node[char]; | |
} | |
node.isEnd = true; | |
} | |
contains(string, index = 0, node = this.root) { | |
if (index === string.length && node.isEnd) return true; | |
if (index === string.length) return false; | |
const char = string[index]; | |
if (char === ".") { | |
for (const key in node) { | |
if (this.contains(string, index + 1, node[key])) return true; | |
} | |
} else { | |
if (node[char] !== undefined) { | |
return this.contains(string, index + 1, node[char]); | |
} | |
} | |
return false; | |
} | |
} | |
// Meeting Rooms 2 (5/3) | |
// Traverse through each event - a meeting - (sorted by their start time) | |
// and track all ongoing events and the "next" one that will end. | |
// A min heap gives us constant time access to this (next event that will end). | |
// For each event: | |
// - Compare and remove (in log n time) the next meeting in the min heap, if it's ended. | |
// - Add the end time of our current meeting to the heap (also a log n time operation) | |
// - Finally, given the total amount of ongoing events changes, re-compute max rooms needed | |
const minMeetingRooms = (intervals) => { | |
const compareFunc = (a, b) => a - b; | |
const minHeap = new MinHeap(compareFunc); | |
intervals.sort((a, b) => a[0] - b[0]); | |
let maxRooms = 0; | |
intervals.forEach((interval) => { | |
if (minHeap.size > 0 && minHeap.peek() <= interval[0]) { | |
minHeap.extract(); | |
} | |
minHeap.insert(interval[1]); | |
maxRooms = Math.max(maxRooms, minHeap.size); | |
}); | |
return maxRooms; | |
}; | |
// Course Schedule (5/1) | |
var canFinish = function (numCourses, prerequisites) { | |
const graph = makeGraph(prerequisites); | |
const globalSeen = new Set(); | |
for (const [node, _] of Object.entries(graph)) { | |
const seen = new Set(); | |
const isNotCyclic = checkForCycle(graph, globalSeen, seen, node); | |
if (!isNotCyclic) return false; | |
} | |
return true; | |
}; | |
function checkForCycle(graph, globalSeen, seen, node) { | |
if (graph[node] === undefined) return true; | |
if (globalSeen.has(node)) return true; | |
seen.add(node); | |
for (const next of graph[node]) { | |
if (seen.has(next)) return false; | |
const isNotCyclic = checkForCycle(graph, globalSeen, seen, next); | |
if (!isNotCyclic) return false; | |
} | |
globalSeen.add(node); | |
seen.delete(node); | |
return true; | |
} | |
function makeGraph(prerequisites) { | |
const graph = {}; | |
for (const [course, dependency] of prerequisites) { | |
if (!graph[dependency]) graph[dependency] = []; | |
graph[dependency].push(course); | |
} | |
return graph; | |
} | |
// Word Search (4/28) | |
// for each char of word, we have 4 choices of neighbors to explore | |
// so if total # of chars is S, one invocation of dfs is 4^S time | |
// therefore upper bound is o(m*n*4^S) time where m is grid row and n is grid col | |
// can be modeled as a graph problem and a dfs approach can be used to | |
// explore every possible path starting from each cell to a target cell | |
// so a possible start of a word to the end of a word | |
// at each new location, slice a character off of the word if they match (2 base cases here) | |
// we can optimize by initializing a visited set at the beginning of every exploration | |
// adding to the set before visiting new locations and removing it if unsuccessful | |
function wordExists(board, word) { | |
for (let row = 0; row < board.length; row++) { | |
for (let col = 0; col < board[row].length; col++) { | |
if (board[row][col] !== word[0]) continue; | |
const seen = new Set(); | |
if (dfs(board, word, row, col, seen)) return true; | |
} | |
} | |
return false; | |
}; | |
function dfs(board, word, row, col, seen) { | |
if (word.length === 0) return true; | |
if (seen.has(`${row}_${col}`)) return false; | |
if (row < 0 || col < 0) return false; | |
if (row > board.length - 1 || col > board[row].length - 1) return false; | |
const char = board[row][col]; | |
if (char !== word[0]) return false; | |
seen.add(`${row}_${col}`); | |
const newWord = word.slice(1, word.length); | |
if ( | |
dfs(board, newWord, row + 1, col, seen) || | |
dfs(board, newWord, row - 1, col, seen) || | |
dfs(board, newWord, row, col + 1, seen) || | |
dfs(board, newWord, row, col - 1, seen) | |
) | |
return true; | |
seen.delete(`${row}_${col}`); | |
return false; | |
} | |
// IsPanlindrome (4/24) | |
// Basics of head/tail in recursion | |
function isPalindrome(str) { | |
if (str.length < 2) return true; | |
const head = str[0]; | |
const mid = str.slice(1, str.length - 1); | |
const tail = str[str.length - 1]; | |
return head === tail && isPalindrome(mid); | |
} | |
isPalindrome('racecar') | |
// Reverse (same as above) | |
function reverse(str) { | |
if (str.length === 0) return ''; | |
const head = str[0]; | |
const tail = reverse(str.slice(1, str.length)); | |
return tail + head; | |
} | |
reverse('gh') | |
// Concat (same as above) | |
function concat(arr) { | |
console.log(COUNT) | |
if (arr.length === 0) return ''; | |
const head = arr[0]; | |
const tail = concat(arr.slice(1, arr.length)); | |
return head + tail; | |
} | |
console.log(concat(['Hello', 'World', 'Gui', 'sa'])) | |
// Shorten Path (3/14) | |
// this problem feels hard - it's important to separate out | |
// general structure from edge cases | |
// maybe you can get help from interviewer about edge cases | |
// so prep to set it up correctly by focusing on generalizing it | |
// ie. what is considered more important | |
// time o(n) | space o(n) | |
function shortenPath(path) { | |
const startWithSlash = path[0] === "/"; | |
const tokens = path.split("/").filter(isImportantToken); | |
const stack = []; | |
if (startWithSlash) { | |
stack.push(""); | |
} | |
for (const token of tokens) { | |
if (token === "..") { | |
if (stack.length === 0 || stack[stack.length - 1] === "..") { | |
stack.push(token); | |
} else if (stack[stack.length - 1] !== "") { | |
stack.pop(); | |
} | |
} else { | |
stack.push(token); | |
} | |
} | |
if (stack.length === 1 && stack[0] === "") return "/"; | |
return stack.join("/"); | |
} | |
function isImportantToken(token) { | |
return token.length > 0 && token !== "."; | |
} | |
// Multi String Search (3/14) | |
// recognize there are various ways of solving this problem | |
// either using small strings or with big strings | |
// and maybe there would be benefits of doing it one way or another | |
// or perhaps we really value are space complexity and we might want to go with | |
// naive solution without building an auxilary data structure | |
// build a modified suffix tree where we dont care for endSymbol at end of suffix | |
// bc we'll traverse until we find just that a small string is contained in it | |
// not necessarily that it denotes the end of an actual string (ends with *) | |
// it will take o(b^2) time/space to build | |
// then iterate through all small strings and for each, check if its contained in the suffix trie | |
// since largest string in smallest strings has len s | |
// there are n small strings so we'll be doing o(ns) time | |
// so we have a time complexity of o(b^2) for building plus o(ns) for finding | |
// so a total time of o(b^2+ns) | |
// similarly space will be o(b^2+n) bc storing is b^2 space, then building same output array of len n | |
// time o(b^2 + ns) | space o(b^2 + n) | |
function multiStringSearch(bigString, smallStrings) { | |
const trie = new SuffixTrie(bigString); | |
return smallStrings.map((string) => trie.contains(string)); | |
} | |
class SuffixTrie { | |
constructor(string) { | |
this.root = {}; | |
this.construct(string); | |
} | |
construct(string) { | |
for (let i = 0; i < string.length; i++) this.insertAt(i, string); | |
} | |
insertAt(i, string) { | |
let node = this.root; | |
for (let j = i; j < string.length; j++) { | |
if (!(string[j] in node)) node[string[j]] = {}; | |
node = node[string[j]]; | |
} | |
} | |
contains(string) { | |
let node = this.root; | |
for (const letter of string) { | |
if (!(letter in node)) return false; | |
node = node[letter]; | |
} | |
return true; | |
} | |
} | |
// Suffix Trie Construction (3/14) | |
// methods | |
// populate from string (string) | |
// insert at (string, i) | |
// contains (string) | |
// populate - calls insert at for each index of string | |
// insert at - initialize node as root | |
// for each letter in string | |
// if letter not in node, create new object | |
// update node to node[letter] | |
// afterwards insert end symbol in current node | |
// contains | |
// initialize node as root | |
// for each letter in string, if not in node, return false | |
// return this.endSymbol in node | |
// contains - where m is length of input string we're looking for | |
// creation - time o(n^2) | space o(n^2) | |
// contains - time o(m) | space o(1) | |
class SuffixTrie { | |
constructor(string) { | |
this.root = {}; | |
this.endSymbol = "*"; | |
this.populateSuffixTrieFrom(string); | |
} | |
populateSuffixTrieFrom(string) { | |
// for each index in string, insertAt(string, index) | |
for (let i = 0; i < string.length; i++) { | |
this.insertAt(i, string); | |
} | |
} | |
insertAt(i, string) { | |
let node = this.root; | |
for (let j = i; j < string.length; j++) { | |
const letter = string[j]; | |
if (!(letter in node)) node[letter] = {}; | |
node = node[letter]; | |
} | |
node[this.endSymbol] = true; | |
} | |
contains(string) { | |
let node = this.root; | |
for (const letter of string) { | |
if (!(letter in node)) return false; | |
node = node[letter]; | |
} | |
return this.endSymbol in node; | |
} | |
} | |
// Is Valid Subsequence | |
// create a pointer to track sequence starting from 0 | |
// move it everytime we find it in array | |
// if pointer has reached end of sequence, return true | |
// in case we reach end of sequence prior to end, break | |
// [5, 1, 22, 25, 6, -1, 8, 10] | |
// [1, 6, -1, 10] | |
// true | |
// time o(n) | space o(1) | |
function isValidSubsequence(array, sequence) { | |
let j = 0; | |
for (let i = 0; i < array.length; i++) { | |
if (array[i] === sequence[j]) { | |
j++; | |
} | |
} | |
return j === sequence.length; | |
} | |
// Merge Overlapping Intervals (3/8) | |
// sort first! with (a, b) => a[0] - b[0] | |
// then merge item by item while updating last item from results array | |
// recognize to use math.max (must decide between largest of two) | |
// wasn't obvious at first - this is the caveat: | |
// if prev end is greater than or equal to curr start | |
// then set prev end to largest between prev end and curr end | |
// time o(nlogn) | space o(1) | |
function mergeOverlappingIntervals(array) { | |
array.sort((a, b) => a[0] - b[0]); | |
const merged = [array[0]]; | |
for (let i = 1; i < array.length; i++) { | |
const intervalOne = merged[merged.length - 1]; | |
const intervalTwo = array[i]; | |
if (intervalOne[1] >= intervalTwo[0]) { | |
merged[merged.length - 1][1] = Math.max(intervalOne[1], intervalTwo[1]); | |
continue; | |
} | |
merged.push(intervalTwo); | |
} | |
return merged; | |
} | |
// First Duplicate Value (3/8) | |
// we use indicies to map values to each other | |
// since valus are 1..n and we can modify input array | |
// use indicies to represent values to generate index | |
// if at array[index] we dont have a negative, we set it to be one | |
// next time we get to that same value, we'll go to the same index | |
// and know that we've seen that value before | |
// time o(n) | space o(1) | |
function firstDuplicateValue(array) { | |
for (const value of array) { | |
const absValue = Math.abs(value); | |
if (array[absValue - 1] < 0) return absValue; | |
array[absValue - 1] *= -1; | |
} | |
return -1; | |
} | |
// Array of Products (3/8) | |
// array fill strategy | |
// continue re-fill by re-computing prev vals at each index | |
// gets us to o(n) runtime (good hint if you're stuck on a o(n^2) solution) | |
// time o(n) | space o(n) | |
function arrayOfProducts(array) { | |
const products = new Array(array.length).fill(1); | |
let currentProduct = 1; | |
for (let i = 0; i < array.length; i++) { | |
products[i] = currentProduct; | |
currentProduct *= array[i]; | |
} | |
currentProduct = 1; | |
for (let i = array.length - 1; i >= 0; i--) { | |
products[i] *= currentProduct; | |
currentProduct *= array[i]; | |
} | |
return products; | |
} | |
// Longest Peak (3/8) | |
// instead of having i = 0 and inside while, skip if i is 0 or i === len - 1 | |
// just have i start at 1 and while should end at len - 1 | |
// instead of left/right, use leftIdx/rightIdx | |
// longestPeakLength isn't initialized as -Infinity, just 0 | |
// since it asks for longestPeakDistance, we don't need a `positions` array aux space | |
// time o(n) | space o(1) | |
function longestPeak(array) { | |
let i = 1; | |
let longestPeakLength = 0; | |
while (i < array.length - 1) { | |
const isPeak = array[i - 1] < array[i] && array[i] > array[i + 1]; | |
if (!isPeak) { | |
i++; | |
continue; | |
} | |
let leftIdx = i - 2; | |
while (leftIdx >= 0 && array[leftIdx] < array[leftIdx + 1]) { | |
leftIdx--; | |
} | |
let rightIdx = i + 2; | |
while (rightIdx < array.length && array[rightIdx] < array[rightIdx - 1]) { | |
rightIdx++; | |
} | |
const currentPeakLength = rightIdx - leftIdx - 1; | |
longestPeakLength = Math.max(longestPeakLength, currentPeakLength); | |
i = rightIdx; | |
} | |
return longestPeakLength; | |
} | |
// Spiral Traverse (3/7) | |
// loop 3 and 4 need to break if there's a single row/row left | |
// second loop is row = startRow + 1, everything else is minus one | |
// last loop is row >, not row >= | |
// last loop is row > startRow, not row > endRow | |
// time o(n) | space o(n) | |
function spiralTraverse(array) { | |
let startRow = 0; | |
let endRow = array.length - 1; | |
let startCol = 0; | |
let endCol = array[0].length - 1; | |
const result = []; | |
while (startRow <= endRow && startCol <= endCol) { | |
for (let col = startCol; col <= endCol; col++) { | |
result.push(array[startRow][col]); | |
} | |
for (let row = startRow + 1; row <= endRow; row++) { | |
result.push(array[row][endCol]); | |
} | |
for (let col = endCol - 1; col >= startCol; col--) { | |
if (startRow === endRow) break; | |
result.push(array[endRow][col]); | |
} | |
for (let row = endRow - 1; row > startRow; row--) { | |
if (startCol === endCol) break; | |
result.push(array[row][startCol]); | |
} | |
startRow++; | |
endRow--; | |
startCol++; | |
endCol--; | |
} | |
return result; | |
} | |
// Monotonic Array (3/7) | |
// time o(n) | space o(1) | |
function isMonotonic(array) { | |
let isNonDecreasing = true; | |
let isNonIncreasing = true; | |
for (let i = 1; i < array.length; i++) { | |
if (array[i] > array[i - 1]) isNonDecreasing = false; | |
if (array[i] < array[i - 1]) isNonIncreasing = false; | |
} | |
return isNonDecreasing || isNonIncreasing; | |
} | |
// Move to Element (3/7) | |
// we need some left and right idx, i and j | |
// iterate through array while incrementing i | |
// decrement j while its j is greater than i and array[j] === toMove | |
// if array[i] equals toMove, swap | |
// time o(n) - we only visit every index once | |
// space o(1) | |
function moveElementToEnd(array, toMove) { | |
let i = 0; | |
let j = array.length - 1; | |
while (i < j) { | |
while (j > i && array[j] === toMove) j--; | |
if (array[i] === toMove) swap(i, j, array); | |
i++; | |
} | |
return array; | |
} | |
function swap(i, j, array) { | |
const temp = array[i]; | |
array[i] = array[j]; | |
array[j] = temp; | |
} | |
// Smallest Difference (3/7) | |
// technique takes advantage of properties of a sorting array | |
// either dec/inc to bring nums closer to each other | |
// javascript .sort needs .((a, b) => a - b) | |
// ask interviewer | |
// is it ok to sort in place, otherwise account for extra space complexity | |
// time - sorting (quicksort or mergesort) | |
// time o(nlog(n) + mlog(m)) | |
// space - constant | |
function smallestDifference(arrayOne, arrayTwo) { | |
arrayOne.sort((a, b) => a - b); | |
arrayTwo.sort((a, b) => a - b); | |
let idxOne = 0; | |
let idxTwo = 0; | |
let smallest = Infinity; | |
let smallestPair = []; | |
while (idxOne < arrayOne.length && idxTwo < arrayTwo.length) { | |
const firstNum = arrayOne[idxOne]; | |
const secondNum = arrayTwo[idxTwo]; | |
let current = Math.abs(firstNum - secondNum); | |
if (firstNum === secondNum) { | |
return [firstNum, secondNum]; | |
} else if (firstNum < secondNum) { | |
idxOne++; | |
} else { | |
idxTwo++; | |
} | |
if (current < smallest) { | |
smallest = current; | |
smallestPair = [firstNum, secondNum]; | |
} | |
} | |
return smallestPair; | |
} | |
// Three Number Sum (3/7) | |
// left/right inc/dec based on currentSum | |
// but inside a for i loop from 0 through array.length - 2 | |
// while resetting left to i + 1 and right to array.length - 1 | |
// time - iterate through main array once and left/right pointer need to iterate through main array once | |
// space - might store every number in array if they are used in some triplets | |
// time o(n^2) | space o(n) | |
function threeNumberSum(array, targetSum) { | |
const triplets = []; | |
array.sort((a, b) => a - b); | |
for (let i = 0; i < array.length - 2; i++) { | |
let left = i + 1; | |
let right = array.length - 1; | |
while (left < right) { | |
let currentSum = array[i] + array[left] + array[right]; | |
if (currentSum === targetSum) { | |
triplets.push([array[i], array[left], array[right]]); | |
left++; | |
right--; | |
} else if (currentSum < targetSum) { | |
left++; | |
} else { | |
right--; | |
} | |
} | |
} | |
return triplets; | |
} | |
// Merge Linked List (2/16) | |
// n length of 1st, m length of 2nd | |
// time o(n+m) | space o(1) | |
// class LinkedList {...} | |
function mergeLinkedLists(headOne, headTwo) { | |
let p1 = headOne; | |
let p1Prev = null; | |
let p2 = headTwo; | |
while (p1 !== null && p2 !== null) { | |
if (p1.value < p2.value) { | |
p1Prev = p1; | |
p1 = p1.next; | |
} else { | |
if (p1Prev !== null) p1Prev.next = p2; | |
p1Prev = p2; | |
p2 = p2.next; | |
p1Prev.next = p1; | |
} | |
} | |
// we've runned out of p1 but still have values in p2 | |
if (p1 === null) p1Prev.next = p2; | |
// what is the new correct head of merged linked list | |
return headOne.value < headTwo.value ? headOne : headTwo; | |
} | |
// Reverse Linked List (2/15) | |
// three pointers, move them together | |
// so long as p2 isnt null, do operations | |
// p2 is analogous to "current node" so initialize p1 to null and p2 to head | |
// p3 = p2.next, then p2.next = p1, then p1 = p2, then p2 = p3 | |
// notice the order (reading in reverse helps) | |
// we loose p1 the second we do p1 = p2, so before we override p1, we do p2.next = p1, etc | |
// time o(n) | space o(1) | |
// class LinkedList {...} | |
function reverseLinkedList(head) { | |
let prevNode = null; | |
let currentNode = head; | |
while (currentNode !== null) { | |
const nextNode = currentNode.next; | |
currentNode.next = prevNode; | |
prevNode = currentNode; | |
currentNode = nextNode; | |
} | |
return prevNode; | |
} | |
// Find Loop (2/14) | |
// set first/second node to head | |
// make second node travel 2x as fast until they meet | |
// reset first to head | |
// then make both travel together until they meet | |
// 1st pointer travels D + P distance | |
// try to visualize this where D is linear distance before loop | |
// and P is some arc | |
// 2nd pointer travels 2D + 2P distance | |
// so total distance is whatever 2nd pointer travelled minus P (so minus the arc) | |
// which turns out to be 2D + P | |
// and then R equals 2D + P - P - D (which is what F travelled) | |
// so R = D | |
// but we're almost done | |
// now we know we need to traverse distance D from where our pointers are | |
// that's why we reset first to head and move them together until they meet again | |
// 2nd pointer doesn't matter, it's travelling at a faster than normal pace | |
// time o(n) | space o(1) | |
// class LinkedList {...} | |
function findLoop(head) { | |
let first = head.next; | |
let second = head.next.next; | |
while (first !== second) { | |
first = first.next; | |
second = second.next.next; | |
} | |
first = head; | |
while (first !== second) { | |
first = first.next; | |
second = second.next; | |
} | |
return first; | |
} | |
// Sum of Linked Lists (2/14) | |
// it's ok to start from left to right from least significant digits | |
// get sum per column (sumOfValues % 10), get carry per column (sumOfValues // 10) | |
// create new linked list node per column | |
// add pointers from the last node we had to this next new node | |
// move current node to new node | |
// things to watch out for: | |
// (1) | |
// lots of variables | |
// carry, nodeOne/Two, valueOne/Two, sumOfValues, newValue, newNode | |
// (2) | |
// set current node's pointer to next linked list node we just created (currentNode.next = newNode) | |
// update current node to be last node we created (currentNode = newNode) | |
// (3) | |
// while loop contains three OR conditions (keeps running if truthy values found) | |
// so instead of creating inner conditions, we conditionalize our values to 0 if null | |
// whatever the longest linked list dictates the length | |
// plus 1 is if we have a carry at end, there's another loop | |
// time o(max(n, m)) + 1 | space o(max(n, m) + 1) | |
// class LinkedList {...} | |
function sumOfLinkedLists(linkedListOne, linkedListTwo) { | |
const newLinkedListHeaderPointer = new LinkedList(0); | |
let currentNode = newLinkedListHeaderPointer; | |
let carry = 0; | |
let nodeOne = linkedListOne; | |
let nodeTwo = linkedListTwo; | |
while (nodeOne !== null || nodeTwo !== null || carry !== 0) { | |
const valueOne = nodeOne !== null ? nodeOne.value : 0; | |
const valueTwo = nodeTwo !== null ? nodeTwo.value : 0; | |
const sumOfValues = valueOne + valueTwo + carry; | |
const newValue = sumOfValues % 10; | |
const newNode = new LinkedList(newValue); | |
currentNode.next = newNode; | |
currentNode = newNode; | |
carry = Math.floor(sumOfValues / 10); | |
nodeOne = nodeOne !== null ? nodeOne.next : null; | |
nodeTwo = nodeTwo !== null ? nodeTwo.next : null; | |
} | |
return newLinkedListHeaderPointer.next; | |
} | |
// Remove Kth Node from From End (2/14) | |
// move first/second pointer, remember to stop on items prev to your "targets" | |
// start by moving second to k (counter from 1 to k inclusive) | |
// if second is null, remove first (update head.value and head.next), return | |
// otherwise, move first and second together | |
// lastly, remove first by updating it entirely (first.next = first.next.next) | |
// time o(length) | space o() | |
class LinkedList { | |
constructor(value) { | |
this.value = value; | |
this.next = null; | |
} | |
} | |
function removeKthNodeFromEnd(head, k) { | |
let counter = 1; | |
let first = head; | |
let second = head; | |
while (counter <= k) { | |
second = second.next; | |
counter++; | |
} | |
if (second === null) { | |
head.value = head.next.value; | |
head.next = head.next.next; | |
return; | |
} | |
while (second.next !== null) { | |
first = first.next; | |
second = second.next; | |
} | |
first.next = first.next.next; | |
} | |
// Number of Binary Tree Topologies (2/10) | |
// time o(n^2) | space o(n) | |
function numberOfBinaryTreeTopologies(n, cache = { 0: 1 }) { | |
if (n in cache) return cache[n]; | |
let numOfTrees = 0; | |
for (let leftSize = 0; leftSize < n; leftSize++) { | |
const rightSize = n - 1 - leftSize; | |
const totalLeft = numberOfBinaryTreeTopologies(leftSize, cache); | |
const totalRight = numberOfBinaryTreeTopologies(rightSize, cache); | |
numOfTrees += totalLeft * totalRight; | |
} | |
cache[n] = numOfTrees; | |
return cache[n]; | |
} | |
// Ambiguous Measurements (2/10) | |
// time o(low * high * n) | space o(low * high) - where n is | |
// the number of measuring cups | |
function ambiguousMeasurements(measuringCups, low, high) { | |
const cache = {}; | |
return helper(measuringCups, low, high, cache); | |
} | |
function helper(cups, low, high, cache) { | |
const key = getHashableKey(low, high); | |
if (key in cache) return cache[key]; | |
if (low < 0 && high < 0) return false; | |
let canMeasure = false; | |
for (const cup of cups) { | |
const [cupLow, cupHigh] = cup; | |
if (low <= cupLow && cupHigh <= high) { | |
canMeasure = true; | |
break; | |
} | |
canMeasure = helper(cups, low - cupLow, high - cupHigh, cache); | |
if (canMeasure) break; | |
} | |
cache[key] = canMeasure; | |
return canMeasure; | |
} | |
function getHashableKey(low, high) { | |
return `${low}:${high}`; | |
} | |
// Generate Div Tags (2/10) | |
// question not really designed to test time complexity explanation | |
// incrementally build solutions, tack on open/close tags | |
// to start generating a valid string | |
// we need at least an opening tag | |
// what can we do | |
// add <div></div> to end of valid string | |
// or surround valid string with opening/closing tag | |
// add opening tag before we close it | |
// so we have an opening tag to be added on other tail of recursion | |
// catalan number - (2n)!/((n!((n+1)!))) | |
// string generation grows exponentially | |
// time ~o(catalanNum) | space ~o(catalanNum) | |
function generateDivTags(numberOfTags) { | |
const result = []; | |
helper(numberOfTags, numberOfTags, "", result); | |
return result; | |
} | |
function helper(opening, closing, prefix, result) { | |
if (opening > 0) helper(opening - 1, closing, prefix + "<div>", result); | |
if (opening < closing) | |
helper(opening, closing - 1, prefix + "</div>", result); | |
if (closing === 0) result.push(prefix); | |
} | |
// Sudoku Solver (2/10) | |
// computationally intentive task to brute force 81 squares * 9 possibilities | |
// see https://en.wikipedia.org/wiki/Mathematics_of_Sudoku | |
// for recursive matrix traversal | |
// inner loop (col++) will finish first | |
// so base case means we col=0 and increment outer loop (row++) | |
// there's also an inner second base case | |
// if outer loop is *also* finished, then, we exit recursion (return true) | |
// for backtracking, mutate board state if it passes validation | |
// then return true, otherwise, reset board state and return false | |
// for validating row, call includes on board[row] | |
// for validating col, call includes on board.map(r => r[col]) | |
// for sub-dividing a 9x9 grid into three 3x3 grids | |
// set a sub grid col/row start variable to row - row % sqrt | |
// to validate subgrids, use outer/inner row/col loop but from 0-3 only | |
// things to remember: | |
// - pass your startings 0's | |
// - when calling tryIt helper (1) don't mutate state; (2) immediatly return itself; | |
// (3) mutate state from tryIt helper | |
// - use initialized currentRow/currentCol in recursive cases | |
// - for isRowValid, it's board[row].includes, not board.includes | |
// - subgrid for loops is up to subgrid len (ie. 3), not board len | |
// constant board size, so constant time | |
// time o(1) | space o(1) | |
function solveSudoku(board) { | |
solver(0, 0, board); | |
return board; | |
} | |
function solver(row, col, board) { | |
let currentRow = row; | |
let currentCol = col; | |
if (currentCol === board[row].length) { | |
currentRow++; | |
currentCol = 0; | |
if (currentRow === board.length) return true; | |
} | |
if (board[currentRow][currentCol] === 0) { | |
return tryDigitsAtPosition(currentRow, currentCol, board); | |
} | |
return solver(currentRow, currentCol + 1, board); | |
} | |
function tryDigitsAtPosition(row, col, board) { | |
for (let digit = 1; digit < 10; digit++) { | |
if (isValidAtPosition(row, col, board, digit)) { | |
board[row][col] = digit; | |
if (solver(row, col + 1, board)) return true; | |
} | |
} | |
board[row][col] = 0; | |
return false; | |
} | |
function isValidAtPosition(row, col, board, value) { | |
const isColValid = !board[row].includes(value); | |
const isRowValid = !board.map((r) => r[col]).includes(value); | |
if (!isColValid || !isRowValid) return false; | |
const sqrt = Math.floor(Math.sqrt(board.length)); | |
const subgridRowStart = row - (row % sqrt); | |
const subgridColStart = col - (col % sqrt); | |
for (let rowIdx = 0; rowIdx < 3; rowIdx++) { | |
for (let colIdx = 0; colIdx < 3; colIdx++) { | |
const rowToCheck = subgridRowStart + rowIdx; | |
const colToCheck = subgridColStart + colIdx; | |
const existingValue = board[rowToCheck][colToCheck]; | |
if (existingValue === value) return false; | |
} | |
} | |
return true; | |
} | |
// Lowest Common Manager (2/8) | |
// dfs post-order traversal | |
// retrieve sub-tree info by starting from leaf nodes and returning related info | |
// the return - an object (org info) - gets assigned to variable | |
// if org info contains answer (isn't null), return again so we exit (base case 2) | |
// base case 1 happens else where - at the bottom of post-order traversal | |
// most interesting here are the two counter instances | |
// the main one, initialized at root | |
// but we interpret it in reverse | |
// first at the bottom (base case 1), where we increment itself | |
// then in middle (if we can't process base case 2) | |
// where we increment it based on counter retrieved from recursion (a return of itself) | |
// where n is people in org chart and d is depth (height) of org chart | |
// time o(n) | space o(d) | |
function getLowestCommonManager(topManager, reportOne, reportTwo) { | |
return getOrgInfo(topManager, reportOne, reportTwo).lowestCommonManager; | |
} | |
function getOrgInfo(manager, reportOne, reportTwo) { | |
let numImportantReports = 0; | |
for (const directReport of manager.directReports) { | |
const orgInfo = getOrgInfo(directReport, reportOne, reportTwo); | |
if (!!orgInfo.lowestCommonManager) return orgInfo; | |
numImportantReports += orgInfo.numImportantReports; | |
} | |
if (manager === reportOne || manager === reportTwo) { | |
numImportantReports += 1; | |
} | |
const lowestCommonManager = numImportantReports === 2 ? manager : null; | |
return { lowestCommonManager, numImportantReports }; | |
} | |
class OrgChart { | |
constructor(name) { | |
this.name = name; | |
this.directReports = []; | |
} | |
} | |
// Min Heap (1/28) | |
// order of implementation - peak, swap, insert, remove, siftUp, siftDown | |
// remove - swap root/last, pop off heap, siftDown | |
// siftUp - needs currIdx, heap | |
// siftDown - needs currIdx, *endIdx*, heap | |
// remember to exit method with return on if else statement of heap[idxToSwap] < heap[currIdx] | |
// less than or equal to - not just less than | |
// prevents calling on leaf nodes | |
// while loop expression is while currIdx <= endIdx, not heap[currIdx]... | |
// siftDown/siftUp - time o(logn) - everytime we swap a node, we eliminate half of tree | |
// buildHeap - time o(n) - we call siftDown on n nodes | |
// it's not o(nlogn) bc that "distance" isn't equal for every node | |
// but implementing buildHeap siftUp would be nlogn | |
// space - 0(1) all happens in-space | |
// time o(logn) | space o(1) | |
class MinHeap { | |
constructor(array) { | |
this.heap = this.buildHeap(array); | |
} | |
buildHeap(array) { | |
let pIdx = Math.floor((array.length - 2) / 2); | |
for (let currIdx = pIdx; currIdx >= 0; currIdx--) { | |
this.siftDown(currIdx, array.length - 1, array); | |
} | |
return array; | |
} | |
siftDown(currIdx, endIdx, heap) { | |
let childOneIdx = currIdx * 2 + 1; | |
while (currIdx <= endIdx) { | |
const childTwoIdx = currIdx * 2 + 2 <= endIdx ? currIdx * 2 + 2 : -1; | |
let idxToSwap; | |
if (childTwoIdx !== -1 && heap[childTwoIdx] < heap[childOneIdx]) { | |
idxToSwap = childTwoIdx; | |
} else { | |
idxToSwap = childOneIdx; | |
} | |
if (heap[idxToSwap] < heap[currIdx]) { | |
this._swap(idxToSwap, currIdx, heap); | |
currIdx = idxToSwap; | |
childOneIdx = currIdx * 2 + 1; | |
} else { | |
return; | |
} | |
} | |
} | |
siftUp(currIdx, heap) { | |
let pIdx = Math.floor((currIdx - 1) / 2); | |
while (currIdx > 0 && heap[currIdx] < heap[pIdx]) { | |
this._swap(currIdx, pIdx, heap); | |
currIdx = pIdx; | |
pIdx = Math.floor((currIdx - 1) / 2); | |
} | |
} | |
peek() { | |
return this.heap[0]; | |
} | |
remove() { | |
this._swap(0, this.heap.length - 1, this.heap); | |
const removedItem = this.heap.pop(); | |
this.siftDown(0, this.heap.length - 1, this.heap); | |
return removedItem; | |
} | |
insert(value) { | |
this.heap.push(value); | |
this.siftUp(this.heap.length - 1, this.heap); | |
} | |
_swap(i, j, heap) { | |
const temp = heap[i]; | |
heap[i] = heap[j]; | |
heap[j] = temp; | |
} | |
} | |
// Boggle Board (1/28) | |
// create/populate trie | |
// finalWords hash - for words that appear multiple times, also we return all keys from it as final answer | |
// visited - identical to board, but for bool of false | |
// traverse board - for row, col - "explore" method that modifies finalWords | |
// space - building a tree, contain all words list of words with at most "S" chars where "S" len of longest word | |
// for suffix trie, we have at least ws auxilary space | |
// we are also building am aux matrix of booleans | |
// so ws + nm where n is width, m is height | |
// time - ws - bc we iterate through words and they have s chars in them | |
// then we iterate through entire matrix, so a total of nm nodes to travese matrix | |
// for all nm node explored, we go through all their neighbors, and all their neighbor's neighbors, until we exhaust this list | |
// question is "how many neighbors does one node have?" - worst case, it's 8 neighbors | |
// worst case is going to be 8*8*8 a total of s times - so 8^s - therefore ws+nm*8^s | |
// time o(nm*8^s + ws) | space o(nm + ws) | |
function boggleBoard(board, words) { | |
const trie = new Trie(); | |
for (const word of words) { | |
trie.add(word); | |
} | |
const visited = board.map((row) => row.map((_) => false)); | |
const finalWords = {}; | |
for (let row = 0; row < board.length; row++) { | |
for (let col = 0; col < board[row].length; col++) { | |
explore(row, col, trie.root, board, visited, finalWords); | |
} | |
} | |
return Object.keys(finalWords); | |
} | |
function explore(row, col, node, board, visited, finalWords) { | |
if (visited[row][col]) return; | |
const letter = board[row][col]; | |
if (!(letter in node)) return; | |
visited[row][col] = true; | |
node = node[letter]; | |
if ("*" in node) finalWords[node["*"]] = true; | |
const neighbors = getNeighbors(board, row, col); | |
for (const neighbor of neighbors) { | |
explore(neighbor[0], neighbor[1], node, board, visited, finalWords); | |
} | |
visited[row][col] = false; | |
} | |
function getNeighbors(board, row, col) { | |
const positions = []; | |
if (row < board.length - 1) positions.push([row + 1, col]); | |
if (row > 0) positions.push([row - 1, col]); | |
if (col < board[0].length - 1) positions.push([row, col + 1]); | |
if (col > 0) positions.push([row, col - 1]); | |
if (row > 0 && col > 0) { | |
positions.push([row - 1, col - 1]); // north-west | |
} | |
if (row < board.length - 1 && col < board[0].length - 1) { | |
positions.push([row + 1, col + 1]); // south-east | |
} | |
if (row > 0 && col < board[0].length - 1) { | |
positions.push([row - 1, col + 1]); // north-east | |
} | |
if (row < board.length - 1 && col > 0) { | |
positions.push([row + 1, col - 1]); // south-west | |
} | |
return positions; | |
} | |
class Trie { | |
constructor() { | |
this.root = {}; | |
this.endSymbol = "*"; | |
} | |
add(string) { | |
let node = this.root; | |
for (let i = 0; i < string.length; i++) { | |
const char = string[i]; | |
if (!(char in node)) node[char] = {}; | |
node = node[char]; | |
} | |
node[this.endSymbol] = string; | |
} | |
} | |
// Class Photos (1/26) | |
// between 2 lists of student "heights", which are the tallest? | |
// to find out, sort it, then iterate until condition is invalid, otherwise it's valid | |
// valid means a class photo can be taken bc tallest students are in the back | |
// and in the front the student must be strictly smaller than their back counterpart | |
// sorting is needed | |
// time o(nlogn) | space o(1) | |
function classPhotos(redShirtHeights, blueShirtHeights) { | |
redShirtHeights.sort((a, b) => a - b); | |
blueShirtHeights.sort((a, b) => a - b); | |
let isRedLargest = redShirtHeights[0] > blueShirtHeights[0]; | |
for (let i = 0; i < redShirtHeights.length; i++) { | |
if (isRedLargest !== redShirtHeights[i] >= blueShirtHeights[i]) | |
return false; | |
} | |
return true; | |
} | |
// Minimum Waiting Time (1/26) | |
// find waiting time per query, given specific order | |
// say query in order 6,1,2, durations would be 0,6,6+1, respectively | |
// to minimize waiting time, find optimal "order" | |
// in this case, test two orders: 6,1 vs 1,6 give durations 0,6 vs 0,1 | |
// so it doesn't make sense to process largest queries first | |
// | |
// every query needs to wait the remaining amount of queries left | |
// multiplied by the total waiting time so far | |
// first thing is ordering, best is nlogn | |
// time o(nlogn) | space o(1) | |
function minimumWaitingTime(queries) { | |
queries.sort((a, b) => a - b); | |
let totalWaitingTime = 0; | |
for (let i = 0; i < queries.length; i++) { | |
const remainingQueries = queries.length - (i + 1); | |
totalWaitingTime += remainingQueries * queries[i]; | |
} | |
return totalWaitingTime; | |
} | |
// Minimum Passes of Matrix (1/26) | |
// return passes - 1 if no negative vals after convertMatrix, else -1 | |
// maintain nextQueue/currentQueue in a nested while loop fashion (won't affect runtime complexity) | |
// get all positive positions, assign to nextQueue | |
// make currentQueue nextQueue and empty out nextQueue, clear out currentQueue before starting 2nd outer loop | |
// get adjacent positions, loop through them, if -1 is found, flip, push pos as [row][col] to *emptied* nextQueue | |
// increment passes before next outer loop cycle | |
// time - we do a wh operation in beginning to look for positive intergers | |
// but then we'll never have to look at the same item more than once | |
// time o(wh) | space o(wh) | |
function minimumPassesOfMatrix(matrix) { | |
const passes = convertMatrix(matrix); | |
return !containsNegatives(matrix) ? passes - 1 : -1; | |
} | |
function convertMatrix(matrix) { | |
let nextQueue = getAllPositivePositions(matrix); | |
let passes = 0; | |
while (nextQueue.length > 0) { | |
const currentQueue = nextQueue; | |
nextQueue = []; | |
while (currentQueue.length > 0) { | |
const [currentRow, currentCol] = currentQueue.shift(); | |
const positions = getAdjacentPositions(currentRow, currentCol, matrix); | |
for (const [row, col] of positions) { | |
if (matrix[row][col] < 0) { | |
matrix[row][col] *= -1; | |
nextQueue.push([row, col]); | |
} | |
} | |
} | |
passes++; | |
} | |
return passes; | |
} | |
function containsNegatives(matrix) { | |
for (const row of matrix) { | |
for (const value of row) { | |
if (value < 0) return true; | |
} | |
} | |
return false; | |
} | |
function getAllPositivePositions(matrix) { | |
const positivePositions = []; | |
for (let row = 0; row < matrix.length; row++) { | |
for (let col = 0; col < matrix[row].length; col++) { | |
if (matrix[row][col] > 0) positivePositions.push([row, col]); | |
} | |
} | |
return positivePositions; | |
} | |
function getAdjacentPositions(row, col, matrix) { | |
const positions = []; | |
if (row > 0) positions.push([row - 1, col]); | |
if (row < matrix.length - 1) positions.push([row + 1, col]); | |
if (col > 0) positions.push([row, col - 1]); | |
if (col < matrix[row].length - 1) positions.push([row, col + 1]); | |
return positions; | |
} | |
// Cycle in Graph (1/25) | |
// create visited/inStack auxilary arrays, run dfs, mark node as visited and inStack | |
// recursive case sends in neighbor as node | |
// where neighbor element from iterable neighbors edges[node] | |
// recursion returns cycleExists bool so we can do early return if true | |
// as we finish processing, we unmark form inStack | |
// if we visit something still "inStack", its a cycle | |
// reminder: adjacency list - index itself represents vertex (node) while interior list represents outbound edges | |
// time - dfs considers all vertices + edges, space - auxilary DS | |
// there will never be more than v concurrent recursive calls on the call stack | |
// since there are v vertices in the graph and since our algorithm naturally stops at cycles | |
// thus, the recursive calls won't change the O(v) space complexity | |
// time o(v + e) | space o(v) | |
function cycleInGraph(edges) { | |
const visited = edges.map((_) => 0); | |
const inStack = edges.map((_) => 0); | |
for (let node = 0; node < edges.length; node++) { | |
const cycleExists = dfs(node, edges, visited, inStack); | |
if (cycleExists) return true; | |
} | |
return false; | |
} | |
function dfs(node, edges, visited, inStack) { | |
visited[node] = true; | |
inStack[node] = true; | |
const neighbors = edges[node]; | |
for (const neighbor of neighbors) { | |
if (!visited[neighbor]) { | |
const cycleExists = dfs(neighbor, edges, visited, inStack); | |
if (cycleExists) return true; | |
} else if (inStack[neighbor]) { | |
return true; | |
} | |
} | |
inStack[node] = false; | |
return false; | |
} | |
// Remove Islands (1/25) | |
// problem asks to remove interior islands (1s to 0s) if they aren't connected to borders | |
// so run dfs starting at borders, replacing 1s with 2s | |
// then traverse matrix, setting 1's to 0's and 2's to 1's | |
// o(wh) space b/c we need a stack w/ potentially every single item on it | |
// it's better avg space complexity than v1 (not here) with auxilary array | |
// bc our stack might not always be as big as the matrix itself | |
// time o(wh) | space o(wh) | |
function removeIslands(matrix) { | |
for (let row = 0; row < matrix.length; row++) { | |
for (let col = 0; col < matrix[row].length; col++) { | |
const rowIsBorder = row === 0 || row === matrix.length - 1; | |
const colIsBorder = col === 0 || col === matrix[row].length - 1; | |
const isBorder = rowIsBorder || colIsBorder; | |
if (!isBorder) continue; | |
dfs(matrix, row, col); | |
} | |
} | |
for (let row = 0; row < matrix.length; row++) { | |
for (let col = 0; col < matrix[row].length; col++) { | |
const rowIsBorder = row === 0 || row === matrix.length - 1; | |
const colIsBorder = col === 0 || col === matrix[row].length - 1; | |
const isBorder = rowIsBorder || colIsBorder; | |
const color = matrix[row][col]; | |
if (color === 1) { | |
matrix[row][col] = 0; | |
} else if (color === 2) { | |
matrix[row][col] = 1; | |
} | |
} | |
} | |
return matrix; | |
} | |
function dfs(matrix, row, col) { | |
const stack = [[row, col]]; | |
while (stack.length > 0) { | |
const curr = stack.pop(); | |
const [row, col] = curr; | |
if (matrix[row][col] === 1) { | |
matrix[row][col] = 2; | |
pushIfValid(matrix, row + 1, col, stack); | |
pushIfValid(matrix, row - 1, col, stack); | |
pushIfValid(matrix, row, col + 1, stack); | |
pushIfValid(matrix, row, col - 1, stack); | |
} | |
} | |
} | |
function pushIfValid(matrix, row, col, stack) { | |
if (row >= 0 && row < matrix.length && col >= 0 && col < matrix[row].length) { | |
stack.push([row, col]); | |
} | |
} | |
// Node Depths (1/24) | |
// at root node, depth is 0. have all parents inform child (my depth + 1) | |
// time o(n) | space o(n) | |
function nodeDepths(root) { | |
let sum = 0; | |
const stack = [{ node: root, depth: 0 }]; | |
while (stack.length > 0) { | |
const curr = stack.pop(); | |
if (!curr.node) continue; | |
sum += curr.depth; | |
stack.push({ node: curr.node.left, depth: curr.depth + 1 }); | |
stack.push({ node: curr.node.right, depth: curr.depth + 1 }); | |
} | |
return sum; | |
} | |
// to avoid having to store a global sum variable, we can pass sums as returns | |
// time o(n) | space o(h) | |
// time - we're traversing through every node in the tree, n nodes | |
// at every node, we're only doing constant time operations | |
// space - where h is height - max function calls in the call stack at one point | |
// is the height of the tree (or depth of deepest node in tree) | |
// fyi, for an unbalanced tree, o(h) starts looking like o(n) | |
function nodeDepthsRecursive(node, depth = 0) { | |
if (!node) return 0; | |
return ( | |
depth + nodeDepths(node.left, depth + 1) + nodeDepths(node.right, depth + 1) | |
); | |
} | |
// Branch Sums (1/24) | |
// pre-order traversal, don't forget to return after array.push | |
// we'll have as many branches as we have branchSums | |
// in other words, !node.left && !node.right condition counts n branches (space complexity) | |
// time o(n) | space o(n) | |
function branchSums(root) { | |
const branchSums = []; | |
helper(root, 0, branchSums); | |
return branchSums; | |
} | |
function helper(node, currentSum, branchSums) { | |
if (!node) return; | |
currentSum += node.value; | |
if (!node.left && !node.right) { | |
branchSums.push(currentSum); | |
return; | |
} | |
helper(node.left, currentSum, branchSums); | |
helper(node.right, currentSum, branchSums); | |
} | |
// Shifted Binary Search (1/14) | |
// move to the sorted side, negate the premise that target should be there | |
// time o(log n) | space o(log n) | |
function shiftedBinarySearch(array, target) { | |
return helper(array, target, 0, array.length - 1); | |
} | |
function helper(array, target, left, right) { | |
while (left <= right) { | |
const mid = Math.floor((left + right) / 2); | |
const leftVal = array[left]; | |
const rightVal = array[right]; | |
const potentialMatch = array[mid]; | |
console.log(potentialMatch); | |
if (target === potentialMatch) return mid; | |
if (leftVal < potentialMatch) { | |
if (target < potentialMatch && target >= leftVal) { | |
right = mid - 1; | |
} else { | |
left = mid + 1; | |
} | |
} else { | |
if (target > potentialMatch && target <= rightVal) { | |
left = mid + 1; | |
} else { | |
right = mid - 1; | |
} | |
} | |
} | |
return -1; | |
} | |
// Reverse Words In String (1/12) | |
// time o(n) | space o(n) | |
function reverseWordsInString(string) { | |
if (string.length === "") return ""; | |
const result = []; | |
const sections = splitHelper(string); | |
for (let i = sections.length; i >= 0; i--) { | |
result.push(sections[i]); | |
} | |
return result.join(""); | |
} | |
function splitHelper(string) { | |
const result = []; | |
let left = 0; | |
for (let i = 0; i < string.length; i++) { | |
const prev = string[i - 1]; | |
const curr = string[i]; | |
if ((prev !== " " && curr === " ") || (prev === " " && curr !== " ")) { | |
result.push(string.slice(left, i)); | |
left = i; | |
} | |
} | |
result.push(string.slice(left)); | |
return result; | |
} | |
// Valid IP Addresses (1/12) | |
// 3 nested loops using Max.min as limits gives behavior we want | |
// slice 1 -> 0, i / slice 2 -> i, j / slice 3 -> j, k / slice 4 -> k, end | |
// reminder about data types (parseInt, toString) during validation | |
// time o(1) | space o(1) | |
function validIPAddresses(string) { | |
const results = []; | |
for (let i = 0; i < Math.min(string.length, 4); i++) { | |
const ipAddressSections = [[], [], [], []]; | |
ipAddressSections[0] = string.slice(0, i); | |
if (!isValid(ipAddressSections[0])) continue; | |
for (let j = i + 1; j < i + Math.min(string.length - i, 4); j++) { | |
ipAddressSections[1] = string.slice(i, j); | |
if (!isValid(ipAddressSections[1])) continue; | |
for (let k = j + 1; k < j + Math.min(string.length - j, 4); k++) { | |
ipAddressSections[2] = string.slice(j, k); | |
ipAddressSections[3] = string.slice(k); | |
if (!isValid(ipAddressSections[2])) continue; | |
if (!isValid(ipAddressSections[3])) continue; | |
results.push(ipAddressSections.join(".")); | |
} | |
} | |
} | |
return results; | |
} | |
function isValid(section) { | |
if (section === '') return false; | |
if (section.length > 1 && section[0] === '0') return false; | |
if (parseInt(section) < 0 || parseInt(section) > 255) return false; | |
return true; | |
} | |
// Longest Palindromic Substring (1/12) | |
// odd palindrome - left/right is i - i, i + 1 | |
// even palindrome - left/right is i - 1, i | |
// expand left/right while chars are equal, return leftIdx, rightIdx | |
// compute longest based on length of even vs odd variables | |
// time o(n^2) | space(n) | |
function longestPalindromicSubstring(string) { | |
let currentLongest = [0, 1]; | |
for (let i = 1; i < string.length; i++) { | |
const odd = getLongestPalindromeFrom(string, i - 1, i + 1); | |
const even = getLongestPalindromeFrom(string, i - 1, i); | |
const longest = even[1] - even[0] > odd[1] - odd[0] ? even : odd; | |
if (longest[1] - longest[0] > currentLongest[1] - currentLongest[0]) | |
currentLongest = longest; | |
} | |
return string.slice(currentLongest[0], currentLongest[1]); | |
} | |
function getLongestPalindromeFrom(string, leftIdx, rightIdx) { | |
while (leftIdx >= 0 && rightIdx < string.length) { | |
if (string[leftIdx] !== string[rightIdx]) break; | |
leftIdx--; | |
rightIdx++; | |
} | |
return [leftIdx + 1, rightIdx]; | |
} | |
// Longest Substring Without Duplicating (1/6) | |
// ensure we account for old duplicates using max of lastSeen + 1 vs charIdx | |
// use location = [left, right], no need for extra auxilary space | |
// time o(n) | space o(n) | |
function longestSubstringWithoutDuplication(string) { | |
let longest = [0, 1]; | |
const lastSeen = {}; | |
let left = 0; | |
for (let i = 0; i < string.length; i++) { | |
const char = string[i]; | |
if (char in lastSeen) { | |
left = Math.max(left, lastSeen[char] + 1); | |
} | |
if (longest[1] - longest[0] < i + 1 - left) { | |
longest = [left, i + 1]; | |
} | |
lastSeen[char] = i; | |
} | |
return string.slice(longest[0], longest[1]); | |
} | |
// Apartment Hunting (1/5) | |
// precompute list of min distances per req | |
// for req in reqs | |
// initialize distances list with 0s per block | |
// initialize lastSeen as Infinity, this saves index i from 0 to blocks.length the the loops below | |
// move forward, then backwards - tracking the last seen location per req | |
// forward (left/right for i loop), save lastSeen as i if blocks[i][req] | |
// then continue to compute distances list with distance between i vs lastSeen location of the current req | |
// backwards (right/left for i loop), save lastSeen as i if blocks[i][req] | |
// then continue to compute distances list as distances[i] = distanceBetween(i, lastSeen) | |
// and distance between current vs lastSeen from right/left | |
// this ensures right to left distances are computed and compared with previous computations... | |
// | |
// | |
// initialize/fill maxDistancePerBlock to -Infinity, same size as we have blocks | |
// for minDistances in minDistancesPerReq, compute maxDistancePerBlock | |
// for distance in maxDistancePerBlock, compute minDistance and track its idx | |
// return minDistance's idx | |
// time | space o(B + BR) = B(R+1) = o(BR) | |
function apartmentHunting(blocks, reqs) { | |
const minDistances = getMinDistancesPerReq(blocks, reqs); // o(B*R) space | |
const maxDistancePerBlock = blocks.map((_) => -Infinity); // o(B) space | |
blocks.forEach((block, idx) => { | |
minDistances.forEach((minDistance) => { | |
maxDistancePerBlock[idx] = Math.max( | |
maxDistancePerBlock[idx], | |
minDistance[idx] | |
); | |
}); | |
}); | |
return getMinDistanceIdx(maxDistancePerBlock); | |
} | |
function getMinDistancesPerReq(blocks, reqs) { | |
const results = []; | |
reqs.forEach((req) => { | |
const distances = blocks.map((_) => 0); | |
let lastSeen = Infinity; | |
for (let i = 0; i < blocks.length; i++) { | |
if (blocks[i][req]) lastSeen = i; | |
distances[i] = distanceBetween(i, lastSeen); | |
} | |
for (let i = blocks.length - 1; i >= 0; i--) { | |
if (blocks[i][req]) lastSeen = i; | |
distances[i] = Math.min(distances[i], distanceBetween(i, lastSeen)); | |
} | |
results.push(distances); | |
}); | |
return results; | |
} | |
function distanceBetween(a, b) { | |
return Math.abs(a - b); | |
} | |
function getMinDistanceIdx(maxDistancePerBlock) { | |
let min = Infinity; | |
let minIdx = 0; | |
for (let i = 0; i < maxDistancePerBlock.length; i++) { | |
if (maxDistancePerBlock[i] < min) { | |
min = maxDistancePerBlock[i]; | |
minIdx = i; | |
} | |
} | |
return minIdx; | |
} | |
// Min Rewards (12/30) | |
// reset everything to 1 knowing all students get at least that | |
// in a left/right pass-throguh, compute rewards as curr is prev + 1 (if curr is bigger) | |
// in a right/left pass-through, re-compute as max of curr vs prev + 1 | |
// time o(n) | space o(n) | |
function minRewards(scores) { | |
const rewards = scores.map((_) => 1); | |
for (let i = 1; i < scores.length; i++) { | |
if (scores[i] > scores[i - 1]) rewards[i] = rewards[i - 1] + 1; | |
} | |
for (let i = scores.length - 2; i >= 0; i--) { | |
if (scores[i] > scores[i + 1]) | |
rewards[i] = Math.max(rewards[i], rewards[i + 1] + 1); | |
} | |
return rewards.reduce((a, b) => a + b); | |
} | |
// Largest Range (12/28) | |
// store every num in visited table pointing to a bool true | |
// if marked as true, expand outwards to find range it contains | |
// as we expand, check if new nums are in visited and if so, mark as visited (set false) | |
// time o(n) | space o(n) | |
function largestRange(array) { | |
let longestLength = 0; | |
let bestRange = []; | |
const nums = {}; | |
for (num of array) { | |
nums[num] = true; | |
} | |
for (num of array) { | |
if (!(num in nums)) continue; | |
nums[num] = false; | |
let currentLength = 1; | |
let left = num - 1; | |
let right = num + 1; | |
while (left in nums) { | |
nums[left] = false; | |
left--; | |
currentLength++; | |
} | |
while (right in nums) { | |
nums[right] = false; | |
right++; | |
currentLength++; | |
} | |
if (currentLength > longestLength) { | |
longestLength = currentLength; | |
bestRange = [left + 1, right - 1]; | |
} | |
} | |
return bestRange; | |
} | |
// Sub Array Sort (12/28) | |
// first pass - find min/max values within out of order indicies | |
// sec/third pass - from left/right and right/left, find final positions | |
// time o(n) | space o(1) | |
function subarraySort(array) { | |
let min = Infinity; | |
let max = -Infinity; | |
for (let i = 0; i < array.length; i++) { | |
const num = array[i]; | |
if (isOutOfOrder(num, array, i)) { | |
min = Math.min(min, array[i]); | |
max = Math.max(max, array[i]); | |
} | |
} | |
if (min === Infinity) return [-1, -1]; | |
let start = 0; | |
let end = array.length - 1; | |
while (min >= array[start]) start++; | |
while (max <= array[end]) end--; | |
return [start, end]; | |
} | |
function isOutOfOrder(num, array, i) { | |
if (i === 0) return num > array[i + 1]; | |
if (i === array.length - 1) return num < array[i - 1]; | |
return num > array[i + 1] || num < array[i - 1]; | |
} | |
// Four Number Sum (12/26) | |
// 1st inner for loop - check for results | |
// 2nd inner for loop - create quadruplets | |
// avg: o(n^2) time | o(n^2) space | |
// worst: o(n^3) time | o(n^2) space | |
function fourNumberSum(array, targetSum) { | |
const results = []; | |
const hash = {}; | |
for (let i = 0; i < array.length; i++) { | |
for (let k = i + 1; k < array.length; k++) { | |
const currentSum = array[i] + array[k]; | |
const remainder = targetSum - currentSum; | |
if (hash[remainder]) { | |
const pairs = hash[remainder]; | |
pairs.forEach((pair) => results.push([...pair, array[i], array[k]])); | |
} | |
} | |
for (let j = 0; j < i; j++) { | |
const currentSum = array[i] + array[j]; | |
if (hash[currentSum]) { | |
hash[currentSum].push([array[i], array[j]]); | |
} else { | |
hash[currentSum] = [[array[i], array[j]]]; | |
} | |
} | |
} | |
return results; | |
} | |
// Merge Overlapping Intervals (12/26) | |
// sort first! | |
// merge item by item while updating last item from results array | |
// time o(n log n)| space o(n) | |
function mergeOverlappingIntervals(array) { | |
array.sort((a, b) => a[0] - b[0]); | |
const results = []; | |
results.push(array[0]); | |
for (let i = 1; i < array.length; i++) { | |
const intervalOne = results[results.length - 1]; | |
const intervalTwo = array[i]; | |
if (intervalTwo[0] <= intervalOne[1]) { | |
results[results.length - 1][1] = Math.max(intervalOne[1], intervalTwo[1]); | |
} else { | |
results.push(array[i]); | |
} | |
} | |
return results; | |
} | |
// Min Height BST (12/07) | |
// remember to pass null as that's when we arrive at null children nodes | |
// o(n) time | o(n) space | |
function minHeightBst(array) { | |
return constructMinHeightBst(array, 0, array.length - 1); | |
} | |
function constructMinHeightBst(array, startIdx, endIdx) { | |
if (endIdx < startIdx) return null; | |
const midIdx = Math.floor((startIdx + endIdx) / 2); | |
const bst = new BST(array[midIdx]); | |
bst.left = constructMinHeightBst(array, startIdx, midIdx - 1); | |
bst.right = constructMinHeightBst(array, midIdx + 1, endIdx); | |
return bst; | |
} | |
// Insertion Sort (12/07) | |
// divide into two subarrays (sorted, non-sorted), swap as needed until item 0 | |
// best: o(n) time | o(1) space | |
// avg: o(n^2) time | o(1) space | |
// worst: o(n^2) time | o(1) space | |
function insertionSort(array) { | |
for (let i = 1; i < array.length; i++) { | |
let j = i; | |
while (j > 0 && array[j] < array[j - 1]) { | |
swap(j, j - 1, array); | |
j--; | |
} | |
} | |
return array; | |
} | |
function swap(j, i, array) { | |
const temp = array[j]; | |
array[j] = array[i]; | |
array[i] = temp; | |
} | |
// Run Length Encoding (12/07) | |
// use array, not string | |
// use for i loop, not while, no need to handle incrementing i separately | |
// good variable names perfect to avoid cryptic code | |
// push when 9 or when they don't match | |
// handle last run explicitly | |
// o(n) time | o(n) space - where n is length of input | |
function runLengthEncoding(string) { | |
const encodedStringCharacters = []; | |
let currentRunLength = 1; | |
for (let i = 1; i < string.length; i++) { | |
const currentCharacter = string[i]; | |
const previousCharacter = string[i - 1]; | |
if (currentCharacter !== previousCharacter || currentRunLength === 9) { | |
encodedStringCharacters.push(currentRunLength.toString()); | |
encodedStringCharacters.push(previousCharacter); | |
currentRunLength = 0; | |
} | |
currentRunLength++; | |
} | |
encodedStringCharacters.push(currentRunLength.toString()); | |
encodedStringCharacters.push(string[string.length - 1]); | |
return encodedStringCharacters.join(""); | |
} | |
// Binary tree diameter (12/06) | |
class BinaryTree { | |
constructor(value) { | |
this.value = value; | |
this.left = null; | |
this.right = null; | |
} | |
} | |
class TreeInfo { | |
constructor(diameter, height) { | |
this.diameter = diameter; | |
this.height = height; | |
} | |
} | |
function binaryTreeDiameter(tree) { | |
return helper(tree).diameter; | |
} | |
function helper(node) { | |
if (node === null) return new TreeInfo(0, 0); | |
const left = helper(node.left); | |
const right = helper(node.right); | |
const longestPathThroughRoot = left.height + right.height; | |
const maxDiameterSoFar = Math.max(left.diameter, right.diameter); | |
const currentDiameter = Math.max(longestPathThroughRoot, maxDiameterSoFar); | |
const currentHeight = 1 + Math.max(left.height, right.height); | |
return new TreeInfo(currentDiameter, currentHeight); | |
} | |
// Group Anagrams (12/06) | |
function groupAnagrams(words) { | |
const anagrams = {}; | |
for (const word of words) { | |
const sortedWord = word.split("").sort().join(""); | |
if (sortedWord in anagrams) { | |
anagrams[sortedWord].push(word); | |
} else { | |
anagrams[sortedWord] = [word]; | |
} | |
} | |
return Object.values(anagrams); | |
} | |
// Breath-first search (12/06) | |
class Node { | |
constructor(name) { | |
this.name = name; | |
this.children = []; | |
} | |
addChild(name) { | |
this.children.push(new Node(name)); | |
return this; | |
} | |
breadthFirstSearch(array) { | |
const queue = [this]; | |
while (queue.length > 0) { | |
const curr = queue.shift(); | |
array.push(curr.name); | |
curr.children.forEach((child) => { | |
queue.push(child); | |
}); | |
} | |
return array; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment