Skip to content

alishsuper/Grokking-the-Coding-Interview

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Grokking the Coding Interview in 16 Patterns

Pattern 1: Sliding Window

They are subsets of dynamic programming problems, through the approach to solving them is quite different from the one in solving tabulation and memoization problems.

How do you identify them ?

The first thing you want to be able to do is identify a problem that uses a sliding window paradigm.

Some giveaways:

  1. The problem will involve a data structure that is ordered and iterable like an array or a string.
  2. You are looking for some contiguous subrange in that array/string, like a longest, shortest or target value, or whether a value is contained with the iterable.
  3. There is an apparent naïve or brute force solution that runs in O(N2), O(2N) or some other large time complexity.

The biggest giveaway is that the thing you are looking for is often some kind of optimal, like the longest sequence or shortest sequence of something that satisfies a given condition exactly.

The thing about sliding window problems is that most of the time they can be solved in O(N) time and O(1) space complexity.

Example, Bit-Flip Problem

Given a binary array, find the maximum number of zeroes in an array with one flip of a subarray allowed. A flip operation switches all 0s to 1s and vice versa.

OR

You are given a binary string(i.e. with characters 0 and 1) S consisting of characters S1, S2, …, SN. In a single operation, you can choose two indices L and R such that 1 ≤ L ≤ R ≤ N and flip the characters SL, SL+1, …, SR. By flipping, we mean change character 0 to 1 and vice-versa.

Your aim is to perform ATMOST one operation such that in final string number of 1s is maximized. If you don’t want to perform the operation, return an empty array. Else, return an array consisting of two elements denoting L and R. If there are multiple solutions, return the lexicographically smallest pair of L and R.

S = 010

Pair of [L, R] | Final string
_______________|_____________
[1 1]          | 110
[1 2]          | 100
[1 3]          | 101
[2 2]          | 000
[2 3]          | 001

We see that two pairs [1, 1] and [1, 3] give same number of 1s in final string. So, we return [1, 1].

Abstract Idea

Static Sliding Window

static-sliding-window

Dead Giveaways:

  • max sum subarray of size K.

Dynamically Resizable Window

Dead giveaway:

  • smallest sum >= some value S.

Dynamic Variant w/ Auxiliary Data Structure

Dead giveaway:

  • Longest substring w/ no more than k distinct characters.
  • String permutations.

Example Problems

Pattern 2: Two Pointer

How do you identify ??

Two pointer technique is normally used for searching and it uses two pointer in one loop over the given data structure.

In order to use two pointers, most of the times the data structure needs to be ordered in some way, which helps us to reduce the time complexity from O(n2) or O(n3) to O(n) of just one loop with two pointers and search each item just one time.

So depending on whether the input string is sorted or not, the two-pointer can take O(n log n) time complexity or even better which is O(n).

Types of two-pointers

  1. Opposite Directional: One pointer starts from the beginning while the other pointer starts from the end. They move toward each other until they both meet or some condition satisfy.

  2. Equi-Directional: Both start from the beginning, one slow-runner and the other is fast-runner.

Description

Given a sort array A, having N integers, find if there exists any pair of elements (A[i], A[j]) such that their sum is equal to X

function isPairSum(A[], arrayLength, targetSum)
	startPointer := 0
	endPointer := arrayLength - 1
	
	while startPointer < endPointer
		if A[i] + A[j] == targetSum
			return True
		else if A[i] + A[j] < targetSum
			startPointer += 1
		else endPointer -= 1
	return False

Time Complexity: O(N)

Example Problems

Pattern 3: Fast & Slow pointers

The Fast & Slow pointer approach, also known as the Hare & Tortoise algorithm, is a pointer algorithm that uses two pointers which move through the array (or sequence/LinkedList) at different speeds. This approach is quite useful when dealing with cyclic LinkedLists or arrays.

By moving at different speeds (say, in a cyclic LinkedList), the algorithm proves that the two pointers are bound to meet. The fast pointer should catch the slow pointer once both the pointers are in a cyclic loop.

One of the famous problems solved using this technique was Finding a cycle in a LinkedList. Let’s jump onto this problem to understand the Fast & Slow pattern.

Example Problems

Pattern 4: Merge Intervals

This pattern describes an efficient technique to deal with overlapping intervals. In a lot of problems involving intervals, we either need to find overlapping intervals or merge intervals if they overlap.

Given two intervals (a and b), there will be six different ways the two intervals can relate to each other:

  1. a and bdo not overlap
  2. a and b overlap, b ends after a
  3. a completely overlaps b
  4. a and b overlap, a ends after b
  5. b completly overlaps a
  6. a and b do not overlap

Understanding the above six cases will help us in solving all intervals related problems.

Example Problems

Pattern 5: Cyclic Sort

