Algorithms

Algorithm's and Data Structures

What do we need to know?

  • Foundation in coding (Need to know how to code)

  • Data structures (Tools, used to solving problems)
    Data structures requires a lot of math,
    In the context of coding interview,
    you don't need that much knowledge.

  • Data structures are foundational building blocks for tackling coding interviews

What are data structures?

  • A collection of data values, relationships among them and functions or operations that can be applied on them.

There are a lot of types of data structures.
Be well versed in all foundational data structures.
What are the benefits and drawbacks of one data structure over the other.

Complexity Analysis

Is the bed rock of coding interview problems
Single problem can have multiple solutions,
Some are a lot better than others.
Can you find a better solution?
does it have a better Complexity?

Types of Complexity

  • Time Complexity
    Measure of execution time for a algorithm.

  • Space Complexity Measure of memory a algorithm takes.

The relationships between data values in a data structure,
has ramifications for space and time complexity.

An algorithm can have a better space complexity or,
time complexity or, space time complexity
over another algorithm.

We should use the one which is better suited in the current context.

Memory

A computer has a finite amount of bounded memory slots.

Value of variable is stored in a memory slot that is free or back to back free memory slots (memory addresses), in case of an array.

  • Memory slot's always hold 8 bits or 1 byte of information.

  • A 8 bit number can represent any value between 0-256.

  • A lot of programming can represent values in 32bit or 64bit integers.
    00000000 00000000 00000000 00000011 (number 3 represent in 32 bits)
    2^7.2^6.2^5.2^4.2^3.2^2.2^1.2^0
    128.64.32.16.8.4.2.1
    00000011

![image](./images/memory_slot.png "Memory Slot" =500x)

so we need 4 consecutive memory slots, to represent the number 1.
The most significant bit is represented to the left (endianness).
A value will always have a constant number of memory slots.

Accessing memory slot can be done very fast, inexpensive operation.

Pointers

Variable can point to the memory address of an other memory address. pointers can point to a memory slot significant distance away.

Big O Notation

An array a has multiple functions.

ts
a = [1, 2, 3];

For grabbing value of an index

ts
func1(a) = 1 + a[0];
// O(1) instant

For sum,

ts
func2(a) = sum(a);
// O(N) longer

For enumerating / iterating an array

ts
func3(a) = [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2]...]
// O(N^2) longest
python
for (number in array) # loop1
  for (number_2 in array) # loop2
    return [number, number_2]

we cannot express, performance of a function using time,
speed is always dependant on the size of the array.

Time complexity is the measure of the execution time,
relative to the size of input.

Asymptotic analysis

In Asymptotic analysis, we don't care about the exact number of elementary operations.
consider the following functions and their time complexities

ts
function = (number: number) => void;
// func = func1 + func2 + func3
// Time complexity = O(N^2 + N + 1)

// since N^2 is the most significant complexity,
// it is retained while other's are dropped
Time complexity = O(N^2)

Big O notation always refer to the worst case scenarios,

Arguments are treated separately

ts
function = (m: any[], n: any[]) => sum(m, n);
// keeping the M and N separately, since they are unique arguments.
// O(M + N)`

Types of space-time complexities

  • O(1)

  • O(log(N))

  • O(N)

  • O(Nlog(N))

  • O(N^2), O(N^3), O(N^4) exponents also matter

  • O(2^N)

  • O(N!)

What the hell is log?

text
logb(N) = p
log2(N) = 4

base to what power is equal to number

text
b^P = N
2^4 = 16

in Asymptotic analysis it is always assumed that we use log base 2.
in Mathematics log base 10 is the default.

When is space-time log(N)?

Ask yourself the following questions.

Am I cutting the size of input by half during every step of my function? if yes then we are dealing with log(N)
If I double the size of input, do I need only one extra operation

the double
2^y = N; aka log(N) = y
2^0 = 1; aka log(1) = 0
2^1 = 2; aka log(2) = 1
2^2 = 4; aka log(4) = 2
2^3 = 8; aka log(8) = 3
2^4 = 16; aka log(16) = 4
2^5 = 32; aka log(32) = 5

text
 Time (y)
5|
 |
4|                                                               . log(N)
 |
3|                              .
 |
2|              .
 |
1|      .
 .__._____________________________________________________________ N
0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16

Arrays

No title

A 64 bit static array of length 3 requires,
24 slots of free memory slots

Static vs Dynamic Arrays.

  • C++ / Java uses static arrays Length is specified when creating an array. Memory slots are created for that array length. No new memory slots are created after operations.

  • Python / JavaScript uses dynamic arrays.

Operations on an array

  • Initializing an array.

    text
    O(8 memory slots for each 64 bit number * N);
    O(N) ST;

    since the time complexity depends on the array length.

  • Accessing value for array index.

    text
    O(1) ST

    Complexity is always instant array[2]
    no matter the length of array.
    OS finds the beginning memory slot of an array,
    calculates the memory slot for the index, using

    text
    memory slot address + (index * (number bit length) / 8)
  • Updating value of array index is instant array[2] = x.
    O(1) ST

  • Looping / Traversing an array.
    O(N) T time complexity.
    O(1) S space complexity, since we are not creating any new slots.

  • Copying an array.
    O(N) ST
    traverse through array and create a new array memory blocks.
    copying is taxing.

  • Insert to the end of the array.
    static array,

    ts
    [1, 2, "new", 3];

    we need to shift memory blocks to the right (even for append or prepend).
    OS does not guarantee, free memory blocks on the right.
    OS will Copy a new array (setting new memory blocks) O(N) ST.
    OS will Delete aka Wipe old memory blocks O(1) S.
    O(N) ST

    for dynamic arrays,
    generally, the compiler, allocates double the required memory slots, when array is initialized.
    [1, 2, 3, -, -, -]

    O(1) ST for appending elements to the array, with enough free buffers.
    [1, 2, 3, 'new', -, -]

    O(N) ST for appending elements to the array, after the buffer is exhausted.
    [1, 2, 3, 4, 5, 6, 'new', -, -, -, -, -, -, -]
    the OS will copy a new array with 2x the buffer.

    Amortized Analysis,
    Space time complexity,
    generalized to, O(1) ST for dynamic arrays

    If an interviewer explicitly asks for worst case scenario, use O(N) ST

  • Insert to Middle O(N) ST since we always end up shifting the array, by 1.

  • Insert to Start O(N) ST since we always end up shifting all elements in the array, by 1.

  • Pop in the end of array.
    O(1) ST

  • Pop in the middle or beginning of array.
    O(N) ST, since we need to shift the array.

Linked Lists

Linked list is very similar to an array, the way linked list is stored in memory is different from an array.

limitation's of an array, values will be stored in back to back memory slots.

In linked list, value can be stored in random memory slots.

![image](./images/linked_list.png "Linked List" =500x)

Singly Linked List

text
array = [2, 1, 5, 6];
linked_list = 2 -> 1 -> 5 -> 6 -> null;

node 1 (2) has a pointer pointing to the next memory address, for the proceeding node. The pointer value is stored in the next memory slot. In the image, 22nd memory slot is used to store the pointer value for the next node value.

Doubly Linked List

Every node has two pointers, pointer to previous node and next node.

memory slots, representation

text
| 2 | pointer to next | pointer to prev | memory_slot ..
text
array = [2, 1, 5, 6];
linked_list = null <- 2 <-> 1 <-> 5 <-> 6 -> null;

Circularly Linked List

Does not have a clear head or tail, because tail points to its head. Can be singly circular linked list or doubly circular linked list.

Can be efficient to use circularly linked list, to implement a fixed length queue.

Operations on an linked list

  • Initializing an linked list.

    text
    O(2N) ST;
    O(N) ST;

    1 memory slot for value 1 memory slot for the pointer to next value

  • Accessing value for index.

    To access a value in the middle of linked list (5) we need to traverse the array until the pointer to the desired, index is found.

    memory slot next to value 2 will tell me where to look for 1's value. memory slot next to value 1 will tell me where to look for 5's value.

    text
    linked_list = 2 -> 1 -> 5 -> 6 -> null;
    text
    O(index) T = O(N) T
    O(1) S
  • Updating value of array index is instant array[2] = x

    text
    O(index) T = O(N) T
    O(1) S
  • Looping / Traversing O(N) T.
    O(1) S.

  • Copying O(N) ST

  • Insert / Delete

    Prepending or Pop

    text
    'new' -> 2 -> 1 -> 5 -> 6 -> null
    O(1) TS

    Inserting in the middle

    text
    2 -> 1 -> 'new' -> 5 -> 6 -> null
    O(N) TS to traverse until node 2
    O(1) to modify next pointer for 1
    O(1) to create new value in memory_slot for value 'new'
    O(1) to create next pointer for 'new'

    Appending or Pop

    Singly linked list

    text
    2 -> 1 -> 5 -> 6 -> 'new' -> null
    O(N) TS to traverse until end
    O(1) to create new node

    Doubly linked list

    find the previous value, pointer of head. created new node at the end of linked list

    text
    2 -> 1 -> 5 -> 6 -> 'new' -> null
    O(1) to create new node

Hash Tables

Lot of modern programming, hash tables are built in. JS hash table is object Python is dictionary

Hash table, is a datastructure, with key value pairs.

text
"foo" => 1
"bar" => 2
"baz" => 3

insertion, delete, lookup are constant time operations O(1).

Why is hash table better than arrays?

Our keys can be strings / integers unlike arrays, this depends on the type of hashing function used.

Hash tables are built on top of arrays.

text
// Hash
"key" => value
"foo" => 1
"bar" => 2
"baz" => 3

// Array
// can be in random order.
[2, 1, 3]

A hashing function is used to transform the key foo into an index and viceversa.

Example of an hashing function.

A string is converted to an number, most commonly, using ascci number for each character in the string, and summing it up.

ascci_sum % length_of_array

A linked list is used, when two keys point to the same array index.

[2, 1 -> 3]

What happens when two similar keys are used?

Hash tables rely on highly optimized hash functions to minimize the number of collisions that occur when storing values: cases where two keys map to the same index.

In the worst caseinsertion, delete, lookup take O(N) space-time complexity.

Advanced hashing functions, almost always minimize the collisions.

What happens when a new key is added / removed from an hash, after hash buffer is exhausted.

The old object is copied, new key is added, hash key is recalculated, memory slot's are re-initialized.

Amortized analysis, assume, O(1), worst case scenario is O(N).

Initializing an hash function.

O(N) space-time, depends on number of key-value pairs.

Stacks and Queues

  • Stacks use the LIFO, last in first out principle

  • Queues use the FIFO, first in first out principle

  • Both Stacks and Queues have O(1) constant time and space, Insertion and Deletion

  • Stacks and Queues, do not let insertion or deletion in the middle.

Under the hood,

  • Stacks use a dynamic array or singly linked array.
    with a reference to the last element in the array.

  • Queues use a doubly linked array,
    with a reference to the first element in the array.

Operations on Stacks and Queues,

  • Insertion and Deletion to the end of the queue and beginning of queue.
    O(1), a doubly liked array, enables it.

  • Insertion and Deletion to the end of the stack and end of stack.
    O(1), a singly liked array, enables it.

  • Peeking, the fist element in a queue and last element in a stack.
    O(1)

  • Searching,
    O(1), since it involves traversing the array, until the index is found.

Strings

strings are stored in memory, as and array of integers, using ASCII notations. ASCII has 256 character's represented in interger, for other types of character are represented using two bytes.

All operations on a single character string, is O(N)

Operations on a string,

  • Traversing, O(N) ST

  • Get, O(1) ST

  • Copying, O(N) ST

  • Mutating a string, in, JS, C#, Python... strings are not mutable, strings are copied, and replaced O(N) Adding two strings together, O(N + M)

    if we require, multiple operations to be done on a string, its better to convert the string into an array of characters, mutate and join the array

    • C allows us to mutate strings.

    python
    string = 'this is a string'
    newString = ''
    for character in string: # O(N) to traverse an existing array
      newString += character # O(N) to replace an existing array
    # O(N^2) complexity for the above algo

Graphs

Graphs ar a collection of vertices (coordinates) and edges.

  • Connected Graph A graph is connected if, any node, can reach everyother node.

  • Disconnected Graph

  • Directed Graph All edges are represented by arrows, arrows represent direction.

  • Cyclic Graph In a graph if 3 or more nodes, have a infinate loop. If we reach a node that was already visited, we are in a cycle. Cyclic graph, we need to make sure, we do not get stuck in a infinite, cycle.

  • Acyclic Graph

Graphs are very useful, relationships, grids, ... Graphs can be represented using matrices (adjacent list), hash (each node has a value and list of connected notes).

Can a certain input be represented as a graph? When we represent a graph in code, we store, all nodes N, edges of all nodes E. O(N + E) space complexity.

traversing an graph, depth first search, breath first search, will work on this in the comming questions. O(N + E) time complexity.

Trees

A trees are a prominent data structures in computer science. A tree is a type of graph, with a root structure.

A graph is a tree if Directed, Acyclic (Each node has only on parent), Connected.

Type of trees,

  • Binary trees A tree, whose nodes have upto 2 child notes

    text
        0
      /  \
     1    2
    / \  /
    3  4 5
  • Balanced Binary Tree roughly 2 nodes for each node

    text
        0
      /  \
     1    2
    / \  / \
    3  4 5  6
             \
  • Unbalanced Binary Tree more needs on one side than the other

    text
        0
      /
     1
    / \
    3  4
        \
         5
  • K-ary tree A tree, whose nodes can have k (0 / 2 -inf) child nodes,

  • complete tree, since tree is filled up from left

    text
          0
        /  \
       1    2
      / \  / \
      3
  • incomplete tree, since tree is not filled up from the left

    text
          0
        /  \
       1    2
      / \  / \
              3
  • A full binary tree, has all its nodes with 2 / 0 child nodes.

    text
          0
        /  \
       1    2
           / \
          3   4

Operations on a tree

  • Creating / Storing O(N) S

  • Traversal through all nodes O(N) ST

  • Traversal from top to bottom along one path, in a balanced binary tree. O(log(N)) since we reducing, eliminating, half of the input at each operation.

  • Traversal on an unbalanced binary tree O(N)

A tree is complete, if all the levels are filled up and, the final level is filled up from left to right.

Get Different Number

Given an array arr of unique nonnegative integers, implement a function getDifferentNumber that finds the smallest nonnegative integer that is NOT in the array.

Even if your programming language of choice doesn’t have that restriction (like Python), assume that the maximum value an integer can have is MAX_INT = 2^31-1. So, for instance, the operation MAX_INT + 1 would be undefined in our case.

Your algorithm should be efficient, both from a time and a space complexity perspectives.

Solve first for the case when you’re NOT allowed to modify the input arr. If successful and still have time, see if you can come up with an algorithm with an improved space complexity when modifying arr is allowed. Do so without trading off the time complexity.

Analyze the time and space complexities of your algorithm.

txt
input:  arr = [0, 1, 2, 3]

output: 4
ts
const swapUntilCorrect = (arr: number[], idx: number) => {
	const element = arr[idx];
	console.table({ element, idx, arr });
	if (element === idx) return;
	if (element > arr.length - 1) return;
	[arr[element], arr[idx]] = [arr[idx], arr[element]];
	return swapUntilCorrect(arr, idx);
};

function getDifferentNumber(arr: number[]) {
	for (let idx = 0; idx < arr.length; idx++) {
		swapUntilCorrect(arr, idx);
	}

	for (let idx = 0; idx < arr.length; idx++) {
		if (arr[idx] !== idx) return idx;
	}

	return arr.length;
}

console.log(getDifferentNumber([0, 5, 4, 1, 3, 6, 2]));
  • Time complexity is O(N) and space complexity is O(1)

  • Because on every swap an element falls into its correct position

  • If an element is in its correct position, we don't need to do more computations on it

txt
0, 2, 3, 4
^ first iterateion, leave 0 as it is

0, 3, 2, 4 < swap 2 and 3, since 3 is not at the correct index
   ^
0, 4, 2, 3 < 4 cannot be swapped because 4 its out of range

0, 4, 2, 3 < 2 is at the correct index
      ^
0, 4, 2, 3 < 3 is at the correct index
         ^

now 1 is the missing number, since at 1st index, 4 is present instead of one

Minimum Size Subarray Sum

ts
// [2,3,1,2,4,3]
//  L
//  R
//  L  R
function minSubArrayLen(target: number, nums: number[]): number {
	let variableIdx = 0;
	let sum = 0;
	let min = Infinity;

	for (let fixedIdx = 0; fixedIdx < nums.length; fixedIdx++) {
		const valueAtFixedIdx = nums[fixedIdx];
		sum += valueAtFixedIdx;

		while (sum >= target) {
			min = Math.min(min, fixedIdx - variableIdx + 1);
			const valueAtVariableIdx = nums[variableIdx];
			sum -= valueAtVariableIdx;
			variableIdx += 1;
		}
	}

	if (min === Infinity) {
		return 0;
	}

	return min;
}

Key points

txt
[2, 3, 1, 2, 4, 3], target = 7
 0  1  2  3  4  5

- when sum is graeter than or equal to target, we need to shrink the window
  eg,
  2 + 3 + 1 + 2 = 8, 8 is greater than 7 ( set min length to 4 )

  shrink the window by removing 2
  3 + 1 + 2 = 6, 6 is less than 7 so break the loop

- when sum is less than target, we need to expand the window
  eg,
  2 + 3 = 5, 5 is less than 7
  expand the window by adding 1
  2 + 3 + 1 = 6, 6 is less than 7
  expand the window by adding 2
  2 + 3 + 1 + 2 = 8, 8 is greater than 7, set min length here
  shrink the window by removing 2
  3 + 1 + 2 = 6, 6 is less than 7
  expand the window by adding 4
  2 + 3 + 1 + 2 + 4 = 12, 12 is greater than 7, set min length here
  shrink the window by removing 2
  3 + 1 + 2 + 4 = 10, 10 is greater than 7, set min length here
  shrink the window by removing 3
  1 + 2 + 4 = 7, 7 is equal to 7, set min length here
  expand the window by adding 3
  2 + 3 + 1 + 2 + 4 + 3 = 15, 15 is greater than 7, set min length to Min(prevMin, 4)

Shortest subarray with sum at least K

ts
const shortestSubarray = (nums: number[], target: number) => {
	// hold the sum to the elements at the ith index of the nums array
	const prefixSums = new Array(nums.length + 1).fill(0);
	for (let idx = 0; idx < nums.length; idx++) {
		prefixSums[idx + 1] = prefixSums[idx] + nums[idx];
	}

	console.log({ prefixSums });

	let minSubArrayLength = Infinity;
	// holds the index of the prefix sum
	const deque = [];

	for (let idx = 0; idx < prefixSums.length; idx++) {
		const sumAtIdx = prefixSums[idx];
		console.log('DEQUE__BEFORE', { idx, deque, sumAtIdx, minSubArrayLength });
		while (deque.length > 0 && sumAtIdx - prefixSums[deque[0]] >= target) {
			console.log('DEQUE__AFT--1', { idx, deque });
			const frontIdx = deque.shift();
			const newSubArrayLength = idx - frontIdx;
			minSubArrayLength = Math.min(minSubArrayLength, newSubArrayLength);
		}
		while (
			deque.length > 0 &&
			sumAtIdx <= prefixSums[deque[deque.length - 1]]
		) {
			console.log('DEQUE__AFT--2', { idx, deque });
			deque.pop();
		}
		deque.push(idx);
	}

	return minSubArrayLength === Infinity ? -1 : minSubArrayLength;
};

console.log(shortestSubarray([2, -1, 2], 3));

// Prefix sums hold the sum of the elements at the ith index of the nums array
// [0, 2, 1, 3]
//
// we remove from front if target is reached, set minLength and remove from deque
// we remove form back to keep the deque in monotonically increasing order

Best seat

ts
export const bestSeat = (seats: number[]) => {
	// [0, 1, 2, 3, 4, 5, 6]
	// [1, 0, 1, 0, 0, 0, 1] => 4
	let maxConsequtiveSeats: number[] = []; // O(N)
	let currentConsequtiveSeats: number[] = []; // O(N)
	for (let idx = 0; idx < seats.length; idx++) {
		const el = seats[idx];
		if (el !== 0) {
			if (maxConsequtiveSeats.length < currentConsequtiveSeats.length) {
				maxConsequtiveSeats = currentConsequtiveSeats;
			}
			currentConsequtiveSeats = []; // creating a new empty array is O(1)
			continue;
		}
		currentConsequtiveSeats.push(idx);
	}
	if (maxConsequtiveSeats.length === 0) {
		return -1;
	}
	if (maxConsequtiveSeats.length === 1) {
		return maxConsequtiveSeats[0];
	}
	const isEven = maxConsequtiveSeats.length % 2 === 0;
	const middle = Math.floor(maxConsequtiveSeats.length / 2);
	if (isEven) {
		return maxConsequtiveSeats[middle - 1];
	}
	return maxConsequtiveSeats[middle];
};

console.log(bestSeat([1, 0, 1, 0, 0, 0, 1]));
console.log(bestSeat([1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1]));
console.log(bestSeat([1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1]));

Subarray Sort

subarray_sort

ts
type Range = [number, number];

const findLargestOutOfOrderNum = (array: number[]): number => {
	let largestOutOfOrderNum = -Infinity;
	for (let idx = 0; idx < array.length; idx++) {
		const el = array[idx];
		const nextEl = array[idx + 1];
		if (!nextEl) {
			break;
		}
		if (nextEl < el) {
			largestOutOfOrderNum = Math.max(largestOutOfOrderNum, el);
		}
	}
	return largestOutOfOrderNum;
};

const findSmallestOutOfOrderNum = (array: number[]): number => {
	let smallestOutOfOrderNum = Infinity;
	for (let idx = array.length - 1; idx >= 0; idx--) {
		const el = array[idx];
		const nextEl = array[idx - 1];
		if (!nextEl) {
			break;
		}
		if (nextEl > el) {
			smallestOutOfOrderNum = Math.min(smallestOutOfOrderNum, el);
		}
	}
	return smallestOutOfOrderNum;
};

const findNewPlacementForLargestOutOfOrderNum = (
	number: number,
	array: number[]
): number => {
	let minIdx = array.length - 1;
	for (let idx = 0; idx < array.length; idx++) {
		const el = array[idx];
		if (el >= number) {
			minIdx = Math.min(idx - 1, minIdx);
			continue;
		}
		minIdx = array.length - 1;
	}
	return minIdx;
};

const findNewPlacementForSmallestOutOfOrderNum = (
	number: number,
	array: number[]
): number => {
	let maxIdx = 0;
	for (let idx = array.length - 1; idx >= 0; idx--) {
		const el = array[idx];
		if (el <= number) {
			maxIdx = Math.max(idx + 1, maxIdx);
			continue;
		}
		maxIdx = 0;
	}
	return maxIdx;
};

export function subarraySort(array: number[]): Range {
	const largestOutOfOrderNum = findLargestOutOfOrderNum(array);
	const smallestOutOfOrderNum = findSmallestOutOfOrderNum(array);
	if (
		largestOutOfOrderNum === -Infinity ||
		smallestOutOfOrderNum === Infinity
	) {
		return [-1, -1];
	}
	return [
		findNewPlacementForSmallestOutOfOrderNum(smallestOutOfOrderNum, array),
		findNewPlacementForLargestOutOfOrderNum(largestOutOfOrderNum, array),
	];
}

console.log(subarraySort([1, 2, 4, 3, 8, -1, 7, 10])); // [2, 6]
console.log(subarraySort([1, 2, 4, 7, 10, 11, 7, 12, 6, 7, 16, 18, 19])); // [3, 9]
console.log(subarraySort([1, 2, 4, 7, 10, 11, 7, 12, 7, 7, 16, 18, 19]));

Non-constructible change

ts
export function nonConstructibleChange(coins: number[]) {
	coins.sort((a, b) => a - b);
	let currentChangeCreated = 0;
	for (const coin of coins) {
		if (coin > currentChangeCreated + 1) return currentChangeCreated + 1;
		currentChangeCreated += coin;
	}
	return currentChangeCreated + 1;
}

Pitfalls,

  • The brute force approach is too slow O(N^2), involves iterating through all the possible combinations of coins.

  • Sort the array, so that we can iterate through the coins in order.

  • coin > currentChangeCreated + 1, return currentChangeCreated + 1, since we can't create the change.

  • Add coin to currentChangeCreated, since we can create the change.


Tournament winner

ts
const updateMax = (
	currentMax: { team: string; score: number },
	winner: string,
	tempScores: { [key: string]: number }
) => {
	if (tempScores[winner] > currentMax.score) {
		currentMax.team = winner;
		currentMax.score = tempScores[winner];
	}
};

export function tournamentWinner(competitions: string[][], results: number[]) {
	const currentMax = { team: '', score: 0 };
	const tempScores: { [key: string]: number } = {};
	results.forEach((result, index) => {
		const winner = competitions[index][result ? 0 : 1];
		if (tempScores[winner]) {
			tempScores[winner] += 3;
			updateMax(currentMax, winner, tempScores);
			return;
		}
		tempScores[winner] = 3;
		updateMax(currentMax, winner, tempScores);
		return;
	});
	return currentMax.team;
}

Tandem Bicycle

ts
const sort = (arr: number[]) => arr.sort((a, b) => b - a);

export function tandemBicycle(
	redShirtSpeeds: number[],
	blueShirtSpeeds: number[],
	fastest: boolean
) {
	const maxIdx = redShirtSpeeds.length - 1;
	sort(redShirtSpeeds);
	sort(blueShirtSpeeds);
	return redShirtSpeeds
		.map((redShirtSpeed, idx) =>
			// [max, min] for fastest
			// [max, max] for slowest
			Math.max(redShirtSpeed, blueShirtSpeeds[fastest ? maxIdx - idx : idx])
		)
		.reduce((a, b) => a + b, 0);
}

Class Photo

ts
export function classPhotos(
	redShirtHeights: number[],
	blueShirtHeights: number[]
) {
	redShirtHeights.sort((a, b) => b - a);
	blueShirtHeights.sort((a, b) => b - a);

	const isRedShirtFirst = redShirtHeights[0] < blueShirtHeights[0];

	let isPossible = true;

	if (isRedShirtFirst) {
		for (let idx = 0; idx < redShirtHeights.length; idx++) {
			const redShirtHeight = redShirtHeights[idx];
			const blueShirtHeight = blueShirtHeights[idx];
			if (blueShirtHeight <= redShirtHeight) {
				isPossible = false;
				break;
			}
		}
		return isPossible;
	}

	for (let idx = 0; idx < blueShirtHeights.length; idx++) {
		const redShirtHeight = redShirtHeights[idx];
		const blueShirtHeight = blueShirtHeights[idx];
		if (redShirtHeight <= blueShirtHeight) {
			isPossible = false;
			break;
		}
	}

	return isPossible;
}

Optimal freelancing

ts
export function optimalFreelancing(jobs: Record<string, number>[]) {
	// O(n) S
	const days = new Array(7).fill(false);
	let totalPayment = 0;
	jobs
		// O(nlogn) T
		.sort((a, b) => b.payment - a.payment)
		.map((el, index) => {
			// O(1) T since days in weeks are fixed
			for (let slot = el.deadline - 1; slot >= 0; slot -= 1) {
				if (typeof days[slot] === 'boolean' && !days[slot]) {
					days[slot] = true;
					totalPayment += el.payment;
					break;
				}
				continue;
			}
		});
	return totalPayment;
}

Pitfalls

  • Only 7 days in a week

  • 1 job takes 1 day

  • Cant exceed deadline for the job

  • Create days empty array

  • Sort jobs by payment (highest first)

  • Assign jobs to day based on deadline

  • If assignable to day, add payment to totalPayment


Minimum waiting time

ts
const sum = (acc: number, curr: number) => acc + curr;
export const minimumWaitingTime = (queries: number[]) => {
	return queries
		.sort((a, b) => a - b)
		.map((el, index, array) => array.slice(0, index).reduce(sum, 0))
		.reduce(sum);
};

Pitfalls

txt
[1, 2, 3, 4, 5]
 0  1  3  6  10
 -> 0
    -> 1 + 0
       -> 2 + 1
          -> 1 + 2 + 3 slice of previous array

Transpose matrix

ts
export const transposeMatrix = (matrix: number[][]): number[][] => {
	const newMatrix: number[][] = [];
	matrix.forEach((row, rowIdx) => {
		row.forEach((col, colIdx) => {
			if (!newMatrix[colIdx]) {
				newMatrix[colIdx] = [];
			}
			newMatrix[colIdx][rowIdx] = col;
		});
	});
	return newMatrix;
};

Find three largest numbers

ts
// O(n) time | O(1) space

export const findThreeLargestNumbers = (array: number[]) => {
	let first = -Infinity;
	let second = -Infinity;
	let third = -Infinity;
	for (let num of array) {
		if (num > first) {
			third = second; // move the first largest to second
			second = first; // move the second largest to third
			first = num;
			continue;
		}
		if (num > second) {
			third = second; // move the second largest to third
			second = num;
			continue;
		}
		if (num > third) {
			third = num;
			continue;
		}
	}
	return [third, second, first];
};

Pitfalls

  • -Infinity is the smallest number in JS


Merge Binary Trees

txt
tree1 =   1
        /   \
       3     2
     /   \
    7     4

tree2 =   1
        /   \
       5     9
     /      / \
    2      7   6

output =  2
        /   \
      8      11
    /  \    /  \
  9     4  7    6
ts
// This is an input class. Do not edit.
export class BinaryTree {
	value: number;
	left: BinaryTree | null;
	right: BinaryTree | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

type Node = BinaryTree | null | undefined;

const mergeTrees = (node1: Node, node2: Node, newNode: BinaryTree) => {
	newNode.value = (node1?.value || 0) + (node2?.value || 0);
	newNode.left = node1?.left || node2?.left ? new BinaryTree(0) : null;
	newNode.right = node1?.right || node2?.right ? new BinaryTree(0) : null;

	if (newNode.left) {
		mergeTrees(node1?.left, node2?.left, newNode.left);
	}

	if (newNode.right) {
		mergeTrees(node1?.right, node2?.right, newNode.right);
	}
};

export function mergeBinaryTrees(tree1: BinaryTree, tree2: BinaryTree) {
	// Write your code here.
	const newNode = new BinaryTree(0);
	mergeTrees(tree1, tree2, newNode);
	return newNode;
}

Majority Element

ts
export function majorityElement(
	array: number[],
	count: number = 1,
	majority: number = array[0]
): number {
	for (let index = 1; index < array.length; index++) {
		const element = array[index];
		if (count === 0) {
			return majorityElement(array.slice(index), 1, array[index]);
		}
		if (element === majority) {
			count++;
			continue;
		}
		count--;
	}
	return majority;
}
txt
[1, 1, 2, 2, 7, 2, 2]
count = 1,
majority = 1

Missing Numbers

ts
export function missingNumbers(nums: number[]) {
	const numsAsObj: {
		[key: number]: boolean;
	} = {};
	const numsNotFound = [];
	nums.forEach(el => {
		numsAsObj[el] = true;
	});
	for (let el = 1; el <= nums.length + 2; el += 1) {
		if (!numsAsObj[el]) {
			numsNotFound.push(el);
		}
	}
	return numsNotFound;
}
ts
export function missingNumbers(nums: number[]) {
	let expectedSum = 0;
	for (let el = 1; el <= nums.length + 2; el += 1) {
		expectedSum += el;
	}

	const actualSum = nums.reduce((acc, el) => acc + el, 0);
	const diff = expectedSum - actualSum;
	const average = Math.floor(diff / 2);

	let expectedSumLeft = 0;
	let expectedSumRight = 0;
	for (let el = 1; el <= nums.length + 2; el += 1) {
		if (el <= average) {
			expectedSumLeft += el;
			continue;
		}
		expectedSumRight += el;
	}

	let left = 0;
	let right = 0;

	for (let index = 0; index < nums.length; index += 1) {
		const el = nums[index];

		if (el <= average) {
			left += el;
			continue;
		}
		right += el;
	}

	return [expectedSumLeft - left, expectedSumRight - right];
}
txt
1435                      : actual_sum   : 13
123456 (array.length + 2) : expected_sum : 21
                            diff         : 8
                            avg          : 4

1234                        ex_left_sum  : 10
56                          ex_right_sum : 11

143                         left_sum     : 8
5                           right_sum    : 5

expected_sum_left - left   : 2
expected_sum_right - right : 6

Next grater element

ts
export function nextGreaterElement(array: number[]) {
	const resultStack: number[] = new Array(array.length).fill(-1);
	for (let index = 0; index < array.length; index++) {
		const element = array[index];
		for (
			let compareIndex = index + 1 > array.length - 1 ? 0 : index + 1;
			compareIndex < array.length;
			compareIndex = compareIndex + 1 > array.length - 1 ? 0 : compareIndex + 1
		) {
			if (compareIndex === index) break;
			const compareElement = array[compareIndex];
			if (compareElement > element) {
				resultStack[index] = compareElement;
				break;
			}
		}
	}
	return resultStack;
}

Three number sum

ts
export const threeNumberSort = (array: number[], order: number[]) => {
	const [first, second, third] = order;
	let firstIdx = 0;
	let secondIdx = 0;
	let thirdIdx = array.length - 1;
	while (secondIdx <= thirdIdx) {
		const value = array[secondIdx];
		if (value === first) {
			[array[firstIdx], array[secondIdx]] = [value, array[firstIdx]];
			firstIdx++;
			secondIdx++;
			continue;
		}
		if (value === second) {
			secondIdx++;
			continue;
		}
		[array[thirdIdx], array[secondIdx]] = [value, array[thirdIdx]];
		thirdIdx--;
		continue;
	}
	return array;
};

use second as current value

txt
array = [1, 0, 0, -1, -1, 1, 1]
order = [0, -1, 1]

1 0 0 -1 -1 1 1
f             t
S

1 0 0 -1 -1 1 1
f              t
  S -> since second is curr, move S++

0 1 0 -1 -1 1 1
  f           t
    S

0 0 1 -1 -1 1 1
    f         t
       S

0 0 1 -1 -1 1 1
    f         t
       S

0 0 1  1  -1 1 -1
    f        t
           S

0 0 1  1  1 -1 -1
    f     t
          S

0 0 1  1  1 -1 -1
    f     t
             S --> second idx > third idx, break

Two Number Sum

Write a function that takes in a non-empty array of distinct integers and an integer representing a target sum. If any two numbers in the input array sum up to the target sum, the function should return them in an array, in any order. If no two numbers sum up to the target sum, the function should return an empty array.

Note that the target sum has to be obtained by summing two different integers in the array; you can't add a single integer to itself in order to obtain the target sum.

You can assume that there will be at most one pair of numbers summing up to the target sum.

Sample Input

python
array = [3, 5, -4, 8, 11, 1, -1, 6]
targetSum = 10

Sample Output

python
[-1, 11] # the numbers could be in reverse order

Solution One

Time Complexity O(n^4)
Space Complexity O(n)

typescript
export function twoNumberSum(array: number[], targetAmount: number) {
  let selectedIndex: number = 0;
  // initializing an array O(n) S
  const pairs: number[] = [];
  // iterating over the array O(n)
  for (const index in array) {
    selectedIndex = +index;
    // iterating over the array O(n^2) T
    for (const subIndex in array) {
      const subNumber = array[+subIndex]
      if (selectedIndex === +subIndex) continue;
      if (
        array[subIndex] + array[selectedIndex] === targetAmount &&
        // iterating over the array O(n^3) T
        !pairs.includes(array[subIndex]) &&
        // iterating over the array O(n^4) T
        !pairs.includes(array[selectedIndex]) T
      ) {
        // updating an array O(1) TS
        pairs.push(array[selectedIndex]);
        // updating an array O(1) TS
        pairs.push(array[subIndex]);
        break;
      }
    }
  }
  return pairs;
}

Solution Two

Time Complexity O(4n)
Space Complexity O(2n)

typescript
type Array = number[];
type ArrayAsObject = {
	[element: number]: boolean;
};

export function twoNumberSum(array: Array, targetAmount: number) {
	// initializing an array O(n) ST
	const pairs: number[] = [];

	// initializing an object O(n) ST
	const arrayAsObject: ArrayAsObject = {};

	// O(n) T
	array.forEach(value => {
		arrayAsObject[value] = true;
	});

	// O(n) T
	for (const iterator in array) {
		const value = array[iterator];
		const difference = targetAmount - value;

		if (arrayAsObject[difference] && value !== difference) {
			// O(1) S
			pairs.push(value, difference);
			break;
		}
	}

	return pairs;
}

Solution Three

Time Complexity O(3n)
Space Complexity O(2n)

typescript
type Array = number[];
type ArrayAsObject = {
	[element: number]: boolean;
};

export function twoNumberSum(array: Array, targetAmount: number) {
	// initializing an array O(n) S
	const pairs: number[] = [];

	// initializing an array O(n) S
	const arrayAsObject: ArrayAsObject = {};

	// O(n) T
	for (const iterator in array) {
		const value = array[iterator];
		const difference = targetAmount - value;

		if (arrayAsObject[difference] && value !== difference) {
			pairs.push(value, difference); // O(1) S
			break;
		}

		// O(1) S
		arrayAsObject[value] = true;
	}

	return pairs;
}

Solution Four

Time Complexity O(nlog(n))
Space Complexity O(1)

typescript
export const twoNumberSum = (
	array: number[],
	targetAmount: number,
	left: number = 0,
	right: number = 0,
	isSorted = false
): number[] => {
	// Sort in place,
	// O(n) time, O(1) space
	const sortedArray = isSorted ? array : array.sort((a, b) => a - b);

	const leftValue = left || 0;
	const rightValue = right || sortedArray.length - 1;
	const selectedLeftElement = sortedArray[leftValue];
	const selectedRightElement = sortedArray[rightValue];
	const currentSum = selectedRightElement + selectedLeftElement;

	if (leftValue > rightValue) return [];

	if (currentSum === targetAmount)
		return [selectedLeftElement, selectedRightElement];

	if (currentSum > targetAmount)
		// reduced array length by 1
		// log(n) time
		return twoNumberSum(
			sortedArray,
			targetAmount,
			leftValue,
			rightValue - 1,
			true
		);

	if (currentSum < targetAmount)
		// reduced array length by 1
		// log(n) time
		return twoNumberSum(
			sortedArray,
			targetAmount,
			leftValue + 1,
			rightValue,
			true
		);

	return [];
};

Four number sum

Write a function that takes in a non-empty array of distinct integers and an integer representing a target sum. The function should find all quadruplets in the array that sum up to the target sum and return a two-dimensional array of all these quadruplets in no particular order.

If no four numbers sum up to the target sum, the function should return an empty array.

Sample Input

python
array = [7, 6, 4, -1, 1, 2]
targetSum = 16

28127.67 - 100000 =


Sorted Squared Array

write a function that takes in a non-empty array of integers that are sorted in ascending order and returns a new array of the same length with the squares of the original integers also sorted in ascending order.

Sample Input

python
array = [1, 2, 3, 5, 6, 8, 9]

Sample Output

python
[1, 4, 9, 25, 36, 64, 81]

Solution One

ts
export function sortedSquaredArray(array: number[]) {
	return array.map(num => num ** 2).sort((a, b) => a - b);
}

Solution Two

ts
// array is pre-sorted
export function sortedSquaredArray(array: number[]) {
	const sortedArray: number[] = [];
	let leftPointer = 0;
	let rightPointer = array.length - 1;

	array.reduceRight((_acc, _currentValue, index) => {
		const leftValue = array[leftPointer];
		const rightValue = array[rightPointer];

		const leftSquared = leftValue ** 2;
		const rightSquared = rightValue ** 2;

		if (leftSquared > rightSquared) {
			sortedArray[index] = leftSquared;
			leftPointer += 1;
			return 0;
		}
		sortedArray[index] = rightSquared;
		rightPointer -= 1;
		return 0;
	}, 0);
	return sortedArray;
}

Explanation

consider the following input

ts
array = [ -10, 1, 2, 3, 9 ]

for each iteration,
we want to compare the left and right values

ts
// leftPointer = 0;
// rightPointer = 4;
// leftValue = -10;
// rightValue = 9;
// leftSquared = 100;
// rightSquared = 81;

// 100 > 81,
// 100 is added to the sortedArray
// leftPointer is incremented
// rightPointer is stays the same

sortedArray = [ <4 empty items>, 100 ]
ts
// leftPointer = 1;
// rightPointer = 4;
// leftValue = 1;
// rightValue = 9;
// leftSquared = 1;
// rightSquared = 81;

// 1 < 81,
// 81 is added to the sortedArray
// leftPointer is stays the same
// rightPointer is decremented

sortedArray = [ <3 empty items>, 81, 100 ]
ts
sortedArray = [ <2 empty items>, 9, 81, 100 ]
ts
sortedArray = [ <1 empty items>, 4, 9, 81, 100 ]
ts
sortedArray = [ 1, 4, 9, 81, 100 ]

Is Valid Subsequence

Given two non-empty arrays of integers, write a function that determines whether the second array is a subsequence of the first one.

A subsequence of an array is a set of numbers that aren't necessarily adjacent in the array but that are in the same order as they appear in the array. For instance, the numbers [1, 3, 4] form a subsequence of the array [1, 2, 3, 4], and so do the numbers [2, 4]. Note that a single number in an array and the array itself are both valid subsequences of the array.

Sample Input

python
array = [5, 1, 22, 25, 6, -1, 8, 10]
sequence = [1, 6, -1, 10]

Sample Output

python
true

Solution One

Time Complexity O(2n)
Space Complexity O(n)

typescript
type ArrayHashMap = { [el: number]: [number] };

export function isValidSubsequence(array: number[], sequence: number[]) {
	// O(n) S
	const arrayHashMap: ArrayHashMap = {};

	// O(n) T
	array.forEach((el, index) => {
		if (!arrayHashMap[el]) return (arrayHashMap[el] = [index]);
		arrayHashMap[el].push(index);
	});

	let lastSequenceIndex = 0;

	// O(n) T
	return sequence.every(el => {
		const [arrayIndex] = arrayHashMap?.[el] ?? [];
		if (typeof arrayIndex === 'number')
			if (arrayIndex >= lastSequenceIndex) {
				lastSequenceIndex = arrayIndex;
				arrayHashMap[el].shift();
				return true;
			}
		return false;
	});
}

Solution Two

Time Complexity O(n)
Space Complexity O(1)

typescript
export function isValidSubsequence(array: number[], sequence: number[]) {
	// reverse for loop over array
	// O(n) T
	for (let i = array.length; i >= 0; i -= 1) {
		// O(1) S
		// O(1) T
		if (array[i] === sequence[sequence.length - 1]) sequence.pop(); // O(1) S
	}
	return sequence.length === 0;
}

Monotonic Array

Write a function that takes in an array of integers and returns a boolean representing whether the array is monotonic.

An array is said to be monotonic if its elements, from left to right, are entirely non-increasing or entirely non-decreasing.

Non-increasing elements aren't necessarily exclusively decreasing; they simply don't increase. Similarly, non-decreasing elements aren't necessarily exclusively increasing; they simply don't decrease.

Note that empty arrays and arrays of one element are monotonic.

Sample Input

text
array = [-1, -5, -10, -1100, -1100, -1101, -1102, -9001]

Sample Output

text
true
go
package main

import "fmt"

func IsMonotonic(array []int) bool {
	isIncreasing := true
	isDecreasing := true
	for index := 0; index < len(array); index += 1 {

		el := array[index]

		nextEl, nextElErr := func() (int, error) {
			if index < len(array)-1 {
				return array[index+1], nil
			}
			return 0, fmt.Errorf("Index out of range")
		}()

		if nextElErr != nil {
			break
		}

    if (isIncreasing == false && isDecreasing == false) {
      break
    }

    if nextEl == el {
      continue
    }

		// increasing
		if nextEl > el {
			isDecreasing = false
			continue
		}

		// decreasing
		isIncreasing = false
	}
	return isIncreasing || isDecreasing
}

func main() {
	println("Hello World")
	println(IsMonotonic([]int{1, 1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 8, 9, 10, 11}))
}

Sorted Squared Array

There's an algorithms tournament taking place in which teams of programmers compete against each other to solve algorithmic problems as fast as possible. Teams compete in a round robin, where each team faces off against all other

teams. Only two teams compete against each other at a time, and for each competition, one team is designated the home team, while the other team is the away team. In each competition there's always one winner and one loser; there are no ties. A team receives 3 points if it wins and 0 points if it loses. The winner of the tournament is the team that receives the most amount of points.

Given an array of pairs representing the teams that have competed against each other and an array containing the results of each competition, write a function that returns the winner of the tournament. The input arrays are named competitions and results, respectively. The competitions array has elements in the form of [homeTeam, awayTeam], where each team is a string of at most 30 characters representing the name of the team. The results array contains information about the winner of each corresponding competition in the competitions array. Specifically, results[i] denotes the winner of competitions[i], where a 1 in the results array means that the home team in the corresponding competition won and a 0 means that the away team won.

It's guaranteed that exactly one team will win the tournament and that each team will compete against all other teams exactly once. It's also guaranteed that the tournament will always have at least two teams.

Sample Input

javascript
competitions = [
  ["HTML", "C#"],
  ["C#", "Python"],
  ["Python", "HTML"],
]
results = [0, 0, 1]

Sample Output

javascript
"Python" // C# beats HTML, Python Beats C#, and Python Beats HTML.
// HTML - 0 points
// C# - 3 points
// Python - 6 points

Move Elements To End

You're given an array of integers and an integer. Write a function that moves all instances of that integer in the array to the end of the array and returns the array.

The function should perform this in place (i.e., it should mutate the input array) and doesn't need to maintain the order of the other integers.

Sample Input

text
array = [2, 1, 2, 2, 2, 3, 4, 2]
toMove = 2

Sample Output

text
[1, 3, 4, 2, 2, 2, 2, 2] // the numbers 1, 3, and 4 could be ordered differently

Solution

typescript
export function moveElementToEnd(
	array: number[],
	toMove: number,
	leftPointer: number = 0,
	rightPointer: number = array.length - 1
): number[] {
	const elAtLeftPointer = array[leftPointer];
	const elAtRightPointer = array[rightPointer];
	const isElAtLeftPointerEqToMove = elAtLeftPointer === toMove;
	const isElAtRightPointerEqToMove = elAtRightPointer === toMove;

	if (leftPointer >= rightPointer) return array;

	// if both left pointer and right pointer has the desired value
	if (isElAtLeftPointerEqToMove && isElAtRightPointerEqToMove) {
		// move left pointer value to right
		array.splice(leftPointer, 1);
		array.push(elAtLeftPointer);
		// since last element is already in a desired position move right pointer inward
		return moveElementToEnd(array, toMove, leftPointer, rightPointer - 1);
	}

	// swap el positions,
	// if right has non desired value
	// if left has desired value
	if (isElAtLeftPointerEqToMove && !isElAtRightPointerEqToMove) {
		array.splice(leftPointer, 1, elAtRightPointer);
		array.splice(rightPointer, 1, elAtLeftPointer);
		// since now the left pointer will not have the desired value move it right, by one place
		// since now the right pointer will have the desired value move it left, by one place
		return moveElementToEnd(array, toMove, leftPointer + 1, rightPointer - 1);
	}

	// if left is not having desired value move on to next value
	if (!isElAtRightPointerEqToMove && isElAtRightPointerEqToMove) {
		// since right is already at desired value move left
		return moveElementToEnd(array, toMove, leftPointer + 1, rightPointer - 1);
	}

	return moveElementToEnd(array, toMove, leftPointer + 1, rightPointer);
}
typescript
export function moveElementToEnd(array: number[], toMove: number): number[] {
	let prevToMoveElPoss = 0;
	array.forEach((currentEl, currentIndex) => {
		if (currentEl !== toMove) {
			// avoids 0, 0
			if (prevToMoveElPoss != currentIndex) {
				// swapping
				array[prevToMoveElPoss] = array[currentIndex];
				array[currentIndex] = toMove;
			}
			prevToMoveElPoss += 1;
		}
	});
	return array;
}

Smallest Difference

Write a function that takes in two non-empty arrays of integers, finds the pair of numbers (one from each array) whose absolute difference is closest to zero, and returns an array containing these two numbers, with the number from the first array in the first position.

Note that the absolute difference of two integers is the distance between them on the real number line. For example, the absolute difference of -5 and 5 is 10, and the absolute difference of -5 and -4 is 1.

You can assume that there will only be one pair of numbers with the smallest difference.

Sample Input:

python
arrayOne = [-1, 5, 10, 20, 28, 3]
arrayTwo = [26, 134, 135, 15, 17]

Sample Output:

python
[28, 26]
typescript
type Pair = [number, number] | [];
const assendingSorter = (a: number, b: number) => a - b;

export const smallestDifference = (
  arrayOne: number[],
  arrayTwo: number[],
  smallestPairDiscovered: Pair = [],
  arrayOnePointer: number = 0,
  arrayTwoPointer: number = 0,
  isArraysSorted = false
): Pair => {
  const arrayOneSorted = isArraysSorted
    ? arrayOne
    : arrayOne.sort(assendingSorter);
  const arrayTwoSorted = isArraysSorted
    ? arrayTwo
    : arrayTwo.sort(assendingSorter);

  const arrayOneMaxIndex = arrayOneSorted.length - 1;
  const arrayTwoMaxIndex = arrayTwoSorted.length - 1;

  const isArrayOnePointerExhausted = arrayOnePointer > arrayOneMaxIndex;
  const isArrayTwoPointerExhausted = arrayTwoPointer > arrayTwoMaxIndex;

  // A: [ -01, 03, 05, 010, 020, 28 ]
  //                             X
  // B: [ 15, 17, 26, 134, 135,     ]
  //              X
  // There is further no possible solutions,
  // since the smallest possible pair is already found.
  if (isArrayOnePointerExhausted || isArrayTwoPointerExhausted)
    return smallestPairDiscovered;

  const currentPair: Pair = [
    arrayOne[isArrayOnePointerExhausted ? arrayOneMaxIndex : arrayOnePointer],
    arrayTwo[isArrayTwoPointerExhausted ? arrayTwoMaxIndex : arrayTwoPointer],
  ];

  const [firstValue, lastValue] = currentPair;

  const currentPairDiff: number = Math.abs(lastValue - firstValue);

  if (currentPairDiff === 0) return currentPair;

  const isSmallestPairDiscoveredValid = smallestPairDiscovered.length === 2;
  const [smallestPairFirstValue = 0, smallestPairLastValue = 0] =
    isSmallestPairDiscoveredValid ? smallestPairDiscovered : [];
  const smallestPairDiscoveredDiff = Math.abs(
    smallestPairLastValue - smallestPairFirstValue
  );

  const newPair =
    isSmallestPairDiscoveredValid &&
      smallestPairDiscoveredDiff < currentPairDiff
      ? smallestPairDiscovered
      : currentPair;

  if (firstValue > lastValue)
    return smallestDifference(
      arrayOne,
      arrayTwo,
      newPair,
      arrayOnePointer,
      arrayTwoPointer + 1
    );

  return smallestDifference(
    arrayOne,
    arrayTwo,
    newPair,
    arrayOnePointer + 1,
    arrayTwoPointer
  );
};

console.log(smallestDifference([-1, 5, 10, 20, 28, 3], [26, 134, 135, 15, 17]));

Three Number Sum

Write a function that takes in a non-empty array of distinct integers and an integer representing a target sum. The function should find all triplets in the array that sum up to the target sum and return a two-dimensional array of all these triplets. The numbers in each triplet should be ordered in ascending order, and the triplets themselves should be ordered in ascending order with respect to the numbers they hold. If no three numbers sum up to the target sum, the function should return an empty array.

if no three numbers sum up to the target sum, the function should return an empty array.

Sample Input:

python
array = [12, 3, 1, 2, -6, 5, -8, 6]
python
[[ -8, 2, 6 ], [ -8, 3, 5 ], [ -6, 1, 5 ]]
typescript
type Triplet = [number, number, number];
type SumPair = [number, number];

export function threeNumberSum(array: number[], targetSum: number): Triplet[] {
	// O(N)
	const sumCombos: { [T: string]: SumPair[] } = {};
	// O(N)
	for (const num of array) {
		// O(N^2)
		for (const subNum of array) {
			if (num !== subNum) {
				const sum = num + subNum;
				const existingKey = sumCombos[sum];
				const newPair: SumPair = [num, subNum];
				if (existingKey) {
					sumCombos[sum] = [...existingKey, newPair];
					continue;
				}
				sumCombos[sum] = [newPair];
			}
		}
	}

	// O(N)
	const tripletsAsObj: { [T: string]: Triplet } = {};
	// O(N)
	const subComboKeys = Object.keys(sumCombos).map(el => +el);
	// O(N)
	for (const num of array) {
		// O(N^2)
		for (const sum of subComboKeys) {
			const sumAsNum = +sum;
			if (num + sumAsNum === targetSum) {
				const pairs = sumCombos[sum];
				// O(N^3)
				for (const pair of pairs) {
					if (!pair.includes(num)) {
						const triplet = [...pair, num].sort((a, b) => a - b) as Triplet;
						tripletsAsObj[triplet.join(',')] = triplet;
					}
				}
			}
		}
	}
	// O(N)
	return (
		Object.values(tripletsAsObj)
			// O(N)
			.sort((a, b) => a[2] - b[2])
			// O(N)
			.sort((a, b) => a[1] - b[1])
			// O(N)
			.sort((a, b) => a[0] - b[0])
	);
}
typescript
type Triplet = [number, number, number];

export const threeNumberSum = (
	array: number[],
	targetAmount: number,
	pairsFound: Triplet[] = []
): any => {
	// Sort in place,
	// O(n) time, O(1) space
	const sortedArray = array.sort((a, b) => a - b);

	const sortedArrayLength = sortedArray.length;
	const sortedArrayMaxIndex = sortedArrayLength - 1;

	// O(n)
	for (
		let fixedPointer = 0;
		fixedPointer <= sortedArrayMaxIndex;
		fixedPointer += 1
	) {
		const fixedPointerValue = sortedArray[fixedPointer];
		let leftPointer = fixedPointer + 1;
		let rightPointer = sortedArrayMaxIndex;

		if (leftPointer > sortedArrayMaxIndex) break;
		if (rightPointer < 0) break;

		// O(n^2)
		for (let index = 0; index <= sortedArrayMaxIndex; index += 1) {
			const leftPointerValue = sortedArray[leftPointer];
			const rightPointerValue = sortedArray[rightPointer];

			const currentSum =
				fixedPointerValue + leftPointerValue + rightPointerValue;

			// move to next pointer iteration
			if (leftPointerValue >= rightPointerValue) break;

			if (currentSum < targetAmount) {
				leftPointer += 1;
				continue;
			}

			if (currentSum > targetAmount) {
				rightPointer -= 1;
				continue;
			}

			// if triplet is found
			if (currentSum === targetAmount) {
				pairsFound.push([
					fixedPointerValue,
					leftPointerValue,
					rightPointerValue,
				]);
				// if triplet is found,
				// close the gap between left and right pointer
				leftPointer += 1;
				rightPointer -= 1;
				continue;
			}

			// break if all conditions fail
			break;
		}
	}
	return pairsFound;
};

console.log(threeNumberSum([12, 3, 1, 2, -6, 5, -8, 6], 0));

Four Number Sum

Write a function that takes in a non-empty array of distinct integers and an integer representing a target sum. The function should find all quadruplets in the array that sum up to the target sum and return a two-dimensional array of all these quadruplets in no particular order.

if no four numbers sum up to the target sum, the function should return an empty array.

Sample Input:

python
array = [7, 6, 4, -1, 1, 2]
targetSum = 16

Sample Output:

python
[[7, 6, 4, -1], [7, 6, 1, 2]]
typescript
type Quadruplet = [number, number, number, number];

export const fourNumberSum = (
  array: number[],
  targetAmount: number,
  pairsFound: Quadruplet[] = []
): any => {
  // Sort in place,
  // O(n) time, O(1) space
  const sortedArray = array.sort((a, b) => a - b);

  const sortedArrayLength = sortedArray.length;
  const sortedArrayMaxIndex = sortedArrayLength - 1;

  // O(n)
  for (
    let fixedPointerRight = sortedArrayMaxIndex;
    fixedPointerRight > 0;
    fixedPointerRight -= 1
  ) {
    const fixedPointerRightValue = sortedArray[fixedPointerRight];
    // O(n^2)
    for (
      let fixedPointerLeft = 0;
      fixedPointerLeft <= sortedArrayMaxIndex;
      fixedPointerLeft += 1
    ) {
      const fixedPointerLeftValue = sortedArray[fixedPointerLeft];
      let leftPointer = fixedPointerLeft + 1;
      let rightPointer = fixedPointerRight - 1;

      if (leftPointer > sortedArrayMaxIndex) break;
      if (rightPointer < 0) break;

      // O(n^3)
      for (let index = 0; index <= sortedArrayMaxIndex; index += 1) {
        const leftPointerValue = sortedArray[leftPointer];
        const rightPointerValue = sortedArray[rightPointer];

        const currentSum =
          fixedPointerLeftValue +
          leftPointerValue +
          rightPointerValue +
          fixedPointerRightValue;

        // move to next pointer iteration
        if (leftPointerValue >= rightPointerValue) break;

        if (currentSum < targetAmount) {
          leftPointer += 1;
          continue;
        }

        if (currentSum > targetAmount) {
          rightPointer -= 1;
          continue;
        }

        // if triplet is found
        if (currentSum === targetAmount) {
          pairsFound.push([
            fixedPointerLeftValue,
            leftPointerValue,
            rightPointerValue,
            fixedPointerRightValue,
          ]);
          // if triplet is found,
          // close the gap between left and right pointer
          leftPointer += 1;
          rightPointer -= 1;
          continue;
        }

        // break if all conditions fail
        break;
      }
    }
  }
  return pairsFound;
};

typescript
export const firstDuplicateValue = (array: number[]) => {

  // O(N) space complexity,
  // worst case scenario, we have to store all elements in the array
  // O(1) time complexity
  const seen = new Set();
  // O(N) time complexity, to iterate through array
  for (const el of array) {
    // O(1) time complexity
    if (seen.has(el)) return el;
    // O(1) time complexity
    seen.add(el);
  }
	return -1;
};
typescript
//          0  1  2  3  4  5  6
// input = [2, 1, 5, 2, 3, 3, 4]
export const firstDuplicateValue = (array: number[]) => {
  // O(N) time complexity
  for (let index = 0; index < array.length; index++) {
    const element = array[index];
    const elementAbs = Math.abs(element);
    // we know that the element is always a number b/w 1 and n
    // we know that each element will have a unique index position
    // trying to establish a relationship between the element and the index
    const diff = elementAbs - 1;
    // we know that there will always be an index position for, 
    // el - 1 since el is always b/w 1 and n
    const elementAtDiff = array[diff];
    if (elementAtDiff < 0) return elementAbs;
    array[diff] = elementAtDiff * -1;
  }
  return -1;
};

Longest Substring Without Duplication

ts
interface LastFoundAtIdx {
	[key: string]: number;
}

export function longestSubstringWithoutDuplication(string: string) {
	const lastFoundAt: LastFoundAtIdx = {};

	let startIdxMaxLen = 0;
	let endIdxMaxLen = 0;

	let startIdx = 0;

	for (let idx = 0; idx < string.length; idx++) {
		const char = string[idx];
		const lastFoundIdx = lastFoundAt[char];
		const isLastFoundIdxValid =
			lastFoundIdx !== undefined && lastFoundIdx >= startIdx;
		const longestLength = endIdxMaxLen - startIdxMaxLen;

		let currentLength = idx - startIdx;

		if (isLastFoundIdxValid) {
			startIdx = lastFoundIdx + 1;
			currentLength = idx - 1 - startIdx;
		}

		if (currentLength > longestLength) {
			startIdxMaxLen = startIdx;
			endIdxMaxLen = idx;
		}

		lastFoundAt[char] = idx;
	}

	return string.substring(startIdxMaxLen, endIdxMaxLen + 1);
}

console.log(longestSubstringWithoutDuplication('clementisacap'));
console.log(longestSubstringWithoutDuplication('abc'));
console.log(longestSubstringWithoutDuplication('abcdef'));
  • O(N) ST

  • Keep track of the last found index of each character.

  • If the character is found again, move the startIdx to the last found index + 1.

txt
{c: 0, l: 1, e: 2, m: 3 }
cleme
   ^ move startIdx to the last found index + 1

longest was clem
current is e
           ementisac

ementisaca
        ^ move startIdx to the last found index + 1

longest is changed to ementisac
current is c
           cap
  • We need to update MaxLen's even if the character is not found again.


Generate documents

ts
type Map = { [T: string]: number };

export function generateDocument(characters: string, document: string) {
	const characterMap: Map = {};
	for (const char of characters) {
		if (typeof characterMap[char] === 'number') {
			characterMap[char] = characterMap[char] + 1;
			continue;
		}
		characterMap[char] = 1;
	}
	for (const char of document) {
		if ((characterMap[char] ?? 0) - 1 < 0) return false;
		characterMap[char] = characterMap[char] - 1;
	}
	return true;
}

semordnilap

ts
export function semordnilap(words: any[]) {
	for (let index = 0; index < words.length; index++) {
		const word = words[index];
		if (word === undefined) {
			continue;
		}
		const reversedWord = word.split('').reverse().join('');
		const indexOfReversedWord = words.indexOf(reversedWord);
		if (indexOfReversedWord > -1 && indexOfReversedWord !== index) {
			words[index] = [word, reversedWord];
			words.splice(indexOfReversedWord, 1, undefined);
			continue;
		}
		words.splice(index, 1, undefined);
	}
	return words.filter(word => word !== undefined);
}

Common characters

ts
export function commonCharacters(strings: string[]) {
	let string = [...new Set(strings[0].split('')).values()];
	// o(n)
	for (let index = 1; index < strings.length; index++) {
		// o(m)
		for (let charIdx = 0; charIdx < string.length; charIdx++) {
			const char = string[charIdx];
			const indexOfChar = strings[index].indexOf(char);
			if (indexOfChar === -1) {
				string.splice(charIdx, 1);
			}
		}
	}
	return string;
}

Input

txt
["abc", "bcd", "cbaccd"]

Output

txt
["b", "c"]

Anagrams

ts
export function groupAnagrams(words: string[]) {
	const anagrams: {
		[key: string]: string[];
	} = {};
	// O(N)
	for (let index = 0; index < words.length; index++) {
		const el = words[index];
		// O(KlogK)
		const sortedEl = el.split('').sort().join('');
		if (anagrams[sortedEl]) {
			anagrams[sortedEl].push(el);
			continue;
		}
		anagrams[sortedEl] = [el];
	}
	return Object.values(anagrams);
}

Reverse words in string

ts
export const reverseWordsInString = (str: string): string => {
	if (str.trim().length === 0) return str;
	const words: string[] = [];
	for (let index = str.length - 1; index >= 0; index -= 1) {
		const nextChar = str[index - 1];
		if (nextChar === ' ') {
			const word = str.slice(index);
			str = str.slice(0, index);
			words.push(`${word}${words.length ? '' : ' '}`);
			continue;
		}
		if (index === 0) {
			words.push(str.trim());
			continue;
		}
	}
	return words.join('');
};

IP address

Sample Input string = "1921680" Sample Output [ "1.9.216.80", "1.92.16.80", "1.92.168.0", "19.2.16.80", "19.2.168.0", "19.21.6.80", "19.21.68.0", "19.216.8.0", "192.1.6.80", "192.1.68.0", "192.16.8.0" ] // The IP addresses could be ordered differently.

ts
// O(1) ST since input is always fixed length
const isValidPart = (string: string): boolean => {
	if (+string > 255) return false;
	// 0 is valid, but 00, 01, 001, etc. are not
	return string.length === `${+string}`.length;
};

export const validIPAddresses = (string: string): string[] => {
	const ipAddressParts: string[] = [];

	for (let first = 1; first <= 3; first += 1) {
		let parts = ['', '', '', ''];
		parts[0] = string.slice(0, first);
		if (!isValidPart(parts[0])) continue;
		for (let second = first + 1; second <= first + 3; second += 1) {
			parts[1] = string.slice(first, second);
			if (!isValidPart(parts[1])) continue;
			for (let third = second + 1; third <= second + 3; third += 1) {
				parts[2] = string.slice(second, third);
				// grab the rest of the string
				parts[3] = string.slice(third);
				if (!isValidPart(parts[2])) continue;
				if (!isValidPart(parts[3])) continue;
				ipAddressParts.push(parts.join('.'));
			}
		}
	}

	return ipAddressParts;
};

Underscorify Substring

Write a function that takes in two strings: a main string and a potential substring of the main string. The function should return a version of the main string with every instance of the substring in it wrapped between underscores.

if two or more instances of the substring in the main string overlap each other or sit side by side, the underscores relevant to these substrings should only appear on the far left of the leftmost substring and on the far right of the rightmost substring. If the main string does not contain the other string at all, the function should return the main string intact.

Sample Input:

python
string = "testthis is a testtest to see if testestest it works"
substring = "test"
python
"_test_this is a _testtest_ to see if _testestest_ it works"
typescript

Linked Lists

No title


Remove duplicates from linked list

ts
export function removeDuplicatesFromLinkedList(
	linkedList: LinkedList,
	current: LinkedList = linkedList,
	nextNode: LinkedList | null = current.next
): LinkedList {
	if (nextNode === null) return linkedList;
	if (current.value === nextNode.value) {
		current.next = nextNode.next;
		return removeDuplicatesFromLinkedList(linkedList, current, current.next);
	}
	return removeDuplicatesFromLinkedList(linkedList, nextNode, nextNode.next);
}

Find Middle Node

mermaid
flowchart TD
    array_1[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    array_2[1, 3, 5, 7, 9]
  • Iterating through the array, with a pointer that moves 2x faster than the other pointer.

  • The faster pointer will reach the end of the array when the slower pointer is at the middle of the array.

ts
export class LinkedList {
	value: number;
	next: LinkedList | null;

	constructor(value: number) {
		this.value = value;
		this.next = null;
	}
}

function* traverseLinkedList(
	linkedList: LinkedList | null,
	is2X = false
): Generator<LinkedList> {
	if (!linkedList) return;
	yield linkedList;
	yield* traverseLinkedList(
		is2X
			? linkedList.next
				? linkedList.next.next
				: linkedList
			: linkedList.next,
		is2X
	);
}

export function middleNode(
	linkedList: LinkedList,
	iterator = traverseLinkedList(linkedList),
	fastIterator = traverseLinkedList(linkedList, true)
): LinkedList {
	const item = iterator.next();
	const fastItem = fastIterator.next();

	if (fastItem.done || !fastItem?.value?.next) {
		return item.value;
	}

	return middleNode(linkedList, iterator, fastIterator);
}

Sum of linked lists

text
Requirement

2 -> 4 -> 7 -> 1        1742
9 -> 4 -> 5              549
----------------        ----
1 -> 9 -> 2 -> 2        2291

Answer
     1    0
2 -> 4 -> 7 -> 1
9 -> 4 -> 5
----------------
1 -> 9 -> 2 -> 2

2 + 9 = 11;
11 / 10 = 1;
11 % 10 = 1;

4 + 4 + 1 = 9;

7 + 5 + 0 = 13;
ts
// This is an input class. Do not edit.
export class LinkedList {
	value: number;
	next: LinkedList | null;

	constructor(value: number) {
		this.value = value;
		this.next = null;
	}
}

function* traverseLinkedList(
	node: LinkedList | null | undefined
): Generator<number> {
	if (!node) return;
	yield node.value;
	yield* traverseLinkedList(node.next);
}

export function sumOfLinkedLists(
	headOne: LinkedList | null | undefined,
	headTwo: LinkedList | null | undefined,
	newLinkedList: LinkedList = new LinkedList(0),
	headNew: LinkedList | null = newLinkedList,
	iteratorOne = traverseLinkedList(headOne),
	iteratorTwo = traverseLinkedList(headTwo),
	carry = 0
): LinkedList {
	const { value: first = 0 } = iteratorOne.next();
	const { value: second = 0 } = iteratorTwo.next();

	if (!headNew) {
		return newLinkedList;
	}

	const sum = first + second;
	const newCarry = sum > 9 ? +(sum / 10).toPrecision(1) : 0;
	const value = (sum % 10) + carry;

	headNew.value = value;

	const isNextLinkRequired = newCarry > 0 || headOne?.next || headTwo?.next;

	if (isNextLinkRequired) {
		headNew.next = new LinkedList(0);
	}

	return sumOfLinkedLists(
		headOne?.next,
		headTwo?.next,
		newLinkedList,
		isNextLinkRequired ? headNew.next : null,
		iteratorOne,
		iteratorTwo,
		newCarry
	);
}

Pitfalls

  • carry = sum > 9 ? +(sum / 10).toPrecision(1) : 0;, need to grab integer part of the number.

  • value = (sum % 10) + carry;, need to add carry to the sum, reminder.


Merging Linked Lists

You're given two Linked Lists of potentially unequal length. These Linked Lists potentially merge at a shared intersection node. Write a function that returns the intersection node or returns None / null if there is no intersection.

Each LinkedList node has an integer value as well as a next node pointing to the next node in the list or to None / null if it's the tail of the list.

Note: Your function should return an existing node. It should not modify either Linked List, and it should not create any new Linked Lists.

Sample Input

txt
linkedListOne = 2 -> 3 -> 1 -> 4
linkedListTwo = 8 -> 7 -> 1 -> 4
mermaid
flowchart LR
    A[2] --> B[3] --> C[1] --> D[4]
    E[8] --> F[7] --> C

Sample Output

txt
1 -> 4 // The lists intersect at the node with value 1
golang
package main

import "fmt"

// This is an input struct. Do not edit.
type LinkedList struct {
	Value int
	Next  *LinkedList
}

func findMerge(head1, head2, node1 *LinkedList, node2 *LinkedList) *LinkedList {
    // if addr no match
	if &node1.Value == &node2.Value {
		return node1
	}
    // if no next values, return nil
	if node1.Next == nil && node2.Next == nil {
		return nil
	}
    // if end reached
	if node1.Next == nil {
		return findMerge(head1, head2, head2, node2.Next)
	}
    // if end reached
	if node2.Next == nil {
		return findMerge(head1, head2, node1.Next, head1)
	}
    // nedt iteration
	return findMerge(head1, head2, node1.Next, node2.Next)
}

func MergingLinkedLists(linkedListOne *LinkedList, linkedListTwo *LinkedList) *LinkedList {
	returnVal := findMerge(linkedListOne, linkedListTwo, linkedListOne, linkedListTwo)
	fmt.Printf("fmt %v\n", returnVal)
	return returnVal
}

func main() {
	l1 := &LinkedList{Value: 1}
	l1.Next = &LinkedList{Value: 2}
	l1.Next.Next = &LinkedList{Value: 3}
	l1.Next.Next.Next = &LinkedList{Value: 4}

	l2 := &LinkedList{Value: 5}
	l2.Next = l1.Next.Next

	MergingLinkedLists(l1, l2)
}

Next Greater Element

txt
Input = [2, 5, -3, -4, 6, 7, 2]
Output = [5, 6, 6, 6, 7, -1, 5]
  • Find the next greater element in the array.

  • If there is no greater element, return -1.

ts
export const nextGreaterElement = (array: number[]): number[] => {
	// resultArray: store the next greater element at each index
	// stack: Stores index of elements as we iterate through the array (only for the first loop)

	const resultArray: number[] = new Array(array.length).fill(-1);
	const stack: number[] = [];

	for (let index = 0; index < array.length * 2; index += 1) {
		const currentIdx = index % array.length; // 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6
		const currentEl = array[currentIdx];

		// if the element at the top of the stack is less than the current element
		// then the corresponding index in the resultArray will be the current element
		while (
			stack.length > 0 &&
			resultArray[stack[stack.length - 1]] < currentEl
		) {
			const idx = stack.pop() as number; // remove idx from the stack
			resultArray[idx] = currentEl; // updated the resultArray[idx] with the current element
		}

		if (index >= array.length) continue;
		stack.push(currentIdx);
	}

	return resultArray;
};

nextGreaterElement([2, 5, -3, -4, 6, 7, 2]);

The above time complexity is O(2n) for the outer loop + O(n) for the inner loop (we only add and pop elements from the stack once).


Find the winner of the circular game

ts
interface EliminatedPlayers {
	[T: number]: true;
}

const findNextPossiblePlayer = (
	noOfPlayers: number,
	currentPlayer: number,
	eliminatedPlayers: EliminatedPlayers,
	traversedPlayers: number = 0
): number => {
	if (currentPlayer in eliminatedPlayers) {
		return findNextPossiblePlayer(
			noOfPlayers,
			(currentPlayer + 1) % noOfPlayers,
			eliminatedPlayers,
			traversedPlayers + 1
		);
	}
	return currentPlayer;
};

const findTheWinner = (
	noOfPlayers: number,
	playerCount: number,
	eliminatedPlayers: EliminatedPlayers = {},
	eliminatedPlayersCount: number = 0,
	currentPlayer = 0
): number => {
	if (eliminatedPlayersCount === noOfPlayers - 1) {
		return (
			findNextPossiblePlayer(noOfPlayers, currentPlayer, eliminatedPlayers) + 1
		);
	}
	const playersVisited = [currentPlayer];
	let nextPlayer: number;
	while (playersVisited.length <= playerCount) {
		nextPlayer = findNextPossiblePlayer(
			noOfPlayers,
			(playersVisited[playersVisited.length - 1] + 1) % noOfPlayers,
			eliminatedPlayers
		);
		playersVisited.push(nextPlayer);
	}
	const eliminatedPlayer = playersVisited[playerCount - 1];
	eliminatedPlayers[eliminatedPlayer] = true;
	return findTheWinner(
		noOfPlayers,
		playerCount,
		eliminatedPlayers,
		eliminatedPlayersCount + 1,
		nextPlayer
	);
};
ts
const findTheWinner = (noOfPlayers: number, playerCount: number): number => {
	let lastPlayer = 0;
	for (let player = 1; player <= noOfPlayers; player++) {
		lastPlayer = (lastPlayer + playerCount) % player;
		console.log({ player, lastPlayer });
	}
	return lastPlayer + 1;
};

console.log(findTheWinner(5, 2)); // 3
  • Hot potato problem,

    txt
    last_count = (last_count + no_to_skip) % idx

Reverse Polish notation

ts
export function reversePolishNotation(tokens: string[]) {
	const stack: string[] = [];
	for (const item of tokens) {
		stack.push(item);
		const [a, b, operator] = stack.slice(-3);
		if (a && b && operator && !+operator) {
			stack.pop();
			stack.pop();
			stack.pop();
			let value = eval(`${a} ${operator} ${b}`);
			if (operator === '/') {
				const [int, _] = value.toString().split('.');
				value = int;
			}
			stack.push(value);
			continue;
		}
	}
	return +stack[0];
}

console.log(reversePolishNotation(['50', '3', '17', '+', '2', '-', '/']));

Best digits

ts
const removeFromStack = (
	stack: string[],
	numDigits = 0,
	numRemoved = 0,
	idx: number = stack.length - 1
): number => {
	if (idx === 0) {
		return numRemoved;
	}
	if (numRemoved === numDigits) {
		return numRemoved;
	}
	const digit = stack[idx];
	const prevDigit = stack[idx - 1];
	if (prevDigit <= digit) {
		// remove previous digit if previous digit is smaller
		stack.splice(idx - 1, 1);
		numRemoved++;
		return removeFromStack(stack, numDigits, numRemoved, idx - 1);
	}
	return numRemoved;
};

export function bestDigits(number: string, numDigits: number) {
	const stack: string[] = [];
	let numRemoved = 0;

	for (let idx = 0; idx < number.length; idx++) {
		const digit = number[idx];
		stack.push(digit);
		numRemoved = removeFromStack(stack, numDigits, numRemoved);
	}

	// if not enough digits are removed, remove from the end
	if (numRemoved !== numDigits) {
		stack.splice(
			stack.length - (numDigits - numRemoved),
			numDigits - numRemoved
		);
	}

	return stack.join('');
}

Closing brackets

ts
export const balancedBrackets = (string: string) => {
	const openingBrackets = '{[(';
	const closingBrackets = '}])';
	const stack: string[] = [];

	for (const char of string) {
		console.table({ char, stack });
		if (openingBrackets.includes(char)) {
			stack.push(char);
			continue;
		}
		if (closingBrackets.includes(char) === false) continue;
		if (stack.length === 0) return false;
		const lastElement = stack[stack.length - 1];
		const closingBracketIndex = closingBrackets.indexOf(char);
		const openingBracketIndex = openingBrackets.indexOf(lastElement);
		if (closingBracketIndex === openingBracketIndex) {
			stack.pop();
			continue;
		}
		return false;
	}

	return stack.length === 0;
};

Minmax stack

ts
export class MinMaxStack {
	stack: number[] = [];
	minMaxStack: { min: number; max: number }[] = [];

	// o(1)
	peek() {
		return this.stack[this.stack.length - 1];
	}

	// o(1)
	pop() {
		this.minMaxStack.pop();
		// if an element is popped from the stack,
		// the previous minMax version is restored
		return this.stack.pop();
	}

	// o(1) push to end
	push(number: number) {
		const newMinMax = { min: number, max: number };
		if (this.minMaxStack.length > 0) {
			// A new version of the minMax object is created and pushed to the minMaxStack
			const latestMinMax = this.minMaxStack[this.minMaxStack.length - 1];
			newMinMax.min = Math.min(latestMinMax.min, number);
			newMinMax.max = Math.max(latestMinMax.max, number);
		}
		this.minMaxStack.push(newMinMax);
		this.stack.push(number);
	}

	// o(1)
	getMin() {
		return this.minMaxStack[this.minMaxStack.length - 1].min;
	}

	// o(1)
	getMax() {
		return this.minMaxStack[this.minMaxStack.length - 1].max;
	}
}

Pitfalls

  • versioning the minMax object


Sort stack

mermaid
flowchart TD
    subgraph construct_stack
        7[-5] --> |2| 8[-5, 2] --> |-2| 9[-5,poping last value] --> |-2| 10[-5, -2] --> |pushing| 11[-5, -2, 2] --> |4| 12[-5, -2, 2, 4] -->
        |3| 13[Pop -5, -2, 2,] --> |3| 14[-5, -2, 2, 3] --> |pushing| 15[-5, -2, 2, 3, 4] --> |1| 16[Pop -5, 2, -2, 3, 4] --> |1| 17[...]
    end
    subgraph sort_stack
        array[-5, 2, -2, 4, 3, 1] --> 1[-5, 2, -2, 4, 3] --> 2[-5, 2, -2, 4] --> 3[-5, 2, -2] --> 4[-5, 2] --> 5[-5] --> 6[_]
    end
ts
const constructStack = (stack: number[], current: number) => {
	// if stack is empty or,
	// the element to be inserted is greater than the top element of stack
	if (stack.length === 0 || stack[stack.length - 1] <= current) {
		stack.push(current);
		return stack;
	}
	// remove last element that is smaller than prev element
	// ..., remove  until stack is empty or the element to be inserted is greater than the top element of stack
	const top = stack.pop() as number;
	constructStack(stack, current);
	// push back the removed element's
	stack.push(top);
	// return stack
	return stack;
};

// called first
export function sortStack(stack: number[]) {
	if (stack.length === 0) {
		// if empty stack, continue to construct stack
		return stack;
	}
	// emptying stack until empty
	const topEl = stack.pop() as number;
	sortStack(stack);
	// construct stack in reverse order
	// first element of stack
	return constructStack(stack, topEl);
}

Lowest Common Manager

ts
// This is an input class. Do not edit.
class OrgChart {
	name: string;
	directReports: OrgChart[];

	constructor(name: string) {
		this.name = name;
		this.directReports = [];
	}
}

const findReports = (
	topManager: OrgChart,
	reportOne: OrgChart,
	reportTwo: OrgChart
): { count: number; manager: OrgChart | null } => {
	let count = 0;
	if (topManager === reportOne || topManager === reportTwo) {
		count++;
	}
	for (let report of topManager.directReports) {
		const { count: newCount, manager } = findReports(
			report,
			reportOne,
			reportTwo
		);
		if (manager !== null) {
			return { count, manager };
		}
		count += newCount;
		if (count >= 2) {
			return { count, manager: topManager };
		}
	}
	return { count, manager: null };
};

// Helper function to find the lowest common manager in the subtree.
// Main function to get the lowest common manager.
export function getLowestCommonManager(
	topManager: OrgChart,
	reportOne: OrgChart,
	reportTwo: OrgChart
): OrgChart {
	const result = findReports(topManager, reportOne, reportTwo);
	console.log('result', result);
	return result.manager!;
}

Stratergy

txt
        A
      B   C
    D E < F G
  H I <
  • Time complexity is O(N) and space complexity is O(d) where d is the depth of the tree


Evaluate expression tree

ts
export class BinaryTree {
	value: number;
	left: BinaryTree | null;
	right: BinaryTree | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

function* depthFirstSearch(tree: BinaryTree): Generator<number, void, void> {
	const { value, left, right } = tree;
	if (left) {
		yield* depthFirstSearch(left);
	}
	if (right) {
		yield* depthFirstSearch(right);
	}
	if (!value) return;
	yield value;
}

export function evaluateExpressionTree(tree: BinaryTree) {
	const stack = [];
	for (const value of depthFirstSearch(tree)) {
		stack.push(value);
		const [left = 0, right, operator] = stack.slice(-3);
		if (operator < 0) {
			stack.pop();
			stack.pop();
			stack.pop();
			switch (operator) {
				case -1:
					stack.push(left + right);
					break;
				case -2:
					stack.push(left - right);
					break;
				case -3:
					const [int = '', dec = ''] = `${left / right}`.split('.');
					stack.push(+int);
					break;
				case -4:
					stack.push(left * right);
					break;
			}

			continue;
		}
	}
	return stack.length === 1 ? stack[0] : 0;
}

const tree = new BinaryTree(-2);
tree.left = new BinaryTree(2);
tree.right = new BinaryTree(3);
tree.right.left = new BinaryTree(5);
tree.right.right = new BinaryTree(1);

console.log(evaluateExpressionTree(tree));

Find closest value in BST

txt
tree =   10
       /     \
      5      15
    /   \   /   \
   2     5 13   22
 /           \
1            14

target = 12
Sample Output = 13
ts
// O(log(n)) time | O(1) space

class BST {
	value: number;
	left: BST | null;
	right: BST | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

function* traverseTree({
	node = null,
	target = Infinity,
	closesetDiff = Infinity,
}: {
	node: BST | null;
	target: number;
	closesetDiff?: number;
}): Generator<number, void> {
	if (!node) return;

	const { value, left = null, right = null } = node;

	const isLeftValid = !!left;
	const isRightValid = !!right;

	if (!isLeftValid && !isRightValid) return;

	const leftDiff = isLeftValid ? Math.abs(target - left.value) : Infinity;
	const rightDiff = isRightValid ? Math.abs(target - right.value) : Infinity;

	const isLeftCloser = leftDiff < rightDiff;
	const isRightCloser = rightDiff < leftDiff;

	const isDiffCloser = isLeftCloser
		? leftDiff < closesetDiff
		: isRightCloser
		  ? rightDiff < closesetDiff
		  : false;

	if (!isDiffCloser) {
		yield value;
		return;
	}

	if (isLeftCloser) {
		yield* traverseTree({
			node: left,
			target,
			closesetDiff: leftDiff,
		});
		return;
	}

	yield* traverseTree({
		node: right,
		target,
		closesetDiff: rightDiff,
	});
	return;
}

export function findClosestValueInBst(node: BST, target: number) {
	const generator = traverseTree({ node, target });
	// generator.next(); Not needed
	return generator.next().value; // the first yield is the closest value
}

Pitfalls

  • yield value when no more nodes to traverse.

  • yield* traverseTree when there are more nodes to traverse.

  • Since we are not yielding anything in the beginning, we don't need to call generator.next().


BST Construction

BST vs BT,

  • BST and BT has two or less children

  • BST has left < root >= right BST should not have duplicate values (optional)

  • BT has no such constraint

ts
export class BST {
	value: number;
	left: BST | null;
	right: BST | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}

	insert(value: number, node: BST = this): BST {
		if (value < node.value && node.left) return this.insert(value, node.left);
		if (value >= node.value && node.right)
			return this.insert(value, node.right);
		if (value < node.value) node.left = new BST(value);
		if (value >= node.value) node.right = new BST(value);
		return this;
	}

	contains(value: number, node: BST = this): boolean {
		if (value === node.value) return true;
		if (value < node.value && node.left) return this.contains(value, node.left);
		if (value >= node.value && node.right)
			return this.contains(value, node.right);
		return false;
	}

	findLeftMostRightNode(
		previousNode: BST,
		node: BST | null = previousNode.right
			? previousNode.right
			: previousNode.left,
		wentLeft: boolean = false
	): number {
		const direction = wentLeft ? 'left' : 'right';
		if (!node) return previousNode.value;
		if (node.left) return this.findLeftMostRightNode(node, node.left, true);
		if (node.right) return this.findLeftMostRightNode(node, node.right, false);
		const value = node.value;
		previousNode[direction] = null;
		return value;
	}

	remove(
		value: number,
		previousNode: BST = this,
		node: BST | null = previousNode,
		wentLeft: boolean = !!previousNode?.left,
		isRoot = true
	): BST {
		const direction = wentLeft ? 'left' : 'right';
		if (!node) return this;
		if (value === node?.value) {
			if (!node.left && !node.right) {
				previousNode[direction] = null;
				return this;
			}
			if (!node.right || !node.left) {
				if (isRoot) {
					if (!node.right) {
						this.value = node?.left?.value ?? this.value;
						this.right = node?.left?.right ?? null;
						this.left = node?.left?.left ?? null;
						return this;
					}
					this.value = node?.right?.value ?? this.value;
					this.right = node?.right?.right ?? null;
					this.left = node?.right?.left ?? null;
					return this;
				}
				previousNode[direction] = node.right ? node.right : node.left;
				return this;
			}
			node.value = this.findLeftMostRightNode(node);
			return this;
		}
		if (value < node.value && node?.left) {
			return this.remove(value, node, node.left, true, false);
		}
		if (value >= node.value && node?.right) {
			return this.remove(value, node, node.right, false, false);
		}
		return this;
	}
}
txt
// Assume the following BST has already been created:
         10
       /     \
      5      15
    /   \   /   \
   2     5 13   22
 /           \
1            14

// All operations below are performed sequentially.
insert(12):   10
            /     \
           5      15
         /   \   /   \
        2     5 13   22
      /        /  \
     1        12  14

remove(10):   12
            /     \
           5      15
         /   \   /   \
        2     5 13   22
      /           \
     1            14

contains(15): true
  • The remove operation is tricky, since we need to find the left most right node (12 in this case),

  • move it to the node to be removed (10 in this case) and then remove the left most right node (12 in this case).

  • left < root >= right is the key to BST.

  • if value < node.value then go left

  • if value >= node.value then go right


Reconstruct BST

ts
export class BST {
	value: number;
	left: BST | null;
	right: BST | null;

	constructor(
		value: number,
		left: BST | null = null,
		right: BST | null = null
	) {
		this.value = value;
		this.left = left;
		this.right = right;
	}
}

// O(N)
export const reconstructBst = (
	preOrderTraversalValues: number[],
	values: number[] = preOrderTraversalValues.slice(1),
	tree: BST = new BST(preOrderTraversalValues[0])
): BST => {
	if (values.length === 0) return tree;

	// O(N^2)
	const rightIdx = values.findIndex(val => val >= tree.value);
	const left = rightIdx > -1 ? values.slice(0, rightIdx) : values;

	const right = rightIdx > -1 ? values.slice(rightIdx) : [];

	if (left.length > 1) {
		tree.left = reconstructBst(left, left.slice(1), new BST(left[0]));
	} else if (left.length === 1) {
		tree.left = new BST(left[0]);
	}

	if (right.length > 1) {
		tree.right = reconstructBst(right, right.slice(1), new BST(right[0]));
	} else if (right.length === 1) {
		tree.right = new BST(right[0]);
	}

	return tree;
};

console.log(
	JSON.stringify(reconstructBst([10, 4, 2, 1, 5, 17, 19, 18]), null, 4)
);
txt
[<10>, 4, 2, 1, 5] [17, 19, 18]
[<4>, 2, 1] [5]
[<2>, 1] []
[<1>] []
[<5>] []
[<17>] [19, 18]
[<19>] [18]
[<18>] []
  • split the array into left and right, based on the root value.

  • value < root go left

  • value >= root go right

  • the root becomes the node


Split binary tree

mermaid
graph TB;
    1((1))
    9((9))
    5((5))
    2((2))
    3((3))
    102((102))

    1 --> 9
    1 --> 20
    5 --> 102
    9 --> 5 --> l((=))
    9 --> 2
    2 --> 3
    2 --> s((=))

    20((20)) --> 30((30)) --> t((=))
    10((10))
    35((35))
    25((25))

    20 --> 10
    10 --> 35
    10 --> 25

*ignore ='s

ts
// Time O(n) | Space O(h)
// where h is the height of the tree
export class BinaryTree {
	value: number;
	left: BinaryTree | null;
	right: BinaryTree | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

const calculateTreeSum = (node: BinaryTree | null): number => {
	if (!node) return 0;
	return (
		node.value + calculateTreeSum(node.left) + calculateTreeSum(node.right)
	);
};

const findSplitPoint = (
	tree: BinaryTree | null,
	desiredSum: number = 0,
	canBeSplit: boolean = false
): { sum: number; canBeSplit: boolean } => {
	if (!tree) return { sum: 0, canBeSplit: false };
	if (canBeSplit) return { sum: desiredSum, canBeSplit: true };
	const { left, right, value } = tree || {};

	// goes to the left most root node
	const { sum: leftSum, canBeSplit: leftCanBeSplit } = findSplitPoint(
		left,
		desiredSum,
		canBeSplit
	);

	// goes to the right most root node
	const { sum: rightSum, canBeSplit: rightCanBeSplit } = findSplitPoint(
		right,
		desiredSum,
		canBeSplit
	);

	const currentSum = value + leftSum + rightSum;

	return {
		sum: currentSum,
		// when traversing back up the tree,
		// if the current sum is equal to the desired sum, then we know that the tree can be split
		canBeSplit: leftCanBeSplit || rightCanBeSplit || currentSum === desiredSum,
	};
};

export function splitBinaryTree(tree: BinaryTree) {
	const treeSum = calculateTreeSum(tree);
	if (treeSum % 2 !== 0) return 0;
	const desiredSum = treeSum / 2;
	const { canBeSplit } = findSplitPoint(tree, desiredSum);
	if (!canBeSplit) return 0;
	return desiredSum;
}

Pitfalls

  • Need to traverse from bottom to top.

  • At each node, check if the node and its children can break off from the tree.

  • if can be split with node, if we can split left node, if we can split right node, return desired sum.


BST traversal

txt
tree =   10
       /     \
      5      15
    /   \       \
   2     5       22
 /
1
ts
class BST {
	value: number;
	left: BST | null;
	right: BST | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

// [10, 5, 2, 1, 5, 15, 22]
function* preOrder(node: BST | null): Generator<number> {
	if (node === null) return;

	// yield current node top
	yield node.value;
	// run the same function recursively on the left node
	// yielding from top to bottom
	yield* preOrder(node.left);
	// then right node
	yield* preOrder(node.right);
}

// [1, 2, 5, 5, 10, 15, 22]
function* inOrder(node: BST | null): Generator<number> {
	if (node === null) return;
	// go to the left most leaf node
	yield* inOrder(node.left);
	// yield from bottom to top, root node
	yield node.value;
	// then right node (bottom to top)
	yield* inOrder(node.right);
}

// [1, 2, 5, 5, 22, 15, 10]
function* postOrder(node: BST | null): Generator<number> {
	if (node === null) return;
	// from bottom to top left
	yield* postOrder(node.left);
	// from bottom to top right
	yield* postOrder(node.right);
	// root node
	yield node.value;
}

export function inOrderTraverse(tree: BST | null, array: number[]) {
	for (const next of inOrder(tree)) {
		array.push(next);
	}
	return array;
}

export function preOrderTraverse(tree: BST | null, array: number[]) {
	for (const next of preOrder(tree)) {
		array.push(next);
	}
	return array;
}

export function postOrderTraverse(tree: BST | null, array: number[]) {
	for (const next of postOrder(tree)) {
		array.push(next);
	}
	return array;
}

Branch Sums

Write a function that takes in a Binary Tree and returns a list of its branch sums ordered from leftmost branch sum to rightmost branch sum.

A branch sum is the sum of all values in a Binary Tree branch. A Binary Tree branch is a path of nodes in a tree that starts at the root node and ends at any leaf node.

Each BinaryTree node has an integer value, a left child node, and a right child node. Children nodes can either be BinaryTree nodes themselves or None / null.

Sample Input

text
tree =     1
        /     \
       2       3
     /   \    /  \
    4     5  6    7
  /   \  /
 8    9 10

Sample Output

text
[15, 16, 18, 10, 11]
// 15 == 1 + 2 + 4 + 8
// 16 == 1 + 2 + 4 + 9
// 18 == 1 + 2 + 5 + 10
// 10 == 1 + 3 + 6
// 11 == 1 + 3 + 7
go
// *Type,
// tells the compiler that the value is a pointer to the "type"
func GeneratePath(root *BinaryTree, path []int, sums *[]int) *[]int {
	value := root.Value
	left := root.Left
	right := root.Right

	if left == nil && right == nil {
		// uppend to global sums
		sum := 0
		for _, value := range path {
			sum += value
		}
    // updating the value of the pointer
		*sums = append(*sums, sum+value)
		// we return the pointer after operations
		return sums
	}

	if left != nil {
		GeneratePath(left, append(path, value), sums)
	}

	if right != nil {
		GeneratePath(right, append(path, value), sums)
	}

	return sums
}

func BranchSums(root *BinaryTree) []int {
	// & value creates a pointer,
	// since the array needs to be global,
	// ie, each recursive call needs to append to the same array
	return *GeneratePath(root, []int{}, &[]int{})
}

Node Depths

The distance between a node in a Binary Tree and the tree's root is called the node's depth. Write a function that takes in a Binary Tree and returns the sum of its nodes' depths. Each Binary Tree node has an integer value, a left child node, and a right child node. Children nodes can either be Binary Tree nodes themselves or None / null.

Sample Input

text
tree =    1
        /   \
       2     3
      /  \    \
     4    5    6
            / \
           7   8

Sample Output

text
16
typescript
interface Node {
	value: number;
	left?: Node | null;
	right?: Node | null;
}

let total = 0;
const calculateNodeDepths = (
	currentNodeObject: Node | null = { value: 0 },
	currentDepth = 0
): number => {
	if (!currentNodeObject) return currentDepth;
	// both left and right needs to be called
	[currentNodeObject?.left, currentNodeObject?.right].forEach(el => {
		if (el?.value) {
			if (currentNodeObject?.value) {
				// map current child to parent
				total += currentDepth + 1;
			}
		}
		// call child of current left and right node.
		calculateNodeDepths(el, currentDepth + 1);
	});
	return total;
};

export function nodeDepths(root: BinaryTree) {
	return calculateNodeDepths(root);
}

// This is the class of the input binary tree.
class BinaryTree {
	value: number;
	left: BinaryTree | null;
	right: BinaryTree | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

const root = new BinaryTree(1);
root.left = new BinaryTree(2);
root.left.left = new BinaryTree(4);
root.left.left.left = new BinaryTree(8);
root.left.left.right = new BinaryTree(9);
root.left.right = new BinaryTree(5);
root.right = new BinaryTree(3);
root.right.left = new BinaryTree(6);
root.right.right = new BinaryTree(7);
const actual = nodeDepths(root);
console.log(actual, 'Actual');

Time Complexity: O(n) Space Complexity: O(h) where h is the height of the tree

The reference to the memory address is removed from the call-stack when the function returns. At every level the function invocation is added back to the call-stack with reference to the memory location again.


Depth-first search

You're given a Node class that has a name and an array of optional children nodes. When put together, nodes form an acyclic tree-like structure.

Implement the depthFirstSearch method on the Node class, which takes in an empty array, traverses the tree using the Depth-first Search approach (specifically navigating the tree from left to right), stores all of the nodes' names in the input array, and returns it.

if you're unfamiliar with Depth-first Search, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input: graph = A / |
B C D / \ /
E F G H / \
I J K

Sample Output: ["A", "B", "E", "F", "I", "J", "C", "D", "G", "K", "H"]

typescript
// Do not edit the class below except
// for the depthFirstSearch method.
// Feel free to add new properties
// and methods to the class.
export class Node {
  name: string;
  children: Node[];

  constructor(name: string) {
    this.name = name;
    this.children = [];
  }

  addChild(name: string) {
    this.children.push(new Node(name));
    return this;
  }

  depthFirstSearch(
    array: string[] = [],
    node: { name: string; children: Node[] } = {
      name: this.name,
      children: this.children,
    }
  ) {
    array.push(node.name);
    node.children.forEach(el => this.depthFirstSearch(array, el));
    return array;
  }
}

const graph = new Node('A');
graph.addChild('B').addChild('C').addChild('D');
graph.children[0].addChild('E').addChild('F');
graph.children[2].addChild('G').addChild('H');
graph.children[0].children[1].addChild('I').addChild('J');
graph.children[2].children[0].addChild('K');

console.log(graph.depthFirstSearch());

Validate BST

Write a function that takes in a potentially invalid Binary Search Tree (BST) and returns a boolean representing whether the BST is valid.

Each BST node has an integer value, a left child node, and a right child node. A node is said to be a valid BST node if and only if it satisfies the BST property: its value is strictly greater than the values of every node to its left; its value is less than or equal to the values of every node to its right; and its children nodes are either valid BST nodes themselves or None / null.

A BST is valid if and only if all of its nodes are valid BST nodes.

Sample Input

text
tree =   10
       /     \
      5      15
    /   \   /   \
   2     5 13   22
 /           \
1            14

Sample Output

text
true
ts
class BST {
	value: number;
	left: BST | null;
	right: BST | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

function* generateQueue(
	node: BST,
	leftLimit: number = -Infinity,
	rightLimit: number = Infinity
): Generator<{ value: number; isValid: boolean }> {
	const { value, left, right } = node;

	console.table({ value, leftLimit, rightLimit });

	if (value < leftLimit || value >= rightLimit) {
		yield { value, isValid: false };
		return;
	}

	yield { value, isValid: true };

	if (left?.value) yield* generateQueue(left, leftLimit, value);

	if (right?.value) yield* generateQueue(right, value, rightLimit);
}

export function validateBst(tree: BST) {
	for (const isValid of generateQueue(tree)) {
		console.log(isValid);
		if (!isValid.isValid) return isValid.isValid;
	}
	return true;
}

const root = new BST(10);
root.left = new BST(5);
root.left.left = new BST(2);
root.left.left.left = new BST(1);
root.left.right = new BST(5);
root.right = new BST(15);
root.right.left = new BST(13);
root.right.left.right = new BST(14);
root.right.right = new BST(22);
console.log(validateBst(root));

Split Binary Tree

Write a function that takes in a Binary Tree with at least one node and checks if that Binary Tree can be split into two Binary Trees of equal sum by removing a single edge. If this split is possible, return the new sum of each Binary Tree, otherwise return 0. Note that you do not need to return the edge that was removed.

The sum of a Binary Tree is the sum of all values in that Binary Tree.

Each BinaryTree node has an integer value, a left child node, and a right child node. Children nodes can either be BinaryTree nodes themselves or None / null.

Sample Input

text
tree =     1
        /     \
       3       -2
     /   \    /  \
    6    -5  5    2
  /
 2

Sample Output 6 // Remove the edge to the left of the root node, // creating two trees, each with sums of 6


Find Nodes Distance K

You're given the root node of a Binary Tree, a target value of a node that's contained in the tree, and a positive integer k. Write a function that returns the values of all the nodes that are exactly distance k from the node with target value.

The distance between two nodes is defined as the number of edges that must be traversed to go from one node to the other. For example, the distance between a node and its immediate left or right child is 1. The same holds for the distance between a node and its parent.

In a tree of three nodes where the root node has a left and right child, the left and right children are distance 2 from each other.

Each Binary Tree node has an integer value, a left child node, and a right child node. Children nodes can either be Binary Tree nodes themselves or None / null.

Note that all Binary Tree node values will be unique, and your function can return the output values in any order.

Sample Input

text
tree = 1
     /   \
    2     3
   /  \    \
  4    5    6
           / \
          7   8
target = 3
k = 2

Sample Output

text
[2, 7, 8] // These values could be ordered differently.
// 2, 7, and 8 are at distance 2 from the target node of value 3.
// Note: distance 0 is the node with the target value.
typescript
// This is an input class. Do not edit.
export class BinaryTree {
	value: number;
	left: BinaryTree | null;
	right: BinaryTree | null;

	constructor(value: number) {
		this.value = value;
		this.left = null;
		this.right = null;
	}
}

interface Node {
	value: number;
	left?: Node | null;
	right?: Node | null;
}

const parentChildNodes: { [node: string]: Node } = {};

let targetNodeObject: Node = { value: 0 };

const mapParentNodes = (
	currentNodeObject: Node | null = { value: 0 },
	targetNodeValue?: number,
	currentDistance = 0,
	distanceK?: number
) => {
	if (!currentNodeObject) return;
	if (currentNodeObject?.value === targetNodeValue) {
		targetNodeObject = currentNodeObject;
	}
	// both left and right needs to be called
	[currentNodeObject?.left, currentNodeObject?.right].forEach(el => {
		if (el?.value) {
			if (currentNodeObject?.value) {
				// map current child to parent
				parentChildNodes[el?.value] = currentNodeObject;
			}
		}
		// call child of current left and right node.
		mapParentNodes(el, targetNodeValue, currentDistance, distanceK);
	});
	return [parentChildNodes, targetNodeObject];
};

// Searches around node for nodes at distance k
// T - target node (assume, node numbering is not in accordance with problem )
//    ----------------
//    | 2 hop zone 3 |
//    | ---------/-  |
//    | | 1 hop 2 |  |
//    | |     /   |  |
//    | |    1 <-T|  |
//    | |  /   \  |  |
//    | | 0     4 |  |
//    | -/-\---/-\-  |
//    | 5   6 7   8  |
//    ---------------

// grab all the nodes within the target node's k distance

let seen = new Set();
const depthFirstSearch = (
	currentDepth: number = 1,
	targetDepth: number = 0,
	nodesFound: Node[] = []
): number[] => {
	// exit conditions
	const newNodesFound: Node[] = nodesFound.reduce(
		(prevNodeList: Node[], currEl: Node): Node[] => {
			const { value = 0, right = null, left = null } = currEl;
			if (!value) return [];
			if (seen.has(value)) return [];
			seen.add(value);

			const parentNode = parentChildNodes[value];
			const rightNode = right;
			const leftNode = left;

			const newNodes: Node[] = [];

			if (parentNode && !seen.has(parentNode?.value)) newNodes.push(parentNode);
			if (rightNode && !seen.has(rightNode?.value)) newNodes.push(rightNode);
			if (leftNode && !seen.has(leftNode?.value)) newNodes.push(leftNode);

			return prevNodeList.concat(newNodes);
		},
		[]
	);
	if (currentDepth < targetDepth)
		return depthFirstSearch(currentDepth + 1, targetDepth, newNodesFound);

	return newNodesFound.map(el => el.value);
};

export function findNodesDistanceK(
	tree: BinaryTree,
	targetNodeValue: number,
	distanceK: number
) {
	mapParentNodes(tree, targetNodeValue);
	return depthFirstSearch(1, distanceK, [targetNodeObject]);
}

Many ways to traverse a graph

txt
Input = 4, 3
Output = 10 ways to traverse the graph
ts
export function numberOfWaysToTraverseGraph(
	width: number,
	height: number
): number {
	if (height === 1 || width === 1) return 1;
	return (
		numberOfWaysToTraverseGraph(width - 1, height) +
		numberOfWaysToTraverseGraph(width, height - 1)
	);
}

many_ways_to_traverse

  • At each point in the graph, we have two options: go right or go down.

  • At most each traversal will not exceed the width + height.

  • The time complexity is O(2^(n+m)) where n is the width and m is the height. (exponential time complexity, because we double the number of recursive calls at each step).

  • Space complexity is O(n+m) where n is the width and m is the height. (max depth of the call stack will be n+m).


Optimized solution

ts
export function numberOfWaysToTraverseGraph(
	width: number,
	height: number
): number {
	// O(n*m) ST
	const resultArray = new Array(height)
		.fill(0)
		.map(() => new Array(width).fill(0));

	// O(n) T
	for (let row = 0; row < height; row++) {
		// O(m) T
		for (let col = 0; col < width; col++) {
			// edges of grid
			if (row === 0 || col === 0) {
				resultArray[row][col] = 1;
				continue;
			}
			resultArray[row][col] =
				resultArray[row - 1][col] + resultArray[row][col - 1];
			continue;
		}
	}

	return resultArray[height - 1][width - 1];
}
  • Traverse from top to bottom and left to right.

  • The left and top edges of the grid will always be 1.

  • The rest will be the sum of the top and left element.

  • The end of the grid will be the result.

many_ways_to_traverse

  • Time complexity is O(n*m) where n is the width and m is the height.

  • Space complexity is O(n*m) where n is the width and m is the height.


Has single cycle ( GRAPH )

ts
export function hasSingleCycle(array: number[]) {
	let visitedCount = 0;
	let fistIdxVisited = 0;
	let idx = 0;
	while (visitedCount < array.length) {
		if (idx === 0) fistIdxVisited += 1;
		if (fistIdxVisited > 1) break; // you are going in circles

		const jumpTo = array[idx];
		let jumpToIdx = (idx + jumpTo) % array.length;
		if (jumpToIdx < 0) {
			jumpToIdx = array.length + jumpToIdx;
		}
		idx = jumpToIdx;
		visitedCount += 1;
	}

	return visitedCount === array.length && idx === 0;
}
txt
[2, 3, 1, -4, -4,  2] --- jump to values
 0, 1, 2,  3,  4,  5  --- index
0 -> 2 -> 3 -> 5 -> 1 -> 4 -> 0  --- no of jumps to reach start idx again === 6,
                                     you where already at 0th idx in the beginning, so ignore the first jump

Considering an array length of 5 (0 is first idx, 4 is the last idx)

txt
[2, 3, 1, -4, -4] --- jump to values
 0, 1, 2,  3,  4  --- index

```ts
10 % 5 = 0 // returns the first idx
6 % 5 = 1
5 % 5 = 1
-22 % 5 = -2 // the value is right, but we need to normalise it to 3 ( send idx from last )
             // array.length + -2 = 5 + -2 = 3

There is a possibility that you are going in mini cycles.

txt
[1, -1, 1, -1] -- the same cycle is played multiple times

Airport connections

airport_connections

ts
class Airport {
	name: string;
	destinations: Airport[];

	constructor(name: string) {
		this.name = name;
		this.destinations = [];
	}

	addDestination(airport: Airport) {
		this.destinations.push(airport);
	}
}

interface Graph {
	[key: string]: Airport;
}

const getReachableAirports = (
	graph: Graph,
	originatingAirport: string,
	airport: string = originatingAirport,
	reachableAirports: Set<string> = new Set()
): Set<string> => {
	const airportNode = graph[airport];
	const destinations = airportNode.destinations;

	for (let destination of destinations) {
		if (reachableAirports.has(destination.name)) continue;
		if (destination.name === originatingAirport) continue;
		reachableAirports.add(destination.name);
		getReachableAirports(
			graph,
			originatingAirport,
			destination.name,
			reachableAirports
		);
	}

	return reachableAirports;
};

const getUnreachableAirports = (graph: Graph, startingAirport: string) => {
	const unreachableAirports: Set<[string, number]> = new Set();
	const reachableAirports = getReachableAirports(graph, startingAirport);
	for (let airport in graph) {
		if (reachableAirports.has(airport) || airport === startingAirport) continue;
		unreachableAirports.add([
			airport,
			getReachableAirports(graph, airport).size,
		]);
	}
	return Array.from(unreachableAirports).sort((a, b) => b[1] - a[1]);
};

export function airportConnections(
	airports: string[],
	routes: [string, string][],
	startingAirport: string
) {
	const graph: Graph = {};

	for (let airport of airports) {
		if (graph[airport] !== undefined) continue;
		graph[airport] = new Airport(airport);
	}

	for (let route of routes) {
		const [source, destination] = route;
		graph[source].addDestination(graph[destination]);
	}

	let unreachableAirports = getUnreachableAirports(graph, startingAirport);
	const destinationsAdded = [];
	while (unreachableAirports.length > 0) {
		console.log(unreachableAirports);
		graph[startingAirport].addDestination(graph[unreachableAirports[0][0]]);
		destinationsAdded.push([startingAirport, unreachableAirports[0][0]]);
		unreachableAirports = getUnreachableAirports(graph, startingAirport);
	}

	console.log(destinationsAdded);

	return destinationsAdded.length;
}

console.log(
	airportConnections(
		[
			'BGI',
			'CDG',
			'DEL',
			'DOH',
			'DSM',
			'EWR',
			'EYW',
			'HND',
			'ICN',
			'JFK',
			'LGA',
			'LHR',
			'ORD',
			'SAN',
			'SFO',
			'SIN',
			'TLV',
			'BUD',
		],
		[
			['DSM', 'ORD'],
			['ORD', 'BGI'],
			['BGI', 'LGA'],
			['SIN', 'CDG'],
			['CDG', 'SIN'],
			['CDG', 'BUD'],
			['DEL', 'DOH'],
			['DEL', 'CDG'],
			['TLV', 'DEL'],
			['EWR', 'HND'],
			['HND', 'ICN'],
			['ICN', 'JFK'],
			['JFK', 'LGA'],
			['EYW', 'LHR'],
			['LHR', 'SFO'],
			['SFO', 'SAN'],
			['SFO', 'DSM'],
			['SAN', 'EYW'],
		],
		'LGA'
	)
);
  • Find all unreachable airports from the starting airport.

  • Add a connection from the starting airport to the most unreachable airport.

  • Find all unreachable airports from the starting airport again.

  • Continue until there are no unreachable airports.


Course Schedule

You are given numCourses and a list of prerequisites, where each element in prerequisites[i] = [a_i, b_i] means you must complete course b_i before taking course a_i. Write a function canFinish that returns True if it's possible to finish all courses, and False if it's not. You need to complete all the courses for the result to be True.

Assume there are no duplicate prerequisites, and numCourses is a non-negative number.

txt
Input: numCourses = 3, prerequisites = [[2, 1], [1, 0]]
Output: True
Explanation: You can complete course 0 first, then course 1, and finally course 2.

Input: numCourses = 4, prerequisites = [[3, 2], [2, 1], [1, 0], [0, 3]]
Output: False
Explanation: The prerequisites form a cycle, making it impossible to finish all courses.

Input: numCourses = 5, prerequisites = [[4, 2], [3, 1], [2, 0]]
Output: True
Explanation: You can complete the courses in order from course 0 to course 4.
ts
interface Graph {
	[Key: number]: number;
}

const checkForDependencyError = (
	graph: Graph,
	checkFor: number,
	before: number
): boolean => {
	const value = graph[before];
	if (value === undefined) return false;
	if (value === checkFor) return true;
	return checkForDependencyError(graph, checkFor, value);
};

function canFinish(numCourses: number, prerequisites: [number, number][]) {
	// your code goes here
	const graph: Graph = {};

	for (let idx = 0; idx < prerequisites.length; idx++) {
		const [after, before] = prerequisites[idx];
		graph[after] = before;
		const isError = checkForDependencyError(graph, after, before);
		if (isError) return false;
	}

	return true;
}

// debug your code below
console.log(
	canFinish(4, [
		[1, 0],
		[2, 1],
		[3, 2],
	])
);
txt
Make a graph of the prerequisites

1 -depends on-> 0
2 -depends on-> 1 -depends on-> 0
3 -depends on-> 2 -depends on-> 1 -depends on-> 0

No cycle in graph, so return true


[[3, 2], [2, 1], [1, 0], [0, 3]]

3 -depends on-> 2 -depends on-> 1 -depends on-> 0 -depends on-> 3

Cycle in graph, so return false

Traverse Matrix

ts
type Range = [number, number];

export function searchInSortedMatrix(
	matrix: number[][],
	target: number,
	rowIdx = 0,
	colIdx = matrix[rowIdx].length - 1
): Range {
	const element = matrix[rowIdx][colIdx];

	if (element === target) return [rowIdx, colIdx];

	if (element > target) {
		console.log('going left');
		return searchInSortedMatrix(matrix, target, rowIdx, colIdx - 1);
	}

	if (element < target) {
		console.log('going down');
		return searchInSortedMatrix(matrix, target, rowIdx + 1, colIdx);
	}

	return [-1, -1];
}

Pitfalls

  • start at top right corner

  • if left is greater than target go left

  • if bottom is lesser than target go down


BFS

txt
Sample Input
graph = A
     /  |  \
    B   C   D
   / \     / \
  E   F   G   H
     / \   \
    I   J   K

Sample Output
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"]
ts
type QueueItem = {
	name: string;
	children: Node[];
};

export class Node {
	name: string;
	children: Node[];
	queue: QueueItem[] = [];

	constructor(name: string) {
		this.name = name;
		this.children = [];
	}

	addChild(name: string): Node {
		this.children.push(new Node(name));
		return this;
	}

	enqueue(node: QueueItem) {
		this.queue.push(node);
	}

	dequeue() {
		return this.queue.shift();
	}

	// access first item in queue
	*iterateChildren(node: QueueItem = this.queue[0]): Generator<string> {
		if (!node) return;
		for (const child of node.children) {
			// put children of item in queue
			this.enqueue(child);
		}
		// dequeue item
		const pop = this.dequeue();
		if (pop) yield pop.name;
		yield* this.iterateChildren();
	}

	breadthFirstSearch(array: string[]): string[] {
		this.enqueue({ name: this.name, children: this.children });
		for (const name of this.iterateChildren()) {
			array.push(name);
		}
		return array;
	}
}

Pitfalls

  • We need a queue to keep track of the children of the current node.


Breadth-first Search

You're given a Node class that has a name and an array of optional children nodes. When put together, nodes form an acyclic tree-like structure.

Implement the breadthFirstSearch method on the Node class, which takes in an empty array, traverses the tree using the Breadth-first Search approach (specifically navigating the tree from left to right), stores all of the nodes' names in the input array, and returns it.

If you're unfamiliar with Breadth-first Search, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input

text
graph = A
     /  |  \
    B   C   D
   / \     / \
  E   F   G   H
     / \   \
    I   J   K

Sample Output

py
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"]
ts
interface BranchNode {
	children: BranchNode[];
	name: string;
}

export class Node {
	name: string;
	children: BranchNode[];
	queue: BranchNode[] = [];

	constructor(name: string) {
		this.name = name;
		this.children = [];
	}

	addChild(name: string): Node {
		this.children.push(new Node(name));
		return this;
	}

	*generateQueue(node: BranchNode = this.queue[0]): Generator<string> {
		if (!node) {
			return;
		}

		const { name, children } = node;

		yield name;

		children.forEach(el => this.queue.push(el));

		this.queue.shift();

		yield* this.generateQueue();
	}

	breadthFirstSearch() {
		this.queue.push({ name: this.name, children: this.children });
		return [...this.generateQueue()];
	}
}

Kth Largest Element

py
import heapq
from typing import List, TypedDict, Tuple

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        return heapq.nlargest(k, nums)[-1]

What is a heap, heap is a maximally efficient implementation of a priority queue. heap is a abstract data type, not a data structure.

A heap is used whenever you need to keep track of something with the highest / lowest priority.

js
.isEmpty(); // O(1)
.insertWithPriority(); // O(log(n))
.pullHighestPriorityElement(); // O(log(n))
.peek();

Types of heaps,

The binary tree used in heaps, is a complete binary tree.

A complete binary tree is a binary tree in which every level of the tree is fully filled, except for perhaps the last level.

  • Min binary heap, (filling from left to right, bottom to top)

    text
              8              // filling from top to bottom
            /   \            // filling from left to right
           /     \
          12     23
         /  \   /  \
        17  31 30   44
       /  \
    102    18
    • Every nodes children must be greater than or equal to the parent node.

    • Finding value of children nodes, 2i + 1 and 2i + 2

    • Finding value of parent, (i - 1) / 2

  • Max binary heap, (filling form left to right, top to bottom)

    text

The peak operation on a heap returns the highest priority element in the heap in O(1) time. The highest priority element in a min-heap is the minimum element. The highest priority element in a max-heap is the maximum element.

Heaps are not sorted, they are only partially ordered. The only ordering guaranteed is that the highest priority element is at the root of the heap.

Heap restoration, When we insert or remove an element from a heap, we need to bubble up or bubble down the element we inserted or removed to restore the heap.

ts
export class MinHeap {
	heap: number[];

	constructornedt iterationarray: number[]) {
		this.heap = this.buildHeap(array);
	}

	private static calculateLastParentIdx(array: number[]) {
		return Math.floor((array.length - 2) / 2);
	}

	private static calculateParentIdx(childIdx: number, array: number[]) {
		return Math.floor((childIdx - 1) / 2);
	}

	private static calculateLeftChildIdx(parentIdx: number, array: number[]) {
		return 2 * parentIdx + 1;
	}

	private static calculateRightChildIdx(parentIdx: number, array: number[]) {
		return 2 * parentIdx + 2;
	}

	// O(n) time | O(1) space
	buildHeap(
		array: number[],
		lastParentIdx = MinHeap.calculateLastParentIdx(array)
	): number[] {
		if (lastParentIdx < 0) return array;
		this.siftDown(lastParentIdx, array);
		return this.buildHeap(array, lastParentIdx - 1);
	}

	private swapValues(
		childIdx: number,
		parentIdx: number,
		array: number[],
		siftDownAfterSwap: boolean = true,
		siftUpAfterSwap: boolean = false
	) {
		[array[childIdx], array[parentIdx]] = [array[parentIdx], array[childIdx]];
		if (siftDownAfterSwap) {
			this.siftDown(childIdx, array);
		}
		if (siftUpAfterSwap) {
			this.siftUp(parentIdx, array);
		}
		return array;
	}

	// O(log(n)) time | O(1) space
	siftDown(parentIdx: number, array: number[]): number[] {
		const arrayMaxIdx = array.length - 1;

		let leftIdx = MinHeap.calculateLeftChildIdx(parentIdx, array);
		const rightIdx = MinHeap.calculateRightChildIdx(parentIdx, array);

		const isLeftIdxValid = leftIdx <= arrayMaxIdx && leftIdx > -1;
		const isRightIdxValid = rightIdx <= arrayMaxIdx && rightIdx > -1;

		const parent = array[parentIdx];
		const left = array[leftIdx];
		const right = array[rightIdx];

		if (!isLeftIdxValid && !isRightIdxValid) return array;

		if (!isLeftIdxValid) {
			if (right < parent) {
				return this.swapValues(rightIdx, parentIdx, array);
			}
			return array;
		}

		if (!isRightIdxValid) {
			if (left < parent) {
				return this.swapValues(leftIdx, parentIdx, array);
			}
			return array;
		}

		if (left < right) {
			if (left < parent) {
				return this.swapValues(leftIdx, parentIdx, array);
			}
		}

		if (right < parent) {
			return this.swapValues(rightIdx, parentIdx, array);
		}

		return array;
	}

	// O(log(n)) time | O(1) space
	siftUp(childIdx: number, array: number[]): number[] {
		// hitting roof / no child found
		if (childIdx <= 0) return array;
		let parentIdx = MinHeap.calculateParentIdx(childIdx, array);
		if (parentIdx < 0) return array;

		if (array[childIdx] < array[parentIdx])
			return this.swapValues(childIdx, parentIdx, array, false, true);

		return array;
	}

	// O(1) time | O(1) space
	peek() {
		return this.heap[0];
	}

	// O(log(n)) time | O(1) space
	remove() {
		this.swapValues(this.heap.length - 1, 0, this.heap, false);
		const removedValue = this.heap.pop();
		this.siftDown(0, this.heap);
		return removedValue;
	}

	// O(log(n)) time | O(1) space
	insert(value: number) {
		this.heap.push(value);
		this.siftUp(this.heap.length - 1, this.heap);
	}
}

Dynamic Programming

No title


No of ways to make change

ts
export const numberOfWaysToMakeChange = (
	targetAmount: number,
	denoms: number[]
) => {
	// (noOfWays) ST
	const noOfWays = new Array(targetAmount + 1).fill(0);
	noOfWays[0] = 1;

	// O(denom) T
	denoms.forEach(denom => {
		// O(denom * noOfWays) T
		console.log({ denom, noOfWays });
		noOfWays.forEach((amount, amountIdx) => {
			if (amountIdx === 0) return;
			if (denom <= amountIdx) {
				console.log({
					amount,
					denom,
					prev: noOfWays[amountIdx - denom],
				});
				// amountIdx, no of ways to make change in current coin
				// amountIdx - denom, This is the amount we would have left after using one coin of the current denomination (denom).
				// The number of ways to make change for amountIdx - denom tells us how many ways we could form this smaller amount.
				noOfWays[amountIdx] += noOfWays[amountIdx - denom];
			}
		});
	});

	return noOfWays[noOfWays.length - 1];
};

numberOfWaysToMakeChange(10, [1, 5, 10, 25]);
  • The noOfWays array tracks the number of ways to make change for the given amount. [0 coins, 1 coin, 2 coins, 3 coins, 4 coins, 5 coins, 6 coins, 7 coins, 8 coins, 9 coins, 10 coins]

  • The first coin is always 1, because there is exactly 1 way to make change for 0 amount ( by using no coins).

    txt
               [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    noOfWays = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
  • In first pass with denominator 1, we can make 1 way to make change for each amount. ( 1, 1, 1 + 1, 1 + 1 + 1, ...)

    txt
               [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    noOfWays = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
  • In the second pass with denominator 5, There are two ways to make change for amounts greater than or equal to 5

    txt
               [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    noOfWays = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3 ]
                               ^ 1 * 5
                               ^ just 5 cent coin
                                              ^
                                              ^ 1 * 10
                                              ^ 5 cent coin + 1 * 5
                                              ^ 5 + 5
  • The last noOfWay will be the total number of ways to make change for the target amount.

Caveats

txt
noOfWays[amountIdx] += noOfWays[amountIdx - denom];

noOfWays[amountIdx], tells us how many ways we could form this amount using the previous combination of denominations.
noOfWays[amountIdx - denom], tells us how many ways we could form this amount when using one coin of the current denomination

Min number of coins to make change

ts
export const minNumberOfCoinsForChange = (
	targetAmount: number,
	denoms: number[]
) => {
	const minNoOfCoins = new Array(targetAmount + 1).fill(Infinity);
	minNoOfCoins[0] = 0;

	denoms.forEach(denom => {
		console.log({ denom, minNoOfCoins });
		minNoOfCoins.forEach((noOfCoins, noOfCoinsIdx) => {
			if (noOfCoinsIdx === 0) return;
			if (denom <= noOfCoinsIdx) {
				console.log({
					noOfCoins,
					prev: noOfCoins - denom,
					prevValue: minNoOfCoins[noOfCoinsIdx - denom] + 1,
				});
				minNoOfCoins[noOfCoinsIdx] = Math.min(
					noOfCoins,
					minNoOfCoins[noOfCoinsIdx - denom] + 1
				);
				console.log('final', minNoOfCoins[noOfCoinsIdx]);
			}
		});
	});

	return minNoOfCoins[targetAmount] !== Infinity
		? minNoOfCoins[targetAmount]
		: -1;
};

minNumberOfCoinsForChange(7, [1, 5, 10]);
  • Same as the previous example, but instead of adding the number of ways, we are adding the minimum number of coins.

Caveats

txt
// minNoOfCoins[noOfCoinsIdx],
tells us the minimum number of coins we could form this amount using the previous combination of denominations.

// minNoOfCoins[noOfCoinsIdx - denom]
gives us the number of coins needed to make the remaining amount after subtracting the current coin denomination.

// + 1, when we use one coin of the current denomination, we add 1 to the number of coins.

minNoOfCoins[noOfCoinsIdx] = Math.min(
    noOfCoins,
    minNoOfCoins[noOfCoinsIdx - denom] + 1
);

Longest increasing subsequence

ts
const buildSequence = (
	array: number[],
	maxSumSeq: number[],
	maxSumSeqIdx: null | number,
	maxSeq: number[] = []
): number[] => {
	if (maxSumSeqIdx === null) return maxSeq;
	const valueAtMaxSumSeqIdx = array[maxSumSeqIdx];
	const prevSeq = maxSumSeq[maxSumSeqIdx];

	return buildSequence(array, maxSumSeq, prevSeq, [
		valueAtMaxSumSeqIdx,
		...maxSeq,
	]);
};

export const maxSumIncreasingSubsequence = (
	array: number[]
): [number, number[]] => {
	const maxSum = [...array];
	const maxSumSeq = new Array(array.length).fill(null);
	let maxSumSeqIdx = 0;

	for (let fixedIdx = 0; fixedIdx < array.length; fixedIdx++) {
		const fixed = array[fixedIdx];
		for (let floatingIdx = 0; floatingIdx < fixedIdx; floatingIdx++) {
			const floating = array[floatingIdx];
			const canBePlacedBelowFixed = floating < fixed;
			if (!canBePlacedBelowFixed) continue;

			const maxSumAtFixed = maxSum[fixedIdx];
			const maxSumAtFloating = maxSum[floatingIdx];
			const newPossibleMaxSum = maxSumAtFloating + fixed; // new possible max sum = max sum at floating + fixed, including the current element in the sequence
			const isNewPossibleSumGreater = newPossibleMaxSum > maxSumAtFixed;

			if (!isNewPossibleSumGreater) continue;

			maxSum[fixedIdx] = newPossibleMaxSum;
			maxSumSeq[fixedIdx] = floatingIdx;
		}
		const currentMaxSum = maxSum[maxSumSeqIdx];
		const maxSumAtFixed = maxSum[fixedIdx];
		const hasMaxSumIncreased = maxSumAtFixed > currentMaxSum;
		if (!hasMaxSumIncreased) continue;
		maxSumSeqIdx = fixedIdx;
	}

	return [maxSum[maxSumSeqIdx], buildSequence(array, maxSumSeq, maxSumSeqIdx)];
};

console.log(maxSumIncreasingSubsequence([10, 70, 20, 30, 50, 11, 30])); // [110, [10, 20, 30, 50]]

Max subset sum no adjacent

ts
export function maxSubsetSumNoAdjacent(array: number[], sum = 0) {
	if (array.length === 0) return sum;

	const [first = 0, second = 0, third = 0, fourth = 0, fifth = 0] = array;

	const firstOne = first + third;
	const firstTwo = first + fourth;
	const secondOne = second + fourth;
	const secondTwo = second + fifth;

	if (firstOne > firstTwo && firstOne > secondOne && firstOne > secondTwo) {
		return maxSubsetSumNoAdjacent(array.slice(2), sum + firstOne); // O(N^2)
	}
	if (firstTwo > firstOne && firstTwo > secondOne && firstTwo > secondTwo) {
		return maxSubsetSumNoAdjacent(array.slice(3), sum + firstTwo);
	}
	if (secondOne > firstOne && secondOne > firstTwo && secondOne > secondTwo) {
		return maxSubsetSumNoAdjacent(array.slice(3), sum + secondOne);
	}
	return maxSubsetSumNoAdjacent(array.slice(4), sum + secondTwo);
}

console.log(maxSubsetSumNoAdjacent([75, 105, 120, 75, 90, 135]));
// 75, 105, 120, 75, 90, 135 = 330
// 75 + 120 + 135 = 330
// 75 + 75 + 135 = 285
// 105 + 75 + 135 = 315
// 105 + 90 = 195

// 1. select b/w 1st and 2nd element
// 2. select b/w 1st and 3rd element
// 3. select b/w 1st and 4th element
// 4. select b/w 2nd and 4th element
// 5. select b/w 2nd and 5th element
ts
export function maxSubsetSumNoAdjacent(array: number[]) {
	if (array.length === 0) return 0;
	if (array.length === 1) return array[0];

	let [first, second] = array;
	second = Math.max(first, second);

	for (let idx = 2; idx < array.length; idx++) {
		const el = array[idx];
		const current = Math.max(second, first + el);
		first = second;
		second = current;
	}

	return second;
}

console.log(maxSubsetSumNoAdjacent([75, 105, 120, 75, 90, 135]));
// 75, 105,               120,                     75,											90,											 135
// 75, Math.max(105, 75), 120,                     75, 											90, 										 135
// 75, 105,               Math.max(120 + 75, 105), 75, 											90, 										 135
// 75, 105,               195,                     Math.max(75 + 105, 195), 90, 										 135
// 75, 105,               195,                     195,                     Math.max(195 + 90, 195), 135
// 75, 105,               195,                     195,                     285,                     Math.max(195 + 135, 285) = 330
  • In the second approach, instead of slicing the array (O(N^2)), we are iterating through the array (O(N)).

  • We need to choose b/w current element + 2nd previous element and the previous element.

  • If we choose the maximum of the two, and keep track of the previous element, we can solve the problem in O(N) time.

  • Remembering which step we took before, and whats the current sum.


Levenshtein Distance

levenshtein_distance

ts
export function levenshteinDistance(
	rowString: string, // either of the two can be made equal to the other
	colString: string
) {
	const editMatrix: number[][] = new Array(rowString.length + 1)
		.fill(0)
		.map(() => new Array(colString.length + 1).fill(0));

	for (let rowIdx = 0; rowIdx < editMatrix.length; rowIdx++) {
		for (let colIdx = 0; colIdx < editMatrix[0].length; colIdx++) {
			if (rowIdx === 0 && colIdx === 0) continue;
			if (rowIdx === 0) {
				editMatrix[0][colIdx] = editMatrix[0][colIdx - 1] + 1;
				continue;
			}
			if (colIdx === 0) {
				editMatrix[rowIdx][0] = editMatrix[rowIdx - 1][0] + 1;
				continue;
			}
			const rowChar = rowString[rowIdx - 1];
			const colChar = colString[colIdx - 1];
			const left = editMatrix[rowIdx][colIdx - 1];
			const top = editMatrix[rowIdx - 1][colIdx];
			const topLeft = editMatrix[rowIdx - 1][colIdx - 1];
			if (rowChar === colChar) {
				editMatrix[rowIdx][colIdx] = topLeft; // no operation needed so dont add + 1
				continue;
			}
			editMatrix[rowIdx][colIdx] = Math.min(left, top, topLeft) + 1; // operation is needed so add + 1
		}
	}
	return editMatrix[rowString.length][colString.length];
}

console.log(levenshteinDistance('biting', 'mitten'));
  • If either string is empty, the edit distance is the length of the other string

  • The left represents the insertion, the top represents the deletion, and the top-left represents the substitution.

  • If the chars match, the edit distance is the same as the top-left cell. ( since no change is needed )

  • If the chars don't match, the edit distance is the minimum of the left, top, and top-left cell + 1. ( since we need to make a change )


Common sub sequence

ts
const buildSequence = (
	editMatrix: number[][],
	string: string,
	sequence: string[] = [],
	rowIdx = editMatrix.length - 1,
	colIdx = editMatrix[0].length - 1
): string[] => {
	if (rowIdx <= 0 || colIdx <= 0) return sequence;
	const left = editMatrix[rowIdx][colIdx - 1];
	const top = editMatrix[rowIdx - 1][colIdx];
	const current = editMatrix[rowIdx][colIdx];

	if (current === top)
		return buildSequence(editMatrix, string, sequence, rowIdx - 1, colIdx);

	if (current === left)
		return buildSequence(editMatrix, string, sequence, rowIdx, colIdx - 1);

	// characters match so move diagonally
	return buildSequence(
		editMatrix,
		string,
		[string[rowIdx - 1], ...sequence], // <- make sure you are using rowIdx when sending rowString for reference
		rowIdx - 1,
		colIdx - 1
	);
};

export function longestCommonSubsequence(rowString: string, colString: string) {
	const editMatrix: number[][] = new Array(rowString.length + 1)
		.fill(0)
		.map(() => new Array(colString.length + 1).fill(0));

	for (let rowIdx = 0; rowIdx < editMatrix.length; rowIdx++) {
		const charAtRowIdx = rowString[rowIdx - 1];
		for (let colIdx = 0; colIdx < editMatrix[0].length; colIdx++) {
			if (rowIdx === 0 || colIdx === 0) continue;
			const topLeft = editMatrix[rowIdx - 1][colIdx - 1];
			const left = editMatrix[rowIdx][colIdx - 1];
			const top = editMatrix[rowIdx - 1][colIdx];

			const charAtColIdx = colString[colIdx - 1];

			if (charAtRowIdx === charAtColIdx) {
				editMatrix[rowIdx][colIdx] = topLeft + 1;
				continue;
			}

			editMatrix[rowIdx][colIdx] = Math.max(left, top);
		}
	}

	console.table(editMatrix);

	return buildSequence(editMatrix, rowString);
}

console.log(longestCommonSubsequence('ZXVVYZW', 'XKYKZPW'));
  • If either string is empty there is no common chars in both so initialize the matrix with 0.

    txt
    ┌─────────┬───┬───┬───┬───┬───┬───┬───┬───┐
    │ (index) │ - │ z │ x │ v │ v │ y │ z │ w │
    ├─────────┼───┼───┼───┼───┼───┼───┼───┼───┤
    │ -       │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ < zeros here
    │ x       │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 1 │
    │ k       │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │
    │ y       │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │
    │ k       │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │
    │ z       │ 0 │ 1 │ 1 │ 2 │ 2 │ 2 │ 2 │ 2 │
    │ p       │ 0 │ 1 │ 1 │ 2 │ 2 │ 3 │ 3 │ 3 │
    │ w       │ 0 │ 1 │ 1 │ 2 │ 2 │ 3 │ 3 │ 4 │
    └─────────┴───┴───┴───┴───┴───┴───┴───┴───┘
  • If the characters match, add +1 to the top-left cell, because that's the expected sequence of common chars

  • If the characters don't match, take the max of the left and top cells. (which gives the max sequence of both)

  • For rebuilding from sequence,

    • Iterate from the end of the matrix

    • If the current cell is equal to the top cell, move up (this means there was no matching chars)

    • If the current cell is equal to the left cell, move left (this means there was no matching chars)

    • If there was not score match, jump to top left cell, adding the value of rowIdx - 1 to the sequence.


Min number of jumps

ts
export function minNumberOfJumps(array: number[]) {
	const jumps = new Array(array.length).fill(Infinity);
	jumps[0] = 0; // you start at the first index, so you don't need to jump

	for (let fixedIdx = 0; fixedIdx < array.length; fixedIdx++) {
		const fixed = array[fixedIdx];
		for (let variableIdx = 0; variableIdx < fixedIdx; variableIdx++) {
			const variable = array[variableIdx];

			const fixedjumps = jumps[fixedIdx];
			const variablejumps = jumps[variableIdx];

			const isItPossibleToReachFixedIdx = variable >= fixedIdx - variableIdx;

			if (!isItPossibleToReachFixedIdx) continue;

			const maxJump = Math.min(fixedjumps, variablejumps + 1);
			jumps[fixedIdx] = maxJump;
		}
	}
	console.log(jumps);
	return jumps[array.length - 1];
}

console.log(minNumberOfJumps([3, 4, 2, 1, 2, 3, 7, 1, 1, 1, 3])); // 4
  • We start at 0th idx, so the jump score is 0 for 0th idx by default;

  • By default we assume that the jump score is very high (Infinity) the opp of what we need.

  • The goal is to jump min no of times.

  • We can jump if the variableIdx score allows us to reach the fixedIdx.

  • If we are able to jump we can set the fixedIdx score to variableIdx score + 1.


Maximum profit for job scheduling

leet code

ts
function jobScheduling(
	startTime: number[],
	endTime: number[],
	profit: number[]
): number {
	const times = endTime
		.map((_, idx) => [startTime[idx], endTime[idx], profit[idx]])
		.sort((a, b) => a[1] - b[1]);
	const profits = times.map(time => time[2]);

	for (let fixedIdx = 0; fixedIdx < times.length; fixedIdx++) {
		for (let variableIdx = 0; variableIdx < fixedIdx; variableIdx++) {
			const [start, , profit] = times[fixedIdx];
			const [, varEnd] = times[variableIdx];

			const isConflicting = varEnd > start;
			if (isConflicting) {
				profits[fixedIdx] = Math.max(profits[fixedIdx], profits[variableIdx]);
				continue;
			}
			profits[fixedIdx] = Math.max(
				profits[fixedIdx],
				profits[variableIdx] + profit
			);
		}
	}

	return profits[profits.length - 1];
}

console.log(
	jobScheduling([1, 2, 3, 4, 6], [3, 5, 10, 6, 9], [20, 20, 100, 70, 60])
);

maxProfitWithKTransactions

ts
// 1. Prices, on each day, are given in an array
export function maxProfitWithKTransactions(
	prices: number[],
	noOfTransactions: number
): number {
	// if there is no price, our profit will be zero
	if (prices.length === 0) return 0;
	const profits: number[][] = [];
	for (let idx = 0; idx < noOfTransactions + 1; idx++) {
		profits.push(new Array(prices.length).fill(0));
	}

	console.table(profits);

	for (let transaction = 1; transaction < noOfTransactions + 1; transaction++) {
		let maxProfitSoFar = -Infinity;
		for (let day = 1; day < prices.length; day++) {
			const profitOnPrevDayPrevTransaction = profits[transaction - 1][day - 1];
			const priceOnPrevDay = prices[day - 1];
			const priceOnCurrDay = prices[day];
			const netProfitOnPrevDayPrevTransaction =
				profitOnPrevDayPrevTransaction - priceOnPrevDay;

			maxProfitSoFar = Math.max(
				maxProfitSoFar,
				netProfitOnPrevDayPrevTransaction
			);

			const profitOnPrevDay = profits[transaction][day - 1];
			profits[transaction][day] = Math.max(
				profitOnPrevDay,
				maxProfitSoFar + priceOnCurrDay
			);
		}
	}

	console.table(profits);

	return profits[noOfTransactions][prices.length - 1];
}

maxProfitWithKTransactions([5, 11, 3, 50, 60, 90], 2);
txt
┌─────────┬───┬────┬───┬────┬────┬────┐
│ (trans) │ 5 │ 11 │ 3 │ 50 │ 60 │ 90 │
├─────────┼───┼────┼───┼────┼────┼────┤
│ 0       │ 0 │ 0  │ 0 │ 0  │ 0  │ 0  │ < first row is zero because there is no profit on transaction one yet (you need to buy first)
│ 1       │ 0 │ 0  │ 0 │ 0  │ 0  │ 0  │
│ 2       │ 0 │ 0  │ 0 │ 0  │ 0  │ 0  │
└─────────┴───┴────┴───┴────┴────┴────┘
            ^ first column is zero because there is no profit on day one

┌─────────┬───┬───┬───┬────┬────┬────┐
│ 0       │ 0 │ 0 │ 0 │ 0  │ 0  │ 0  │
│ 1       │ 0 │ 6 │ 6 │ 47 │ 57 │ 87 │
│ 2       │ 0 │ 6 │ 6 │ 53 │ 63 │ 93 │
└─────────┴───┴───┴───┴────┴────┴────┘

value = 6 based on the following formula,

maxSoFar = Math.max(-Infinity, 0 - 5) = -5
curr = Math.max(0, -5 + 11) = 6

Zero sum sub array

link: AlgoExpert

ts
export const zeroSumSubarray = (nums: number[]) => {
	if (nums.length === 0) return false;

	const sums: { [T: number]: undefined | true } = {};

	let currentSum = 0;

	for (let index = 0; index < nums.length; index++) {
		const element = nums[index];
		currentSum += element;
		if (sums[currentSum]) {
			return true;
		}
		sums[currentSum] = true;
	}

	return currentSum === 0;
};

Modified for understanding

ts
export const zeroSumSubarray = (nums: number[]) => {
	if (nums.length === 0) return false;

	const sums: { [T: number]: undefined | number } = {};

	let currentSum = 0;

	for (let index = 0; index < nums.length; index++) {
		console.log(sums);
		const element = nums[index];
		currentSum += element;
		if (typeof sums[currentSum] === 'number') {
			return [sums[currentSum] + 1, index];
		}
		sums[currentSum] = index;
	}

	return [0, nums.length - 1];
};

console.log(zeroSumSubarray([-5, -5, 2, 3, -2]));
// returns [1, 3]

Key points:

  • If the current sum was already encountered, then the array between the [previous_idx_of_encountered_sum + 1, current_idx] will have a zero sum.


Climbing Stairs

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    0[0] --> 1[1]:::blue
    0[0] --> 2[2]
    1 --> 12[2]:::blue
    1 --> 13[3]
    12 --> 123[3]:::blue
    12 --> 124[4]
    123 --> 1234[4]:::blue --> 12345[5]:::green
    1234 --> 12346[6]:::red
    123 --> 1235[5]:::green
    124 --> 1245[5]:::green
    124 --> 1246[6]:::red
    13 --> 135[5]:::green
    13 --> 136[6]:::red
    2 --> 23[3]
    2 --> 24[4]
    23 --> 234[4]
    23 --> 235[5]:::green
    234 --> 2345[5]:::green
    234 --> 2346[6]:::red
    24 --> 245[5]:::green
    24 --> 246[6]:::red
  • Each node on the graph can be memoized to avoid duplicate computation

  • The Items in the graph marked as blue gets computed first, they can be memoized first

ts
// O(2^N) time and O(N) space
function climbStairs(
	steps: number,
	stepsTakenSoFar = 0,
	noOfPaths = 0
): number {
	if (stepsTakenSoFar === steps) {
		return noOfPaths + 1;
	}
	if (stepsTakenSoFar > steps) {
		return noOfPaths;
	}
	return (
		climbStairs(steps, stepsTakenSoFar + 1, noOfPaths) +
		climbStairs(steps, stepsTakenSoFar + 2, noOfPaths)
	);
}
ts
// O(N) time and O(N) space
interface Memo {
	[key: number]: number;
}

const countNoOfPaths = (
	steps: number,
	stepsTakenSoFar = 0,
	noOfPaths = 0,
	memo: Memo = {}
): number => {
	if (stepsTakenSoFar === steps) {
		return noOfPaths + 1;
	}
	if (stepsTakenSoFar > steps) {
		return noOfPaths;
	}
	const alreadyComputedPath = memo[stepsTakenSoFar];
	if (typeof alreadyComputedPath === 'number') {
		return alreadyComputedPath;
	}
	const noOfStepPaths =
		countNoOfPaths(steps, stepsTakenSoFar + 1, noOfPaths, memo) +
		countNoOfPaths(steps, stepsTakenSoFar + 2, noOfPaths, memo);
	memo[stepsTakenSoFar] = noOfStepPaths;
	return noOfStepPaths;
};

function climbStairs(steps: number): number {
	const memo: Memo = {};
	const paths = countNoOfPaths(steps, 0, 0, memo);
	return paths;
}
ts
// O(N) time and O(N) space
function climbStairs(steps: number): number {
	const seq = new Array(steps + 1).fill(0);
	for (let idx = seq.length - 1; idx >= 0; idx--) {
		if (idx === seq.length - 1 || idx === seq.length - 2) {
			seq[idx] = 1;
			continue;
		}
		const [one, two] = [seq[idx + 1], seq[idx + 2]];
		seq[idx] = one + two;
	}
	console.log(seq);
	return seq[0];
}
ts
// O(N) time and O(1) space
function climbStairs(steps: number): number {
	let one = 1;
	let two = 1;
	for (let idx = steps; idx >= 0; idx--) {
		if (idx === steps || idx === steps - 1) {
			continue;
		}
		const oldOne = one;
		one = one + two;
		two = oldOne;
		console.log({ idx, one, two });
	}
	return one;
}

Staircase traversal

ts
// O(n) time | O(n) space
export function staircaseTraversal(
	height: number,
	maxSteps: number,
	stepsTakenSoFar = 0,
	noOfPaths = 0,
	memo: Record<string, number> = {}
): number {
	if (stepsTakenSoFar === height) {
		return noOfPaths + 1;
	}
	if (stepsTakenSoFar > height) {
		return noOfPaths;
	}
	let totalPaths = 0;
	for (let idx = 0; idx < maxSteps; idx++) {
		const branch = stepsTakenSoFar + idx + 1;
		if (typeof memo[branch] === 'number') {
			totalPaths += memo[branch];
			continue;
		}
		const totalPathsForBranch = staircaseTraversal(
			height,
			maxSteps,
			branch,
			noOfPaths,
			memo
		);
		memo[branch] = totalPathsForBranch;
		totalPaths += totalPathsForBranch;
	}
	return totalPaths;
}

House Robber

Decision tree for

text
[1, 2, 4, 8, 7, 9]
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    0[0] --> 1[1]:::blue
    0[0] --> 2[2]
    1 --> 14[4]:::blue
    1 --> 18[8]:::blue
    1 --> 17[7]:::green
    1 --> 19[9]:::green

    14 --> 147[7]:::blue
    14 --> 149[9]:::blue

    2 --> 28[8]:::green
    2 --> 27[7]:::green
    2 --> 29[9]:::green

    28 --> 289[9]:::green

    0 --> 4[4]:::blue
    4 --> 47[7]:::blue
    4 --> 49[9]:::green

    0 --> 08[8]:::green
    08 --> 089[9]:::green

    0 --> 07[7]:::green

    0 --> 09[9]:::green

green is the that path that was already memoised by the blue path

If we dont have memoization, O(N^N) time and O(N) space

ts
interface Memo {
	[key: number]: number;
}

// 1, 2, 3, 8 ...
// 1 + rob(3...) or rob(2...)

// O(N)T | O(N)S
function rob(houses: number[], houseIdx = 0, memo: Memo = {}): number {
	if (houseIdx >= houses.length) {
		return 0;
	}
	if (houseIdx in memo) return memo[houseIdx];
	const robbingCurrentHouse =
		houses[houseIdx] + rob(houses, houseIdx + 2, memo);
	const skippingCurrentHouse = rob(houses, houseIdx + 1, memo);
	memo[houseIdx] = Math.max(skippingCurrentHouse, robbingCurrentHouse);
	return memo[houseIdx];
}
ts
//          1, 2, 3, 1   <--- houses
//   pp, p, 1            <--- sequence
//          Math.max(p, pp + 1)
//          1, 2, 4, 4

// O(N)T | O(N)S
function rob(houses: number[]) {
	const memo: number[] = new Array(houses.length).fill(0);
	for (let house = 0; house < houses.length; house++) {
		const houseVal = houses[house];
		const prevHouseVal = memo[house - 1] ?? 0;
		const prevToPrevHouseVal = memo[house - 2] ?? 0;
		memo[house] = Math.max(prevHouseVal, prevToPrevHouseVal + houseVal);
	}
	return memo[houses.length - 1];
}

console.log(rob([1, 2, 3, 1])); // 4
console.log(rob([2, 7, 9, 3, 1])); // 12

Since we need to track only last 2 values,

ts
// O(N)T | O(1)S
function rob(houses: number[]) {
	let prevHouseVal = 0;
	let prevToPrevHouseVal = 0;
	for (let house = 0; house < houses.length; house++) {
		const houseVal = houses[house];
		const maxHouse = Math.max(prevHouseVal, prevToPrevHouseVal + houseVal);
		prevToPrevHouseVal = prevHouseVal;
		prevHouseVal = maxHouse;
	}
	return prevHouseVal;
}

console.log(rob([1, 2, 3, 1])); // 4
console.log(rob([2, 7, 9, 3, 1])); // 12

Apartment hunting

ts
interface Block {
	[key: string]: boolean;
}

export function apartmentHunting(blocks: Block[], reqs: string[]) {
	const proximities = new Array(blocks.length)
		.fill([])
		.map(() => new Array(reqs.length).fill(Infinity));

	for (let reqIdx = 0; reqIdx < reqs.length; reqIdx++) {
		const req = reqs[reqIdx];

		let closest = Infinity;
		for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
			const block = blocks[blockIdx];
			if (block[req]) {
				closest = blockIdx;
				proximities[blockIdx][reqIdx] = 0;
				continue;
			}
			proximities[blockIdx][reqIdx] = Math.min(
				proximities[blockIdx][reqIdx],
				Math.abs(blockIdx - closest)
			);
		}

		closest = Infinity;
		for (let blockIdx = blocks.length - 1; blockIdx >= 0; blockIdx--) {
			const block = blocks[blockIdx];
			if (block[req]) {
				closest = blockIdx;
				continue;
			}
			proximities[blockIdx][reqIdx] = Math.min(
				proximities[blockIdx][reqIdx],
				Math.abs(blockIdx - closest)
			);
		}
	}

	let maxProximityBlockIdx = 0;
	let maxProximity = Infinity;
	for (let procIdx = 0; procIdx < proximities.length; procIdx++) {
		const proximity = proximities[procIdx];
		const maxProc = Math.max(...proximity);
		if (maxProc < maxProximity) {
			maxProximity = maxProc;
			maxProximityBlockIdx = procIdx;
		}
	}
	return maxProximityBlockIdx;
}

console.log(
	apartmentHunting(
		[
			{
				gym: false,
				school: true,
				store: false,
			},
			{
				gym: true,
				school: false,
				store: false,
			},
			{
				gym: true,
				school: true,
				store: false,
			},
			{
				gym: false,
				school: true,
				store: false,
			},
			{
				gym: false,
				school: true,
				store: true,
			},
		],
		['gym', 'school', 'store']
	)
);

Build a proximity map like this

txt
gym:    1, 0, 0, 1, 2
school: 0, 1, 0, 0, 0
store:  4, 3, 2, 1, 0
txt
loop over block

closest gym is Infinity
- if gym is present, set proximity to 0
    - closest gym is blockIdx             Infinity
- if gym not preset, set proximity to min(proximity at block, abs(blockIdx - closest))

do this forwards and backwards

Min cost climbing stairs

ts
// ON ST
interface Memo {
	[key: number]: number;
}

const climbStairs = (
	cost: number[],
	currIdx = 0,
	pathCost = 0,
	memo: Memo = {}
): number => {
	if (currIdx >= cost.length) {
		return pathCost;
	}

	if (currIdx in memo) {
		return memo[currIdx];
	}

	const leftMax = climbStairs(cost, currIdx + 1, cost[currIdx + 1] ?? 0, memo);
	const rightMax = climbStairs(cost, currIdx + 2, cost[currIdx + 2] ?? 0, memo);

	if (leftMax > rightMax) {
		memo[currIdx] = rightMax + pathCost;
		return memo[currIdx];
	}

	memo[currIdx] = leftMax + pathCost;
	return memo[currIdx];
};

function minCostClimbingStairs(cost: number[]): number {
	const memo: Memo = {};
	const value = climbStairs(cost, 0, cost[0], memo);
	return Math.min(value, memo[1] ?? Infinity);
}
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    0[0, 10] --> 1[1, 15]:::green
    1 --> 12[2, 20]:::green
    1 --> 13[3, 0]:::green

    0 --> 2[2, 20]:::blue
    2 --> 23[3, 0]:::blue
ts
// O(N) S
function minCostClimbingStairs(cost: number[]): number {
	for (let idx = cost.length - 1; idx >= 0; idx--) {
		const curr = cost[idx] ?? 0;
		const next = cost[idx + 1] ?? 0;
		const nextToNext = cost[idx + 2] ?? 0;

		const singleJumpSum = curr + next;
		const doubleJumpSum = curr + nextToNext;

		if (singleJumpSum > doubleJumpSum) {
			cost[idx] = doubleJumpSum;
			continue;
		}

		cost[idx] = singleJumpSum;
	}
	return Math.min(cost[0], cost[1]);
}

console.log(minCostClimbingStairs([10, 15, 20]));
// 10, 15, 20
//         20 + 0
//         20 + - + 0
// 10, 15,  20
//     15 + 20
//     15 + -  + 0
// 10,  15,  20
// 10 + 15
// 10 + -  + 20
//
// 25,  15,  20

Longest Palindrome;

ts
function longestPalindrome(str: string) {
	let longestLeft = 0;
	let longestRight = 0;
	let length = longestRight - longestLeft;

	for (let idx = 0; idx < str.length; idx++) {
		let left = idx;
		let right = idx;

		while (left >= 0 && right < str.length) {
			if (str[left] === str[right]) {
				const currentLength = right - left;
				if (currentLength > length) {
					longestLeft = left;
					longestRight = right;
					length = currentLength;
				}
				left--;
				right++;
				continue;
			}
			break;
		}

		left = idx;
		right = idx + 1;

		while (left >= 0 && right < str.length) {
			if (str[left] === str[right]) {
				const currentLength = right - left;
				if (currentLength > length) {
					longestLeft = left;
					longestRight = right;
					length = currentLength;
				}
				left--;
				right++;
				continue;
			}
			break;
		}
	}

	return str.substring(longestLeft, longestRight + 1);
}

console.log(longestPalindrome('abaxyzzyxf'));
  • Expand from middle out for each character (start, end = start)

  • Expand from middle out for each character (start, end = start + 1)


Count substrings

ts
function countSubstrings(str: string) {
	let count = 0;

	for (let idx = 0; idx < str.length; idx++) {
		let left = idx;
		let right = idx;

		while (left >= 0 && right < str.length) {
			if (str[left] === str[right]) {
				count += 1;
				left--;
				right++;
				continue;
			}
			break;
		}

		left = idx;
		right = idx + 1;

		while (left >= 0 && right < str.length) {
			if (str[left] === str[right]) {
				count += 1;
				left--;
				right++;
				continue;
			}
			break;
		}
	}

	return count;
}

console.log(countSubstrings('aaa'));

Decode ways

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;

    Start["Start_Index_-1"]:::blue
    Start --> A["1_valid_A_Index_0"]:::green
    Start --> B["11_valid_K_Index_1"]:::green

    A --> A1["1_valid_A_Index_1"]:::green
    A --> A2["11_valid_J_Index_2"]:::green

    B --> B1["1_valid_A_Index_2"]:::green
    B --> B2["10_valid_J_Index_3"]:::green

    A1 --> A11["1_valid_A_Index_2"]:::green
    A1 --> A12["10_valid_J_Index_3"]:::green

    A11 --> A111["0_invalid_Index_3"]:::red
    A11 --> A112["06_invalid_Index_4"]:::red

    A12 --> A121["6_valid_F_Index_4"]:::green

    A2 --> A21["0_invalid_Index_3"]:::red
    A2 --> A22["06_invalid_Index_4"]:::red

    B1 --> B11["0_invalid_Index_3"]:::red
    B1 --> B12["06_invalid_Index_4"]:::red

    B2 --> B22["6_valid_Index_4"]:::green

Total no of valid paths for 11106 is 2

ts
const getAlphaMap = (): Map<string, string> => {
	const alphaMap = new Map<string, string>();
	alphaMap.set('1', 'A');
	alphaMap.set('2', 'B');
	alphaMap.set('3', 'C');
	alphaMap.set('4', 'D');
	alphaMap.set('5', 'E');
	alphaMap.set('6', 'F');
	alphaMap.set('7', 'G');
	alphaMap.set('8', 'H');
	alphaMap.set('9', 'I');
	alphaMap.set('10', 'J');
	alphaMap.set('11', 'K');
	alphaMap.set('12', 'L');
	alphaMap.set('13', 'M');
	alphaMap.set('14', 'N');
	alphaMap.set('15', 'O');
	alphaMap.set('16', 'P');
	alphaMap.set('17', 'Q');
	alphaMap.set('18', 'R');
	alphaMap.set('19', 'S');
	alphaMap.set('20', 'T');
	alphaMap.set('21', 'U');
	alphaMap.set('22', 'V');
	alphaMap.set('23', 'W');
	alphaMap.set('24', 'X');
	alphaMap.set('25', 'Y');
	alphaMap.set('26', 'Z');

	return alphaMap;
};

function numDecodings(
	encodedString: string,
	alphMap = getAlphaMap(),
	memo = new Map<number, number>(),
	isLeft = true,
	currIdx = -1,
	noOfPaths = 0
): number {
	let encodedSubString = '';

	if (currIdx === -1) {
		encodedSubString = '';
	} else if (isLeft) {
		encodedSubString = `${encodedString[currIdx] ?? ''}`;
	} else {
		encodedSubString = `${encodedString[currIdx - 1] ?? ''}${encodedString[currIdx] ?? ''}`;
	}

	const isEncodedSubStringValid = alphMap.has(encodedSubString);

	if (currIdx !== -1 && !isEncodedSubStringValid) {
		return noOfPaths;
	}

	if (currIdx === encodedString.length - 1) {
		if (isEncodedSubStringValid) return noOfPaths + 1;
		return noOfPaths;
	}

	if (memo.has(currIdx)) {
		return (memo.get(currIdx) ?? 0) + noOfPaths;
	}

	const leftNoOfPaths = numDecodings(
		encodedString,
		alphMap,
		memo,
		true,
		currIdx + 1,
		noOfPaths
	);

	const righNoOfPaths = numDecodings(
		encodedString,
		alphMap,
		memo,
		false,
		currIdx + 2,
		noOfPaths
	);

	const totalNoOfPaths = leftNoOfPaths + righNoOfPaths;

	memo.set(currIdx, totalNoOfPaths);

	return totalNoOfPaths + noOfPaths;
}

console.log(numDecodings('11106'));
txt
   11106
   1 or 11 (if I go with 11, increase sequence + 2)

    0 1 2 3 4
    1 1 1 0 6
    0 0 0 0 0
    1 0 0 0 0  ( stand alone 1 is valid )
    1 2 0 0 0  ( stand alone 1 and prev with previous value 11 is valid )
    1 2 3 0 0  ( stand alone 1 + prev and prev with previous value 11 is valid )
    1 2 3 2 0  ( stand alone 0 is not valid, 3 cannot be added to curr, 2 can be added to curr )
    1 2 3 2 2  ( stand alone 6 is valid, 2 can be added to curr, 06 is not valid )
txt
    S 1 1 1 0 6
  0 1 0 0 0 0 0

  0 1 1 0 0 0 0 ( 1 is valid, 0+1 )
  0 1 1 0 0 0 0 ( "" + 1 is valid, 0+1 )
  0 1 1 2 0 0 0 ( 1 is valid, 11 is valid )
  0 1 1 2 3 0 0 ( 1 is valid (0 + 2), 11 is valid (2 + 1) )
  0 1 1 2 3 2 0 ( 0 is not valid, 10 is valid (2 + 0))
  0 1 1 2 3 2 2 ( 6 is valid, 2 is valid )
ts
const getAlphaMap = (): Map<string, string> => {
	const alphaMap = new Map<string, string>();
	alphaMap.set('1', 'A');
	alphaMap.set('2', 'B');
	alphaMap.set('3', 'C');
	alphaMap.set('4', 'D');
	alphaMap.set('5', 'E');
	alphaMap.set('6', 'F');
	alphaMap.set('7', 'G');
	alphaMap.set('8', 'H');
	alphaMap.set('9', 'I');
	alphaMap.set('10', 'J');
	alphaMap.set('11', 'K');
	alphaMap.set('12', 'L');
	alphaMap.set('13', 'M');
	alphaMap.set('14', 'N');
	alphaMap.set('15', 'O');
	alphaMap.set('16', 'P');
	alphaMap.set('17', 'Q');
	alphaMap.set('18', 'R');
	alphaMap.set('19', 'S');
	alphaMap.set('20', 'T');
	alphaMap.set('21', 'U');
	alphaMap.set('22', 'V');
	alphaMap.set('23', 'W');
	alphaMap.set('24', 'X');
	alphaMap.set('25', 'Y');
	alphaMap.set('26', 'Z');

	return alphaMap;
};

function numDecodings(encodedString: string): number {
	const alphaMap = getAlphaMap();

	let prevWays = 1;
	let prevToPrevWays = 0;
	for (let idx = 0; idx < encodedString.length; idx++) {
		const encodedChar = encodedString[idx];
		const prevEncodedChar = encodedString[idx - 1] ?? '';

		let currWays = 0;
		if (alphaMap.has(encodedChar)) {
			currWays += prevWays;
			console.log('alphaMap.has(encodedChar)', currWays);
		}
		if (alphaMap.has(`${prevEncodedChar}${encodedChar}`)) {
			currWays += prevToPrevWays;
			console.log('alphaMap.has(`${encodedChar}${prevEncodedChar}`)', currWays);
		}

		console.log({
			idx,
			encodedChar,
			prevEncodedChar,
			currWays,
			prevWays,
			prevToPrevWays,
		});

		prevToPrevWays = prevWays;
		prevWays = currWays;
	}

	return prevWays;
}

console.log(numDecodings('11106'));

Coin Change

ts
const traverseDown = (
	remainingAmount: number,
	coins: number[],
	memo: Map<number, number>
): number => {
	const memoisedValue = memo.get(remainingAmount);
	if (memoisedValue !== undefined) {
		return memoisedValue;
	}
	if (remainingAmount < 0) {
		return -1; // not a valid path
	}
	if (remainingAmount === 0) {
		return 0; // a valid path
	}
	let minNoOfCoins = Infinity;
	for (let coin of coins) {
		const remainingNoOfCoins = Math.min(
			traverseDown(remainingAmount - coin, coins, memo),
			minNoOfCoins
		);
		if (remainingNoOfCoins <= -1) continue;
		// +1 for the current coin
		minNoOfCoins = Math.min(minNoOfCoins, remainingNoOfCoins + 1);
	}
	const noOfCoins = minNoOfCoins === Infinity ? -1 : minNoOfCoins;
	memo.set(remainingAmount, noOfCoins);
	// return -1 here also if no valid paths
	return noOfCoins;
};

function coinChange(coins: number[], amount: number): number {
	const memo = new Map<number, number>();
	return traverseDown(amount, coins, memo);
}

console.log(coinChange([1, 3, 4, 5], 7)); // 2
mermaid
flowchart LR
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[7]:::green
    root --> 7_1[6 - 1]
    root --> 7_3[4 - 3]
    root --> 7_4[3 - 4]
    root --> 7_5[2 - 5]

    7_1 --> 7_1_1[5 - 1]
    7_1 --> 7_1_3[3 - 3]
    7_1 --> 7_1_4[2 - 4]
    7_1 --> 7_1_5[1 - 5]

    7_3 --> 7_3_1[3 - 1]
    7_3 --> 7_3_3[1 - 3]
    7_3 --> 7_3_4[0 - 4]:::blue
    7_3 --> 7_3_5[-1 - 5]:::red

    7_4 --> 7_4_1[2 - 1]
    7_4 --> 7_4_3[0 - 3]:::blue
    7_4 --> 7_4_4[-1 - 4]:::red
    7_4 --> 7_4_5[-2 - 5]:::red

    7_5 --> 7_5_1[1 - 1]
    7_5 --> 7_5_3[-1 - 3]:::red
    7_5 --> 7_5_4[-2 - 4]:::red
    7_5 --> 7_5_5[-3 - 5]:::red

    7_1_1 --> 7_1_1_1[4 - 1]
    7_1_1 --> 7_1_1_3[2 - 3]
    7_1_1 --> 7_1_1_4[1 - 4]
    7_1_1 --> 7_1_1_5[0 - 5]:::blue

    7_1_3 --> 7_1_3_1[2 - 1]
    7_1_3 --> 7_1_3_3[0 - 3]:::blue
    7_1_3 --> 7_1_3_4[-1 - 4]:::red
    7_1_3 --> 7_1_3_5[-2 - 5]:::red

    7_1_4 --> 7_1_4_1[1 - 1]
    7_1_4 --> 7_1_4_3[-1 - 3]:::red
    7_1_4 --> 7_1_4_4[-2 - 4]:::red
    7_1_4 --> 7_1_4_5[-3 - 5]:::red

    7_1_5 --> 7_1_5_1[0 - 1]:::blue
    7_1_5 --> 7_1_5_3[-2 - 3]:::red
    7_1_5 --> 7_1_5_4[-3 - 4]:::red
    7_1_5 --> 7_1_5_5[-4 - 5]:::red

    7_3_1 --> 7_3_1_1[2 - 1]
    7_3_1 --> 7_3_1_3[0 - 3]:::blue
    7_3_1 --> 7_3_1_4[-1 - 4]:::red
    7_3_1 --> 7_3_1_5[-2 - 5]:::red

    7_3_3 --> 7_3_3_1[0 - 1]:::blue
    7_3_3 --> 7_3_3_3[-2 - 3]:::red
    7_3_3 --> 7_3_3_4[-3 - 4]:::red
    7_3_3 --> 7_3_3_5[-4 - 5]:::red

    7_4_1 --> 7_4_1_1[1 - 1]
    7_4_1 --> 7_4_1_3[-1 - 3]:::red
    7_4_1 --> 7_4_1_4[-2 - 4]:::red
    7_4_1 --> 7_4_1_5[-3 - 5]:::red

    7_5_1 --> 7_5_1_1[0 - 1]:::blue
    7_5_1 --> 7_5_1_3[-2 - 3]:::red
    7_5_1 --> 7_5_1_4[-3 - 4]:::red
    7_5_1 --> 7_5_1_5[-4 - 5]:::red
txt
Time Complexity: no_of_coins ^ depth_of_decision_tree, no_of_coins * amount (max depth possible)
Space Complexity: no_of_coins ( see level 1, we create n recusive stacks )

With memoisation
Time Complexity: no_of_coins * amount
Space Complexity: no_of_coins
  • We keep track of no of coins for each vaid path.

  • If a path is not valid, we return -1 and it is not considered in the min calculation

  • If a path is valid, we add 1 to the no of coins for that path

ts
function coinChange(coins: number[], amount: number): number {
	const dp: number[] = new Array(amount + 1).fill(Infinity);
	dp[0] = 0; // there is exactly one way to make 0 amount, that is by not selecting any coin
	for (let denom = 1; denom < dp.length; denom++) {
		for (let coin of coins) {
			// we cannot make the demonination with the coin whose value is greater than the demonination
			// assuming may not be sorted
			if (coin > denom) continue;
			// keep current coins to make denom or
			// coins to make prev denom + 1
			dp[denom] = Math.min(dp[denom], dp[denom - coin] + 1);
		}
	}
	const lastValue = dp[dp.length - 1];
	return lastValue === Infinity ? -1 : lastValue;
}

// console.log(coinChange([1, 2, 5], 11)); // 3
console.log(coinChange([1, 2], 3)); // 3
txt
┌─────────┬───┬──────────┬──────────┬──────────┬────────┐
│ (index) │ 0 │ 1        │ 2        │ 3        │ Values │
├─────────┼───┼──────────┼──────────┼──────────┼────────┤
│ denom   │   │          │          │          │ 1      │
│ coin    │   │          │          │          │ 1      │
│ dp      │ 0 │ Infinity │ Infinity │ Infinity │        │ < Math.min(Infinity, 0(prev denom) + 1)
└─────────┴───┴──────────┴──────────┴──────────┴────────┘
┌─────────┬───┬───┬──────────┬──────────┬────────┐
│ (index) │ 0 │ 1 │ 2        │ 3        │ Values │
├─────────┼───┼───┼──────────┼──────────┼────────┤
│ denom   │   │   │          │          │ 1      │
│ coin    │   │   │          │          │ 2      │
│ dp      │ 0 │ 1 │ Infinity │ Infinity │        │
└─────────┴───┴───┴──────────┴──────────┴────────┘
┌─────────┬───┬───┬──────────┬──────────┬────────┐
│ (index) │ 0 │ 1 │ 2        │ 3        │ Values │
├─────────┼───┼───┼──────────┼──────────┼────────┤
│ denom   │   │   │          │          │ 2      │
│ coin    │   │   │          │          │ 1      │
│ dp      │ 0 │ 1 │ Infinity │ Infinity │        │
└─────────┴───┴───┴──────────┴──────────┴────────┘
┌─────────┬───┬───┬───┬──────────┬────────┐
│ (index) │ 0 │ 1 │ 2 │ 3        │ Values │
├─────────┼───┼───┼───┼──────────┼────────┤
│ denom   │   │   │   │          │ 2      │
│ coin    │   │   │   │          │ 2      │
│ dp      │ 0 │ 1 │ 2 │ Infinity │        │
└─────────┴───┴───┴───┴──────────┴────────┘
┌─────────┬───┬───┬───┬──────────┬────────┐
│ (index) │ 0 │ 1 │ 2 │ 3        │ Values │
├─────────┼───┼───┼───┼──────────┼────────┤
│ denom   │   │   │   │          │ 3      │
│ coin    │   │   │   │          │ 1      │
│ dp      │ 0 │ 1 │ 1 │ Infinity │        │
└─────────┴───┴───┴───┴──────────┴────────┘
┌─────────┬───┬───┬───┬───┬────────┐
│ (index) │ 0 │ 1 │ 2 │ 3 │ Values │
├─────────┼───┼───┼───┼───┼────────┤
│ denom   │   │   │   │   │ 3      │
│ coin    │   │   │   │   │ 2      │
│ dp      │ 0 │ 1 │ 1 │ 2 │        │
└─────────┴───┴───┴───┴───┴────────┘
2

Coin Change 2

ts
type Memo = Map<string, number>;

const recursiveNoOfWays = (
	amount: number,
	coins: number[],
	index: number = 0,
	memo: Memo
): number => {
	console.table({ amount, coins, index });
	if (amount === 0) return 1;
	if (amount < 0) return 0;
	if (index >= coins.length) return 0;

	if (memo.has(`${amount}-${index}`)) {
		return memo.get(`${amount}-${index}`) as number;
	}

	const val =
		recursiveNoOfWays(amount - coins[index], coins, index, memo) +
		recursiveNoOfWays(amount, coins, index + 1, memo);

	memo.set(`${amount}-${index}`, val);

	return val;
};

const change = (amount: number, coins: number[]) => {
	const memo: Memo = new Map();
	return recursiveNoOfWays(amount, coins, 0, memo);
};

console.log(change(5, [1, 2, 5])); // 4
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[5 - X]:::blue
    root -->|1| 4_1[4]
    root -->|2| 3_2[3]
    root -->|5| 0_5[0]:::green

    4_1 -->|1| 3_1_1[3]
    3_1_1 -->|1| 3_1_1_1[2]
    3_1_1_1 -->|1| 3_1_1_1_1[1] -->|1| 3_1_1_1_1_1[0]:::green
    3_1_1_1 -->|2| 3_1_1_1_2[0]:::green
    3_1_1 -->|2| 3_1_1_2[1] -->|1| 3_1_1_2_1[0]:::green

    4_1 -->|2| 2_1_2[2]

    2_1_2 -->|1| 2_1_2_1[1] -->|1| 2_1_2_1_1[0]:::green
    2_1_2 -->|2| 2_1_2_2[0]:::green

    3_2 -->|1| 3_2_1[2]
    3_2_1 -->|1| 3_2_1_1[1] --> |1| 3_2_1_1_1[0]:::green
    3_2_1 -->|2| 3_2_1_2[0]:::green
    3_2 -->|2| 3_2_2[1] --> |1| 3_2_2_1[0]:::green

    root:::blue
txt
1,1,1,1,1
1,1,1,2
1,1,2,1 << we need to avoid this duplicate path
1,2,1,1
1,2,2

At each node me make no_of_coins decisions
Max depth can be amount

Time Complexity: no_of_coins ^ amount
Space Complexity: no_of_coins

With memoisation

Time Complexity: no_of_coins * amount
Space Complexity: no_of_coins
ts
// O(no_of_coins * target_amount) time complexity
// O(no_of_coins * target_amount) space complexity
const change = (targetAmount: number, coins: number[]): number => {
	const dp: number[][] = Array.from({ length: coins.length + 1 }, () =>
		Array.from({ length: targetAmount + 1 }, (_, idx) => (idx === 0 ? 1 : 0))
	);
	// O(c)
	for (let coinIdx = 1; coinIdx < coins.length + 1; coinIdx++) {
		const coin = coins[coinIdx - 1];
		// O(t)
		for (let amount = 1; amount < targetAmount + 1; amount++) {
			const noOfWaysWithoutThisCoin = dp[coinIdx - 1][amount];
			const noOfWaysWithThisCoin = dp[coinIdx][amount - coin] ?? 0;
			dp[coinIdx][amount] = noOfWaysWithoutThisCoin + noOfWaysWithThisCoin;
		}
	}
	return dp[dp.length - 1][dp[0].length - 1];
};

console.log(change(5, [1, 2, 5])); // 4
txt
               0, 1, 2, 3, 4, 5   < target amount
1 coin         1, 0, 0, 0, 0, 0
1, 2 coins     1, 0, 0, 0, 0, 0
1, 2, 5 coins  1, 0, 0, 0, 0, 0

first row first column is 1, because there is only one way to make 0 amount, that is by not selecting any coin

               0, 1, 2, 3, 4, 5   < target amount
1 coin         1, 1, 1, 1, 1, 1
1, 2 coins     1, 1, 2, x, 3, 3
1, 2, 5 coins  1, 1, 2, 2, 3, 4

No of ways to make 3 amount with 1, 2, coins =
No of ways to make 3 amount with 1 coin +
No of ways to make 1 amount with 1, 2 coins

dp[1][3] = dp[0][3] + dp[1][3 - 2] = 1 + 1 = 2

               0, 1, 2, 3, 4, 5   < target amount
1 coin         1, 1, 1, 1, 1, 1
1, 2 coins     1, 1, 2, 2, 3, 3
1, 2, 5 coins  1, 1, 2, 2, 3, 4
ts
// O(no_of_coins * target_amount) time complexity
// O(target_amount) space complexity

const change = (targetAmount: number, coins: number[]): number => {
	// you only need to keep track of the previous row
	let dp = Array.from({ length }, (_, idx) => (idx === 0 ? 1 : 0));

	for (const coin of coins) {
		console.log({ coin, dp });
		for (let amount = 1; amount < targetAmount + 1; amount++) {
			dp[amount] += dp[amount - coin] ?? 0;
		}
	}

	return dp[targetAmount];
};
txt
1, 1, 1, 1, 1, 1
1, 1, 2, 2, 3, 3
1, 1, 2, 2, 3, 4

override the same array,
since it has the data of the previous iteration

Target Sum

ts
// Without memoisation
// O(2^n) time complexity where n is the length of the nums array

// With memoisation
// O(n * range of possible sum values) ST complexity
function findTargetSumWays(
	nums: number[],
	target: number,
	numIdx = 0,
	memo = new Map<string, number>()
): number {
	if (numIdx === nums.length) {
		return target === 0 ? 1 : 0;
	}
	if (numIdx > nums.length) {
		return 0;
	}

	const num = nums[numIdx];

	if (memo.has(`${numIdx}-${target}`)) {
		return memo.get(`${numIdx}-${target}`) as number;
	}

	const add = findTargetSumWays(nums, target + num, numIdx + 1);
	const sub = findTargetSumWays(nums, target - num, numIdx + 1);
	const total = add + sub;
	memo.set(`${numIdx}-${target}`, total);

	return total;
}

console.log(findTargetSumWays([1, 1, 1, 1, 1], 3)); // 5
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;

    3[3]:::blue

    3 --> 3_p[2]
    3 --> 3_n[4]

    %% Level 2
    3_p --> 3_p_p[1]
    3_p --> 3_p_n[3]

    3_n --> 3_n_p[3]
    3_n --> 3_n_n[5]

    %% Level 3
    3_p_p --> 3_p_p_p[0]:::green
    3_p_p --> 3_p_p_n[2]

    3_p_n --> 3_p_n_p[2]
    3_p_n --> 3_p_n_n[4]

    3_n_p --> 3_n_p_p[2]
    3_n_p --> 3_n_p_n[4]

    3_n_n --> 3_n_n_p[4]
    3_n_n --> 3_n_n_n[6]

    %% Level 4
    3_p_p_p --> 3_p_p_p_p[-1]
    3_p_p_p --> 3_p_p_p_n[1]

    3_p_p_n --> 3_p_p_n_p[1]
    3_p_p_n --> 3_p_p_n_n[3]

    3_p_n_p --> 3_p_n_p_p[1]
    3_p_n_p --> 3_p_n_p_n[3]

    3_p_n_n --> 3_p_n_n_p[3]
    3_p_n_n --> 3_p_n_n_n[5]

    3_n_p_p --> 3_n_p_p_p[1]
    3_n_p_p --> 3_n_p_p_n[3]

    3_n_p_n --> 3_n_p_n_p[3]
    3_n_p_n --> 3_n_p_n_n[5]

    3_n_n_p --> 3_n_n_p_p[3]
    3_n_n_p --> 3_n_n_p_n[5]

    3_n_n_n --> 3_n_n_n_p[5]
    3_n_n_n --> 3_n_n_n_n[7]

    %% Level 5
    3_p_p_p_p --> 3_p_p_p_p_p[-2]:::red
    3_p_p_p_p --> 3_p_p_p_p_n[0]:::green

    3_p_p_p_n --> 3_p_p_p_n_p[0]:::green
    3_p_p_p_n --> 3_p_p_p_n_n[2]:::red

    3_p_p_n_p --> 3_p_p_n_p_p[0]:::green
    3_p_p_n_p --> 3_p_p_n_p_n[2]:::red

    3_p_p_n_n --> 3_p_p_n_n_p[2]:::red
    3_p_p_n_n --> 3_p_p_n_n_n[4]:::red

    3_p_n_p_p --> 3_p_n_p_p_p[0]:::green
    3_p_n_p_p --> 3_p_n_p_p_n[2]:::red

    3_p_n_p_n --> 3_p_n_p_n_p[2]:::red
    3_p_n_p_n --> 3_p_n_p_n_n[4]:::red

    3_p_n_n_p --> 3_p_n_n_p_p[2]:::red
    3_p_n_n_p --> 3_p_n_n_p_n[4]:::red

    3_p_n_n_n --> 3_p_n_n_n_p[4]:::red
    3_p_n_n_n --> 3_p_n_n_n_n[6]:::red

    3_n_p_p_p --> 3_n_p_p_p_p[0]:::green
    3_n_p_p_p --> 3_n_p_p_p_n[2]:::red

    3_n_p_p_n --> 3_n_p_p_n_p[2]:::red
    3_n_p_p_n --> 3_n_p_p_n_n[4]:::red

    3_n_p_n_p --> 3_n_p_n_p_p[2]:::red
    3_n_p_n_p --> 3_n_p_n_p_n[4]:::red

    3_n_p_n_n --> 3_n_p_n_n_p[4]:::red
    3_n_p_n_n --> 3_n_p_n_n_n[6]:::red

    3_n_n_p_p --> 3_n_n_p_p_p[2]:::red
    3_n_n_p_p --> 3_n_n_p_p_n[4]:::red

    3_n_n_p_n --> 3_n_n_p_n_p[4]:::red
    3_n_n_p_n --> 3_n_n_p_n_n[6]:::red

    3_n_n_n_p --> 3_n_n_n_p_p[4]:::red
    3_n_n_n_p --> 3_n_n_n_p_n[6]:::red

    3_n_n_n_n --> 3_n_n_n_n_p[6]:::red
    3_n_n_n_n --> 3_n_n_n_n_n[8]:::red
  • We should terminate the paths only in level 5

ts
const addValueToMap = (
	map: Map<number, number>,
	key: number,
	value: number
) => {
	if (!map.has(key)) {
		map.set(key, value);
		return;
	}
	map.set(key, map.get(key)! + value);
};

function findTargetSumWays(nums: number[], target: number): number {
	let dp = new Map<number, number>();
	dp.set(0, 1);

	for (const num of nums) {
		const newDp = new Map<number, number>();
		for (const [possition, ways] of dp) {
			if (ways === 0) {
				newDp.set(possition, 2);
				continue;
			}
			addValueToMap(newDp, possition + num, ways);
			addValueToMap(newDp, possition - num, ways);
		}
		// I don't need to do this because
		// 1. even if dp refers to the newDP.
		// 2. newDP is not being used in the next iteration.
		// 3. it gets re-initialized in the next iteration.
		dp = newDp;
	}

	return dp.get(target) ?? 0;
}

// console.log(findTargetSumWays([1, 1, 1, 1, 1], 3)); // 5
// console.log(findTargetSumWays([1, 0], 1)); // 2
console.log(findTargetSumWays([1000], 1000)); // 1
txt
( at each node we make + / - decision )
Time Complexity: 2 ^ (nums (max depth possible) * target_sum)
Space Complexity: no_of_nums ( at each level we make + / - decision )

With memoisation
Time Complexity: 2 * nums * target_sum (sum at each node)
txt
    -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5
0    0,  0,  0,  0,  0, 1, 0, 0, 0, 0, 0

base case, the middle value is 1,
because there is only one way to make 0 amount, that is by not using any coin

1    0,  0,  0,  0,  1, 0,  1, 0, 0, 0, 0
2    0,  0,  0,  1,  0, 2,  0, 1, 0, 0, 0
3    0,  0,  1,  0,  3, 0,  3, 0, 1, 0, 0
4    0,  1,  0,  4,  0, 6,  0, 4, 0, 1, 0
5    1,  0,  5,  0, 10, 0, 10, 0, 5, 0, 1



    -1, 0, 1
0    0, 1, 0
1    1, 0, 1
0    2, 0, 2

processing 0 creates two paths for each existing way.
So double every occurence of non-zero values.


        -1000, 0, 1000
0           0, 1,    0
1000        1, 0,    1

knapsack Problem

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    10[10]:::blue
    10 -->|1,2| 8 -->|4,3| 5 -->|5,6| -1[-1 no_cap]:::red
    8 --> |5,6| 2 -->|4,3| -1.1[-1 no_cap]:::red
    8 --> |6,7| 1 -->|5,6| -5[-5, no_cap]:::red

    10 ---->|4, 3| 7 --> |1, 2| 5.1[5] -->|5, 6| -1.2[-1 no_cap]:::red
    7 --> |5, 6| 1.1[1] -->|1, 2| -1.3[-1 no_cap]:::red
    7 --> |6, 7| 0:::green

    10 --->|5, 6| 4 -->|1, 2| 3 -->|4, 3| -1.4[-1 no_cap]:::red
    4 --> |4, 3| 1.2[1] -->|1, 2| -1.5[-1 no_cap]:::red
    4 --> |6, 7| -3[-3, no_cap]:::red

    10 ---->|6, 7| 3.1[3] -->|1, 2| 1.4[1] -->|4, 3| -1.6[-1 no_cap]:::red
    3.1 --> |4, 3| 0.1[0]:::green
    3.1 --> |5, 6| -3.2[-3, no_cap]:::red
txt
At each node we need to remember
- the current capacity used
- the path travelled so we dont repeat the same path

We can memoize the following keys
- Current capacity
- Next Item index (sub-tree idx)
ts
type TraversalPathTaken = Map<number, number>;

const recursiveKnapSack = (
	items: [number, number][],
	capacity: number,
	traversalPathTaken: TraversalPathTaken = new Map(),
	maxValue: number = 0,
	memo: Map<string, [number, TraversalPathTaken] | null> = new Map()
): null | [number, TraversalPathTaken] => {
	if (capacity < 0) {
		return null;
	}

	if (capacity === 0) {
		return [maxValue, traversalPathTaken];
	}

	if (traversalPathTaken.size === items.length) {
		return [maxValue, traversalPathTaken];
	}

	let newMaxValue = maxValue;
	let newMaxValuePath: null | TraversalPathTaken = null;

	for (let itemIdx = 0; itemIdx < items.length; itemIdx++) {
		if (traversalPathTaken.has(itemIdx)) continue;

		if (memo.has(`${itemIdx}-${capacity}`)) {
			const memoizedValue = memo.get(`${itemIdx}-${capacity}`);

			if (!memoizedValue) continue;

			const [memoizedVal, memoizedPath] = memoizedValue;
			if (memoizedVal <= newMaxValue) continue;

			newMaxValue = memoizedVal;
			newMaxValuePath = memoizedPath;
		}

		const [value, weight] = items[itemIdx];
		const subTree = recursiveKnapSack(
			items,
			capacity - weight,
			new Map(traversalPathTaken).set(itemIdx, value),
			maxValue + value
		);

		memo.set(`${itemIdx}-${capacity}`, subTree);

		if (!subTree) continue;

		const [subTreeVal, subTreePath] = subTree;
		if (subTreeVal <= newMaxValue) continue;

		newMaxValue = subTreeVal;
		newMaxValuePath = subTreePath;
	}

	if (!newMaxValuePath) return [maxValue, traversalPathTaken];

	return [newMaxValue, newMaxValuePath];
};

export function knapsackProblem(
	items: [number, number][],
	capacity: number
): [number, number[]] {
	const [maxValue, path] = recursiveKnapSack(items, capacity)!;
	return [maxValue, [...path.keys()]];
}

console.log(
	knapsackProblem(
		[
			[1, 2],
			[4, 3],
			[5, 6],
			[6, 7],
		],
		10
	)
); // [10, [1, 3]]
txt
Time Complexity: (no_of_items_in_knapsack - level) ^ capacity
Space Complexity: no_of_items_in_knapsack

Memoisation
Time Complexity: no_of_items_in_knapsack * capacity
Space Complexity: no_of_items_in_knapsack * capacity
txt
┌──────────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┐
│ (index)      │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ < capacity
├──────────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤
│ [v, w]       │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0  │
│ [1, 2]       │ 0 │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1  │
│ [4, 3]       │ 0 │ 0 │ 1 │ 4 │ 4 │ 5 │ 5 │ 5 │ 5 │ 5 │ 5  │
│ [5, 6]       │ 0 │ 0 │ 1 │ 4 │ 4 │ 5 │ 5 │ 5 │ 6 │ 9 │ 9  │
│ [6, 7]       │ 0 │ 0 │ 1 │ 4 │ 4 │ 5 │ 5 │ 6 │ 6 │ 9 │ 10 │
└──────────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘
  ^
  items

Formula,

if weight for item exceeds the capacity so far, then
accept last value

lastValue = dp[itemIdx - 1][capForItem], go up one row
lastValueExcludingCurrentCapacity = dp[itemIdx - 1][capForItem - weight], go up one row and go left by weight
Math.max(lastvalue, value + lastValueExcludingCurrentCapacity)

┌──────────────┬───┬────┬───┬─────┬───┬───┬───┬───┬───┬───┬────┐
│ (index)      │ 0 │ 1  │ 2 │ 3   │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │
├──────────────┼───┼────┼───┼─────┼───┼───┼───┼───┼───┼───┼────┤
│ [v, w]       │ 0 │ 0- │ 0 │ 0   │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0  │
│ [1, 2]       │ 0 │ 0  │ 1 │ 1   │ 1 │ 1 │ 1 │ 1 │ 1 │ 1 │ 1  │ 3. last value (4) !== backtracking start (1), that means we took the item (1, 2)
│ [4, 3]       │ 0 │ 0  │ 1 │ 4-  │ 4 │ 5 │ 5 │ 5 │ 5 │ 5 │ 5  │ 1. last value != backtracking start, that means we took the item (6, 7)
│ [5, 6]       │ 0 │ 0  │ 1 │ 4-  │ 4 │ 5 │ 5 │ 5 │ 6 │ 9 │ 9  │ < last value
│ [6, 7]       │ 0 │ 0  │ 1 │ 4   │ 4 │ 5 │ 5 │ 6 │ 6 │ 9 │ 10 │ < backtracking start
└──────────────┴───┴────┴───┴─────┴───┴───┴───┴───┴───┴───┴────┘
                             2. move left by weight (10 - 7 prev weight)
                             < last value
                             < backtracking start
                             last value === backtracking start === 4, that means we did not take the item (5, 6),
                             so move up one row
ts
export function knapsackProblem(
	items: [number, number][],
	capacity: number
): [number, number[]] {
	const dp = Array.from({ length: items.length + 1 }, () =>
		Array.from({ length: capacity + 1 }, () => 0)
	);
	for (let itemIdx = 1; itemIdx < items.length + 1; itemIdx++) {
		const [value, weight] = items[itemIdx - 1];
		for (let capForItem = 1; capForItem <= capacity; capForItem++) {
			const notTakingItem = dp[itemIdx - 1][capForItem];
			if (capForItem < weight) {
				dp[itemIdx][capForItem] = notTakingItem;
				continue;
			}
			const lastValueExculdingCurrentCapacity =
				dp[itemIdx - 1]?.[capForItem - weight] ?? 0;
			const takingItem = value + lastValueExculdingCurrentCapacity;
			dp[itemIdx][capForItem] = Math.max(notTakingItem, takingItem);
		}
	}

	const totalValueInKnapSack = dp[items.length][capacity];
	const knapSackContents: number[] = [];
	for (let itemIdx = items.length, cap = capacity; itemIdx > 0; itemIdx--) {
		if (dp[itemIdx][cap] !== dp[itemIdx - 1][cap]) {
			knapSackContents.push(itemIdx - 1);
			cap -= items[itemIdx - 1][1];
		}
	}
	return [totalValueInKnapSack, knapSackContents.reverse()];
}

console.log(
	knapsackProblem(
		[
			[1, 2],
			[4, 3],
			[5, 6],
			[6, 7],
		],
		10
	)
); // [10, [1, 3]]

Disk Stacking

ts
type Disk = [number, number, number];
type DiskWithIdx = [Disk, number];
type StackAsMap = Set<number>;

const isPrevDiskSmaller = (prevDisk: Disk, disk: Disk) => {
	const [prevWidth, prevDepth, prevHeight] = prevDisk;
	const [width, depth, height] = disk;
	return prevWidth < width && prevDepth < depth && prevHeight < height;
};

type TraverseDisksReturn = [DiskWithIdx[], StackAsMap, number];

type Memo = Map<string, TraverseDisksReturn | null>;

const createMemoKey = (diskId: number, stackAsMap: StackAsMap) => {
	return `${diskId}-${[...stackAsMap.keys()].join('-')}`;
};

const traverseDisks = (
	disks: Disk[],
	stack: DiskWithIdx[] = [],
	stackAsMap: StackAsMap = new Set(),
	stackHeight = 0,
	memo: Memo = new Map()
): [DiskWithIdx[], StackAsMap, number] => {
	const [prevDisk, prevDiskIdx = 0] = stack[stack.length - 1] ?? [];

	console.log('stack', stack);

	let tallestStack = stack;
	let tallestStackAsMap = stackAsMap;
	let tallestStackHeight = stackHeight;

	for (let diskIdx = prevDiskIdx; diskIdx < disks.length; diskIdx++) {
		if (tallestStackAsMap.has(diskIdx)) continue; // disk has already been stacked

		const disk = disks[diskIdx];

		const memoKey = createMemoKey(diskIdx, stackAsMap);

		if (prevDisk && !isPrevDiskSmaller(prevDisk, disk)) continue; // disk is not stackable <<<<<<<<

		if (memo.has(memoKey)) {
			const memoValue = memo.get(memoKey);
			if (!memoValue) continue;
			[tallestStack, tallestStackAsMap, tallestStackHeight] = memoValue;
		}

		const [stackWithDisk, stackWithDiskAsMap, stackHeightWithDisk] =
			traverseDisks(
				disks,
				[...stack, [disk, diskIdx]],
				new Set(stackAsMap).add(diskIdx),
				stackHeight + disk[2],
				memo
			);

		if (stackHeightWithDisk <= tallestStackHeight) {
			memo.set(memoKey, null);
			continue;
		}

		tallestStack = stackWithDisk;
		tallestStackHeight = stackHeightWithDisk;
		tallestStackAsMap = stackWithDiskAsMap;

		memo.set(memoKey, [tallestStack, tallestStackAsMap, tallestStackHeight]);
	}

	return [tallestStack, tallestStackAsMap, tallestStackHeight];
};

export function diskStacking(disks: Disk[]) {
	disks.sort((a, b) => b[2] - a[2]);
	console.log('disks', disks);
	return traverseDisks(disks)[0].map(([disk]) => disk);
}

console.log(
	diskStacking([
		[2, 1, 2],
		[3, 2, 3],
		[2, 2, 8],
		[2, 3, 4],
		[2, 2, 1],
		[4, 4, 5],
	])
); // [[2, 1, 2], [3, 2, 3], [4, 4, 5]]
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[Root]:::blue
    root --> 2_2_1[2, 2, 1]
    root --> 2_1_2[2, 1, 2] --> 3_2_3[3, 2, 3]:::yellow --> 4_4_5[4, 4, 5]:::yellow
    root --> 3_2_3_1[3, 2, 3]:::yellow --> 4_4_5_1[4, 4, 5]:::yellow
    root --> 2_3_4[2, 3, 4]
    root --> 4_4_5_2[4, 4, 5]
    root --> 2_2_8[2, 2, 8]
txt
Time Complexity: no_of_disks^no_of_disks (max depth is no_of_disks)
Space Complexity: no_of_disks

With memoisation
Time Complexity: no_of_disks * no_of_disks = no_of_disks^2
Space Complexity: no_of_disks
ts
type Disk = [number, number, number];

const isPrevDiskSmaller = (prevDisk: Disk, disk: Disk) => {
	const [prevWidth, prevDepth, prevHeight] = prevDisk;
	const [width, depth, height] = disk;
	return prevWidth < width && prevDepth < depth && prevHeight < height;
};

export function diskStacking(disks: Disk[]) {
	// sort the disks by height
	disks.sort((a, b) => a[2] - b[2]);

	const stackHeights = disks.map(disk => disk[2]);
	const sequences = new Array(disks.length).fill(null);

	let maxHeightIdx = 0;

	for (let fixedIdx = 1; fixedIdx < stackHeights.length; fixedIdx++) {
		const fixedDisk = disks[fixedIdx];
		const fixedDiskHeight = fixedDisk[2];

		for (let variableIdx = 0; variableIdx < fixedIdx; variableIdx++) {
			const variableDisk = disks[variableIdx];

			if (!isPrevDiskSmaller(variableDisk, fixedDisk)) continue;

			const stackHeightUpToFixedIdx = stackHeights[fixedIdx];
			const stackHeightUpToVariableIdx = stackHeights[variableIdx];
			const newStackHeightUpToFixedIdx =
				stackHeightUpToVariableIdx + fixedDiskHeight;

			if (newStackHeightUpToFixedIdx <= stackHeightUpToFixedIdx) continue;

			stackHeights[fixedIdx] = newStackHeightUpToFixedIdx;
			sequences[fixedIdx] = variableIdx;
		}

		// NOTE: Potential cause of bug
		// check if maxStackHeight is outdated, only when the fixed variable loop is completed
		const maxStackHeightSoFar = stackHeights[maxHeightIdx];
		const newStackHeightUpToFixedIdx = stackHeights[fixedIdx];
		if (newStackHeightUpToFixedIdx <= maxStackHeightSoFar) continue;

		maxHeightIdx = fixedIdx;
	}

	const stack = [];

	while (maxHeightIdx !== null) {
		const disk = disks[maxHeightIdx];
		stack.unshift(disk);
		maxHeightIdx = sequences[maxHeightIdx];
	}

	return stack;
}
txt
disks = [[2, 1, 2], [3, 2, 3], [2, 2, 8], [2, 3, 4], [2, 2, 1], [4, 4, 5]]

sorted_disks = [[2, 2, 1], [2, 1, 2], [3, 2, 3], [2, 3, 4], [4, 4, 5], [2, 2, 8]]
heights = [1, 2, 3, 4, 5, 8]
sequences = [null, null, null, null, null, null]

1. [2, 2, 1] is stackable on top of [3, 2, 3]

           4, updated height at fixed index
              ( this tells that @idx=2, the max stack height we can build is 4 )
    [1, 2, 3, 4, 5, 8]
           F
     V

                 2 is stackable on top of 0
    [null, null, 0, null, null, null]
     0,    1,    2, 3,    4,    5



           5
    [1, 2, 3, 4, 5, 8]
           F
        V

                 2 is stackable on top of 1
                   ( updated based on new stack height )
    [null, null, 1, null, null, null]
     0,    1,    2, 3,    4,    5

2. [3, 2, 3] is stackable on top of [4, 4, 5]


                 10 (5 + 5)
                 7  (5 + 2)
           5     6  (5 + 1)
    [1, 2, 3, 4, 5, 8]
                 F
     V

    [null, null, 1, null, 2, null]
    [1,    2,    5, 4,   10,    8]
    [0,    1,    2, 3,    4,    5]

Longest Increasing Subsequence

ts
type Memo = Map<number, number[]>;
const recursiveLongestIncreasingSubsequence = (
	array: number[],
	index: number = -1,
	traversal: number[] = [],
	memo: Memo = new Map()
): number[] => {
	if (index > array.length - 1) return traversal;

	const elementAtIdx = array[index];

	let maxTraversal = [...traversal];
	let maxTraversalSet = new Set(maxTraversal);
	for (let idx = index + 1; idx < array.length; idx++) {
		const element = array[idx];
		if (index !== -1 && elementAtIdx > element) continue;
		if (
			memo.has(idx) &&
			memo.get(idx)!.length > maxTraversal.length &&
			new Set(memo.get(idx)!).size > maxTraversalSet.size
		) {
			maxTraversal = memo.get(idx)!;
			continue;
		}
		const subTraversal = recursiveLongestIncreasingSubsequence(array, idx, [
			...traversal,
			element,
		]);
		memo.set(idx, subTraversal);
		if (
			subTraversal.length <= maxTraversal.length ||
			new Set(subTraversal).size <= maxTraversalSet.size
		)
			continue;
		maxTraversal = subTraversal;
	}

	return maxTraversal;
};

export function longestIncreasingSubsequence(array: number[]) {
	return recursiveLongestIncreasingSubsequence(array);
}

console.log(
	longestIncreasingSubsequence([5, 7, -24, 12, 10, 2, 3, 12, 5, 6, 35])
);

console.log(longestIncreasingSubsequence([-1, 2, 1, 2]));
mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[Root]:::blue
    root --> minus1[-1]
    minus1 --> 2[2]:::yellow --> 2.0.1[2]:::yellow
    minus1 --> 1[1]:::yellow --> 2.1[2]:::yellow
    minus1 --> 2.1.1[2]:::yellow

    root --> 2.3[2] --> 2.4[2]
    root --> 1.1[1] --> 2.5[2]
    root --> 2.6[2]
txt
Two distict paths emerge
-1 -> 2 -> 2
-1 -> 1 -> 2 ( this gets preferred because it has more distinct elements )
ts
export function longestIncreasingSubsequence(array: number[]) {
	const sums: number[] = Array(array.length).fill(1);
	const sequences: number[] = Array(array.length).fill(null);

	let maxLengthIdx = 0;
	for (let fixedIdx = 1; fixedIdx < array.length; fixedIdx++) {
		const fixedNum = array[fixedIdx];
		const sumAtFixedIdx = sums[fixedIdx]; // at the beginning of the iteration

		for (let variableIdx = 0; variableIdx < fixedIdx; variableIdx++) {
			const variableNum = array[variableIdx];

			if (variableNum > fixedNum) continue;

			const newPossibleSum = sums[variableIdx] + sumAtFixedIdx;
			const currentSumAtFixedIdx = sums[fixedIdx]; // updated in previous variableIdx iteration

			if (currentSumAtFixedIdx > newPossibleSum) continue;

			sums[fixedIdx] = newPossibleSum;
			sequences[fixedIdx] = variableIdx;
		}
		if (sums[maxLengthIdx] > sums[fixedIdx]) continue;
		maxLengthIdx = fixedIdx;
	}

	const result = [];
	while (maxLengthIdx !== null) {
		result.unshift(array[maxLengthIdx]);
		maxLengthIdx = sequences[maxLengthIdx];
	}

	return result;
}

// console.log(
// 	longestIncreasingSubsequence([5, 7, -24, 12, 10, 2, 3, 12, 5, 6, 35])
// );
//
// console.log(longestIncreasingSubsequence([-1, 2, 1, 2]));
console.log(longestIncreasingSubsequence([1, 5, -1, 10]));
txt
solution similar to disk stacking
┌──────────────┬──────┬───┬──────┬────┬────────┐
│ (index)      │ 0    │ 1 │ 2    │ 3  │ Values │
├──────────────┼──────┼───┼──────┼────┼────────┤
│ array        │ 1    │ 5 │ -1   │ 10 │        │
│              │      │   │      │    │        │
│ sums         │ 1    │ 1 │ 1    │ 1  │        │ < for fixed idx = 1 (5), 5 is greater than 1, so increase its sum
│ sums         │ 1    │ 2 │ 1    │ 3  │        │ < no valid paths for -1
│ sums         │ 1    │ 2 │ 1    │ 3  │        │ < 1 + 1 | 2 + 1 | 1 + 1, taking 3
│              │      │   │      │    │        │       ***notice that the fixed idx at 3 is considered as 1 not the one manipulated in the previous iteration
│ sequences    │ null │ 0 │ null │ 1  │        │
│ maxLengthIdx │      │   │      │    │ null   │
│ result       │ 1    │ 5 │ 10   │    │        │
└──────────────┴──────┴───┴──────┴────┴────────┘
[ 1, 5, 10 ]

LCS

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    a_b[A, B]:::blue --> a_c[A,C] --> a_a[A, A]:::orange
    a_b --> b_b[B, B]:::orange --> c_c[C, C]:::orange

    a_c --> b_a[B, A]

for example

txt
abc
bca
  • If the characters match, we add +1

  • Else we take the max of two decisions

    • Skip the character in the first string

    • Skip the character in the second string

txt
Time complexity = 2^(m + n)
                  m = length of first string
                  n = length of second string
                  m + n = max decision tree depth

Space complexity = m * n

with memoistaion
Time complexity = m * n
Space complexity = m * n
py
from typing import Dict

Memo = Dict[str, int]


class Solution:
    def recursive(
        self,
        firstText: str,
        secondText: str,
        firstTextIdx: int,
        secondTextIdx: int,
        memo: Memo,
    ) -> int:
        if firstTextIdx == len(firstText) or secondTextIdx == len(secondText):
            return 0

        if memo.get(f"{firstTextIdx}_{secondTextIdx}") is not None:
            return memo[f"{firstTextIdx}_{secondTextIdx}"]

        if firstText[firstTextIdx] == secondText[secondTextIdx]:
            next = self.recursive(
                firstText, secondText, firstTextIdx + 1, secondTextIdx + 1, memo
            )
            memo[f"{firstTextIdx}_{secondTextIdx}"] = 1 + next
            return 1 + next

        left = self.recursive(
            firstText, secondText, firstTextIdx + 1, secondTextIdx, memo
        )
        right = self.recursive(
            firstText, secondText, firstTextIdx, secondTextIdx + 1, memo
        )
        value = max(left, right)

        memo[f"{firstTextIdx}_{secondTextIdx}"] = value

        return value

    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        return self.recursive(text1, text2, 0, 0, dict())


solution = Solution()
print(solution.longestCommonSubsequence("abcde", "ace"))
print(solution.longestCommonSubsequence("lemon", "lomomn"))
py
from typing import Dict

Memo = Dict[str, int]


class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        dp = [[0 for _ in range(len(text1) + 1)] for _ in range(len(text2) + 1)]
        # O(n * m)
        for rowIdx in range(1, len(dp)):
            for colIdx in range(1, len(dp[0])):
                rowChar = text2[rowIdx - 1]  # since loop starts from 1
                colChar = text1[colIdx - 1]

                if rowChar != colChar:
                    dp[rowIdx][colIdx] = max(
                        dp[rowIdx - 1][colIdx], dp[rowIdx][colIdx - 1]
                    )
                    continue

                dp[rowIdx][colIdx] = dp[rowIdx - 1][colIdx - 1] + 1

        return dp[-1][-1]


solution = Solution()
# print(solution.longestCommonSubsequence("abcde", "ace"))
print(solution.longestCommonSubsequence("lemon", "lomomn"))
txt
If characters match, take the previous LCS value (diagonal left up) and add 1
dp[rowIdx][colIdx] = dp[rowIdx - 1][colIdx - 1] + 1

If characters do not match, take the max of the two previous values
dp[rowIdx][colIdx] = max(dp[rowIdx - 1][colIdx], dp[rowIdx][colIdx - 1])
                         carry forward the previous LCS value with the set up characters upto the previous idx
                         or carry forward the previous LCS value with the set up characters upto the current idx
txt
[ "-", "l", "e", "m", "o", "n" ]
[ "l",  1,   1,   1,   1,   1  ]
[ "o",  1,   1,   1,   2,   2  ]
[ "m",  1,   1,  _2,   2,   2  ]   characters m and ma match so take the diagonal left up value and add 1 (2)
[ "o",  1,   1,   2,   2,   3  ]
[ "m",  1,   1,   2,   2,   3  ]
[ "n",  1,   1,   2,   2,   3  ]

best-time-to-buy-and-sell-stock

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[Root]:::blue

    subgraph "1"
        1_0[-1]:::green
        1_1[0]:::yellow
    end

    root --> | buy | 1_0
    root --> | pass | 1_1

    subgraph "5"
        2_0[4]:::orange
        2_1[-1]:::yellow
    end

    1_0 --> | sell | 2_0
    1_0 --> | pass | 2_1

    subgraph "3"
        2_2[2]:::orange
        2_3[-1]:::yellow
    end

    2_1 --> | sell | 2_2
    2_1 --> | pass | 2_3

    subgraph "6"
        2_4[5]:::orange
        2_5[-1]:::yellow
    end

    2_3 --> | sell | 2_4
    2_3 --> | pass | 2_5

    subgraph "4"
        2_6[3]:::orange
        2_7[-1]:::yellow
    end

    2_5 --> | sell | 2_6
    2_5 --> | pass | 2_7

    subgraph "Overflow"
        2_8[0]:::orange
    end

    2_7 --> | sell | 2_8
py
from typing import List

Memo = dict[str, int]


class Solution:
    def recursiveMaxProfit(
        self,
        prices: List[int],
        idx: int,
        is_holding_stock: bool,
        memo: Memo,
    ) -> int:
        if idx == len(prices):
            return 0

        price = prices[idx]

        if f"{idx}_{is_holding_stock}" in memo:
            return memo[f"{idx}_{is_holding_stock}"]

        profit_if_skip = self.recursiveMaxProfit(
            prices, idx + 1, is_holding_stock, memo
        )

        if is_holding_stock:
            profit = max(profit_if_skip, price)
            memo[f"{idx}_True"] = profit
            return profit

        profit_if_buy = -price + self.recursiveMaxProfit(prices, idx + 1, True, memo)
        profit = max(profit_if_skip, profit_if_buy)
        memo[f"{idx}_False"] = profit
        return profit

    def maxProfit(self, prices: List[int]) -> int:
        return self.recursiveMaxProfit(prices, 0, False, dict())


sol = Solution()
print(sol.maxProfit([7, 1, 5, 3, 6, 4]))  # 5
print(sol.maxProfit([7, 6, 4, 3, 1]))  # 0
txt
Time complexity = 2^n
Space complexity = n

With memoisation
Time complexity = n
Space complexity = n

Sliding Window solution

Sliding Window

py
from typing import List

Memo = dict[str, int]


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        left = 0
        right = 1
        max_profit = 0
        while right < len(prices) and right > left:
            if prices[right] > prices[left]:
                max_profit = max(max_profit, prices[right] - prices[left])
                right += 1
                continue
            left = right
            right += 1
        return max_profit


sol = Solution()
# print(sol.maxProfit([1, 2, 3, 0, 2]))  # 3
print(sol.maxProfit([1, 5, 3, 6, 4]))  # 5
print(sol.maxProfit([7, 6, 4, 3, 1]))  # 0

DP

txt
    [7, 1, 5, 3, 6, 4]
                 ^  for 6, min_so_far = 1,
                           max_profit_so_far = max(max_profit_so_far, 6 - 1) = 5
py
from typing import List

Memo = dict[str, int]


class Solution:
    # O(N)
    def maxProfit(self, prices: List[int]) -> int:
        min_price_so_far = prices[0]
        max_profit_so_far = 0
        for idx in range(1, len(prices)):
            price = prices[idx]
            profit = price - min_price_so_far

            min_price_so_far = min(min_price_so_far, price)
            max_profit_so_far = max(max_profit_so_far, profit)
        return max_profit_so_far


sol = Solution()

best-time-to-buy-and-sell-stock-ii

py
from typing import List

Memo = dict[str, int]


class Solution:
    def recursiveMaxProfit(
        self,
        prices: List[int],
        idx: int,
        is_holding_stock: bool,
        memo: Memo,
    ) -> int:
        if idx >= len(prices):
            return 0

        memo_key = f"{idx}_{is_holding_stock}"

        if memo_key in memo:
            return memo[memo_key]

        profit_if_skipped = self.recursiveMaxProfit(
            prices, idx + 1, is_holding_stock, memo
        )
        profit_on_trade = 0

        # We have already bought the stock and we can sell it
        if is_holding_stock:
            profit_on_trade = prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, False, memo
            )
        else:
            # We can buy the stock
            profit_on_trade = -prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, True, memo
            )

        profit = max(profit_if_skipped, profit_on_trade)

        memo[memo_key] = profit

        return profit

    def maxProfit(self, prices: List[int]) -> int:
        return self.recursiveMaxProfit(prices, 0, False, dict())

    # dp = [[0, [0, 0]] for _ in range(length_of_prices + 1)]
    # for idx in range(length_of_prices - 1, -1, -1):
    #     dp[idx][1][0] = max(dp[idx + 1][1][0], prices[idx] + dp[idx + 1][1][1])
    #     dp[idx][1][1] = max(dp[idx + 1][1][1], -prices[idx] + dp[idx + 1][1][0])
    def maxProfitDp(self, prices: List[int]) -> int:
        length_of_prices = len(prices)
        # [max profit when holding stock, max profit when not holding stock]
        dp = [[0, 0] for _ in range(length_of_prices + 1)]
        for idx in range(length_of_prices - 1, -1, -1):
            dp[idx][0] = max(dp[idx + 1][0], prices[idx] + dp[idx + 1][1])
            dp[idx][1] = max(dp[idx + 1][1], -prices[idx] + dp[idx + 1][0])

        print(dp)

        # We should return the max profit when we don't have any stock
        # having stock without selling it is not a valid answer
        return dp[0][1]

    def maxProfitDpOptimised(self, prices: List[int]) -> int:
        max_profit_when_holding_stock = 0
        max_profit_when_not_holding_stock = 0

        length_of_prices = len(prices)
        for idx in range(length_of_prices - 1, -1, -1):
            max_profit_when_holding_stock = max(
                max_profit_when_holding_stock,
                prices[idx] + max_profit_when_not_holding_stock,
            )
            max_profit_when_not_holding_stock = max(
                max_profit_when_not_holding_stock,
                -prices[idx] + max_profit_when_holding_stock,
            )

        return max_profit_when_not_holding_stock

sol = Solution()
# print(sol.maxProfit([7, 1, 5, 3, 6, 4]))  # 7
print(sol.maxProfitDp([7, 1, 5, 3, 6, 4]))  # 7
print(sol.maxProfitDpOptimised([7, 1, 5, 3, 6, 4]))  # 7

Sliding Window solution

Sliding Window

py
from typing import List

Memo = dict[str, int]


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        left = 0
        right = 1
        max_profit = 0
        while right < len(prices) and right > left:
            if prices[right] > prices[left]:
                max_profit += prices[right] - prices[left]
            left = right
            right += 1
        return max_profit


sol = Solution()
print(sol.maxProfit([7, 4, 10, 15, 3, 10]))

best-time-to-buy-and-sell-stock-iii

py
from typing import List

Memo = dict[str, int]


class Solution:
    # O(N) ST because memo key is n * 2 decisions * 2 max trades
    def recursiveMaxProfit(
        self,
        prices: List[int],
        idx: int,
        is_holding_stock: bool,
        no_of_trades: int,
        memo: Memo,
    ) -> int:
        if idx >= len(prices):
            return 0

        if no_of_trades >= 2:
            return 0

        memo_key = f"{idx}_{is_holding_stock}_{no_of_trades}"

        if memo_key in memo:
            return memo[memo_key]

        profit_if_skipped = self.recursiveMaxProfit(
            prices, idx + 1, is_holding_stock, no_of_trades, memo
        )
        profit_on_trade = 0

        # We have already bought the stock and we can sell it
        if is_holding_stock:
            profit_on_trade = prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, False, no_of_trades + 1, memo
            )
        else:
            # We can buy the stock
            profit_on_trade = -prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, True, no_of_trades, memo
            )

        profit = max(profit_if_skipped, profit_on_trade)

        memo[memo_key] = profit

        return profit

    def maxProfit(self, prices: List[int]) -> int:
        return self.recursiveMaxProfit(prices, 0, False, 0, dict())

    def maxProfitDp(self, prices: List[int]) -> int:
        length_of_prices = len(prices)

        # dp[i][t] = trade count
        # dp[i][t][0] = profit when holding stock
        # dp[i][t][1] = profit when not holding stock

        dp: List[List[List[int]]] = [
            [[0, 0] for _ in range(2 + 1)] for _ in range(length_of_prices + 1)
        ]
        for idx in range(length_of_prices - 1, -1, -1):
            for trade_idx in range(2):
                dp[idx][trade_idx][0] = max(
                    dp[idx + 1][trade_idx][0],
                    prices[idx] + dp[idx + 1][trade_idx + 1][1],
                )
                dp[idx][trade_idx][1] = max(
                    dp[idx + 1][trade_idx][1], -prices[idx] + dp[idx + 1][trade_idx][0]
                )

        return dp[0][0][1]

    def generateDpArray(self) -> List[List[int]]:
        return [[0, 0] for _ in range(2 + 1)]

    def maxProfitOptimised(self, prices: List[int]) -> int:
        length_of_prices = len(prices)

        # dp[i][t] = trade count
        # dp[i][t][0] = profit when holding stock
        # dp[i][t][1] = profit when not holding stock

        prev = self.generateDpArray()
        next = self.generateDpArray()

        for idx in range(length_of_prices - 1, -1, -1):
            for trade_idx in range(2):
                next[trade_idx][0] = max(
                    prev[trade_idx][0],
                    prices[idx] + prev[trade_idx + 1][1],
                )
                next[trade_idx][1] = max(
                    prev[trade_idx][1], -prices[idx] + prev[trade_idx][0]
                )
            if idx == 0:
                break
            prev = next
            next = self.generateDpArray()

        return next[0][1]


sol = Solution()
print(sol.maxProfitDp([3, 3, 5, 0, 0, 3, 1, 4]))  # 6
print(sol.maxProfitOptimised([3, 3, 5, 0, 0, 3, 1, 4]))  # 6

best-time-to-buy-and-sell-stock-iv

py
from typing import List

Memo = dict[str, int]


class Solution:
    # O(NK) ST because memo key is n * 2 decisions * K max trades
    def recursiveMaxProfit(
        self,
        prices: List[int],
        max_trades: int,
        idx: int,
        is_holding_stock: bool,
        no_of_trades: int,
        memo: Memo,
    ) -> int:
        if idx >= len(prices):
            return 0

        if no_of_trades >= max_trades:
            return 0

        memo_key = f"{idx}_{is_holding_stock}_{no_of_trades}"

        if memo_key in memo:
            return memo[memo_key]

        profit_if_skipped = self.recursiveMaxProfit(
            prices, max_trades, idx + 1, is_holding_stock, no_of_trades, memo
        )
        profit_on_trade = 0

        # We have already bought the stock and we can sell it
        if is_holding_stock:
            profit_on_trade = prices[idx] + self.recursiveMaxProfit(
                prices, max_trades, idx + 1, False, no_of_trades + 1, memo
            )
        else:
            # We can buy the stock
            profit_on_trade = -prices[idx] + self.recursiveMaxProfit(
                prices, max_trades, idx + 1, True, no_of_trades, memo
            )

        profit = max(profit_if_skipped, profit_on_trade)

        memo[memo_key] = profit

        return profit

    def maxProfit(self, max_trades: int, prices: List[int]) -> int:
        return self.recursiveMaxProfit(prices, max_trades, 0, False, 0, dict())

    def maxProfitDp(self, max_trades: int, prices: List[int]) -> int:
        length_of_prices = len(prices)

        # dp[i][t] = trade count
        # dp[i][t][0] = profit when holding stock
        # dp[i][t][1] = profit when not holding stock

        dp: List[List[List[int]]] = [
            [[0, 0] for _ in range(max_trades + 1)] for _ in range(length_of_prices + 1)
        ]
        for idx in range(length_of_prices - 1, -1, -1):
            for trade_idx in range(max_trades):
                dp[idx][trade_idx][0] = max(
                    dp[idx + 1][trade_idx][0],
                    prices[idx] + dp[idx + 1][trade_idx + 1][1],
                )
                dp[idx][trade_idx][1] = max(
                    dp[idx + 1][trade_idx][1], -prices[idx] + dp[idx + 1][trade_idx][0]
                )

        return dp[0][0][1]

    def generateDpArray(self, max_trades: int) -> List[List[int]]:
        return [[0, 0] for _ in range(max_trades + 1)]

    def maxProfitOptimised(self, max_trades: int, prices: List[int]) -> int:
        length_of_prices = len(prices)

        # dp[i][t] = trade count
        # dp[i][t][0] = profit when holding stock
        # dp[i][t][1] = profit when not holding stock

        prev = self.generateDpArray(max_trades)
        next = self.generateDpArray(max_trades)

        for idx in range(length_of_prices - 1, -1, -1):
            for trade_idx in range(max_trades):
                next[trade_idx][0] = max(
                    prev[trade_idx][0],
                    prices[idx] + prev[trade_idx + 1][1],
                )
                next[trade_idx][1] = max(
                    prev[trade_idx][1], -prices[idx] + prev[trade_idx][0]
                )
            if idx == 0:
                break
            prev = next
            next = self.generateDpArray(max_trades)

        return next[0][1]


sol = Solution()
print(sol.maxProfit(2, [2, 4, 1]))
print(sol.maxProfitOptimised(2, [2, 4, 1]))

Best time to buy and sell stock with cooldown

py
from typing import List

Memo = dict[str, int]


class Solution:
    # O(N) ST because memo key is n * 2 decisions
    def recursiveMaxProfit(
        self,
        prices: List[int],
        idx: int,
        is_holding_stock: bool,
        memo: Memo,
    ) -> int:
        if idx >= len(prices):
            return 0

        memo_key = f"{idx}_{is_holding_stock}"

        if memo_key in memo:
            return memo[memo_key]

        profit_if_skipped = self.recursiveMaxProfit(
            prices, idx + 1, is_holding_stock, memo
        )
        profit_on_trade = 0

        # We have already bought the stock and we can sell it
        if is_holding_stock:
            profit_on_trade = prices[idx] + self.recursiveMaxProfit(
                prices, idx + 2, False, memo
            )  # Cool down, so we skip trading on the next day
        else:
            # We can buy the stock
            profit_on_trade = -prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, True, memo
            )

        profit = max(profit_if_skipped, profit_on_trade)

        memo[memo_key] = profit

        return profit

    def maxProfit(self, prices: List[int]) -> int:
        return self.recursiveMaxProfit(prices, 0, False, dict())

    def maxProfitDp(self, prices: List[int]) -> int:
        length_of_prices = len(prices)
        # [max profit when holding stock, max profit when not holding stock]
        dp = [[0, 0] for _ in range(length_of_prices + 2)]
        for idx in range(length_of_prices - 1, -1, -1):
            dp[idx][0] = max(dp[idx + 1][0], prices[idx] + dp[idx + 2][1])
            dp[idx][1] = max(dp[idx + 1][1], -prices[idx] + dp[idx + 1][0])

        print(dp)

        # We should return the max profit when we don't have any stock
        # having stock without selling it is not a valid answer
        return dp[0][1]

    def maxProfitDpOptimised(self, prices: List[int]) -> int:
        length_of_prices = len(prices)

        prev_2_prev = [0, 0]
        prev = [0, 0]
        next = [0, 0]

        for idx in range(length_of_prices - 1, -1, -1):
            next[0] = max(prev[0], prices[idx] + prev_2_prev[1])
            next[1] = max(prev[1], -prices[idx] + prev[0])

            if idx == 0:
                break

            prev_2_prev = prev
            prev = next
            next = [0, 0]

        return next[1]


sol = Solution()

print(sol.maxProfit([1, 2, 3, 0, 2]))  # 3
print(sol.maxProfitDp([1, 2, 3, 0, 2]))  # 3
print(sol.maxProfitDpOptimised([1, 2, 3, 0, 2]))  # 3

Best time to buy and sell stock with transaction fee

py
from typing import List

Memo = dict[str, int]


class Solution:
    # O(N) ST because memo key is n * 2 decisions
    def recursiveMaxProfit(
        self,
        prices: List[int],
        idx: int,
        is_holding_stock: bool,
        fee: int,
        memo: Memo,
    ) -> int:
        if idx >= len(prices):
            return 0

        memo_key = f"{idx}_{is_holding_stock}"

        if memo_key in memo:
            return memo[memo_key]

        profit_if_skipped = self.recursiveMaxProfit(
            prices, idx + 1, is_holding_stock, fee, memo
        )

        profit_on_trade = 0

        # We have already bought the stock and we can sell it
        if is_holding_stock:
            profit_on_trade = (
                prices[idx]
                - fee
                + self.recursiveMaxProfit(prices, idx + 1, False, fee, memo)
            )
        else:
            # We can buy the stock
            profit_on_trade = -prices[idx] + self.recursiveMaxProfit(
                prices, idx + 1, True, fee, memo
            )

        profit = max(profit_if_skipped, profit_on_trade)

        memo[memo_key] = profit

        return profit

    def maxProfit(self, prices: List[int], fee: int) -> int:
        return self.recursiveMaxProfit(prices, 0, False, fee, dict())

    def maxProfitDpOptimised(self, prices: List[int], fee: int) -> int:
        max_profit_when_holding_stock = 0
        max_profit_when_not_holding_stock = 0

        length_of_prices = len(prices)
        for idx in range(length_of_prices - 1, -1, -1):
            max_profit_when_holding_stock = max(
                max_profit_when_holding_stock,
                prices[idx] -fee + max_profit_when_not_holding_stock,
            )
            max_profit_when_not_holding_stock = max(
                max_profit_when_not_holding_stock,
                -prices[idx] + max_profit_when_holding_stock,
            )

        return max_profit_when_not_holding_stock

sol = Solution()
print(sol.maxProfit([1, 3, 2, 8, 4, 9], 2))  # 8
print(sol.maxProfitDpOptimised([1, 3, 2, 8, 4, 9], 2))  # 8

Jump game 1

py
from typing import List, Dict

Memo = Dict[int, bool]


class Solution:
    # n^n
    # aft memo n*n = n^2
    def recursiveJump(self, nums: List[int], idx: int, memo: Memo) -> bool:
        if idx == len(nums) - 1:
            return True

        if idx >= len(nums):
            return False

        next_jump = nums[idx]

        if next_jump == 0:
            return False

        next_jump_max_idx = idx + next_jump

        if next_jump_max_idx == len(nums) - 1:
            return True

        memoised_result = memo.get(next_jump_max_idx, None)
        if memoised_result is not None:
            return memoised_result

        for next_jump_idx in range(idx + 1, next_jump_max_idx + 1):
            if self.recursiveJump(nums, next_jump_idx, memo):
                return True
            memo[next_jump_idx] = False

        return False

    def canJump(self, nums: List[int]) -> bool:
        return self.recursiveJump(nums, 0, dict())

    def canJumpDp(self, nums: List[int]) -> bool:
        nums_length = len(nums)

        dp = [False for _ in range(nums_length)]
        dp[-1] = True

        for idx in range(nums_length - 2, -1, -1):
            can_jump_upto = nums[idx]
            can_jump_upto_idx = min(idx + can_jump_upto, nums_length - 1)
            for jump_idx in range(idx, can_jump_upto_idx + 1):
                if dp[jump_idx]:
                    dp[idx] = True
                    break

        return dp[0]

    def canJumpGreedy(self, nums: List[int]) -> bool:
        length = len(nums)
        max_reach = 0

        for idx in range(length):
            can_reach = nums[idx]
            can_reach_idx = idx + can_reach
            max_reach = max(max_reach, can_reach_idx)
            if max_reach >= length - 1:
                return True
            if idx >= max_reach:
                return False

        return False


sol = Solution()
print(sol.canJumpGreedy([2, 3, 1, 1, 4]))  # True
print(sol.canJumpGreedy([3, 2, 1, 0, 4]))  # False
print(sol.canJumpGreedy([1, 2, 3]))  # True
print(sol.canJumpGreedy([0]))  # True
print(sol.canJumpGreedy([0, 2, 3]))  # False

Jump game 2

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[2]:::blue

    root --> 3
    3 --> 1.1[1] --> 1.1.2[1] --> 4
    3 --> 1.2[1] --> 4.1[4]
    3 --> 4.2[4]

    root --> 1 --> 1.2.1[1] --> 4.3[4]
py
from typing import List, Dict

Memo = Dict[int, float]


class Solution:
    # O(m^n)
    # O(m^n) nodes + O(n) stack space

    # With memory optimisation
    # O(m*n) ~ 0(n^2) time
    # O(2n)
    def recursiveJump(
        self, nums: List[int], idx: int, jumps_so_far, memo: Memo
    ) -> float:
        length_nums = len(nums)
        if idx >= length_nums - 1:
            return jumps_so_far

        if idx in memo:
            return memo[idx]

        next_max_jump = nums[idx]
        next_max_jump_idx = min(idx + next_max_jump, length_nums)

        min_jumps_so_far = float("inf")
        for jump_idx in range(idx + 1, next_max_jump_idx + 1):
            min_jumps_so_far = min(
                jumps_so_far + self.recursiveJump(nums, jump_idx, 1, memo),
                min_jumps_so_far,
            )

        memo[idx] = min_jumps_so_far
        return memo[idx]

    def jump(self, nums: List[int]) -> int:
        max_jumps = self.recursiveJump(nums, 0, 0, dict())
        if max_jumps == "inf":
            return -1
        return int(max_jumps)

    def jumpDp(self, nums: list[int]) -> int:
        length = len(nums)
        dp = [float("inf") for _ in range(length)]
        dp[0] = 0

        # O(N)
        for idx in range(length):
            # O(M) ~ O(N)
            for jdx in range(idx + 1, min(idx + nums[idx] + 1, length)):
                dp[jdx] = min(dp[jdx], dp[idx] + 1)

        return int(dp[-1])

    # Greedy
    # 0, 1, 2, 3, 4

    # 2, 3, 1, 1, 4
    # ^ (max_reach = 2, curr_max_reach_end = 2, idx = 0)

    # 2, 3, 1, 1, 4
    #    ^ (max_reach = 4, curr_max_reach_end = 2, idx = 4)

    # 2, 3, 1, 1, 4
    #       ^ (max_reach = 4, curr_max_reach_end = 4, idx = 4, jumps = 1), increment jumps,
    #       when idx = curr_max_reach_end


    def jumpGreedy(self, nums: list[int]) -> int:
        length = len(nums)

        jumps = 0
        max_reach = 0
        curr_max_reach_end = 0

        for idx in range(length - 1):
            max_reach = max(max_reach, min(idx + nums[idx], length - 1))
            if idx == curr_max_reach_end:
                curr_max_reach_end = max_reach
                jumps += 1

        return jumps


sol = Solution()
# print(sol.jump([2, 3, 0, 1, 4]))  # 2
# print(sol.jump([2, 3, 1, 1, 4]))  # 2
# print(sol.jump([1, 2, 1, 1, 1]))  # 3

# print(sol.jumpDp([2, 3, 0, 1, 4]))  # 2
# print(sol.jumpDp([2, 3, 1, 1, 4]))  # 2
print(sol.jumpGreedy([2, 3, 1, 1, 4]))
# print(sol.jumpDp([1, 2, 1, 1, 1]))  # 3

Fibonacci

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    f_5[f5]:::blue
    f_5 --> f_4[f4]
    f_5 --> f_3[f3]

    f_4 --> f_3.1[f3]
    f_4 --> f_2[f2]

    f_3 --> f_2.1[f2]
    f_3 --> f_1[f1]

F5 = F4 + F3

py

from typing import Dict

Memo = Dict[int, int]


class Solution:
    def fibRecursive(self, target: int, memo: Memo) -> int:
        if target == 0:
            return 0

        if target == 1:
            return 1

        if target in memo:
            return memo[target]

        memo[target] = self.fibRecursive(target - 1, memo) + self.fibRecursive(
            target - 2, memo
        )

        return memo[target]

    def fib(self, n: int) -> int:
        return self.fibRecursive(n, dict())

    def fibDp(self, target: int) -> int:

        if target == 0:
            return 0

        if target == 1:
            return 1

        dp = [0 for _ in range(target + 1)]
        dp[-1] = 0
        dp[-2] = 1

        for idx in range(target - 2, -1, -1):
            dp[idx] = dp[idx + 1] + dp[idx + 2]

        return dp[0]

    def fibDpOptimised(self, target: int) -> int:

        if target == 0:
            return 0

        if target == 1:
            return 1

        left = 1
        right = 0

        for _ in range(target - 2, -1, -1):
            new_left = left + right
            right = left
            left = new_left

        return left


sol = Solution()
print(sol.fib(2))
print(sol.fibDp(3))
print(sol.fibDpOptimised(3))

Jump game 3

py
from typing import Set, List

Traversed = Set[int]


class Solution:
    # O(2^n) // travesed path avoids duplicate paths, so O(2 * n) = O(n)
    # O(2n) // stack space + traversed set
    def canReachRecursion(self, arr: List[int], idx: int, traversed: Traversed) -> bool:
        arr_len = len(arr)
        arr_last_idx = arr_len - 1

        if idx > arr_last_idx:
            return False

        if idx < 0:
            return False

        value_at_idx = arr[idx]

        if value_at_idx == 0:
            return True

        if idx in traversed:
            return False

        traversed.add(idx)

        return self.canReachRecursion(
            arr, idx - value_at_idx, traversed
        ) or self.canReachRecursion(arr, idx + value_at_idx, traversed)

    def canReach(self, arr: List[int], start: int) -> bool:
        return self.canReachRecursion(arr, start, set())

    def canReachStack(self, arr: List[int], start: int) -> bool:
        arr_len = len(arr)
        arr_last_idx = arr_len - 1

        visited = set([])
        stack = [start]

        while len(stack) > 0:
            idx = stack.pop()

            if idx in visited:
                continue

            visited.add(idx)
            value_at_idx = arr[idx]

            if value_at_idx == 0:
                return True

            left = idx - value_at_idx
            if left >= 0:
                stack.append(left)

            right = idx + value_at_idx
            if right <= arr_last_idx:
                stack.append(right)

        return False


sol = Solution()
print(sol.canReach([4, 2, 3, 0, 3, 1, 2], 5))
print(sol.canReach([3, 0, 2, 1, 2], 2))

print(sol.canReachStack([4, 2, 3, 0, 3, 1, 2], 5))
print(sol.canReachStack([3, 0, 2, 1, 2], 2))

For [3, 0, 2, 1 (starting point), 2]

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

    root[1]:::blue
    root --> 2 --> 3
    root --> 2.1[2] --> 0

Fibonacci

py
array = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34..]
array[3] = array[2] + array[1] # 2 + 1 = 3
ts
export function getNthFib(num: number): number {
	let lastTwo = [0, 1]; // track last two fibs
	for (let idx = 2; idx < num - 1; idx++) {
		const nextFib = lastTwo[0] + lastTwo[1]; // calculate next fib
		lastTwo = [lastTwo[1], nextFib]; // update last two fibs
	}
	return num > 1 ? lastTwo[1] + lastTwo[0] : 0;
}

Pitfalls

  • The first and second fibs are always 0 and 1.

  • If you are finding fib for 1, return 0


Zero sum sub array

ts
export const zeroSumSubarray = (nums: number[]) => {
	const sums: number[] = new Array(nums.length).fill(0);
	// O(N)
	for (let index = 0; index < nums.length; index++) {
		const element = nums[index];
		// O(log N)
		for (let sumIndex = 0; sumIndex <= index; sumIndex++) {
			sums[sumIndex] += element;
			if (sums[sumIndex] === 0) {
				return true;
			}
		}
	}
	return false;
};
ts
export const zeroSumSubarray = (nums: number[]) => {
	if (nums.length === 0) return false;

	const sums: { [T: number]: undefined | true } = {};

	let currentSum = 0;

	for (let index = 0; index < nums.length; index++) {
		const element = nums[index];
		currentSum += element;
		if (sums[currentSum]) {
			return true;
		}
		sums[currentSum] = true;
	}

	return currentSum === 0;
};

console.log(zeroSumSubarray([1, 2, -2, 3]));

// [1, 2, -2, 3]
// {
//   1: true,
//   3: true
//   1: true
// };

if a sum is repeated, then there is a zero sum subarray


Disk Stacking

txt
[2, 1, 2], [3, 2, 3], [2, 2, 8], [2, 3, 4], [2, 2, 1], [4, 4, 5]
[ 2, 1, 2 ], [ 3, 2, 3 ], [ 4, 4, 5 ]
mermaid
flowchart LR
    subgraph array
        1[Floating pointer 2, 1, 2]
        2[Fixed pointer 3, 2, 3]
        3[2, 2, 8]
        4[2, 3, 4]
        5[2, 2, 1]
        6[4, 4, 5]
    end

    subgraph sums
        s1[2]
        s2[3]
        s3[8]
        s4[4]
        s5[1]
        s6[5]
    end

    subgraph sequences
        sq1[null]
        sq2[null]
        sq3[null]
        sq4[null]
        sq5[null]
        sq6[null]
    end
  1. Sort array by height.

  2. Create sums array filled with height.

  3. Create sequences array with default value null.

  4. Two loops, outer loop is fixed, starts from 1'st index

  5. Inner loop starts from 0 - fixed index.

  6. Check if floating index has higher dimensions.

  7. Check if sum at floating + fixed height > sum at fixed.

  8. update sum at fixed index.

  9. update seq at fixed index to floating index.

  10. Keep track of max seq index,

  11. increase max seq index when max seq for outer loop is hight.

txt
[] // fill from front to end, highest first
  1. get max array using max sub sequence push to end

  2. use seq num at max seq for next iteration.

ts
type Disk = [number, number, number];

const buildFromSequence = (
	array: Disk[] = [],
	sequences: number[] = [],
	maxSeqIndex: number | null = null,
	maxSubSequence: Disk[] = []
): Disk[] => {
	if (maxSeqIndex === null) return maxSubSequence;
	const valueAtMaxSeqIndex = array[maxSeqIndex];
	const seqAtMaxSeqIndex = sequences[maxSeqIndex];
	// 12.
	maxSubSequence.unshift(valueAtMaxSeqIndex);
	// 13.
	return buildFromSequence(array, sequences, seqAtMaxSeqIndex, maxSubSequence);
};

export function diskStacking(array: Disk[]): Disk[] {
	// 1.need to sort the array by height
	array.sort((a, b) => a[2] - b[2]);
	const arrayLength = array.length;

	// 2.
	const sums = array.map(el => el[2]);

	// 3.
	const sequences = new Array(arrayLength).fill(null);

	// 10.
	let maxSeqIndex = 0;
	// 4. fixed pointer
	array.forEach((fixed, fixedIndex) => {
		const [width, depth, height] = fixed;
		// 5. variable pointer, 0 until fixed pointer
		array.slice(0, fixedIndex).forEach((floating, floatingIndex) => {
			const [widhtFloating, depthFloating, heightFloating] = floating;

			// 6. checking if the floating disk is smaller than the fixed disk
			const isIncreasing =
				widhtFloating < width &&
				depthFloating < depth &&
				heightFloating < height;

			if (isIncreasing) {
				const sumAtFloatingIndex = sums[floatingIndex];
				const sumAtFixedIndex = sums[fixedIndex];

				const newPossibleSum = sumAtFloatingIndex + height;

				// 7.
				if (newPossibleSum > sumAtFixedIndex) {
					// 8
					sums[fixedIndex] = newPossibleSum;
					// 9
					sequences[fixedIndex] = floatingIndex;
				}
			}
		});
		const sumAtFixedIndex = sums[fixedIndex];
		const maxSeqLength = sums[maxSeqIndex];
		// 11
		if (sumAtFixedIndex > maxSeqLength) {
			maxSeqIndex = fixedIndex;
		}
	});

	return buildFromSequence(array, sequences, maxSeqIndex);
}

Longest common subsequence

Write a function that takes in two strings and returns their longest common subsequence.

A subsequence of a string is a set of characters that aren't necessarily adjacent in the string but that are in the same order as they appear in the string. For instance, the characters ["a", "c", "d"] form a subsequence of the string "abcd", and so do the characters ["b", "d"]. Note that a single character in a string and the string itself are both valid subsequences of the string.

You can assume that there will only be one longest common subsequence.

Sample Input

txt
str1 = "ZXVVYZW"
str2 = "XKYKZPW"

Sample Output

txt
["X", "Y", "Z", "W"]
ts
const buildSequence = ({
	lengths = [],
	colString = '',
	sequence = [],
	// going from back
	rowIdx = lengths.length - 1,
	colIdx = lengths[0].length - 1,
}: {
	lengths: number[][];
	colString: string;
	sequence?: string[];
	rowIdx?: number;
	colIdx?: number;
}): string[] => {
	if (rowIdx <= 0) return sequence;
	if (colIdx <= 0) return sequence;

	if (lengths[rowIdx][colIdx] === lengths[rowIdx - 1][colIdx])
		return buildSequence({
			lengths,
			colString,
			sequence,
			rowIdx: rowIdx - 1,
			colIdx,
		});

	if (lengths[rowIdx][colIdx] === lengths[rowIdx][colIdx - 1])
		return buildSequence({
			lengths,
			colString,
			sequence,
			rowIdx,
			colIdx: colIdx - 1,
		});

	sequence.unshift(colString[colIdx - 1]);

	return buildSequence({
		lengths,
		colString,
		sequence,
		rowIdx: rowIdx - 1,
		colIdx: colIdx - 1,
	});
};

export function longestCommonSubsequence(colString: string, rowString: string) {
	// 2d array of 0s
	// y axis is rowString
	// x axis is colString
	// each array begins with empty string
	const lengths = new Array(rowString.length + 1)
		.fill(0)
		.map(() => new Array(colString.length + 1).fill(0));

	lengths.forEach((row, rowIdx) => {
		if (rowIdx === 0) return;
		row.forEach((_, colIdx) => {
			if (colIdx === 0) return;
			const elAtRow = rowString[rowIdx - 1];
			const elAtCol = colString[colIdx - 1];
			if (elAtCol === elAtRow)
				return (lengths[rowIdx][colIdx] = lengths[rowIdx - 1][colIdx - 1] + 1);
			lengths[rowIdx][colIdx] = Math.max(
				lengths[rowIdx - 1][colIdx],
				lengths[rowIdx][colIdx - 1]
			);
		});
	});

	return buildSequence({ lengths, colString });
}
  • Build a 2d array with, [Array length of colString + 1, Array length of rowString + 1] (colString first)

  • Fill the 2d array with 0's

  • Loop over array to get rowIdx Loop over row to get colIdx

  • Get char at strings const elAtRow = rowString[rowIdx - 1]; const elAtCol = colString[colIdx - 1];

  • If both characters are equal, Make the value at lengths[rowIdx][colIdx] equal to lengths[rowIdx - 1][colIdx - 1] + 1

  • Else Make the value at lengths[rowIdx][colIdx] equal to Math.max(lengths[rowIdx - 1][colIdx], lengths[rowIdx][colIdx - 1])

  • Build the sequence

    • Start from the bottom right of the 2d array

    • if current row / current col is equal to 0, return sequence

    • If the value at lengths[rowIdx][colIdx] is equal to the value at lengths[rowIdx - 1][colIdx] then move up by 1 row

    • If the value at lengths[rowIdx][colIdx] is equal to the value at lengths[rowIdx][colIdx - 1] then move left by 1 col

    • unsift the character at colString[colIdx - 1] to the sequence

    • else move up by 1 row and left by 1 col


Longest Increasing Subsequence

O(nlog(n)) time | O(n) space

ts
const buildFromSequence = (
	array: number[] = [],
	sequences: number[] = [],
	maxSeqIndex: number | null = null,
	maxSubSequence: number[] = []
): number[] => {
	if (maxSeqIndex === null) return maxSubSequence;
	const valueAtMaxSeqIndex = array[maxSeqIndex];
	const seqAtMaxSeqIndex = sequences[maxSeqIndex];
	// 12.
	maxSubSequence.unshift(valueAtMaxSeqIndex);
	// 13.
	return buildFromSequence(array, sequences, seqAtMaxSeqIndex, maxSubSequence);
};

export const findNewLength = ({
	startIdx = 1,
	endIdx,
	indices,
	array,
	el,
}: {
	startIdx?: number;
	endIdx: number;
	indices: number[];
	array: number[];
	el: number;
}): number => {
	if (startIdx > endIdx) return startIdx;

	const middleIdx = Math.floor((startIdx + endIdx) / 2);
	const indexValueAtMiddleIdx = indices[middleIdx];
	const arrayValueAtMiddleIdx = array[indexValueAtMiddleIdx];

	// comparing middle value with current el
	// if el is greater move right of pivot
	if (arrayValueAtMiddleIdx < el) {
		// move right of pivot
		return findNewLength({
			startIdx: middleIdx + 1,
			endIdx,
			indices,
			array,
			el,
		});
	}

	// if el is smaller move left of pivot
	return findNewLength({
		startIdx,
		endIdx: middleIdx - 1,
		indices,
		array,
		el,
	});
};

export const longestIncreasingSubsequence = (array: number[]) => {
	const sequences: number[] = new Array(array.length).fill(null);
	const indices: number[] = new Array(array.length + 1).fill(null);
	let maxLength = 0;

	array.forEach((el, index) => {
		const newLength = findNewLength({
			indices,
			array,
			el,
			endIdx: maxLength,
		});
		indices[newLength] = index;
		sequences[index] = indices[newLength - 1];

		maxLength = Math.max(maxLength, newLength);
	});

	console.table({ array, sequences, indices, maxLength });
	return buildFromSequence(array, sequences, indices[maxLength]);
};
  • Create sequences array of equal input array length and fill it with null's

  • Create indices array of input array length + 1 and fill it with null's

  • Track current length of the longest subsequence

  • Loop over main array

  • Do a binary search (startIdx = 1, endIdx = maxLength)

    • if startIdx > endIdx return startIdx

    • find mid index Math.floor(startIdx + endIdx / 2)

    • find value at mid index array[indices[mid]]

    • if value at mid index < el move startIdx right by 1 idx

    • if value at mid index >= el move endIdx left by 1 idx

  • update indices[newLength] = index

  • update sequences[index] = indices[newLength - 1]

  • update maxLength = Math.max(maxLength, newLength)

Build the sequence

  • create empty error [], push items from right to left

  • el = array[maxLength]

  • nextIndex = sequences[maxLength]

  • Recursive array building


You're given a non-empty array of arrays where each subarray holds three integers and represents a disk. These integers denote each disk's width, depth, and height, respectively. Your goal is to stack up the disks and to maximize the total height of the stack. A disk must have a strictly smaller width, depth, and height than any other disk below it.

Write a function that returns an array of the disks in the final stack, starting with the top disk and ending with the bottom disk. Note that you can't rotate disks; in other words, the integers in each subarray must represent [width, depth, height] at all times.

You can assume that there will only be one stack with the greatest total height.

Sample Input

python
disks = [[2, 1, 2], [3, 2, 3], [2, 2, 8], [2, 3, 4], [1, 3, 1], [4, 4, 5]]

Sample Output

python
[[2, 1, 2], [3, 2, 3], [4, 4, 5]]
// 10 (2 + 3 + 5) is the tallest height we can get by
// stacking disks following the rules laid out above.

Solution

python

ts
type Disk = [number, number, number];

const buildFromSequence = (
  array: Disk[] = [],
  sequences: number[] = [],
  maxSeqIndex: number | null = null,
  maxSubSequence: Disk[] = []
): Disk[] => {
  if (maxSeqIndex === null) return maxSubSequence;
  const valueAtMaxSeqIndex = array[maxSeqIndex];
  const seqAtMaxSeqIndex = sequences[maxSeqIndex];
  maxSubSequence.unshift(valueAtMaxSeqIndex);
  return buildFromSequence(array, sequences, seqAtMaxSeqIndex, maxSubSequence);
};

export function maxSumIncreasingSubsequence(array: Disk[]): Disk[] {
  const arrayLength = array.length;
  const heights = array.map(el => el[2]);
  const sequences = new Array(arrayLength).fill(null);

  let maxSeqIndex = 0;
  array.forEach((fixed, fixedIndex) => {
    const [width, depth, height] = fixed;
    array.slice(0, fixedIndex).forEach((floating, floatingIndex) => {
      const [widhtFloating, depthFloating, heightFloating] = floating;
      const isIncreasing =
        widhtFloating < width &&
        depthFloating < depth &&
        heightFloating < height;
      if (isIncreasing) {
        const sumAtFloatingIndex = heights[floatingIndex];
        const sumAtFixedIndex = heights[fixedIndex];
        const newPossibleSum = sumAtFloatingIndex + height;
        if (newPossibleSum > sumAtFixedIndex) {
          heights[fixedIndex] = newPossibleSum;
          sequences[fixedIndex] = floatingIndex;
        }
      }
    });
    const lenghtAtFixedIndex = heights[fixedIndex];
    const maxSeqLength = heights[maxSeqIndex];
    if (lenghtAtFixedIndex > maxSeqLength) {
      maxSeqIndex = fixedIndex;
    }
  });

  return buildFromSequence(array, sequences, maxSeqIndex);
}

Nth Fibonacci

The Fibonacci sequence is defined as follows: the first number of the sequence is 0, the second number is 1, and the nth number is the sum of the (n - 1)th and (n - 2)th numbers. Write a function that takes in an integer n and returns the nth Fibonacci number.

Important note: the Fibonacci sequence is often defined with its first two numbers as F0 = 0 and F1 = 1. For the purpose of this question, the first Fibonacci number is F0; therefore, getNthFib(1) is equal to F0, getNthFib(2) is equal to F1, etc..

text
Sample Input #1
n = 2
Sample Output #1
1 // 0, 1
Sample Input #2
n = 6
Sample Output #2
5 // 0, 1, 1, 2, 3, 5

Longest Increasing Subsequence

Given a non-empty array of integers, write a function that returns the longest strictly-increasing subsequence in the array.

A subsequence of an array is a set of numbers that aren't necessarily adjacent in the array but that are in the same order as they appear in the array. For instance, the numbers [1, 3, 4] form a subsequence of the array [1, 2, 3, 4], and so do the numbers [2, 4]. Note that a single number in an array and the array itself are both valid subsequences of the array.

You can assume that there will only be one longest increasing subsequence.

Sample Input array = [5, 7, -24, 12, 10, 2, 3, 12, 5, 6, 35]

Sample Output [-24, 2, 3, 5, 6, 35]

ts
const buildSequence = (
	array: number[],
	sequences: number[],
	maxSubSeqIndex: number,
	sequence: number[] = []
): number[] => {
	if (maxSubSeqIndex === null) return sequence;
	const valueAtMaxSubSeqIndex = array[maxSubSeqIndex];
	const prevMaxSubSeqIndex = sequences[maxSubSeqIndex];
	return buildSequence(array, sequences, prevMaxSubSeqIndex, [
		valueAtMaxSubSeqIndex,
		...sequence,
	]);
};

export const longestIncreasingSubsequence = (array: number[]) => {
	const sequences: number[] = new Array(array.length).fill(null);
	const lengths = new Array(array.length).fill(1);
	let maxSubSeqIndex: number = 0;
	array.forEach((fixed, fixedIndex): void => {
		console.log('<<< FIXED >>>', fixedIndex);
		array.slice(0, fixedIndex).forEach((variable, variableIndex): void => {
			const lengthAtVariableIndex = lengths[variableIndex];
			const isIncreasing = variable < fixed;
			console.log('<<< VARIABLE >>>', variableIndex);
			console.table({
				array,
				lengths__: lengths,
				sequences: sequences,
			});
			if (isIncreasing && lengthAtVariableIndex + 1 > lengths[fixedIndex]) {
				console.log('increasing');
				lengths[fixedIndex] = lengthAtVariableIndex + 1;
				sequences[fixedIndex] = variableIndex;
			}
		});
		const lengthAtFixedIndex = lengths[fixedIndex];
		const currentSubSeqGreater = lengthAtFixedIndex >= lengths[maxSubSeqIndex];
		if (currentSubSeqGreater) maxSubSeqIndex = fixedIndex;
	});
	console.table({ array, sequences, lengths, maxSubSeqIndex });
	return buildSequence(array, sequences, maxSubSeqIndex);
};

console.log(longestIncreasingSubsequence([1, 5, -1, 10]));

Insertion Sort

Write a function that takes in an array of integers and returns a sorted version of that array. Use the Insertion Sort algorithm to sort the array.

If you're unfamiliar with Insertion Sort, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input [8, 5, 2, 9, 5, 6, 3]

Sample Output [2, 3, 5, 5, 6, 8, 9]

typescript
export const insertionSort = (
	array: number[],
	arrayLength: number = array.length,
	isDesending = false,
	lastSwapPointer: number = 0,
	index = 0
): number[] => {
	// pointers
	const nextIndex = index + 1;
	const prevIndex = index - 1;

	// values
	const el = array[index];
	const nextEl = array[nextIndex];
	const prevEl = array[prevIndex];

	if (isDesending) {
		if (prevEl > el) {
			// swap
			[array[prevIndex], array[index]] = [el, prevEl];
		}
		// no more values to desend to so set to last swap pointer
		if (index === 1) {
			isDesending = false;
			index = lastSwapPointer;
			return insertionSort(
				array,
				arrayLength,
				isDesending,
				lastSwapPointer,
				index
			);
		}
		index = prevIndex;
		return insertionSort(
			array,
			arrayLength,
			isDesending,
			lastSwapPointer,
			index
		);
	}
	if (nextEl < el) {
    // swap
		[array[nextIndex], array[index]] = [el, nextEl];
		lastSwapPointer = nextIndex;
		isDesending = true;
	}
	index = nextIndex;
	if (nextIndex === arrayLength) {
		return array;
	}
	return insertionSort(array, arrayLength, isDesending, lastSwapPointer, index);
};
```

```typescript
export const insertionSort = (array: number[]): number[] => {
	// increment to next value
	for (let outerPointer = 1; outerPointer < array.length; outerPointer += 1) {
		// check previous values
		for (
			let innerPointer = outerPointer;
			innerPointer > 0 && array[innerPointer] < array[innerPointer - 1];
			innerPointer -= 1
		) {
			const previnnerPointer = innerPointer - 1;
			const prevEl = array[previnnerPointer];
			const el = array[innerPointer];
			[array[innerPointer - 1], array[innerPointer]] = [el, prevEl];
		}
	}
	return array;
};
text
Time complexity: O(N^2)
since we have to iterate through the array multiple times to sort the array

---------------------
    UP
[8, 5, 2, 9, 5, 6, 3]
[5, 8, 2, 9, 5, 6, 3] swap
    ^
---------------------
       UP
[5, 8, 2, 9, 5, 6, 3]
       ^
[5, 2, 8, 9, 5, 6, 3]
    ^
[5, 2, 8, 9, 5, 6, 3]
 ^
[2, 5, 8, 9, 5, 6, 3]

---------------------
          UP
[2, 5, 8, 9, 5, 6, 3]
          ^
---------------------
             UP
[2, 5, 8, 9, 5, 6, 3]
             ^
[2, 5, 8, 5, 9, 6, 3]
          ^
[2, 5, 5, 8, 9, 6, 3]
       ^
[2, 5, 5, 8, 9, 6, 3]

---------------------
                UP
[2, 5, 5, 8, 9, 6, 3]
                ^
[2, 5, 5, 8, 6, 9, 3]
             ^
[2, 5, 5, 6, 8, 9, 3]
          ^
---------------------
                   UP
[2, 5, 5, 6, 8, 9, 3]
                   ^
[2, 5, 5, 6, 8, 3, 9]
                ^
[2, 5, 5, 6, 3, 8, 9]
             ^
[2, 5, 5, 3, 6, 8, 9]
          ^
[2, 5, 3, 5, 6, 8, 9]
       ^
[2, 3, 5, 5, 6, 8, 9]

---------------------

Insertion sort is better that bubble sort because, it does not have to swap every value in the array. It only swaps values that are out of order.

text
 >  <
[8, 5, 2, 9, 5, 6, 3]
    >  <
[5, 8, 2, 9, 5, 6, 3]
          >  <
[5, 2, 8, 9, 5, 6, 3]
             >  <
[5, 2, 8, 5, 9, 6, 3]
                >  <
[5, 2, 8, 5, 6, 9, 3]
 >  <
[5, 2, 8, 5, 6, 3, 9]
[2, 5, 8, 5, 6, 3, 9]
       >  <
[2, 5, 8, 5, 6, 3, 9]
[2, 5, 5, 8, 6, 3, 9]
...

Bubble Sort

Write a function that takes in an array of integers and returns a sorted version of that array. Use the Bubble Sort algorithm to sort the array.

if you're unfamiliar with Bubble Sort, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input array = [8, 5, 2, 9, 5, 6, 3]

Sample Output [2, 3, 5, 5, 6, 8, 9]

ts
export function bubbleSort(
  array: number[],
  arrayLength = array.length,
  maxIndex = arrayLength - 1
): number[] {
  let noMutations = true;
  let largestValueIndex = maxIndex;
  // O(N^2)
  for (let index = 0; index <= largestValueIndex; index++) {
    const nextIndex = index + 1;

    const currentValue = array[index];
    const nextValue = nextIndex <= maxIndex ? array[nextIndex] : null;

    if (typeof nextValue === 'number' && nextValue < currentValue) {
      // only value is updated in array
      array[index] = nextValue;
      // array length remains unchanged
      array[nextIndex] = currentValue;
      noMutations = false;
    }

    // reduce array iteration length when for loop ends
    // the last index will always be the largest possible element,
    if (index === largestValueIndex) largestValueIndex = largestValueIndex - 1;
  }
  if (noMutations) return array;
  return bubbleSort(array, arrayLength, maxIndex);
}

Time complexity: O(N^2) since we have to iterate through the array multiple times to sort the array

Space complexity: O(1)

Best case: O(N) // if input array is already sorted


Quick Sort

Write a function that takes in an array of integers and returns a sorted version of that array. Use the Quick Sort algorithm to sort the array.

if you're unfamiliar with Quick Sort, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input array = [8, 5, 2, 9, 5, 6, 3]

Sample Output [2, 3, 5, 5, 6, 8, 9]

Solution

typescript
export const quickSort = (array: number[] = []): number[] => {
  if (!array.length) return [];

  const pivotIndex = Math.floor(array.length / 2);
  const pivot = array.slice(pivotIndex)[0];
  const [left, right]: [number[], number[]] = [[], []];

  array.forEach((el, index) => {
    if (pivotIndex === index) return;
    if (el <= pivot) return left.push(el);
    return right.push(el);
  });

  const returnVal = quickSort(left).concat(pivot).concat(quickSort(right));
  console.log('array', {
    pivot,
    left,
    right,
    returnVal,
  });
  return returnVal;
};

//                                                               7
//                                                     [8, 5, 2, 9, 5, 6, 3]
//                                                               ^
//                                                     4   
//                                          [ 8, 5, 2, 5, 6, 3 ]   []
//                                                     ^
//                                             1              5
//                                        [ 5, 2, 3 ]    [ 8, 6 ]
//                                             ^              ^
//                                                    2        6
//        * pivot                            [ ] [ 5, 3 ]    [ 8 ]
//                                                    ^  3     ^
//        * left + right                               [ 5 ]    [ ]               Callstack execution order (LIFO) ^^^ 
//                                                                                Values are returned from left (bottom-up) - pivot - right (bottom-up)
//

Time Complexity: O(nlog(n)) - best, average, O(n^2) - worst Space Complexity: O(log(n)) - best, average, O(n) - worst

Time Complexity is O(n^2) when the pivot is either the start or end of array, and the array is presorted.


Merge Sort

Write a function that takes in an array of integers and returns a sorted version of that array. Use the Merge Sort algorithm to sort the array.

If you're unfamiliar with Merge Sort, we recommend watching the Conceptual Overview section of this question's video explanation before starting to code.

Sample Input [8, 5, 2, 9, 5, 6, 3]

Sample Output

[2, 3, 5, 5, 6, 8, 9]

typescript
const splitArray = (splits: number[][]): number[][] => {
	if (splits[0].length <= 1) return splits;
    // Time complexity O(Log(N)) since the input array is split in half each time
	return splitArray(
		splits.reduce((prevSplits: number[][], split: number[]): number[][] => {
			const midIndex = Math.ceil(split.length / 2);
			const firstHalf = split.slice(0, midIndex);
			const theRest = split.slice(midIndex);
			if (theRest.length > 0) return prevSplits.concat([firstHalf, theRest]);
			return prevSplits.concat([firstHalf]);
		}, [])
	);
};

// Time complexity O(N), going through each element in array to join arrays
const mergeChunks = (
	firstChunk: number[] = [],
	secondChunk: number[] = [],
	combinedChunk: number[] = []
): number[] => {
	if (firstChunk.length === 0 && secondChunk.length === 0) return combinedChunk;
	const [first = null] = firstChunk;
	const [second = null] = secondChunk;
	const isFirstValid = typeof first === 'number';
	const isSecondValid = typeof second === 'number';
	const firstChuckTrimmed = firstChunk.slice(1);
	const secondChunkTrimmed = secondChunk.slice(1);

	if (!isFirstValid && !isSecondValid) return combinedChunk;

	if (isFirstValid && !isSecondValid) {
		combinedChunk.push(first);
		return mergeChunks(firstChunk.slice(1), secondChunk, combinedChunk);
	}

	if (!isFirstValid && isSecondValid) {
		combinedChunk.push(second);
		return mergeChunks(firstChunk, secondChunk.slice(1), combinedChunk);
	}

	if (!isFirstValid || !isSecondValid) return combinedChunk;

	const isFirstChunkSmallest = first < second;

	if (isFirstValid && isFirstChunkSmallest) {
		combinedChunk.push(first);
	}

	if (isSecondValid && !isFirstChunkSmallest) {
		combinedChunk.push(second);
	}

	return mergeChunks(
		isFirstChunkSmallest ? firstChuckTrimmed : firstChunk,
		!isFirstChunkSmallest ? secondChunkTrimmed : secondChunk,
		combinedChunk
	);
};

export const mergeSort = (
	array: number[],
	splits: number[][] = splitArray([array])
): number[] => {
	if (splits.length === 1) return splits[0];
	const newSplits: number[][] = [];
	for (let index = 0; index <= array.length; index += 2) {
		const midPoint = index + 1;
		const lastPoint = midPoint + 1;
		const [firstChunk = []] = splits.slice(index, midPoint);
		const [secondChunk = []] = splits.slice(midPoint, lastPoint);
		const newChunks = mergeChunks(firstChunk, secondChunk);
		if (newChunks.length > 0) newSplits.push(newChunks);
	}
	return mergeSort(array, newSplits);
};

console.log(
	mergeSort([
		-4, 5, 10, 8, -10, -6, -4, -2, -5, 3, 5, -4, -5, -1, 1, 6, -7, -6, -7, 8,
	])
);

Output

text
== Recursive Splits =
[8, 5, 2, 9, 5, 6, 3] O(Log(N)) since the input array is split in half each time
[8, 5, 2, 9][5, 6, 3]
[8, 5][2, 9][5, 6][3]
[8][5][2][9][5][6][3]

== Recursive joins =
[8][5][2][9][5][6][3] O(N), going through each element in array to join arrays 
[5, 8][2, 9][5, 6][3]
[5, 8][2, 9][5, 6][3]
[2, 5, 8, 9][3, 5, 6]
[2, 5, 8, 9][3, 5, 6]
[2, 3, 5, 5, 6, 8, 9]

Shifted binary search

ts
const findTarget = (
	array: number[],
	target: number,
	leftIdx: number,
	rightIdx: number
): number => {
	const midIdx = Math.floor((leftIdx + rightIdx) / 2);
	const mid = array[midIdx];
	const left = array[leftIdx];
	const right = array[rightIdx];

	if (leftIdx > rightIdx) {
		return -1;
	}

	if (mid === target) {
		return midIdx;
	}

	// is left sorted
	if (mid >= left) {
		if (target >= left && target < mid) {
			// we already know that mid is not the target,
			// so go back to the left side
			return findTarget(array, target, leftIdx, midIdx - 1);
		}
		// target is not in the left side, switch to the right side
		return findTarget(array, target, midIdx + 1, rightIdx);
	}

	if (target > mid && target <= right) {
		// target is in the right side
		return findTarget(array, target, midIdx + 1, rightIdx);
	}

	// target is in the left side
	return findTarget(array, target, leftIdx, midIdx - 1);
};

export const shiftedBinarySearch = (array: number[], target: number) => {
	return findTarget(array, target, 0, array.length - 1);
};

console.log(shiftedBinarySearch([45, 61, 71, 72, 73, 0, 1, 21, 33, 45], 43)); // 8

Key points:

  • In a rotated array, one side will always be sorted.

  • If the left side is not sorted then the right side will be sorted.

  • If the sorted side doesn't have the target, then the target will be on the other side.


Binary Search

py
target = 72
mermaid
flowchart TD
    array[Sorted Array: 0, 1, 21, 33, 45, 45, 61, 71, 72, 73]
    1[0, 1, 21, 33, 45]
    2[45, 61, 71, 72, 73]
    3[45, 61]
    4[71, 72, 73]
    5[71]
    6[72, 73]
    7[72]

    array -->|baseidx = 0| 1
    array -->|baseidx = 5| 2
    2 -->|5| 3
    2 -->|7| 4
    4 -->|7| 5
    4 -->|8| 6
    6 -->|base idx + middle idx| 7
ts
// O(log(n)) time | O(1) space
export const binarySearch = (
	array: number[],
	target: number,
	baseIdx = 0
): number => {
	if (array.length === 0) return -1;

	const middleIdx = Math.floor(array.length / 2);
	const middleValue = array[middleIdx];

	if (middleValue === target) return baseIdx + middleIdx;
	if (array.length === 1) return -1;

	if (target >= middleValue)
		return binarySearch(array.slice(middleIdx), target, baseIdx + middleIdx); // <-- inc

	return binarySearch(array.slice(0, middleIdx), target, baseIdx); // <-- same
};

Pitfalls

  • Base idx needs to be passed down to the recursive call

  • If you are going left, base idx stays the same

  • If you are going right, base idx needs to be incremented by the middle idx

Prefix Ties

mermaid
flowchart TD
    classDef green fill:#ccffcc,stroke:#000,stroke-width:2px,color:#000000;
    classDef red fill:#ffcccc,stroke:#000,stroke-width:2px,color:#000000;
    classDef blue fill:#ccccff,stroke:#000,stroke-width:2px,color:#000000;
    classDef orange fill:#ffcc99,stroke:#000,stroke-width:2px,color:#000000;
    classDef yellow fill:#ffff99,stroke:#000,stroke-width:2px,color:#000000;

  root[-] --> a --> p --> p1[p] --> l --> e --> end_app1le[$]:::red
  p --> e1 --> end_ape1[$]:::red

startsWith("ap") func is very efficient in tries. O(1)

ts
interface Children {
	[key: string]: Node;
}

class Node {
	name: string = '';
	children: Children = {};

	constructor(name: string) {
		this.name = name;
	}

	addChild(name: string) {
		const child = new Node(name);
		this.children[name] = child;
	}

	hasChild(name: string): boolean {
		if (this.children[name]) return true;
		return false;
	}

	getChild(name: string): Node {
		return this.children[name];
	}
}

class Trie {
	tree: Node = new Node('');

	constructor() {}

	// N = characters in the word
	// O(N)T | O(N)S
	insert(word: string, currIdx = 0, node = this.tree): void {
		const char = word[currIdx];
		if (!char) {
			node.addChild('$');
			return;
		}
		if (node.hasChild(char)) {
			this.insert(word, currIdx + 1, node.children[char]);
			return;
		}
		node.addChild(char);
		this.insert(word, currIdx + 1, node.children[char]);
		return;
	}

	// O(N)T | O(N)S, recursive stack
	search(word: string, currIdx = 0, node = this.tree): boolean {
		const char = word[currIdx];
		if (!char) {
			return node.hasChild('$');
		}
		if (!node.hasChild(char)) {
			return false;
		}
		return this.search(word, currIdx + 1, node.getChild(char));
	}

	// O(N)T | O(N)S recursive stack
	startsWith(word: string, currIdx = 0, node = this.tree): boolean {
		const char = word[currIdx];
		if (!char) {
			return true;
		}
		if (!node.hasChild(char)) {
			return false;
		}
		return this.startsWith(word, currIdx + 1, node.getChild(char));
	}
}

/**
 * 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)
 */

Prefix Ties but match .

ts
interface Children {
	[key: string]: Node;
}

class Node {
	name: string = '';
	children: Children = {};

	constructor(name: string) {
		this.name = name;
	}

	addChild(name: string) {
		const child = new Node(name);
		this.children[name] = child;
	}

	hasChild(name: string): boolean {
		if (this.children[name]) return true;
		return false;
	}

	getChild(name: string): Node {
		return this.children[name];
	}

	getChildren(): Node[] {
		return Object.values(this.children);
	}
}

class WordDictionary {
	tree: Node = new Node('');

	constructor() {}

	addWord(word: string, currIdx = 0, node = this.tree): void {
		const char = word[currIdx];
		if (!char) {
			node.addChild('$');
			return;
		}
		if (node.hasChild(char)) {
			this.addWord(word, currIdx + 1, node.children[char]);
			return;
		}
		node.addChild(char);
		this.addWord(word, currIdx + 1, node.children[char]);
		return;
	}

	// O(N)T | O(clidren_with_wild_cards^N)S
	search(word: string, currIdx = 0, node = this.tree): boolean {
		const char = word[currIdx];
		if (!char) {
			return node.hasChild('$');
		}
		if (char === '.') {
			for (let child of node.getChildren()) {
				if (this.search(word, currIdx + 1, child)) return true;
			}
			return false;
		}
		if (!node.hasChild(char)) {
			return false;
		}
		return this.search(word, currIdx + 1, node.getChild(char));
	}
}

Word search 2

ts
interface Children {
	[key: string]: PrefixTrie;
}

class PrefixTrie {
	public node: string;
	public children: Children = {};

	constructor(char: string) {
		this.node = char;
	}

	hasChild(char: string): boolean {
		return this.children[char] !== undefined;
	}

	addChild(char: string): PrefixTrie {
		if (this.hasChild(char)) {
			return this.children[char];
		}
		this.children[char] = new PrefixTrie(char);
		return this.children[char];
	}

	hasWord(word: string, currentIdx = 0): boolean {
		const char = word[currentIdx];
		if (!char) return true;
		if (!this.hasChild(char)) return false;
		return this.hasWord.call(this.children[char], word, currentIdx + 1);
	}

	addWord(word: string, currentIdx = 0): void {
		const char = word[currentIdx];
		if (!char) return;
		const child = this.addChild(char);
		this.addWord.call(child, word, currentIdx + 1);
	}
}

interface AlreadyTraversedPath {
	[key: string]: boolean;
}

const dfs = (
	board: string[][],
	rowIdx: number,
	colIdx: number,
	trie: PrefixTrie,
	traversedString = '',
	alreadyTraversedPath: AlreadyTraversedPath = {},
	found: { [key: string]: boolean } = {},
	wordsHash: { [key: string]: boolean } = {}
) => {
	if (
		alreadyTraversedPath[`${rowIdx}-${colIdx}`] ||
		!trie.hasWord(traversedString)
	) {
		const validString = traversedString.slice(0, -1);
		if (wordsHash[validString]) {
			found[validString] = true;
		}
		return;
	}

	const char = board[rowIdx][colIdx];
	traversedString += char;
	alreadyTraversedPath[`${rowIdx}-${colIdx}`] = true;

	const left = board[rowIdx]?.[colIdx - 1];
	const right = board[rowIdx]?.[colIdx + 1];
	const top = board[rowIdx - 1]?.[colIdx];
	const bottom = board[rowIdx + 1]?.[colIdx];

	if (right) {
		dfs(
			board,
			rowIdx,
			colIdx + 1,
			trie,
			traversedString,
			{
				...alreadyTraversedPath,
			},
			found,
			wordsHash
		);
	}
	if (bottom) {
		dfs(
			board,
			rowIdx + 1,
			colIdx,
			trie,
			traversedString,
			{
				...alreadyTraversedPath,
			},
			found,
			wordsHash
		);
	}
	if (left) {
		dfs(
			board,
			rowIdx,
			colIdx - 1,
			trie,
			traversedString,
			{
				...alreadyTraversedPath,
			},
			found,
			wordsHash
		);
	}
	if (top) {
		dfs(
			board,
			rowIdx - 1,
			colIdx,
			trie,
			traversedString,
			{
				...alreadyTraversedPath,
			},
			found,
			wordsHash
		);
	}
	if (wordsHash[traversedString]) {
		found[traversedString] = true;
	}
	return;
};

function findWords(board: string[][], words: string[]): string[] {
	const wordsHash: { [key: string]: boolean } = {};
	const trie = new PrefixTrie('');
	for (const word of words) {
		trie.addWord(word);
		wordsHash[word] = true;
	}
	const found: { [key: string]: boolean } = {};
	for (let rowIdx = 0; rowIdx < board.length; rowIdx++) {
		for (let colIdx = 0; colIdx < board[rowIdx].length; colIdx++) {
			dfs(board, rowIdx, colIdx, trie, '', {}, found, wordsHash);
		}
	}
	return Object.keys(found);
}
  • Create a trie for all the words

  • check if dfs traversed word is present in the trie

  • if its not return, break traversal

Phone number memonics

ts
const memonicMap: { [key: string]: string[] } = {
	2: ['a', 'b', 'c'],
	3: ['d', 'e', 'f'],
	4: ['g', 'h', 'i'],
	5: ['j', 'k', 'l'],
	6: ['m', 'n', 'o'],
	7: ['p', 'q', 'r', 's'],
	8: ['t', 'u', 'v'],
	9: ['w', 'x', 'y', 'z'],
};
export function phoneNumberMnemonics(
	phoneNumber: string,
	currentIdx = 0,
	combinations: string[] = []
): string[] {
	if (currentIdx === phoneNumber.length) {
		return combinations;
	}
	const number = phoneNumber[currentIdx];
	const characters = memonicMap[number];
	if (currentIdx === 0 && !characters) {
		combinations.push(number);
		return phoneNumberMnemonics(phoneNumber, currentIdx + 1, combinations);
	}
	if (currentIdx === 0) {
		return phoneNumberMnemonics(phoneNumber, currentIdx + 1, characters);
	}
	if (!characters) {
		for (let idx = 0; idx < combinations.length; idx++) {
			combinations[idx] = `${combinations[idx]}${number}`;
		}
		return phoneNumberMnemonics(phoneNumber, currentIdx + 1, combinations);
	}
	const newCombinations = [];
	for (
		let combinationIdx = 0;
		combinationIdx < combinations.length;
		combinationIdx++
	) {
		const combination = combinations[combinationIdx];
		for (let charIdx = 0; charIdx < characters.length; charIdx++) {
			newCombinations.push(`${combination}${characters[charIdx]}`);
		}
	}
	return phoneNumberMnemonics(phoneNumber, currentIdx + 1, newCombinations);
}

Time and space is O(K^N) where K is the number of characters in the phone number and N is the length of the phone number O(4 memonic*map_length ^ N digits) * O(n); no of combinations _ at each combination we are creating a new string of length n by pushing a new character


Permutations

ts
const generatePermutations = (
	arr: any[],
	permutations: typeof arr = [],
	fixedIndex = 0
) => {
	if (fixedIndex === arr.length - 1) {
		permutations.push([...arr]);
		return;
	}

	for (
		let variableIndex = fixedIndex, maxIndex = arr.length - 1;
		variableIndex <= maxIndex;
		variableIndex++
	) {
		// swap fixed and variableIndex value
		[arr[fixedIndex], arr[variableIndex]] = [
			arr[variableIndex],
			arr[fixedIndex],
		];
		generatePermutations(arr, permutations, fixedIndex + 1);
		// swap back
		[arr[fixedIndex], arr[variableIndex]] = [
			arr[variableIndex],
			arr[fixedIndex],
		];
	}
};

export const getPermutations = (arr: any[]) => {
	const permutations: any[] = [];
	generatePermutations(arr, permutations);
	return permutations;
};

Pitfalls

  • permutations.push(array.slice()); need to push a copy of the array, not the reference.

  • Need to swap back the items after the recursive call.

Execution pattern

txt
[1, 2, 3, 4]
 Fv
[1, 2, 3, 4] => [2, 1, 3, 4] ...
 F  v               Fv
[1, 2, 3, 4] => [3, 2, 1, 4] ...
 F     v            Fv

System Design

No title


Why do we need to learn system design?

  • Coding interviews test the software engineer on problem solving.

  • Systems design interview,
    require a lot of knowledge on how to build,
    robust, scalable, maintainable web applications.

What are Design Fundamentals?

  • Questions are very vague, and open ended.

  • You need to ask questions to clarify the scope of the system (functions, features, tech-scope).

  • A proposed solution is not enough, you need to justify your solution.

  • You need to be able to identify and handle trade-offs.

  • You need to convince the interviewer that your design choices are good.

  • This is nearly impossible without understanding the fundamentals of system design.

  • Design fundamentals are in different flavors,

    • Underlying / foundational knowledge.

    • Key characteristics of a system.
      ( availability, scalability, latency, throughput, consistency, reliability, durability, maintainability, cost, etc. )

    • Actual components of a system.
      ( load balancer, cache, database, rate limiter, etc. )

    • Real world technologies.
      ( Nginx, AWS S3, MongoDB, Redis, MySQL, etc. )
      This is the most important part of system design.

Client Server Architecture

  • This is the foundation of modern computing.

  • A client requests data from a server, a server responds to the client, with data.

  • The client does not know the behind the scenes of the server.

mermaid
graph LR
A[Client] --req--> B[Server]
B --res--> A

Storage

  • Storage is a key component of any system.

  • DB serves two purposes:

    • Storing / Reading

    • Recording / Querying data.

  • DB is just a server,

  • Persistence, A data stored in a DB, should be available even after the server is undergoes outages.

  • Data Storage mechanisms,

    • Disk If DB is stored in disk, it can be recovered even after a server outage.

    • Memory RAM If DB is stored in memory, it cannot be recovered after a server outage, faster than disk.

  • There are multiple storage vendors, Storage is very complex, there are many database offerings,

  • Different databases are optimized for different use cases.

  • Distributed DB, (will learn later)

  • Data Consistency

Latency and Throughput

Latency

  • How long does it take for data to traverse from one point in the system to another.

  • Network Latency, (network latency is the time it takes for a packet to travel from source to destination)

  • Disk Latency, (disk latency is the time it takes for a disk to read or write data)

  • Memory Latency, (memory latency is the time it takes for a memory to read or write data)

  • Certain operations have more latency than others.
    1 millisecond = 1000 microseconds

    Operation
    Latency

    Sequential Read from Memory

    250 microseconds

    Sequential Read from SSD

    1000 microseconds

    Sequential Read from HDD

    2 milliseconds

    Send 1MB over 1Gbps network

    10 milliseconds

    Send a packet (<1MB) on a round trip from CA to Netherlands

    150 milliseconds

  • Certain types of systems really care about low latency ( video games )

  • Certain types of systems really don't care about low latency ( accuracy of stock prices, or weather forecast, more up time )

Throughput

  • How much work can a machine do in a given amount of time.

  • Throughput is measured in operations per second.

  • How many requests can a server handle per second, how many bits can a network transfer per second.

  • Naive way to increase throughput is to pay to increase system resource.

  • Certain types of systems really care about high throughput ( video streaming )

  • A better way to increase throughput is to optimize the system. ( caching, load balancing, etc. )

Throughput and Latency is not correlated.

system
throughput
latency

high

high

low

low

low

hight

ideal

high

low

Availability

What is availability?

  • Availability is the probability that a system will be up in a given time.

  • How resistant is a system to failure, How fault tolerant is a system.

  • Most systems have a implied availability of 99.9%. There is a implied guarantee of availability, when you use a system.

  • Any system designer has to account for availability.

How is availability measured?

Availability is measured in 9s. ( 99.9% = 3 9s )

No of 9's
Downtime per year

99.9%

8.76 hours

99.99%

52.56 minutes

99.999%

5.26 minutes

99.9999%

31.5 seconds

Highly available systems have 5 9s.
Gold standard for availability.

Why is availability important?

  • Airplane manufactures have a implied availability of 99.9999%. Any amount of downtime is unacceptable.

  • Cloud providers, if parts of cloud providers go down, it can affect millions of users.

SLA

  • SLA ( Service Level Agreement )
    Agreement between a service provider and a customer, guaranteeing a certain level of availability.

  • SLA is part of an SLO ( Service Level Objective )

  • Even though availability is taken seriously, it is not the only thing that matters.

  • High availability is expensive, and non trivial to achieve.

  • You need to decide which parts of your system need high availability, and which parts of your system can tolerate downtime.

Redundancy

No title

  • A system should not have a single point of failure.

  • Adding multiple servers, to handle requests, need load balancer.

  • now load balancer is a single point of failure.

Passive Redundancy

  • Just like a twin engine airplane, you need to have redundancy. If one engine fails, the other engine can keep the plane in the air.

Active Redundancy

  • If multiple servers are active, and one server fails, the other servers can reconfigure themselves to handle the load. ( Leader election, Zookeeper, etc. )

  • In order to restore crashed servers, We need human intervention and certain processes need to be followed, in order to achieve this.

Caching

  • Caching is used to reduce latency and increase throughput.

  • Caching is used to increase speed and reduce load on the database.

  • Caching is storing data in a place other than the source, to increase fetch speed

  • Client can cache data, server can cache data, database can cache data.

  • CPU caches are a thing, they are used to increase speed of computation.

  • Caching occurs by default in a lot of places.

  • Client / Server can cache, network requests, computational large tasks, etc.

  • Multiple servers queries the same data from the database, Here a server reaches out to a single caching server, before reaching out to the database.

  • Repeated queries can be cached, to reduce latency.

Examples of caching in the real world

  • Browser caching, ( browser caches images, css, js, etc. )

  • Server level caching / Redis in memory caching, ( caching frequently accessed data ) Hashing requests, and storing the result in a cache. if the same request is made again, the result is fetched from the cache.

  • Write through cache, ( data is written to the cache and the database )

    • Cache is updated first, then the database is updated.

    • if the data is not in the cache, it is fetched from the database, and written to the cache.

  • Write back cache, ( data is written to the cache first, and then the database )

    • Only cache is updated first. database is asynchronously updated, with the data in the cache. ( interval of 5 minutes, or 10 minutes, etc. )

    • if the data is not in the cache, it is fetched from the database,

    • Downside, if the server crashes before scheduled database update, the data in the cache is lost.

  • YouTube comments section

    • Multiple servers are used to serve the comments section.

    • Every server has a copy of the comments section.

    • Clients reach out to the server, to fetch the comments section, server returns in memory cache.

    • Client updates comment, Server updates the comment in the cache, waits for a scheduled time to update the database.

    • Meanwhile the stale data is served to the client, by the other servers.

    Solution,

    • Single caching server [Redis] is used to store the comments section.

    • Few data like ( viewcount, likes, etc. ) are stored in the database, and are updated asynchronously, since we do not care about the accuracy of the data.

  • If data is static, it can be cached for a long time. If data is mutable, caching is tricky, since the data can change at any time.

  • Rule of thumb,

    • Imutable data can be cached for a long time.

    • mutable data should be cached for a short time.

    • If accuracy / consistency / staleness is not important, caching is fine.

    • If cache invalidation mechanisms are in place, caching is fine.

Eviction policies

  • If the cache is full, and a new data needs to be cached, the cache needs to make space for the new data.

  • Eviction policies are used to decide which data to remove from / keep in the cache.

Policies

  • LRU ( Least Recently Used )

    • Remove the data that was least recently used.

  • LFU ( Least Frequently Used )

    • Remove the data that was least frequently used.

  • FIFO ( First In First Out )

    • Remove the data that was first inserted into the cache.

  • LIFO ( Last In First Out )

    • Remove the data that was last inserted into the cache.

  • Random

    • Remove a random data from the cache.

Policy is decided based on the use case.

Forward Proxy AKA Proxy Server

Client knows its taking to a proxy server. Server does not know its talking to a proxy server.

  • Sits between the client and the server.

  • Forwards requests from the client to the server.

  • Client needs to configure the proxy server, to use it.

  • Forward proxy is used to hide the identity of the client.

  • The source IP address of the client is hidden from the server. Its going to be the IP address of the proxy server.

  • VPNs are a type of forward proxy.

  • We can use a forward proxy to bypass firewalls, network restrictions, etc.

Reverse Proxy

Client thinks the reverse proxy is the server. Server knows its talking to a reverse proxy.

  • Sits between the client and the server.

  • Client wants to send a request to the server.

  • When the client sends a request to the server, the request is intercepted by the reverse proxy.

  • Reverse proxy decides which server to send the request to.

  • Reverse proxy forwards the response to the client.

Why use a reverse proxy?

  • Reverse proxy can be used to load balance requests. A load balancer can distribute request load to multiple servers.

  • Reverse proxy can take care of logging, rate-limiting, authentication, caching, filtering / firewall, etc.

Load Balancing

  • A client communicates with a server.

  • Multiple clients can communicate with a single server, taking up all the resources of the server.

  • Vertical scaling, ( increasing the resources of a single server )

  • Horizontal scaling, ( increasing the number of servers )

  • The load balancer is a server that sits between the set of client and a set of server instances.

  • The load balancer evenly distributes the load to the different server instances.

  • Load balancer prevents a single resource from being overloaded. Increases throughput, and reduces latency, makes better use of your resources.

  • Load balancer will be aware about the existence new instances of the server.

  • Load balancer can act as a reverse proxy.

  • Load balancer can be used at the DNS level, to distribute load to different servers. A single domain name can be mapped to multiple IP addresses. Different client requests can be routed to different IP addresses, based on the load balancer config.

Types of load balancer,

  • Hardware load balancer Expensive, but fast. Have limited configuration options.

  • Software load balancer

  • How does a load balancer aware of new instances of the server? When a new instance of the server is created or deleted, it can register / unregister itself with the load balancer.

Load balancer decides which server to send the request to, based on the following configs,

  • Random ( Randomly choose a server )

  • Round Robin ( Send request to each server in a round robin fashion ) Goes through the list of servers in one direction. top to bottom then bottom to top, etc.

  • Weighted Round Robin, Each server has a weight assigned to it. The server with the highest weight gets the most requests. If one of the srever is slow, we can reduce the weight of the server.

  • Least Connections ( Send request to the server with the least number of connections )

  • Performance based ( Send request to the server with the least latency ) Performs health checks on the servers (throughput, latency, CPU usage ), and sends request to the server with the least latency.

  • IP Hash Client IP address is hashed to a number, and the request is sent to the server based on the number. IP based load balancing, is useful when the client needs to maintain a session (session cache) with one server. If the hashing algo is subpar, one server can get overloaded with requests creating a hotspot.

  • Path based ( Send request to the server based on the path ) If the request is for /api, send it to server 1, if the request is for /auth, send it to server 2, etc. If we want to deploy a large change to the auth api, only the auth server needs to be updated.

Server selection strategies depend on the use case. We can use multiple strategies (multiple load balancers) at the same time.

  • Load balancer A, can be used to distribute load to different regions. ( US, Europe, Asia, etc. )

  • Load balancer A, B and C can communicate with each other. If a server is down, the load balancer can send the request to another load balancer. ( Load balancer A, B and C can be used to distribute load to different servers in the same region. )

Hashing

  • A action that converts a arbitrary input into a fixed length output (typically a number).

  • Request hashing,

    • Client-1 request always gets routed by load balancer to server-1.

    • This is useful when the client needs to maintain in memory cache with one server.

    • mermaid
      graph LR
      client-1 --> LoadBalancer
      LoadBalancer --> server-1
      client-2 --> LoadBalancer
      LoadBalancer --> server-2
      client-3 --> LoadBalancer
      LoadBalancer --> server-3
      client-4 --> LoadBalancer
      LoadBalancer --> server-4
    • A industry standard hashing algorithm like SHA256, ensures that the hash is evenly distributed.

    • If a server dies / is added, the hash routing logic needs to be updated.

    • When the routing logic is updated, the client request redirection changes.

    • All in memory cache is lost.

    js
    function pickServerSimple(clientId, servers) {
    	return servers[clientId % servers.length];
    }
  • Consistent Hashing techniques

    • Circular hash space is used to map the servers.

    • Conceptually, the hash space is a circle (360 deg), and the servers are mapped to the circle (deg).

    • Client request is hashed to a number (deg), and the request is sent to the server with the closest number in clockwise / anticlockwise (deg).

    • When a server is added / removed, most of the client and server mappings are unchanged.

    • Only the client and server mappings that are close to the server that was added / removed are changed.

    • There is always some level of consistency in the client and server mappings.

    • Servers can have multiple virtual nodes (by using multiple hashing algos).

      • Each server can be mapped to multiple numbers in the hash space.

      • This ensures that the load is evenly distributed.

      • When a server is added / removed, the load is evenly distributed.

    • If you want to add more weight for certain servers, you can add more virtual nodes for those servers.

  • Rendezvous Hashing

    • For every user, we calculate a score for each server.

    js
    const usernames = ['user-1', 'user-2', 'user-3', 'user-4'];
    const serverSetOne = ['server-1', 'server-2', 'server-3', 'server-4'];
    const serverSetTwo = ['server-1', 'server-2', 'server-3'];

    in the above example, we have 4 users and 4 servers. user-1 is mapped to server-1 has its highest ranking server. removing server-4 from the server set, will not change the ranking of user-1.

    if server-1 is removed from the server set, the next highest ranking server for user-1 is server-2.

    js
    function pickServerRendezvous(username, servers) {
    	// for username, calculate the score for each server
    }

Relational Databases

  • Databases types are divided into two major categories,

    • Relational databases

      • Data stored here is in the form of tables aka "relations".

      • Tables store certain types of data.

      • Columns represent attributes of the data.

      • Rows represent a single instance of the data.

      • Data in a relational database is very structured, with a well defined schema.

      • Any entry in the database must follow the schema.

      • Most of them support SQL (Structured Query Language). SELECT from some_table WHERE some_column = some_value;

      • SQL's are very powerful, and can be used to perform complex queries.

      • SQL databases need to perform ACID transactions.

        • Atomicity

          • "All" suboperations need to work "or" everything fails "nothing".

          • If a transaction fails, the database should be rolled back to the previous state.

        • Consistency

          • The database should always be in a consistent state.

          • Any transaction that violates the database schema should be rejected.

          • Any future transactions will take into account the previous transactions.

          • All transactions will know about the previous transactions.

        • Isolation

          • Transactions should not interfere with each other.

          • Transactions executed in parallel will always end up being executed in sequence, as if they were executed in a queue.

        • Durability

          • Once a transaction is committed, it should be persisted.

          • Data stored in a database is effectively permanent ( store on disk ).

      • Whenever a TRANSACTION is performed on a database, the next TRANSACTION hangs until the previous TRANSACTION is completed.

        sql
        $ BEGIN TRANSACTION;
        BEGIN
        $ UPDATE users SET balance = balance - 100 WHERE id = 1;
        UPDATE 1
        ...
        sql
        $ BEGIN TRANSACTION;
        BEGIN
        $ UPDATE users SET balance = balance + 100 WHERE id = 1;
        # hangs until the previous transaction is completed
      • Index

        • For the following query, SELECT * FROM users WHERE transaction_amount >= 100; This is a linear search, and will take a long time to execute.

        • To speed up the query, we can create an index on the transaction_amount column.

        • we can create a additional data structure (A additional table with transaction_amounts, in sorted order), that allows us to quickly find the rows that match the query.

        • Having a database index, will take up more space, When writing to the database, the index needs to be updated.

        • Write operations will be slower, but read operations will be a lot faster.

    • Non-relational databases

      • Do not impose tabular structure on the data.

      • Data is stored in the form of documents, key-value pairs, etc.

      • Any entry in the database does not need to follow a schema.

      • Non-relational databases are also called NoSQL databases.

      • Using Python to run some business logic on, large scale distributed databases with terabytes of data, is not possible without loading the database in run-time memory.

The field of databases is very broad. You don't need to know everything about databases to ace your systems design interview.

Google cloud datastore is a NoSQL database.

  • does not support SQL queries.

  • only has eventual consistency.

  • updates do not reflect immediately.

  • ACID transactions are immediately available.

Selecting Databases for your application, is a trade-off between consistency and performance.

KEY-VALUE stores

  • Structure relational databases impose on the data, may be very restrictive for certain applications.

  • For most cases non-relational databases are sufficient or more useful.

  • Mapping from key strings to arbitrary values.

  • key
    value

    foo

    bool

    bar

    string

    baz

    any[]

  • Caching is a very common use case for key-value stores. We can cache all kinds of unstructured data.

  • Another case, is dynamic configuration. We can store all kinds of configuration data in a key-value store.

  • Because you are accessing values through a key, the lookup is O(1) time complexity, very fast.

  • Types of key-value databases

    • In-memory

      • Redis

      • Memcached

    • On-disk

      • LevelDB

      • RocksDB

      • DynamoDB

      • Cassandra

      • Riak

      • Voldemort

      • BerkeleyDB

      • Kyoto Cabinet

      • MemcachedDB

      • Redis

      • Tarantool

      • ZooKeeper

  • Some databases have strong consistency guarantees, while others have eventual consistency.

Eventual Consistency vs Strong Consistency

  • A large scale system is distributed across multiple microservices.

  • A DB server will become a single point of failure.

  • DB's are replicated across multiple servers, to avoid single point of failure's.

  • Every write operation is replicated across multiple servers (eventually vs immediately).

Eventual Consistency

  • Youtube is a good example of eventual consistency.

    • When you upload a video, it takes some time for the video to be available.

    • When you upload a video, it is replicated across multiple servers.

    • When you try to access the video, you may be routed to a server that does not have the video.

    • The video will be available after some time.

    • This is called eventual consistency.

  • The system should be able to tolerate temporary inconsistency.

  • Cost of users seeing stale data, is lower than the cost of blocking read requests.

Strong Consistency

  • Databases read requests need to be blocked, until the write operation is replicated across all servers.

  • Lower performance and availability.

Cost benefit analysis

  • If the cost of users seeing stale data is high, then strong consistency is required. (portfolio info, buy/sell price).

  • If the cost of users seeing stale data is low, then eventual consistency is fine. (youtube views tracker, twitter post / feed).

Specialized Storage Paradigms

The field of storage is very broad. You don't need to know everything about storage to ace your systems design interview. A few specialized storage paradigms are, worth knowing about.

Blob Store

  • A blob is "Binary Large Object",

  • In systems design, "A arbitrary chunk of data". Eg, a image, a video, a file, etc.

  • A blob store is a database that stores blobs.

  • A blob is unstructured large data, that cannot be stored in a relational database.

  • A blob store specializes in storing massing amount of, unstructured data.

  • GCS (Google Cloud Storage) is a blob store.

  • S3 (Amazon Simple Storage Service) is a blob store.

  • Blob stores are optimised for storing and retrieving massive amounts of data.

  • Implementing a blob store is not trivial, and a very hard problem to solve.

  • When accessing a blob, we need to know the key, blobs are not key-value stores.

  • key-value stores are optimized for fast lookups, but not optimized for storing large amounts of data.

Time Series Database

  • InfluxDB / Prometheus, are time series databases.

  • A database that stores data with timestamps.

  • Events are stored in the order they occur, with timestamps.

  • Time series databases are optimized for storing and retrieving data with timestamps.

  • Computing, rolling averages, moving averages, etc, is very fast.

  • Monitoring systems, use time series databases.

  • IOT devices, that constantly send telemetry data, use time series databases.

  • Stock price data, crytocurrency price data, etc, use time series databases, computing historical data based on timestamps.

  • Time series ( data collected over time ).

  • Time series analysis is a field of statistics.

  • TSA can be used to predict future values based on historical data.

  • Components of time-series analysis

    • Trend A line on the graph increasing or decreasing over time.

    • Seasonality A repeating pattern over time (periodic spikes and dips).

    • Cycle A repeating pattern over time, but not necessarily periodic. ( smooth up and down waves ).

    • Variations Random noise in the data.

  • Fundamental models for predicting future values

    • ARIMA ( auto regressive integrated moving average )

    • EXPONENTIAL SMOOTHING

  • Implementation

    • R / Python / matlab, have libraries for time series analysis.

    • Pandas / Matplotlib / Seaborn, have libraries for time series analysis.

Graph Database

  • A graph database is a database that stores data in the form of a graph.

  • SQL, data is stored in the form of tables.

  • NoSQL, data is stored in the form of documents, key-value pairs, etc.

  • A data set that have a lot of relationships between individual data-points, is a good candidate for a graph database.

  • A tabular structure is not a good fit for a data set with a lot of relationships.

  • Queries that involve a lot of joins, are complicated, and slow.

  • Graph databases are built on top of graph theory.

    • Relationships between data-points are stored as edges.

    • In SQL, relationships between data-points are stored as foreign keys.

  • Social networks, are a good example of a data set with a lot of relationships.

    • A social network is a graph.

    • A user is a node.

    • A relationship between users is an edge.

    • Users can have multiple relationships with other users, posts, comments, links, etc.

  • Neo4j is a graph database.

  • Cypher is a query language for Neo4j.

  • SQL queries, which requires multiple relationships to be joined, becomes very complicated and slow.

  • Data is visualized as a graph.

  • Relationships between data-points are stored as edges and directed edges.

Spatial Database

  • Optimized for storing and querying spatial data.

  • Geometric space, locations on a map, locations on a earth, etc.

  • Queries like, "find all restaurants within 5 miles of my location", are very fast.

  • Spacial indexes rather than SQL indexes are used.

  • Indexes are based on trees.

  • Quad trees, A tree where each node has 0 / 4 children. A node with 0 children is a leaf node.

  • Quad tree can be used to represent a 2D space / grid.

  • dots on a map represent locations on a 2D space.

  • 2D space can be recursively divided into 4 quadrants.

  • Sub-quadrants can be recursively divided into 4 quadrants.

  • A square to be a leaf node, if it contains a single dot ( 1 location ).

  • A square can be divided into 4 quadrants, if it contains multiple dots ( multiple locations ).

  • When performing a spacial query, Quad tree can be traversed n(log n) time complexity.

Replication

For improving availability and reliability.

  • If a systems DB is unavailable, the system itself is unavailable.

  • If a systems DB has high latency and low throughput, the system itself has high latency and low throughput.

  • A single DB with high availability and high throughput, is a single point of failure.

    • If this goes down, the system itself is unavailable.

    • A secondary replica DB can be used to solve this problem.

    • The main DB is the primary DB, used for read and write operations.

    • The replica is updated with the primary DB's data.

    • If the primary DB goes down, the replica can be used as the primary DB.

    • The primary DB can be brought back up by restoring data from replica.

    • Any updates to the primary DB, needs to be replicated to the replica, synchronously or asynchronously.

    • If the write operation on replica fails, the write operation on primary DB fails.

    • The replica should always be in sync with the primary DB.

    • This need causes our write operations to be slower, (since we need to wait for the replica to be updated).

  • Replication is used to increase availability in the entire system.

For improving latency and throughput.

  • Two DB's, one DB serving US zone, and one DB serving India zone.

  • The US users and Indian users have very low latency.

  • The post created by a US user, needs to be replicated to the Indian DB and vice versa.

  • Since the post doesn't need to be visible immediately ( eventual consistency vs immediate consistency ), asynchronous replication can be used.

  • The US DB can sync with the Indian DB every x minutes and vice versa.

Many services may not choose this approach, since the data is not immediately available to the user.

Sharding

  • A system with one data-base, is serving a lot of users.

  • Throughput becomes low, latency becomes high.

  • The solution is to vertically scale the DB.

    • Increase the RAM, CPU, etc.

    • This is expensive.

  • Another solution is to horizontally scale the DB.

    • Add more DB's.

    • Throughput becomes high, latency becomes low, by 20x.

  • This would require replication across multiple DB's. If the data required to replicate is very large, this would be a problem. Data replication would take a long time.

  • Splitting the data into multiple DB's, is called sharding.

  • Splitting / Partitioning the data into multiple DB's ( aka shards / data partitions), is called sharding.

  • If the DB is a relational DB, the data is split into multiple tables.

    • payments table can be sharded into multiple tables, based on the payment type / payment method / payee first name, etc.

  • If the DB is a NoSQL DB, the data is split into multiple collections.

  • Certain shards become hot shards, and certain shards become cold shards.

    • Hot shards are shards that are accessed frequently.

    • Cold shards are shards that are accessed infrequently.

  • The sharding strategy needs to be chosen carefully.

    • If the sharding strategy is based on the payment type, and the payment type is not evenly distributed, certain shards will become hot shards, and certain shards will become cold shards.

    • If the sharding strategy is based on the payee first name, and the payee first name is not evenly distributed, certain shards will become hot shards, and certain shards will become cold shards.

  • Consistent hashing, may help us solve this problem. Since we need certain types of data be read and written from the same shard.

  • But if a shard goes down, writes and reads from that shard will fail.

  • In practice, the job of sharding and deciding which shard to read and write from, is done by a load balancer.

  • In a real system, sharding stratergy is based on the data.

    • If the data is evenly distributed, sharding stratergy can be based on the data.

    • If the data is not evenly distributed, sharding stratergy can be based on the data.

  • In a real system, each shards will live on different systems.

Database normalization

Benefits of database normalization.

  • Eliminate redundant data.

  • Reduce modification anomalies.

    • Insert anomalies. Inability to add data to a table, due to missing data in another table.

    • update anomalies. The same data value is stored in multiple places (columns, tables, etc.), can cause inconsistencies.

    • delete anomalies. Deleting data from a table, can cause loss of data in another table.

  • Ensure data dependencies make sense. Relationships between tables are defined.

1NF ( First Normal Form )

  • Data is stored in tables.

  • One or more colums is the primary key, used to identify each row.

  • Each column contains atomic values.

    • Each column contains a single value.

    • Each column contains a single value of the appropriate type.

    • Each column contains a single value of the appropriate type, without repeating groups.

    text
    | order_id | name | email | phone | order_items | <<< not atomic data
    | order_id | name | email | phone | order_item_1 | order_item_2 | order_item_3 | <<< repleating groups
  • One to many relationships Order Table

    text
    | order_id | name | email | phone |

    Order Items Table

    text
    | order_item_id | order_id | item_name | item_price | quantity |

2NF ( Second Normal Form )

  • The table is 1NF.

  • All non-key columns are fully dependent on the primary key.

    text
    order_id | customer_id | name | email | phone | date | <<< name, email and phone are not fully dependent on the primary key, which is *order_id

    Put customer info into a separate table.

    text
    customer_id | name | email | phone |

3NF ( Third Normal Form )

  • The table is 2NF.

  • All non-key columns are not transitively dependent on the primary key.

    text
    customer_id | name | email | phone | city | state | country | <<< state and country are transitively dependent city

    put them into a separate tables.

    text
    city_id | city |
    text
    state_id | state | contry_id |
    text
    country_id | country |

Leader Election

mearmaid
graph LR
A[Third Party Service] --> B[Middleware]
B --> C[Database]
  • A third party service that needs to communicate with our database.

  • A middleware service plugs into our database, and communicates with the third party service.

  • The middleware is a single point of failure.

  • Introducing redundancy to the middleware, is a solution.

mearmaid
graph LR
A[Third Party Service] --> B[Middleware 1]
A --> C[Middleware 2]
A --> D[Middleware 3]
A --> E[Middleware 4]
A --> F[Middleware 5]
B --> G[Database]
C --> G
D --> G
E --> G
F --> G
  • 5 Server instances of the middleware sits between the third party service and the database.

  • The middleware instance, requests should not be duplicated.

  • A single instance needs to process the same payment request.

mearmaid
graph TD
A[Third Party Service] --> C[Leader]
B[Follower] --> C
D[Follower] --> C
E[Follower] --> C
F[Follower] --> C
G[Database]
C --> G
  • The servers need to elect a leader.

  • The leader will process the payment request.

  • The nodes re-elect a leader, if the leader goes down.

  • The implementation of leader election in a large distributed system is complex.

  • The act of multiple nodes agreeing to a consensus, is difficult.

  • In order to elect a leader, the nodes need to communicate with each other.

  • A multiple nodes in a cluster, communicate with each other, to elect a leader, using a consensus algorithm.

  • PAXOS, Raft are consensus algorithms.

  • In the industry, Zookeeper / Etcd are third party services that are used to elect a leader.

  • A lot of systems in Uber, use Zookeeper for leader election.

  • Etcd is a distributed key value store, that is used for leader election.

  • Etcd gaurentees that the data is consistent across all nodes.

  • Etcd is hightly available, with strong consistency.

  • Etcd achevies this by using the Raft consensus algorithm, under the hood.

  • {key-value} pair in Etcd, is used to store leader node info.

  • lease is granted to the leader node.

  • If the leader does not renew the lease, the lease expires.

  • Etcd automatically deletes the key-value pair, if the lease expires.

  • Etcd assumes that the leader node is down, if the key-value pair is deleted.

Peer to peer network

  • A peer to peer network is a network of nodes, that communicate with each other.

  • Deploying or transferring large files to thousands of nodes at the same time, repeatedly.

  • Deploying a very large ML model.

  • Rolling out a new version of the software.

  • A large file is split into multiple chunks.
    5 GB files is divided into 1 MB numbered chunks.

  • Each chunk is sent to a different nodes.

  • Nodes reach out to other nodes, to get the chunks that they do not have.

  • Throughput in a peer to peer network is high.

  • In order for a peer to peer network to function,
    each node needs to know the IP address of other nodes.

  • Peer selection, peer discovery, peer exchange, are some of the problems that need to be solved.

  • An orchestrator / tracker node, is a node that is responsible for orchestrating the peer to peer network.

  • Gossip / epidemic protocol is used to solve the peer discovery problem.
    "You don't have this chunk, but I do. Here you go."
    "I don't have it, but that node does. Here is the IP address of that node."

  • Hash tables are shared between nodes,
    to keep track of the chunks that each node has.
    DHT (Distributed Hash Table) is used to solve this problem.

  • Kraken is a peer to peer network, that is used by Uber.

  • BitTorrent is a peer to peer network, that is used by Facebook.

  • IPFS is a peer to peer network, that is used by Netflix.

  • Torrenting, one machine distributes a file in chunks to many machines.

Polling vs Streaming

  • A data changes frequently (stock price, weather, etc.).

  • Messages or temperature readings need to be shown on the client regularly.

Polling

  • Client sends a request to the server, to get the latest data at regular intervals (every x seconds).

  • Polling has issues, take an example of instant messaging.

  • If the client polls the server every 5 seconds, the server will be overloaded.

  • The client needs to process and repaint the UI.

  • If the client polls the server every 5 minutes, the client will not get the latest data.

Streaming

  • A client opens a socket connection to the server.

  • A socket connection is a two way communication channel, the connection is kept open, until the client or the server closes the connection.

  • The server pushes new data to the client, as soon as it is available.

  • The client is streaming the data from the server.

Depending on the use case, polling or streaming can be used. Polling can be useful, if the data is not changing frequently. If data does not need to be updated not too frequently, polling can be used.

Configuration

  • Most large scale distributed systems, have a lot of configuration.

  • Configuration is a set of parameters, that the systems use.

  • Configurations are written typically in JSON or YAML.

json
{
	"apiKey": "1234567890",
	"showSystemsExpert": true,
	"supportedLanguages": ["en", "fr", "es"],
	"version": {
		"major": 1,
		"minor": 0,
		"patch": 0
	}
}
yaml
apiKey: 1234567890
showSystemsExpert: true
supportedLanguages:
  - en
  - fr
  - es
version:
  major: 1
  minor: 0
  patch: 0
  • Uber user YAML for configuration.

  • Algoexpert uses JSON for configuration.

Static vs Dynamic Configuration

  • Static configuration is configuration that is set at compile time (.env file). App needs to be redeployed, if the configuration changes.

  • Dynamic configuration is configuration that is set at runtime (database). Dynamic configuration can be changed without restarting the system.

  • Review process, Access controls, are some of the ways to manage configuration changes.

  • With great power comes great responsibility.

Rate Limiting

  • Has lot of security and performance ramifications.

  • Limiting the amount of operations that can be performed in a given amount of time.

  • DOS / DDOS attacks can be prevented by rate limiting. System cant handle the load, if the system is flooded with requests.

  • Rate limiting can be used to prevent (brute force attacks, spamming, scraping, etc.).

  • We can rate limit based on various parameters (IP address, headers, region, etc.).

  • Rate limiting while being very useful, its not the only solution.

  • Access logs are stored generally in a dedicated database (Redis), Which allows us to enforce rate limiting.

  • Tiers can be used to implement rate limiting.

  • If the number of requests is less than the threshold, the request is allowed.

  • A complicated tier system can be really hard to manage.

Logging and monitoring

Logging

  • As your system grows, it becomes more and more difficult to debug.

  • Logging is a way to record events that happen in the system.

  • System logs / JSON logs are stored in a database.

  • Algo expert uses google stack driver for logging.

Monitoring

  • Monitoring is a way to keep track of the health / performance of the system.

  • Monitoring is done by sending metrics to a monitoring system.

  • Monitoring is done by scraping through logs, If the log does not have the right information you will not be able to grab some metrics. If the logs are changed, you risk breaking your entire monitoring system.

  • A time series database is used to store metrics.

  • Prometheus is a time series database, that is used by Uber.

  • Grafana is a visualization tool, that uses entries in a timeseries database to render graphs.

  • These metrics can be used to create dashboards, that can be used to monitor the system.

Alerting

  • If the error rate is above a certain threshold, you may want to send an alert to the engineering team.

Publish and Subscribe Pattern

  • PUB / SUB pattern is used to send messages between nodes.

  • Streaming is used to send messages between nodes.

  • Distributed systems that user streaming, will need to persist data somewhere, in case nodes go down (sent / received messages should not be lost).

  • Storage solutions should be separated from the business logic.

mermaid
graph LR
A[Publisher 1] -- ...M4, M3, M1 --> B[Channel 1] --> C[Subscriber 1]
B[Channel 1] --> D[Subscriber 2]
E[Publisher 2] -- Messages --> F[Channel 2] --> G[Subscriber 3]
H[Publisher 3] -- Messages --> I[Channel 3] --> J[Subscriber 4]
  • Publisher, sends data to channels

  • Multiple subscribers, listen for messages on channels

  • Subscribers do not know about the existence of publishers

  • Publishers do not know about the existence of subscribers

  • Message aka data chunk

  • All of the messages sent through the channel are persisted.
    Subscribers can get the messages that they missed.
    At least once delivery is guaranteed.

    mermaid
    sequenceDiagram
     Channel ->> Subscriber: DATA: M1
     Subscriber ->> Channel: ACK:  M1
     Channel ->> Subscriber: DATA: M2
     Subscriber -->> Channel: ACK-Failed:  M2
     Note right of Channel: Network conection fails
     Channel ->> Subscriber: DATA: M2
     Subscriber ->> Channel: ACK:  M2
  • Idempotentcy,

    The same operation has the same outcome, no matter how many times it is executed.

  • A pubsub system should handle idempotent, data.
    It only gaurentees at least once delivery.
    A message can be delivered multiple times.

  • Pubsub guarantees sequential delivery of messages.

  • Subscribers can replay messages in the order that they were sent.

  • Clear separation of concerns,
    Different channels can be used for different purposes.

  • Pubsubs can have content based filtering Subscribers can subscribe to specific channels, with specific filters.

    mermaid
    graph LR
    A[Publisher 1] -- Sell Rates --> B[Channel 1] -- BTC --> C[Subscriber 1]
    B[Channel 1] -- ETH --> D[Subscriber 2]
  • GCP pubsub, Apache Kafka,
    provides, Autoscalling, E2E encryption, etc out of the box.

Map Reduce

  • Google engineers, wanted to index the entire internet.
    They wanted to do it in a way, that was scalable and fault tolerant.

  • They had to scale horizontally by adding more machines.

  • Parallel processing,
    A distributed system hat can process data in parallel.

  • Map reduce is a programming model, that allows us to process data in parallel.

mermaid
flowchart LR
subgraph Input
1
1.1
1.2
1.4
end
subgraph Shuffle \nand Reduce
4.1
4.2
4.n
end
subgraph K / V pairs generation
2.1
2.2
2.3
2.n
3.1
3.2
3.3
3.5
end
subgraph Output
5.1
5.2
5.n
end
1[Map] --> 2.1(Data chunk 1 \n AA) --> 3.1[K / V pairs\nA: 2] --> 4.1[Reduce]
1.1[Map] --> 2.2(AB) --> 3.2[A: 1\nB: 1] --> 4.1
1.2[Map] --> 2.3(AAC) --> 3.3[A: 2\nC: 1] --> 4.n
1.4[Map] --> 2.n(BBB) --> 3.5[B: 3] --> 4.2
4.1[Reduce A] --> 5.1[A: 5]
4.2[Reduce B] --> 5.2[B: 4]
4.n[Reduce C] --> 5.n[C: 1]
3.2 --> 4.2
3.3 --> 4.1

The Orchestration machine is responsible for,

  • keeping track of all the machines in the cluster.

  • know what chunks are on what machines.

  • sends the map function to all the machines in the cluster,

  • shuffles the key-value pairs to the right machines running the reduce function.

Assumptions,

  • A large file is split into smaller chunks.

  • Chunks are distributed across multiple machines.

  • Federation / Orchestrator Machine, Keeps track of all the machines in the cluster. What chunks are on what machines.

  • Map function, is sent to all the machines in the cluster, with the chunks. to avoid data transfer.

  • Structure of K/V pairs,
    some keys are similar, and some are different.

  • Fault tolerance,
    If a machine goes down, the chunks are redistributed to other machines.
    If a K/V computation fails, it is retried.

  • Our map function is required to be idempotent.

Engineers need to worry about,

  • Map function

    • can emit events as it is processing data.
      A: 1; A:1

    • can emit aggregated values after procession data.
      A: 2

  • Reduce function

  • What kind of K/V pairs are generated.

  • Input data format

  • Output data format

Realworld example,

  • Computing the total no of views for all the videos.

  • Aggregating the total no of logs per service, from a large no of data sources.

Security

You are unlikely to be asked about security in a system design interviews.
Security is a very domain expertise specific topic.

API Design

  • API design is a sibling of system design.

  • Good API design is important for a good system design.

  • API's used by a lot of people, need to be designed well,
    it becomes difficult to change them later.

  • We need to ask more questions about the API,
    to understand the requirements better.
    Where are we using it?
    Why are we suing it for?

  • API Outline

    Entity
    Action
    HTTP Verb
    URL
  • We don't need to write the logic behind the API,
    we just need to design the API.

  • For API design,
    you need to be able to defend your design decisions.

  • There are no right or wrong answers,
    but there are better or worse answers.

  • Always paginate your API's,
    don't return all the data at once.

Create API outline,

txt
# Entity Definitions
### Customer
    - id: uuid
    - name: string
    - email: string
    - address: string
    - phone: string

Create API outline in swagger,

yml
swagger: '2.0'
info:
  version: 1.0.0
  title: Customer API
host: localhost:3000
basePath: /api/v1
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /v1/charges:

Design A Code-Deployment System

Design a global and fast code-deployment system.

Many systems design questions are intentionally left very vague and are literally given in the form of Design Foobar.

It's your job to ask clarifying questions to better understand the system that you have to build.

We've laid out some of these questions below; their answers should give you some guidance on the problem. Before looking at them, we encourage you to take few minutes to think about what questions you'd ask in a real interview.

What questions would I ask?

  • What kind of code are we deploying? Is it a web app? A mobile app? A desktop app?

Clarifying Questions

  • What exactly do we mean by a code-deployment system? Are we talking about building, testing, and shipping code?

    We want to design a system that takes code, builds it into a binary (an opaque blob of data—the compiled code), and deploys the result globally in an efficient and scalable way. We don't need to worry about testing code; let's assume that's already covered.

  • What part of the software-development lifecycle, so to speak, are we designing this for? Is this process of building and deploying code happening when code is being submitted for code review, when code is being merged into a codebase, or when code is being shipped?

    Once code is merged into the trunk or master branch of a central code repository, engineers should be able to trigger a build and deploy that build (through a UI, which we're not designing). At that point, the code has already been reviewed and is ready to ship. So to clarify, we're not designing the system that handles code being submitted for review or being merged into a master branch—just the system that takes merged code, builds it, and deploys it.

  • Are we essentially trying to ship code to production by sending it to, presumably, all of our application servers around the world?
    Yes, exactly.

  • How many machines are we deploying to? Are they located all over the world?

    We want this system to scale massively to hundreds of thousands of machines spread across 5-10 regions throughout the world.

  • This sounds like an internal system. Is there any sense of urgency in deploying this code? Can we afford failures in the deployment process? How fast do we want a single deployment to take?

    This is an internal system, but we'll want to have decent availability, because many outages are resolved by rolling forward or rolling back buggy code, so this part of the infrastructure may be necessary to avoid certain terrible situations. In terms of failure tolerance, any build should eventually reach a SUCCESS or FAILURE state. Once a binary has been successfully built, it should be shippable to all machines globally within 30 minutes.

  • So it sounds like we want our system to be available, but not necessarily highly available, we want a clear end-state for builds, and we want the entire process of building and deploying code to take roughly 30 minutes. Is that correct?

    Yes, that's correct.

  • How often will we be building and deploying code, how long does it take to build code, and how big can the binaries that we'll be deploying get?

    Engineering teams deploy hundreds of services or web applications, thousands of times per day; building code can take up to 15 minutes; and the final binaries can reach sizes of up to 10 GB. The fact that we might be dealing with hundreds of different applications shouldn't matter though; you're just designing the build pipeline and deployment system, which are agnostic to the types of applications that are getting deployed.

  • When building code, how do we have access to the actual code? Is there some sort of reference that we can use to grab code to build?

    Yes; you can assume that you'll be building code from commits that have been merged into a master branch. These commits have SHA identifiers (effectively arbitrary strings) that you can use to download the code that needs to be built.

solution

mermaid
flowchart LR
ci_cd[CI/CD trigger]
user((User))
github[Github]
subgraph Build Cluster
    build_service(Build Service)
end
replication_service[Replication Service]
jobs_database[(Jobs DB)]
subgraph Global
    blob_store[(AWS S3)]
    etcd[(ETCD Global)]
end
subgraph IND
    etcd_region_3[(ETCD IND)]
    blob_store_region_3[(S3 IND)]
    blob_store_region_3 --> kraken_3[kranken]
    kraken_3 --> p2p_ind((P2P Nodes))
    etcd_region_3 --> p2p_ind
end
ci_cd --> build_service
user --> build_service
build_service <--> github
build_service <--> jobs_database
build_service --> blob_store
build_service --> replication_service
replication_service --> blob_store
replication_service --> blob_store_region_3
replication_service --> etcd
replication_service --> jobs_database
uuid
branch_sha
status *idx
created_at *idx
replicated_at
s3_primary_key

string

string

enum string

UTC string

UTC string

string

Once a job is started,
the worker for the job (build service)
sends heart beat updated to the SQL database.

If the heart beat passes a certain threshold,
we show the user a failed error message.

last_heart_beat

string

json
{
	// etcd global and regional stores
	"current_deployment_version": 2.0
}

Design Tinder

Many systems design questions are intentionally left very vague and are literally given in the form of Design Foobar. It's your job to ask clarifying questions to better understand the system that you have to build.

We've laid out some of these questions below; their answers should give you some guidance on the problem. Before looking at them, we encourage you to take few minutes to think about what questions you'd ask in a real interview.

Clarifying Questions To Ask

  • As far as I know, users who sign up on Tinder first create a profile (name, age, job, bio, sexual preference, etc.), after which they can start swiping on other users near them, who appear in a stacked deck of potential matches on the main page of the app. If two users swipe right on each other, they match, and they can now directly communicate with one another. Are we designing all of this?

    Yes, but you don't have to design the messaging feature or any functionality that's available after two users match. You should also design both the Super Like feature and the Undo feature. Super Like allows a user to effectively arrive at the top of another user's deck, indicating that they super-liked them. Undo allows a user to unswipe the last user that they swiped on. So if they accidentally swiped left on someone, they can undo that. But this can only be done for the last user; you can't spam undo.

  • Regarding the Undo feature, can a user undo a match?

    For the sake of this design, let's only allow undoing when you swipe left—not when you swipe right. And if you swipe left, then swipe right, you can no longer undo the left swipe from two swipes ago.

  • Do users have a limited number of right swipes, Super Likes, and Undos per day? What about the number of potential matches in their deck? Is there a daily cap on that number, like 100 or 200 potential matches per day?

    For the sake of this design, let's not have any caps whatsoever. In other words, users will be given an infinite amount of potential matches in their deck (within their distance parameters), and they can endlessly swipe right on them, Super Like them, and undo left swipes. Naturally, if a user were to swipe through every single potential match within their distance parameters, then they would run out of potential matches, but their deck would likely quickly get new potential matches as new users sign up on Tinder.

  • Regarding the deck of potential matches, here are some assumptions that I'm making; let me know if this sounds appropriate. Every user has an endless deck of potential matches that are within their distance parameters, as we just mentioned, and this deck should be ordered in some way (perhaps based on a matchability score). The deck should only consist of users who have either already liked this user or not yet swiped on them. For users who have already swiped left on the main user, we should probably, in a best-effort type of way, try to remove them from the main user's deck. And then, of course, users who have super-liked the main user should be at the top of the deck. Does this seem reasonable?

    This seems reasonable, but you don't actually need to worry about how decks are generated. In other words, you can assume that we have a smart matching algorithm that generates the decks for you based on matchability scores, preferences, distance, etc., and you should just rely on this algorithm and figure out where it fits into your design. So you don't even need to worry about whether potential matches who've swiped left on a user show up in the user's deck; the matching algorithm will take care of that for you.

  • Are we designing the part of the system that notifies users when they have a new match?

    You should think about how a user will be notified of a match if a match occurs in real time, as they swipe right on another user. Otherwise, don't worry about the match-notification system when the user is idle on the app or not using the app at all.

  • As far as scale is concerned, how many users are we designing Tinder for, and where in the world should we assume that they're located?

    Let's assume that we have roughly 50 million users on Tinder. You can assume that they're evenly distributed across the globe, perhaps with hot spots in major urban areas.

  • As far as latency and reliability are concerned, I'm assuming that we want Tinder to be mostly highly available and that we want swipes to feel instant. Is it ok if there's a little bit of loading time when you first open the app or after you've swiped through, say, 200 profiles?

    What you described for latency is great. As far as reliability is concerned, let's not worry too much about it for the sake of this design. You can assume that you have a highly available SQL setup out of the box, without worrying about the availability details.

Solution

mermaid
flowchart LR