This pattern describes an interesting approach to deal with problems involving arrays containing numbers in a given range. For example, take the following problem:

You are given an unsorted array containing numbers taken from the range 1 to ‘n’. The array can have duplicates, which means that some numbers will be missing. Find all the missing numbers.

To efficiently solve this problem, we can use the fact that the input array contains numbers in the range of 1 to ‘n’. For example, to efficiently sort the array, we can try placing each number in its correct place, i.e., placing ‘1’ at index ‘0’, placing ‘2’ at index ‘1’, and so on. Once we are done with the sorting, we can iterate the array to find all indices that are missing the correct numbers. These will be our required numbers.

Example Problems

Pattern 6: In-place Reversal of a LinkedList

In a lot of problems, we are asked to reverse the links between a set of nodes of a LinkedList. Often, the constraint is that we need to do this in-place, i.e., using the existing node objects and without using extra memory.

In-place Reversal of a LinkedList pattern describes an efficient way to solve the above problem.

Example Problems

Pattern 7: Tree Breadth First Search

This pattern is based on the Breadth First Search (BFS) technique to traverse a tree.

Any problem involving the traversal of a tree in a level-by-level order can be efficiently solved using this approach. We will use a Queue to keep track of all the nodes of a level before we jump onto the next level. This also means that the space complexity of the algorithm will be O(W), where W is the maximum number of nodes on any level.

All connections connected to a node is checked at one time.

BFS of above Tree

Breadth First Traversal : 1 2 3 4 5
FUNCTION traverseTreeInBFS
	passIn: root
	if root == None
		return
	queue = Queue()
	queue.enqueue(root)
	while isEmpty(queue) == False
		e = queue.deque()
		print(e)
		
		if e.left != None:
			queue.enqueue(e.left)
		if e.right != None:
			queue.enqueue(e.right)
	passOut: None

Example Problems

Pattern 8: Depth First Search (DFS)

This pattern is based on the Depth First Search (DFS) technique to traverse a tree.

We will be using recursion (or we can also use a stack for the iterative approach) to keep track of all the previous (parent) nodes while traversing. This also means that the space complexity of the algorithm will be O(H), where ‘H’ is the maximum height of the tree.

Example Problems

Pattern 9: Two Heaps

In many problems, where we are given a set of elements such that we can divide them into two parts. To solve the problem, we are interested in knowing the smallest element in one part and the biggest element in the other part. This pattern is an efficient approach to solve such problems.

This pattern uses two Heaps to solve these problems; A Min Heap to find the smallest element and a Max Heap to find the biggest element.

Example Problems

Pattern 10: Subsets

A huge number of coding interview problems involve dealing with Permutations and Combinations of a given set of elements. This pattern describes an efficient Breadth First Search (BFS) approach to handle all these problems.

Example Problems

Pattern 11: Modified Binary Search

As we know, whenever we are given a sorted Array or LinkedList or Matrix, and we are asked to find a certain element, the best algorithm we can use is the Binary Search.

Example Problems

Pattern 12: Bitwise XOR

Exclusive-Or returns a bit value of 1 if both bits are of opposite (different) nature, otherwise Exclusive-OR returns 0.

	0000		
  ^	0000
  -------
  	0000
==============================
  	1111		
  ^	0000
  -------
  	1111
==============================
  	1111		
  ^	1111
  -------
  	0000
==============================
  	1100		
  ^	1010
  -------
  	0110

If you take XOR of a number with 0 (zero), it would return the same number again.

x = 4 => 100
y = 2 => 010

x ^ 0 => 100 ^ 000 => x
6 ^ 0 = 6

If you take XOR of a number with itself, it would return 0 (zero).

x = 4 => 100
y = 2 => 010

x ^ x => 100 ^ 100 => 0
6 ^ 6 = 0

Example:

  1. We can swap the values of two variables without using any third (temp).

    a = 5
    b = 10
    
    a = a ^ b (a = 5 ^ 10 = 15)
    b = a ^ b (b = 15 ^ 10 = 5)
    a = a ^ b (a = 15 ^ 5 = 10)
    
  2. Toggling(flipping) the k-th bit (from right) of a binary number:

    Let  n = 27, k = 3
    we can use: n ^ (1 << (k-1))
    11011 ^ (00001 << 2)
    11011 ^ (00100)
    11111
    
  3. Find the missing number from the list of numbers:

Question: You are given a list of n-1 integers, and these integers are in the range of 1 to n. There are no duplicates in the list. One of the integers is missing in the list, now, we need to find that missing number.

Method 1 By finding sum of first n natural numbers.

  1. First, find the sum of all number from 1 to n.
  2. Subtract all the element of the given list from the sum, and we'll give the missing number.

There might be an integer overflow while adding large numbers.

Method 2 Using XOR operator

  1. Take the XOR of all numbers 1 to n.

  2. Take XOR of all elements of the given array.

  3. XOR of Step 1 and Step 2 will given the required the missing number.

    arr = [4, 2, 1, 6, 8, 5, 3, 9]
    n = 9
    
    step1_result = 1 ^ 2 ^ 3 ^ 4 ^ 5 ^ 6 ^ 7 ^ 8 ^ 9
    step2_result = 4 ^ 2 ^ 1 ^ 6 ^ 8 ^ 5 ^ 3 ^ 9
    
    final_result = step1_result ^ step2_result = 7
    
    final_result 
    = (1 ^ 2 ^ 3 ^ 4 ^ 5 ^ 6 ^ 7 ^ 8 ^ 9) ^ (4 ^ 2 ^ 1 ^ 6 ^ 8 ^ 5 ^ 3 ^ 9) 
    = (1 ^ 1) ^ (2 ^ 2) ^ (3 ^ 3) ^ (4 ^ 4) ^ (5 ^ 5) ^ (6 ^ 6) ^ (7) ^ (8 ^ 8) ^ (9 ^ 9)
    = 0 ^ 0 ^ 0 ^ 0 ^ 0 ^ 0 ^ 7 ^ 0 ^ 0
    = 7 (required result)
    
  4. Construct an array from XOR of all elements of the array except element at same index.

    Given an array A[] having n positive elements. The task is to create an another array B[] such as B[i] is XOR of all elements of the array A[] except A[i].

    Method 1: Using XOR operator O(N)

    1. Find XOR of all elements of the given array.
    2. Now, for each element of A[], calculate A[i] = step1_result ^ A[i]
arr = [4, 1, 2, 6, 8, 5, 3, 9]
step1_result = 4 ^ 1 ^ 2 ^ 6 ^ 8 ^ 5 ^ 3 ^ 9

Example Problems

Pattern 13: Top 'K' Elements

Any problem that asks us to find the top/smallest/frequent ‘K’ elements among a given set falls under this pattern.

The best data structure that comes to mind to keep track of ‘K’ elements is Heap. This pattern will make use of the Heap to solve multiple problems dealing with ‘K’ elements at a time from a set of given elements.

Example Problems

Pattern 14: K-way merge

Example Problems

15. Pattern : 0/1 Knapsack (Dynamic Programming)

In the 0-1 Knapsack problem, we are given a set of items, each with a weight and a value, and we need to determine the number of each item to include in a collection so that the total weight is less than or equal to a given limit and the total value is as large as possible.

Note: We can either take an item or not (0-1 property). For example,

Input:

value = [ 20, 5, 10, 40, 15, 25 ]
weight = [ 1, 2, 3, 8, 7, 4 ]
W = 10
 
Output: Knapsack value is 60
 
value = 20 + 40 = 60
weight = 1 + 8 = 9 < W

The idea is to use recursion to solve this problem. For each item, there are two possibilities:

  1. Include the current item in the knapsack and recur for remaining items with knapsack's decreased capacity. If the capacity becomes negative, do not recur or return -INFINITY.
  2. Exclude the current item from the knapsack and recur for the remaining items.

Finally, return the maximum value we get by including or excluding the current item. The base case of the recursion would be when no items are left, or capacity becomes 0.

Recursive Solution -> Choice Diagram

Example Problems

16. Pattern: Topological Sort (Graph)

A topological ordering is an ordering of the nodes in a directed graph where for each directed edge from node A to node B, node A appears before node B in the ordering.

Time complexity : O(V+E)

Topological ordering are not unique.

Directed Acyclic Graphs (DAG)

Not every graph can have a topological ordering. A graph which contains a cycle cannot have a valid ordering.

The only type of graph which has a valid topological ordering is a Directed Acyclic Graph(DAG) (graphs with directed edges and no cycle).

How do I verify that my graph does not contain a directed cycle?

  • One method is to use Tarjan's strongly connected component algorithm which can be used to find these cycles.

By definition, all rooted trees have a topological ordering since they do not contain any cycles.

1. Pick an unvisited node.
2. Beginning with the selected node, do a DFS exploring only unvisited nodes.
3. On the recursive callback of the DFS, add the current node to the topological ordering in the reverse order.
visited = set()
outputStack = []
function dfs:
	Pass In: graph, node
	if node not in visited
		visited.add(node)
		for neighbour in graph[node]
			dfs(graph, neighbour)
		outputStack.insert(0, node)
	Pass Out: None
function topsort(graph):
	Pass In: graph
	for node in graph
		dfs(graph, node)
	Pass Out: None

Example Problems

17. Miscellaneous

Example Problems

About

Grokking the Coding Interview in 16 Patterns

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages