├── stacks-and-queues ├── sort-stack.mdx ├── animal-shelter.mdx ├── stack-min.mdx ├── three-in-one.mdx ├── queue-via-stacks.mdx └── stack-of-plates.mdx ├── trees-and-graphs ├── random-node.mdx ├── check-subtree.mdx ├── paths-with-sum.mdx ├── assets │ └── route-between-nodes.png ├── route-between-nodes.mdx ├── minimal-tree.mdx ├── bst-sequences.mdx ├── successor.mdx ├── check-balanced.mdx ├── validate-bst.mdx ├── list-of-depths.mdx ├── first-common-ancestor.mdx └── build-order.mdx ├── linked-lists ├── return-kth-to-last.mdx ├── remove-duplicates.mdx ├── delete-middle-node.mdx ├── partition.mdx ├── loop-detection.mdx ├── sum-list.mdx ├── palindrome.mdx └── intersection.mdx ├── recursion-and-dynamic-programming └── power-set.mdx ├── .gitignore ├── arrays-and-strings ├── rotate-matrix.mdx ├── string-compression.mdx ├── string-rotation.mdx ├── palindrome-permutation.mdx ├── zero-matrix.mdx ├── urlify.mdx ├── one-away.mdx ├── check-permutation.mdx └── is-unique.mdx ├── moderate ├── number-swapper.mdx └── word-frequencies.mdx └── README.md /stacks-and-queues/sort-stack.mdx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trees-and-graphs/random-node.mdx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stacks-and-queues/animal-shelter.mdx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trees-and-graphs/check-subtree.mdx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trees-and-graphs/paths-with-sum.mdx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trees-and-graphs/assets/route-between-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpankg/ctci-python-solutions/HEAD/trees-and-graphs/assets/route-between-nodes.png -------------------------------------------------------------------------------- /linked-lists/return-kth-to-last.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/return-kth-to-last) 2 | 3 | # Return Kth to Last Node 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.2 6 | 7 |
8 | 9 | ## Question 10 | 11 | You're given a Linked List. Write a function to find the Kth to last node in that Linked List. 12 | 13 | ``` 14 | Input - 1 -> 2 -> 3 -> 4 -> 5 -> None, k = 2 15 | Output - 4 16 | ``` 17 | 18 |
19 | Solution 20 | 21 | 22 | We can utilize a `slow` and `fast` pointer here. The key is that the `fast` pointer should be `k` nodes ahead of the `slow` pointer. Once the `fast` pointer reaches the end of the Linked List, we can return the node that the `slow` pointer is pointing to. 23 | 24 | ```python 25 | def returnKthToLast(head, k): 26 | fast = head 27 | for _ in range(k): 28 | if fast: 29 | fast = fast.next 30 | else: 31 | return None 32 | slow = head 33 | # fast is now k nodes ahead of slow 34 | 35 | while fast: 36 | fast = fast.next 37 | slow = slow.next 38 | 39 | return slow 40 | ``` 41 | 42 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. 43 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /recursion-and-dynamic-programming/power-set.mdx: -------------------------------------------------------------------------------- 1 | # Power Set 2 | 3 | ## Cracking the Coding Interview (CTCI) 8.4 4 | 5 |
6 | 7 | ## Question 8 | 9 | You are given a set. Write a method that returns all subsets of that set. 10 | 11 | ``` 12 | Example 13 | Input - [1,2,3] 14 | Output - [[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]] 15 | ``` 16 | 17 | 18 |
19 | Solution 20 | 21 | 22 | When adding an element to a set, we can find all new subsets by copying all of the old subsets, and adding the new element to each. We can get all subsets by making recursive calls to smaller sets. 23 | 24 | ```python 25 | import copy 26 | def get_subsets(a_set, index=None): 27 | if index is None: 28 | index = len(a_set) - 1 29 | if index == -1: 30 | return [[]] 31 | old_subsets = get_subsets(a_set, index - 1) 32 | new_subsets = [] 33 | item = a_set[index] 34 | for val in old_subsets: 35 | new_subsets.append(val) 36 | entry = copy.deepcopy(val) 37 | entry.append(item) 38 | new_subsets.append(entry) 39 | return new_subsets 40 | ``` 41 | 42 | The time complexity is $$O(n2^n)$$ and the space complexity is $$O(n2^n)$$. 43 | 44 |
45 | -------------------------------------------------------------------------------- /linked-lists/remove-duplicates.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/remove-duplicates) 2 | 3 | # Remove Duplicates 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.1 6 | 7 |
8 | 9 | ## Question 10 | 11 | You're given an unsorted linked list as input. Write a function that removes duplicate items. 12 | 13 | ``` 14 | Input - 1 -> 2 -> 1 -> 5 -> 2 -> None 15 | Output - 1 -> 2 -> 5 -> None 16 | ``` 17 | 18 |
19 | Solution 20 | 21 | 22 | We can solve this by using a set. We traverse the linked list and check if the `next` node's value in inside the set. If it is, then we modify the `cur` node's `next` value to `cur.next.next`. In other words, we remove the `next` node from the linked list. Otherwise, we add `cur.next`'s value to the set and set `cur` to `cur.next` to continue the iteration. 23 | 24 | ```python 25 | def removeDuplicates(head): 26 | if head is None: 27 | return head 28 | items = set() 29 | items.add(head.val) 30 | cur = head 31 | while cur and cur.next: 32 | if cur.next.val in items: 33 | cur.next = cur.next.next 34 | else: 35 | items.add(cur.next.val) 36 | cur = cur.next 37 | return head 38 | ``` 39 | 40 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$. 41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ON-PAGE TOOLS 2 | JS 3 | Sign in 4 | for more data. Don't have Ahrefs? 5 | Start a trial now 6 | . 7 | 8 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux 9 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux 10 | 11 | ### Linux ### 12 | *~ 13 | 14 | # temporary files which can be created if a process still has a handle open of a deleted file 15 | .fuse_hidden* 16 | 17 | # KDE directory preferences 18 | .directory 19 | 20 | # Linux trash folder which might appear on any partition or disk 21 | .Trash-* 22 | 23 | # .nfs files are created when an open file is removed but is still being accessed 24 | .nfs* 25 | 26 | ### macOS ### 27 | # General 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear in the root of a volume 40 | .DocumentRevisions-V100 41 | .fseventsd 42 | .Spotlight-V100 43 | .TemporaryItems 44 | .Trashes 45 | .VolumeIcon.icns 46 | .com.apple.timemachine.donotpresent 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | ### Windows ### 56 | # Windows thumbnail cache files 57 | Thumbs.db 58 | Thumbs.db:encryptable 59 | ehthumbs.db 60 | ehthumbs_vista.db 61 | 62 | # Dump file 63 | *.stackdump 64 | 65 | # Folder config file 66 | [Dd]esktop.ini 67 | 68 | # Recycle Bin used on file shares 69 | $RECYCLE.BIN/ 70 | 71 | # Windows Installer files 72 | *.cab 73 | *.msi 74 | *.msix 75 | *.msm 76 | *.msp 77 | 78 | # Windows shortcuts 79 | *.lnk 80 | 81 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux -------------------------------------------------------------------------------- /arrays-and-strings/rotate-matrix.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/rotate-matrix) 2 | 3 | # Rotate Matrix 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.7 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are an given an NxN matrix as input. Write a function that rotates the matrix by 90 degrees clockwise. Do this in-place. 12 | 13 | ``` 14 | Input - [ 15 | [ 5, 1, 9,11], 16 | [ 2, 4, 8,10], 17 | [13, 3, 6, 7], 18 | [15,14,12,16] 19 | ] 20 | 21 | Output - [ 22 | [15,13, 2, 5], 23 | [14, 3, 4, 1], 24 | [12, 6, 8, 9], 25 | [16, 7,10,11] 26 | ] 27 | ``` 28 | 29 |
30 | Solution 31 | 32 | 33 | The easiest way to solve this problem is to first [transpose the matrix](https://en.wikipedia.org/wiki/Transpose). After transposing it, you just have to reverse every single row in the transposed matrix. That will rotate the matrix by 90 degrees clockwise. Since we're doing the operation in-place, we don't have to return anything. 34 | 35 | ```python 36 | def rotateMatrix(matrix): 37 | # Transpose the Matrix 38 | for i in range(len(matrix)): 39 | for j in range(i + 1, len(matrix)): 40 | # Switch the row and column indices 41 | matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] 42 | 43 | # Reverse every row 44 | for r in range(len(matrix)): 45 | for i in range(len(matrix[r]) // 2): 46 | # oppI is the opposing index to i 47 | oppI = len(matrix[r]) - 1 - i 48 | matrix[r][i], matrix[r][oppI] = matrix[r][oppI], matrix[r][i] 49 | ``` 50 | 51 | The time complexity is $$O(n \cdot m)$$ where $$n$$ is the number of rows and $$m$$ is the number of columns. 52 | 53 | The space complexity is $$O(1)$$ as we are doing the operation in-place. 54 | 55 |
56 | 57 | -------------------------------------------------------------------------------- /linked-lists/delete-middle-node.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/delete-middle-node) 2 | 3 | # Delete Middle Node 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.3 6 | 7 |
8 | 9 | ## Question 10 | 11 | You're given a pointer to a node in a singly linked list. The node will NOT be the last node in the singly linked list. Write a function that deletes only that node in the linked list in-place. You don't have to return anything since the deletion is in-place. 12 | 13 | ``` 14 | Linked List - 1 -> 2 -> 3 -> 4 -> 5 -> 3 15 | Input - 4 -> 5 -> 3 16 | Result after running function - 1 -> 2-> 3 -> 5 -> 3 17 | ``` 18 | 19 |
20 | Hint 21 | 22 | 23 | We can't solve this by the standard way of deleting a node in a linked list. 24 | 25 |
26 | 27 | In order to do that, we'd typically need a pointer to the node _right before_ the node we want to delete. Then we can set the previous node's `next` to `next.next` and that would remove the node from the linked list. 28 | 29 |
30 | 31 | We don't have access to the previous node. Can you think of another way to delete the node our pointer is pointing at? 32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | Solution 40 | 41 | 42 | We need to modify the original linked list and delete the node that the pointer is pointing at. We can't delete the node because we don't have a pointer to the previous node (check hint). Therefore, we can mimic a deletion by copying over the value from the next node into the node we want to delete. Then, we can just delete the next node in the standard fashion. 43 | 44 | ```python 45 | def deleteNode(node): 46 | node.val = node.next.val 47 | node.next = node.next.next 48 | ``` 49 | 50 | The time complexity is $$O(1)$$ and the space complexity is $$O(1)$$. 51 | 52 |
53 | 54 | -------------------------------------------------------------------------------- /arrays-and-strings/string-compression.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/string-compression) 2 | 3 | # String Compression 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.6 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a string that may or may not contain repeated characters. Write a function that "compresses" the string so that repeated characters that are next to each other are replaced by the character and the count. If the length of the compression is longer than the length of the original string, return the original string. 12 | 13 | ``` 14 | Input - aaaabbccd 15 | Output - a4b2c2d1 16 | 17 | Input - aabcd 18 | Output - aabcd 19 | ``` 20 | 21 |
22 | Solution 23 | 24 | 25 | We can solve this problem by iterating through the string and counting the occurrences of each letter. As we count the occurences, we keep a separate array where we append the letter and the number of times it occurs. At the end, we can just convert that array to a string and return either the compressed array or the original string (whichever one is shorter). 26 | 27 | - Remember that strings are immutable in Python, so when we're building the compressed string, we must do so with a list. After building the compressed string, we can then convert it to a string. 28 | 29 | ```python 30 | def stringCompression(s): 31 | compressedString = [] 32 | cur = s[0] 33 | counter = 1 34 | for l in s[1:]: 35 | if l == cur: 36 | counter += 1 37 | else: 38 | compressedString.append(cur) 39 | compressedString.append(str(counter)) 40 | cur = l 41 | counter = 1 42 | compressedString.append(cur) 43 | compressedString.append(str(counter)) 44 | compressedString = "".join(compressedString) 45 | 46 | if len(compressedString) < len(s): 47 | return compressedString 48 | else: 49 | return s 50 | ``` 51 | 52 | The time complexity is $$O(n)$$ where $$n$$ is the length of s. 53 | 54 | The space complexity is also $$O(n)$$ 55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /linked-lists/partition.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/partition) 2 | 3 | # Partition 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.4 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given an unsorted linked list and an integer (we'll call this the key). Write a function that partitions the linked list around the key. All the nodes with values that are less than the key will come before all the nodes with values that are greater than or equal to the key. 12 | 13 | ``` 14 | Input - 5 -> 6 -> 3 -> 5 -> 4 -> 10 -> None, key = 4 15 | Output - 3 -> 5 -> 6 -> 5 -> 4 -> 10 -> None 16 | ``` 17 | 18 | **The order of the nodes on either side of the partition doesn't matter.** 19 | 20 |
21 | 22 |
23 | Solution 24 | 25 | 26 | We can solve this by using two dummy nodes. One dummy node is `lessThan` and it's the start for a linked list that contains all the nodes with values less than the key. The other dummy node is `greaterEqual` and is the start for a linked list that contains all the nodes with values greater than or equal to the key. 27 | 28 |
29 | 30 | We can then traverse the linked list and add the nodes to either the `lessThan` linked list or the `greaterEqual` linked list. 31 | 32 | After traversing the linked list, we append the `greaterEqual` linked list to the back of the `lessThan` linked list and return the head of the `lessThan` linked list. 33 | 34 | ```python 35 | def partition(head, key): 36 | lessThan = Node(None) 37 | greaterEqual = Node(None) 38 | headL = lessThan 39 | headG = greaterEqual 40 | 41 | while head: 42 | if head.val < key: 43 | headL.next = head 44 | headL = headL.next 45 | else: 46 | headG.next = head 47 | headG = headG.next 48 | head = head.next 49 | 50 | headL.next = greaterEqual.next 51 | return lessThan.next 52 | ``` 53 | 54 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. The space complexity is constant since the only new nodes we create are the dummy nodes, `lessThan` and `greaterEqual`. We will always create two dummy nodes 55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /trees-and-graphs/route-between-nodes.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graphs/route-between-nodes) 2 | 3 | # Route Between Nodes 4 | 5 | ## Cracking The Coding Interview 4.1 6 | 7 |
8 | 9 | # Question 10 | 11 | You are given a directed graph. You're also given a `start` node and an `end` node. 12 | 13 |
14 | 15 | Write an algorithm to check whether there is a route between the two nodes. 16 | 17 |
18 | 19 |
20 | Solution 21 | 22 | We can solve this question using any type of graph traversal algorithm. Breadth-First Search or Depth-First Search would both work optimally. 23 | 24 |
25 | 26 | We start our graph traversal at the `start` node. If we can reach the `end` node, then we return `True`, otherwise `False.` 27 | 28 |
29 | 30 | Both a BFS and a DFS will take $$O(\lvert V \rvert + \lvert E \rvert)$$ time and $$O(\lvert V \rvert)$$ space. 31 | 32 |
33 | 34 | We'll be implementing our algorithm with a Depth First Traversal. 35 | 36 |
37 | 38 | We create a `visited` set that keeps track of which nodes we've already visited in our DFS. 39 | 40 |
41 | 42 | We implement our DFS recursively, where we first check if the current node is equivalent to the `end` node (the node we're searching for). 43 | 44 |
45 | 46 | If it is, then we can return `True`. 47 | 48 |
49 | 50 | Otherwise, we recurse through all the neighbors of the current node and then run a depth first search on each of the neighbors. Of course, we first check to make sure we haven't already visited the neighbor (otherwise we would have an infinite loop in our program). 51 | 52 | ```python 53 | def findRoute(graph, start, end): 54 | visited = set() 55 | return dfs(graph, visited, start, end) 56 | 57 | 58 | def dfs(graph, visited, node, end): 59 | if node == end: 60 | return True 61 | 62 | for neighbor in graph[node]: 63 | if neighbor in visited: 64 | continue 65 | visited.add(neighbor) 66 | if dfs(graph, visited, neighbor, end): 67 | return True 68 | 69 | return False 70 | ``` 71 | 72 | Here's a Python 3 REPL with the code. 73 |
-------------------------------------------------------------------------------- /arrays-and-strings/string-rotation.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/string-rotation) 2 | 3 | # String Rotation 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.9 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given two strings as input. Write a function to check if one string is a rotation of the 12 | other string. An example of a rotation is 13 | 14 |
15 | 16 | `"CodingInterview", "erviewCodingInt"` 17 | 18 | You will also be given a function `isSubstring` that will return a boolean that is `True` if one string is a substring of the other. You can only call this function once. 19 | 20 | ``` 21 | Input - "CodingInterview", "erviewCodingInt" 22 | Output - True 23 | 24 | Input - "Test", "est" 25 | Output - False 26 | ``` 27 | 28 |
29 | Solution 30 | 31 | 32 | Let's label the input strings `x1` and `x2`. If x1 is a rotation of x2 then there must be a rotation point. An example is if `x1` is `"erviewCodingInt"` and `x2` is `"CodingInterview"`. Then, the rotation point is after `"CodingInt"`. We can imagine spliting `x2` at the rotation point into `a` and `b` where `a` is `"CodingInt"` and `b` is `"erview"`. 33 | 34 |
35 | 36 | Then, `x1` is `ba` and `x2` is `ab`. 37 | 38 |
39 | 40 | `x1 = erviewCodingInt = b + a = erview + CodingInt` 41 | 42 | `x2 = CodingInterview = a + b = CodingInt + erview` 43 | 44 |
45 | 46 | So, we want to see if we can split `x2` into `a` and `b` so that `ba = x1` and `ab = x2`. 47 | 48 |
49 | 50 | We can also see that `ba` will always be a substring of `ab + ab`, as there is a `ba` after the first `a`. Therefore, `x1` will always be a substring of `x2 + x2`. 51 | 52 |
53 | 54 | We can check if `x1` is a rotation of `x2` by seeing if `x1` is a substring of `x2 + x2`. 55 | 56 | ```python 57 | def isRotation(x1, x2): 58 | return isSubstring(x1, x2 + x2) 59 | ``` 60 | 61 | The time complexity of this code is $$O(n + m)$$ where $$n$$ is the length of `x1` and $$m$$ is the length of `x2`. We are assuming that `isSubstring` runs in $$O(n + m)$$ time. Concatinating `x2 + x2` will take $$O(m)$$ time. 62 | 63 | Therefore, the time complexity is $$O(n + m)$$. The space complexity is $$O(n + m)$$ as well. 64 | 65 |
66 | 67 | -------------------------------------------------------------------------------- /linked-lists/loop-detection.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/loop-detection) 2 | 3 | # Loop Detection 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.8 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a linked list as input. The linked list may or may not have a cycle. Write a function that returns the node at the beginning of the cycle (if it exists) or `None`. 12 | 13 | ``` 14 | Input - 1 -> 2 -> 3 -> 4 -> 2 (same 2 as earlier) 15 | Output - 2 16 | ``` 17 | 18 | intersecting linked lists 25 | 26 |
27 | Brute Force Solution 28 | 29 | 30 | We can solve this by using a set. We just iterate through the linked list and check if the current node is in our set (have we already seen this node)? If `true`, then we return the node. Otherwise, we add the node to the set and continue iterating. If we reach the end of the linked list, then there obviously isn't a cycle and we can return `None`. 31 | 32 | ```python 33 | def loopDetection(head): 34 | seenNodes = set() 35 | cur = head 36 | 37 | while cur: 38 | if cur in seenNodes: 39 | return cur 40 | else: 41 | seenNodes.add(cur) 42 | cur = cur.next 43 | ``` 44 | 45 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$. 46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | Optimized Solution 54 | 55 | 56 | The fastest we can do is $$O(n)$$ time complexity. But can we improve space complexity? 57 | 58 |
59 | 60 | We can! We can solve this in constant space using Floyd's Cycle Detection Algorithm. This algorithm is pretty simple to understand, but it's best if done with a video. Therefore, we'll let [Gayle explain it herself!](https://www.youtube.com/watch?v=MFOAbpfrJ8g). 61 | 62 | ```python 63 | def intersection(head): 64 | slow, fast = head.next, head.next.next 65 | 66 | while fast and fast.next: 67 | slow = slow.next 68 | fast = fast.next.next 69 | if slow == fast: 70 | return slow 71 | ``` 72 | 73 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. 74 | 75 |
76 | 77 | -------------------------------------------------------------------------------- /arrays-and-strings/palindrome-permutation.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/palindrome-permutation) 2 | 3 | # Palindrome Permutation 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.4 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a string. Write a function that checks if the letters in the string can be rearranged to form a palindrome (or if the input is already a palindrome). You can ignore spaces in the string. 12 | 13 | ``` 14 | Input - "car race" 15 | Output - True (the letters can be rearranged to " racecar") 16 | ``` 17 | 18 |
19 | Solution 20 | 21 | 22 | In order to solve this, we need to think about the properties of a palindrome. You should write a couple of palindromes down and see if you can notice any patterns. 23 | 24 |
25 | 26 | 27 | If a string can be rewritten as a palindrome, then it either 28 | 29 | - has an even number of characters. Ex. `caac` a - 2, c - 2 30 | - has an odd number of characters, where there's only one character that appears an odd number of times. Ex. `cabbbac` a - 2, b - 3, c - 2 31 | 32 |
33 | 34 | In other words, a palindrome is a string that can be reversed to produce the same string. That means that either every character has a matching pair, or there is only one character that appears an odd number of times (and that character will be placed in the middle of the string for the palindrome). 35 | 36 |
37 | 38 | We can count the number of occurrences of each character with a dictionary. If the counts of each character satisfy our condition, then we return `True`. 39 | 40 |
41 | 42 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$ 43 | 44 | ```python 45 | def checkPermutation(s): 46 | counts = {} 47 | for i in s: 48 | if i == " ": 49 | continue 50 | counts[i] = counts.get(i, 0) + 1 51 | hasOdd = False 52 | for value in counts.values(): 53 | if value % 2 == 1: 54 | if hasOdd: 55 | # we have more than one char 56 | # that appears an odd number 57 | # of times 58 | return False 59 | hasOdd = True 60 | 61 | return True 62 | ``` 63 | 64 | Here's a Python 3 REPL with the solution and test cases. 65 | 66 |
-------------------------------------------------------------------------------- /stacks-and-queues/stack-min.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [here](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/stack-min) 2 | 3 | # Stack Min 4 | 5 | ## Cracking the Coding Interview (CTCI) 3.2 6 | 7 |
8 | 9 | ## Question 10 | 11 | Implement a stack that has a `min` function, that returns the minimum element. The stack should also have a `push` and `pop` function. 12 | 13 |
14 | 15 | `push`, `pop` and `min` should all operate in $$O(1)$$ time. 16 | 17 |
18 | 19 |
20 | Solution 21 | 22 | We can solve this by keeping track of minimum element in our stack for each item that gets pushed.
23 | 24 | If a smaller item gets pushed, then that becomes the new minimum until we get an even smaller item. 25 | 26 |
27 | 28 | If the current minimum gets popped off the stack, then the previous minimum becomes the new minimum for our stack. 29 | 30 |
31 | 32 | We can keep keep track of the minimum by using a tuple. Whenever the user pushes an item on to our stack, we'll push a tuple containing the item and the current minimum. 33 | 34 |
35 | 36 | Here's an example 37 | 38 | ``` 39 | Example 40 | a = MinStack() 41 | a.push(10) 42 | # our internal MinStack representation is [(10, 10)] 43 | a.push(4) 44 | # our internal MinStack representation is [(10, 10), (4, 4)] 45 | a.push(6) 46 | # our internal MinStack representation is [(10, 10), (4, 4), (6, 4)] 47 | ``` 48 | 49 | The first item in our tuple is the element at that position in the stack. The second item in our tuple will keep track of the minimum to that point. 50 | 51 |
52 | 53 | Here's the Python 3 implementation. 54 | 55 | ```python 56 | class MinStack: 57 | 58 | def __init__(self): 59 | self.items = [] 60 | 61 | def push(self, item): 62 | if len(self.items) == 0: 63 | self.items.append((item, item)) 64 | else: 65 | self.items.append((item, min(item, self.items[-1][1]))) 66 | 67 | def pop(self): 68 | if len(self.items) > 0: 69 | return self.items.pop()[0] 70 | 71 | def min(self): 72 | if len(self.items) > 0: 73 | return self.items[-1][1] 74 | ``` 75 | 76 | The time complexity for `push`, `pop` and `min` are all $$O(1)$$ 77 | 78 |
79 | 80 | The space complexity is $$O(n)$$ 81 |
-------------------------------------------------------------------------------- /arrays-and-strings/zero-matrix.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/zero-matrix) 2 | 3 | # Zero Matrix 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.8 6 | 7 |
8 | 9 | ## Question 10 | 11 | The input is an N x N matrix where some of the items in the matrix are 0. If any of the items are 0, then set that item's entire row and column to 0s. Items that are not 0 will be represented by an integer. 12 | 13 | ``` 14 | Input - [ 15 | [1,2,3,4], 16 | [5,0,7,8], 17 | [6,1,1,2], 18 | [2,3,4,0] 19 | ] 20 | 21 | Output - [ 22 | [1,0,3,0], 23 | [0,0,0,0], 24 | [6,0,1,0], 25 | [0,0,0,0] 26 | ] 27 | ``` 28 | 29 |
30 | Solution 31 | 32 | 33 | Your inital thought might be to iterate through the matrix and set the row and column to 0 whenever you come across a 0. The issue with that, however, is that the original matrix may have multiple zeros. After we encounter the first 0, we'll come across another 0 and we won't know if that 0 was from the original matrix, or if it was created when we were adding 0's to the row and column of the first 0. 34 | 35 |
36 | 37 | An easy way around this is to use two passes. When we encounter a 0 on the first pass, then we'll set the row and the column to `None` (if we encounter another zero, we will not set that to `None`, since we'll want to set that item's row and column to `None` later in the loop). 38 | 39 | Then, we can have a second pass where we set all the items with `None` to 0. 40 | 41 | ```python 42 | def zeroMatrix(matrix): 43 | for r in range(len(matrix)): 44 | for c in range(len(matrix[r])): 45 | if matrix[r][c] == 0: 46 | setNone(matrix, r, c) 47 | 48 | for r in range(len(matrix)): 49 | for c in range(len(matrix[r])): 50 | if matrix[r][c] == None: 51 | matrix[r][c] = 0 52 | 53 | def setNone(matrix, r, c): 54 | # set the row to None 55 | for i in range(len(matrix[r])): 56 | if matrix[r][i] != 0: 57 | matrix[r][i] = None 58 | 59 | # set the column to None 60 | for i in range(len(matrix)): 61 | if matrix[i][c] != 0: 62 | matrix[i][c] = None 63 | 64 | matrix[r][c] = None 65 | ``` 66 | 67 | The time complexity is $$O(n \cdot m)$$ where $$n$$ is the number of rows and $$m$$ is the number of columns. 68 | 69 | The space complexity is $$O(1)$$ 70 | 71 |
72 | 73 | -------------------------------------------------------------------------------- /arrays-and-strings/urlify.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/urlify) 2 | 3 | # URLify 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.3 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a string with spaces. You want to encode the string as a URL, which means getting rid of the whitespace. 12 | 13 |
14 | 15 | Therefore, the task is to replace every space in the string with a `%20`. 16 | 17 |
18 | 19 | The string will have sufficient trailing spaces at the end to contain the additional characters from the `%20`s. 20 | 21 |
22 | 23 | You'll also be given an integer that represents the length of the string without the trailing spaces. 24 | 25 |
26 | 27 | Strings are immutable in Python, so you'll get a character array instead (list of chars). 28 | 29 | ``` 30 | # string contains 2 trailing spaces for %20 chars 31 | Input - ['h', 'i', ' ', 'b', 'u', 'd', ' ', ' '], 6 32 | Output - ['h', 'i', '%', '2', '0', 'b', 'u', 'd'] 33 | ``` 34 | 35 |
36 | Solution 37 | 38 | 39 | You can solve this using the two pointer technique. We'll create two integer variables that point to specific indexes in our character array. 40 | 41 |
42 | 43 | One variable (`p1`) will point to the the end of the array. 44 | 45 |
46 | 47 | The other variable (`p2`) will point to the end of the "true" string. It is set to the integer that we got in the input (in the example above, it would be set to `7`). 48 | 49 |
50 | 51 | Now, we run a while loop that iterates while `p1 >= 0 and p2 >= 0`. In the while loop... 52 | - If the character at `p2` is not equal to " ", then copy the character at `p2` over to `p1`. Then, decrement both pointers. 53 | - Otherwise, if the character at `p2` is equal to " ", then copy over each char in `02%` to `p1` and decrement `p1` as you go. Remember that we're iterating backwards, which is why we use `02%` instead of `%20`. Then, decrement `p2`. 54 | 55 |
56 | 57 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. 58 | 59 | ```python 60 | def urlify(s, i): 61 | p1, p2 = len(s) - 1, i - 1 62 | while p1 >= 0 and p2 >= 0: 63 | if s[p2] != " ": 64 | s[p1] = s[p2] 65 | p1 -= 1 66 | p2 -= 1 67 | else: 68 | for i in reversed("%20"): 69 | s[p1] = i 70 | p1 -= 1 71 | p2 -= 1 72 | ``` 73 | 74 | Here's a Python 3 REPL with the solution and test cases. 75 | 76 |
77 | 78 | -------------------------------------------------------------------------------- /moderate/number-swapper.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/moderate/number-swapper) 2 | 3 | # Number Swapper 4 | 5 | ## Cracking the Coding Interview (CTCI) 16.1 6 | 7 |
8 | 9 | ## Question 10 | 11 | Write a function to swap a number in place. You cannot use temporary variables. 12 | 13 | ```python 14 | # Swap the values for a and b without using temporary variables! 15 | a = 4 16 | b = 6 17 | # code to swap a and b 18 | print(a) # 6 19 | print(b) # 4 20 | ``` 21 | 22 |
23 | Solution 24 | 25 | If we were allowed to use temporary variables, this question would be pretty easy... 26 | 27 | ```python 28 | temp = a 29 | a = b 30 | b = temp 31 | ``` 32 | 33 | But we can't use a temporary variable. 34 | 35 |
36 | 37 | Instead, we can solve this question with some clever algebra! 38 | 39 |
40 | 41 | To make this easier to understand, we'll give you the solution in Python first! Then, we'll explain how it works. 42 | 43 | ```python 44 | # Swap the values for a and b without using temporary variables! 45 | a = 4 46 | b = 6 47 | a = a - b 48 | b = a + b 49 | a = b - a 50 | print(a) # 6 51 | print(b) # 4 52 | ``` 53 | 54 |
55 | 56 | We'll use $a_0$ to represent the original value of $a$ and $b_0$ to represent the original value of $b$. 57 | 58 | < br /> 59 | 60 | First, we set $a = a_0 - b_0$. That's line 4. 61 | 62 |
63 | 64 | Then, we set $b = a + b_0$. That's line 5. 65 | 66 |
67 | 68 | In other words, this means we're setting $b = (a_0 - b_0) + b_0$. You can do the algebra to see that means we're setting $b = a_0$ 69 | 70 |
71 | 72 | Finally, we set $a = b - a$. That's line 6. 73 | 74 |
75 | 76 | Now remember that in our previous two equations, we set $a = a_0 - b_0$ and $b = (a_0 - b_0) + b_0$. 77 | 78 | 79 | Therefore, we can substitute $b$ and $a$ in $b - a$ to get $a = ((a_0 - b_0) + b_0) - (a_0 - b_0)$. 80 | 81 |
82 | 83 | You can simplify the algebra in that expression to get $a = a_0 - (a_0 - b_0)$. 84 | 85 | Simplify that further to get $a = b_0$ 86 | 87 |
88 | 89 | The time complexity and space complexity are both $$O(1)$$. 90 | 91 |
92 | 93 | 94 | Now, I know what you're wondering. How the hell am I supposed to figure this out in an interview? 95 | 96 |
97 | 98 | Frankly, I have no idea. I don't write the interview questions, I'm just writing the solutions. If you're an interviewer and you're thinking of asking this question, please don't! 99 |
100 | -------------------------------------------------------------------------------- /linked-lists/sum-list.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/sum-list) 2 | 3 | # Sum List 4 | 5 | ## Cracking the Coding Interview (CTCI 2.5) 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given two numbers where each number is represented by a linked list. Each node contains a single digit of the number and the digits are stored in reverse order, such that the 1's digit is at the head. 12 | 13 | ``` 14 | 123 = 3 -> 2 -> 1 15 | ``` 16 | 17 | Write a function that takes these two linked lists and returns the sum of the numbers as a linked list. 18 | 19 | ``` 20 | Input - 9 -> 1 -> 6, 4 -> 3 -> 2 21 | Output - 3 -> 5 -> 8 22 | ``` 23 | 24 |
25 | Solution 26 | 27 | 28 | This question can get extremely tricky with edge cases if we're not careful. Some edge cases include carry overs, numbers of different length, or one (or both) of the linked lists being `None`. 29 | 30 |
31 | 32 | A very elegant solution that deals with all of these edge cases is if you create a new linked list that represents the sum. Initiate the first node in the sum to have a value of `0` and check if `l1` is currently pointing to a node. If it is, add the value for `l1`'s node to our `newNode` and set the `l1` pointer to the next node in that first number's linked list. Then, we check if `l2` is pointing at a node. If it is, we add the value for `l2`'s node to `newNode`. Now, we have to make sure that our `newNode`'s sum is less than 10. Remember that each node represents a single digit. 33 | 34 |
35 | 36 | If `newNode`'s value is greater than 9, we will have a `carryOver` boolean that we'll set to True. This will tell us to increment the next node in our sum by 1. We can now decrement `newNode`'s value by 10. 37 | 38 |
39 | 40 | Now, we continue on doing the same as long as either 41 | 42 | - `l1` is pointing at a node 43 | - `l2` is pointing at a node 44 | - `carryOver` is True 45 | 46 | ```python 47 | class Node: 48 | def __init__(self, val): 49 | self.val = val 50 | self.next = None 51 | 52 | def sumList(l1, l2): 53 | sumList = Node(None) 54 | cur = sumList 55 | carryOver = False 56 | 57 | while l1 or l2 or carryOver: 58 | newNode = Node(0) 59 | 60 | if l1: 61 | newNode.val += l1.val 62 | l1 = l1.next 63 | 64 | if l2: 65 | newNode.val += l2.val 66 | l2 = l2.next 67 | 68 | if carryOver: 69 | newNode.val += 1 70 | carryOver = False 71 | 72 | if newNode.val > 9: 73 | carryOver = True 74 | newNode.val -= 10 75 | 76 | cur.next = newNode 77 | cur = cur.next 78 | 79 | return sumList.next 80 | ``` 81 | 82 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$. 83 | 84 |
85 | 86 | -------------------------------------------------------------------------------- /trees-and-graphs/minimal-tree.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/minimal-tree) 2 | 3 | # Minimal Tree 4 | 5 | ## Cracking the Coding Interview 4.2 6 | 7 |
8 | 9 | # Question 10 | 11 | You are given a **sorted** array with unique integer elements as input. 12 | 13 |
14 | 15 | Write an algorithm that creates a binary search tree with minimal height from that array. 16 | 17 |
18 | 19 |
20 | Solution 21 | 22 | This question is actually quite a bit easier than it may seem. We can solve it using recursion! 23 | 24 |
25 | 26 | We first find the middle element of the sorted array. All the numbers before will be less than this element and all the numbers after will be greater than (because the array is sorted). 27 | 28 |
29 | 30 | Additionally, the number of elements before our middle will be approximately equal to the number of elements after our middle (because we've picked the middle of the list). 31 | 32 |
33 | 34 | So now, we can create the root node of our BST using the middle element. 35 | 36 |
37 | 38 | Then, we recursively call our function to create another BST for all the elements on the left (all the elements less than our middle). That BST becomes our left child. 39 | 40 |
41 | 42 | We recursively call our function again to create a BST for all the elements on the right (all the elements greater than our middle). That BST becomes our right child. 43 | 44 |
45 | 46 | The base case for our recursive function will be if we get an empty array. Then we can just return `None`. 47 | 48 |
49 | 50 | After creating both the left and right subtrees, we can return our tree! 51 | 52 |
53 | 54 | The time complexity is $$O(n)$$. 55 | 56 |
57 | 58 | This is because our function can be represented by the recurrence 59 | 60 |
61 | 62 | $$T(n) = 2 \cdot T(\dfrac{n}{2}) + c$$ 63 | 64 |
65 | 66 | Using the Master Method, we know that means our time complexity is $$O(n)$$. 67 | 68 |
69 | 70 | Learn about the Master Method here. 71 | 72 |
73 | 74 | The space complexity is also $$O(n)$$. 75 | 76 | ```python 77 | class TreeNode: 78 | def __init__(self, val = None): 79 | self.val = val 80 | self.left = None 81 | self.right = None 82 | 83 | def createBST(nums): 84 | if len(nums) == 0: 85 | return None 86 | 87 | middle = len(nums) // 2 88 | 89 | root = TreeNode(nums[middle]) 90 | root.left = createBST(nums[:middle]) 91 | root.right = createBST(nums[middle + 1:]) 92 | 93 | return root 94 | ``` 95 | 96 | Here's a Python 3 REPL with the code. 97 |
-------------------------------------------------------------------------------- /stacks-and-queues/three-in-one.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [here](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/three-in-one) 2 | 3 | # Three in One 4 | 5 | ## Cracking the Coding Interview (CTCI) 3.1 6 | 7 |
8 | 9 | ## Question 10 | 11 | Write a class that mimics three stacks. You can only use a single array as the underlying data structure. 12 | 13 | ``` 14 | Example 15 | a = ThreeStack() 16 | a.push(0, 4) # stack 0, push 4 17 | a.push(1, 5) # stack 1, push 4 18 | a.push(2, 7) # stack 2, push 5 19 | a.push(0, 6) # stack 0, push 6 20 | print(a.pop(0)) # 6 21 | print(a.pop(1)) # 5 22 | print(a.pop(2)) # 7 23 | ``` 24 | 25 |
26 | Beginner Solution 27 | 28 | We can implement our stacks with **fixed** capacity. 29 | 30 |
31 | 32 | Once a stack is filled, a user cannot push more items to it without first popping off items. 33 | 34 |
35 | 36 | We'll implement the stacks using a single array that has a fixed capacity of $$n$$. 37 | 38 |
39 | 40 | The first stack will use elements [0, $$\frac{n}{3}$$). 41 | 42 |
43 | 44 | The second stack will use elements [$$\frac{n}{3}$$, $$\frac{2n}{3}$$). 45 | 46 |
47 | 48 | The third stack will use elements [$$\frac{2n}{3}$$, n). 49 | 50 |
51 | 52 | We will maintain 2 lists of pointers. The first list will keep track of where the tops are for each of the 3 stacks. 53 | 54 |
55 | 56 | The second list will keep track of the limits for each of the three stacks. 57 | 58 | ```python 59 | class ThreeStack: 60 | def __init__(self, capacity = 5): 61 | capacity = capacity * 3 # since we have 3 stacks 62 | self.items = [None] * capacity 63 | # pointers for tops of stacks 0, 1, 2 64 | self.tops = [0, capacity // 3, 2 * (capacity // 3)] 65 | # pointers for limits of stacks 0, 1, 2 66 | self.limits = [capacity // 3, 2 * (capacity // 3), capacity] 67 | 68 | def push(self, stack, item): 69 | if stack > 2: 70 | raise ValueError("stack does not exist") 71 | 72 | if self.tops[stack] >= self.limits[stack]: 73 | raise Exception(f"stack {stack} is full") 74 | 75 | self.items[self.tops[stack]] = item 76 | self.tops[stack] += 1 77 | 78 | 79 | def pop(self, stack): 80 | if stack > 2: 81 | raise ValueError("stack does not exist") 82 | top = self.tops[stack] - 1 83 | if top < 0 or self.items[top] == None: 84 | raise IndexError("pop from empty stack") 85 | 86 | item = self.items[top] 87 | self.items[top] = None 88 | self.tops[stack] = top 89 | 90 | return item 91 | ``` 92 | 93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /arrays-and-strings/one-away.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/one-away) 2 | 3 | # One Away 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.5 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given two strings as input. You want to find out if these two strings are at most one edit away from each other. 12 | 13 | An edit is defined as either 14 | 15 | - inserting a character 16 | - removing a character 17 | - replacing a character 18 | 19 | ``` 20 | Input - "whale", "wrale" 21 | Output - True 22 | 23 | Input - "rake", "care" 24 | Output - False 25 | 26 | Input - "rake", "rake" 27 | Output - True 28 | ``` 29 | 30 |
31 | Solution 32 | 33 | 34 | We can first check the lengths of the two strings. If the lengths differ by more than 1, we can immediately return False since we can insert 1 character at most. 35 | 36 | Then, we iterate through both strings and compare them character by character. 37 | 38 | - if the characters are the same, continue on to the next character for both strings 39 | - elif the characters are different and we haven't made an edit yet 40 | - if the lengths are the same 41 | - try replacing the char at string A with the char at string B 42 | - elif the lengths are different 43 | - try deleting one char from the longer string 44 | - make a note that we've made an edit 45 | - elif the characters are different and we have made an edit 46 | - return False 47 | 48 | ```python 49 | def oneAway(s1, s2): 50 | if abs(len(s1) - len(s2)) > 1: 51 | return False 52 | # p1 is pointer for s1 53 | # p2 is pointer for s2 54 | p1, p2 = 0, 0 55 | madeEdit = False 56 | while p1 < len(s1) and p2 < len(s2): 57 | if s1[p1] == s2[p2]: 58 | p1 += 1 59 | p2 += 1 60 | elif not madeEdit: 61 | madeEdit = True 62 | if len(s1) == len(s2): 63 | p1 += 1 64 | p2 += 1 65 | # this represents a replacement. 66 | # we "pretend" there's a replacement 67 | # and now we can view s1[p1] as equal 68 | # to s2[p2] and increment p1, p2 69 | elif len(s1) < len(s2): 70 | p2 += 1 71 | # we "delete" the char at p2 72 | # and increment p2 while not 73 | # incrementing p1 74 | elif len(s1) > len(s2): 75 | p1 += 1 76 | # same as above, except for p1 now 77 | else: 78 | return False 79 | return True 80 | ``` 81 | 82 | The time complexity is $$O(n)$$ where $$n$$ is the length of s1. That's because our loop is incrementing `p1` and `p2` (unless we have to make an edit, which can only happen at most once) and the lengths of `s1` and `s2` must be at most 1 apart, making them the same under Big O. 83 | 84 | The space complexity is $$O(1)$$ 85 | 86 |
87 | 88 | -------------------------------------------------------------------------------- /linked-lists/palindrome.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/palindrome) 2 | 3 | # Palindrome 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.6 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a linked list. Write a function that checks if the values in the linked list will form a palindrome. In other words, if you reverse the linked list, the values will still be the same. 12 | 13 | ``` 14 | Input - 1 -> 2 -> 2 -> 1 15 | Output - True 16 | 17 | Input - 1 -> 2 -> 3 -> 2 -> 1 18 | Output - True 19 | 20 | Input - 1 -> 2 21 | Output - False 22 | ``` 23 | 24 |
25 | Solution 26 | 27 | 28 | Let's ignore the Linked List component for a second. If you just had a string, how could you tell if it was a palindrome? 29 | 30 |
31 | 32 | You could just create a copy of the string and reverse it. Then, compare that to the original string. 33 | 34 |
35 | 36 | If they're equivalent, then you have a palindrome. 37 | 38 |
39 | 40 | Therefore, with a linked list input, we could just copy the linked list, reverse it and check if it's equivalent to the original linked list. 41 | 42 |
43 | 44 | The issue with this approach is that it requires extra space ($$O(n)$$ space complexity). Can we solve this question using no extra space? 45 | 46 |
47 | 48 | We can, by iterating through half of the linked list, and then reversing the second half. Then, we compare the reversed second half with the original first half and check to make sure they're both equivalent. If the linked list had an odd length, then we just ignore the middle element. 49 | 50 | ```python 51 | def reverseLL(head): 52 | prev = None 53 | while head: 54 | temp = head.next 55 | head.next = prev 56 | prev = head 57 | head = temp 58 | return prev 59 | 60 | def palindrome(head): 61 | # An empty LL is a palindrome 62 | if head is None: 63 | return True 64 | 65 | # get length of linked list 66 | length = 0 67 | cur = head 68 | while cur: 69 | length += 1 70 | cur = cur.next 71 | 72 | # find halfway point 73 | if length % 2 == 0: 74 | halfWay = length // 2 75 | elif length % 2 == 1: 76 | halfWay = length // 2 + 1 77 | 78 | # reverse second half 79 | cur = head 80 | for _ in range(halfWay - 1): 81 | cur = cur.next 82 | 83 | secondHalf = cur.next 84 | cur.next = None 85 | 86 | secondHalf = reverseLL(secondHalf) 87 | 88 | # compare both halves 89 | firstHalf = head 90 | while firstHalf and secondHalf: 91 | if firstHalf.val != secondHalf.val: 92 | return False 93 | firstHalf = firstHalf.next 94 | secondHalf = secondHalf.next 95 | 96 | return True 97 | ``` 98 | 99 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. 100 | 101 |
102 | 103 | -------------------------------------------------------------------------------- /stacks-and-queues/queue-via-stacks.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [here](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/queue-via-stacks) 2 | 3 | # Queue via Stacks 4 | 5 | ## Cracking the Coding Interview (CTCI) 3.4 6 | 7 |
8 | 9 | ## Question 10 | 11 | Build a class `MyQueue` that implements a queue using two stacks. 12 | 13 |
14 | 15 | Your class should implement the methods `enqueue`, `dequeue` and `peek`. 16 | 17 |
18 | 19 |
20 | Solution 21 | 22 |
23 | 24 | The difference between a queue and a stack is that queues operate on a First In First Out basis while stacks operate on a First In Last Out basis. 25 | 26 |
27 | 28 | This means that when you `push` N items on to the stack and then `pop` those N items off the stack, the **order of the N items will be reversed**. This is due to the **Last In First Out** property of stacks. 29 | 30 |
31 | 32 | When you're working with a queue, however, **the order of items is maintained**. If you `enqueue` N items on to a queue and then `dequeue` those N items, the order of the items will remain the same. This is due to the **First In First Out** property of queues. 33 | 34 |
35 | 36 | Therefore, if we want to build a queue with stacks, we have to design our data structure in a way so that the order of the items remains the same, rather than reversing (what a stack naturally does). 37 | 38 |
39 | 40 | We can accomplish this by pushing each item inserted *through two stacks*. The first stack will reverse every item inserted. The second stack will reverse every item inserted *again*, which means going back to the original order. 41 | 42 |
43 | 44 | So how do we implement this in code? 45 | 46 |
47 | 48 | Whenever the user enqueues an item into our data structure, we'll `push` it to `stack1`. 49 | 50 |
51 | 52 | Now, when we want `dequeue` items from our queue, we have to reverse the order. 53 | 54 |
55 | 56 | We first call `_reverseStack` which handles transfering all the items from `stack1` to `stack2`. 57 | 58 |
59 | 60 | Then, we can `pop` the last item in `stack2` and return that. 61 | 62 |
63 | 64 | `peek` works the exact same way as `dequeue` except we are just returning the last item in `stack2` instead of popping it off. 65 | 66 |
67 | 68 | The time complexity is $$O(n)$$ since each item is transferred from one stack to the other *once*. The space complexity is also $$O(n)$$. 69 | 70 | ```python 71 | class MyQueue: 72 | def __init__(self): 73 | self.stack1 = [] 74 | self.stack2 = [] 75 | 76 | def enqueue(self, item): 77 | self.stack1.append(item) 78 | 79 | def _reverseStack(self, operation): 80 | if len(self.stack2) == 0: 81 | if len(self.stack1) == 0: 82 | raise IndexError(f'{operation} from empty queue') 83 | while len(self.stack1): 84 | self.stack2.append(self.stack1.pop()) 85 | 86 | 87 | def dequeue(self): 88 | self._reverseStack("deque") 89 | return self.stack2.pop() 90 | 91 | def peek(self): 92 | self._reverseStack("peek") 93 | return self.stack2[-1] 94 | ``` 95 |
-------------------------------------------------------------------------------- /trees-and-graphs/bst-sequences.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/bst-sequence) 2 | 3 | # Bst Sequence 4 | 5 | ## Cracking The Coding Interview 4.9 6 | 7 |
8 | 9 | # Question 10 | 11 | A binary search tree was created by traversing through an array from left to right and inserting each element. Given a 12 | binary tree with distinct elements, print all possible arrays that could have led to this tree. 13 | 14 | ``` 15 | 16 | InPut - 6 17 | / \ 18 | 3 9 19 | 20 | Output -[[6, 3, 9], 21 | [6, 9, 3]] 22 | 23 | InPut - 6 24 | / \ 25 | 3 9 26 | / \ 27 | 1 4 28 | 29 | Output - [[6, 3, 1, 4, 9], 30 | [6, 3, 1, 9, 4], 31 | [6, 3, 9, 1, 4], 32 | [6, 9, 3, 1, 4], 33 | [6, 3, 4, 1, 9], 34 | [6, 3, 4, 9, 1], 35 | [6, 3, 9, 4, 1], 36 | [6, 9, 3, 4, 1]] 37 | 38 | ``` 39 | 40 |
41 | Solutiion 42 | 43 | Thinking recursive is the key to the solution of this problem, Let's say you have all the sequences for left subtree(left_sequences) 44 | and all the sequences for right subtree(right_sequences) you can get the sequences for that tree by weaving the left_sequences with 45 | right_esquences and then prepending root to all of them. 46 | 47 | ```python 48 | from collections import deque 49 | 50 | def all_sequences(root): 51 | result = [] 52 | if not root: 53 | result.append(deque()) 54 | return result 55 | 56 | # get the sequences from left and right subtrees 57 | left_sequences = all_sequences(root.left) 58 | right_sequences = all_sequences(root.right) 59 | 60 | # weave together each sequence from left sequece to right sequence 61 | for left in left_sequences: 62 | for right in right_sequences: 63 | weaved = [] 64 | weave_lists(left, right, weaved, deque([root.data])) 65 | result.extend(weaved) 66 | 67 | return result 68 | 69 | 70 | def weave_lists(first, second, results, prefix): 71 | if not first or not second: 72 | # deep clone the prefix since it will be changed 73 | # once we go back in recursion 74 | result = prefix.copy() 75 | result.extend(first) 76 | result.extend(second) 77 | results.append(result) 78 | return 79 | 80 | # remove one element from the begining of the list and 81 | # append it to prefix and recurse, and once returned back from 82 | # from recusrion add the element back to begining of the list 83 | # remove it from prefix. 84 | head_first = first.popleft() 85 | prefix.append(head_first) 86 | weave_lists(first, second, results, prefix) 87 | first.appendleft(head_first) 88 | prefix.pop() 89 | 90 | # do the exact steps for second list. 91 | head_second = second.popleft() 92 | prefix.append(head_second) 93 | weave_lists(first, second, results, prefix) 94 | second.appendleft(head_second) 95 | prefix.pop() 96 | 97 | 98 | ``` 99 | Time complexity exponential, Space complexity exponetial(considering the results list) 100 |
101 | -------------------------------------------------------------------------------- /arrays-and-strings/check-permutation.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/check-permutation) 2 | 3 | # Check Permutation 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.2 6 | 7 |
8 | 9 | ## Question 10 | 11 | The input is two strings. Check if the first string is a permutation of the second string. 12 | 13 | ``` 14 | Input - "wazup bro", " orbpuwaz" 15 | Output - True 16 | # the first character in " orbpuwaz" is a " " 17 | 18 | Input - "hiiiya", "hiya" 19 | Output - False 20 | ``` 21 | 22 |
23 | Brute Force Solution 24 | 25 | We can first check to make sure the lengths of both strings are the same. If the lengths are different, then we immediately know that the strings are not permuations of each other. 26 | 27 |
28 | 29 | Sort the characters in both strings. Then, check if the strings are equivalent. 30 | 31 |
32 | 33 | If they are, that means that they consist of the same characters and are a permutation of each other. 34 | 35 |
36 | 37 | The time complexity is $$O(n \log n)$$ since we're sorting the characters in both strings. The space complexity is $$O(n)$$ since we're creating new strings for `s1` and `s2`. 38 | 39 | ```python 40 | def checkPermutation(s1, s2): 41 | if len(s1) != len(s2): 42 | return False 43 | 44 | s1, s2 = sorted(s1), sorted(s2) 45 | return s1 == s2 46 | ``` 47 | 48 |
49 | 50 |
51 | 52 |
53 | Optimized Solution 54 | 55 | We want to check if both strings consist of the same characters. 56 | 57 |
58 | 59 | We can do this by creating a dictionary of character counts for the first string. 60 | 61 |
62 | 63 | We iterate through the first string and count the number of times each character appears. As we iterate through the first string, if we come across a character that is not in the dictionary, we add the character as a key in the dictionary and give it a value of `1`. If the character is already a key in the dictionary, then we increment it's value. 64 | 65 |
66 | 67 | With the dictionary of character counts for the first string, we now want to compare it to the second string and make sure the second string has all the same characters. If so, then we know that the second string is a permutation of the first string. 68 | 69 |
70 | 71 | We iterate through the second string and check if the character is a key in our dictionary. If it's not a key, then we immediately know that the second string *is not* a permutation of the first string and we can return `False`. If the character is a key in our dictionary, then we can decrement the value for that key. If the value is now `0`, then we can delete that key from our dictionary. 72 | 73 |
74 | 75 | If the second string is a permutation of the first string, the dictionary should now be empty. So we'll return `True` if our character counts dictionary is empty and `False` otherwise. 76 | 77 |
78 | 79 | The time complexity is $$O(n)$$. The space complexity is $$O(n)$$. 80 | 81 | ```python 82 | def checkPermutation(s1, s2): 83 | if len(s1) != len(s2): 84 | return False 85 | 86 | count = {} 87 | for i in s1: 88 | count[i] = count.get(i, 0) + 1 89 | for i in s2: 90 | if i in count: 91 | count[i] -= 1 92 | if count[i] == 0: 93 | del count[i] 94 | else: 95 | return False 96 | return len(count) == 0 97 | ``` 98 |
99 | -------------------------------------------------------------------------------- /trees-and-graphs/successor.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/successor) 2 | 3 | # Check Balanced 4 | 5 | ## Cracking The Coding Interview 4.6 6 | 7 |
8 | 9 | # Question 10 | 11 | Write an algorithm to find the next node(i.e. in-order-successor) of a given node in a binary search tree. you may assume that each node 12 | has a link to its parent. 13 | 14 | ``` 15 | 16 | Tree - 6 17 | / \ 18 | 3 9 19 | / \ / \ 20 | 1 4 7 10 21 | 22 | InPut - 6 23 | OutPut - 7 24 | 25 | InPut - 4 26 | OutPut - 6 27 | 28 | InPut - 10 29 | OutPut - None 30 | 31 | ``` 32 | 33 |
34 | Brute Force Solution 35 | 36 | Since every node has a pointer to its parent find the root of the tree by folowing the parent pointer of the given node, 37 | then traverse the tree in-order and copy the the data to an array, and find the next greatest element in the sorted array. 38 | 39 | ```python 40 | 41 | from typing import Optional, List 42 | 43 | 44 | class BinaryTreeNode: 45 | def __init__(self, data, parent): 46 | self.data = data 47 | self.right = None 48 | self.left = None 49 | self.parent = parent 50 | 51 | 52 | def successor(node: BinaryTreeNode) -> Optional[BinaryTreeNode]: 53 | if not node: 54 | return None 55 | root = get_root(node) 56 | sorted_items: List[BinaryTreeNode] = [] 57 | fill_sorted_items(root, sorted_items) 58 | for index in range(len(sorted_items)): 59 | if sorted_items[index].data > node.data: 60 | return sorted_items[index] 61 | return None 62 | 63 | 64 | def fill_sorted_items(root: BinaryTreeNode, sorted_items: List[BinaryTreeNode]) -> None: 65 | if not root: 66 | return 67 | fill_sorted_items(root.left, sorted_items) 68 | sorted_items.append(root) 69 | fill_sorted_items(root.right, sorted_items) 70 | 71 | 72 | def get_root(node: BinaryTreeNode) -> BinaryTreeNode: 73 | if not node.parent: 74 | return node 75 | return get_root(node.parent) 76 | 77 | ``` 78 | 79 | Time complexity O(n). 80 | Space complexity O(n). 81 | 82 |
83 | 84 |
85 | 86 |
87 | Optimized Solution 88 | 89 | Trick to optimize the solution is in understanding how in-order traversal works. If a node has right subtree, then in-order successor 90 | is the left most node on the right subtree, if a node doesn't have a subtree then we have to traverse up using the parent pointer until we are 91 | on left side instead of right side of a node. 92 | 93 | ```python 94 | 95 | from typing import Optional 96 | 97 | 98 | def successor(node: BinaryTreeNode) -> Optional[BinaryTreeNode]: 99 | if not node: 100 | return None 101 | if node.right: 102 | return left_most_child(node.right) 103 | curr: BinaryTreeNode = node 104 | parent: Optional[BinaryTreeNode] = curr.parent 105 | while parent and parent.left != curr: 106 | curr = parent 107 | parent = parent.parent 108 | return parent 109 | 110 | 111 | def left_most_child(node: BinaryTreeNode) -> Optional[BinaryTreeNode]: 112 | while node and node.left: 113 | node = node.left 114 | return node 115 | 116 | 117 | ``` 118 | Time complexity O(h), where h is the height of the tree. 119 | Space complexity O(1). 120 |
-------------------------------------------------------------------------------- /moderate/word-frequencies.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/moderate/word-frequencies) 2 | 3 | # Word Frequencies 4 | 5 | ## Cracking the Coding Interview (CTCI) 16.2 6 | 7 |
8 | 9 | ## Question 10 | 11 | Design a method that finds the frequency of occurances of any given word in a book. How would you change the method if we had to run it multiple times for different query words? 12 | 13 |
14 | 15 |
16 | Single Query Solution 17 | 18 | We'll assume that the book is a list of words. 19 | 20 |
21 | 22 | If we want to check for a word and we're only running the method once, the most efficient way is to just iterate through the list of words and see if any of them are equivalent to the word we're looking for. 23 | 24 |
25 | 26 | We'll assume that capitalization doesn't matter, but this is something that you should check with your interviewer. 27 | 28 | ```python 29 | import string # Importing standard library is okay during interview 30 | 31 | def getWordFrequency(book: List[str], word: str): 32 | count = 0 33 | word = word.trim().lower() 34 | for w in book: 35 | if word == w.trim().lower(): 36 | count += 1 37 | 38 | return count 39 | ``` 40 | 41 | This code runs in $$O(n)$$ time and takes $$O(1)$$ space. 42 | 43 |
44 | 45 |
46 | 47 |
48 | Multiple Query Solution 49 | 50 | The previous solution works great when you're only calling the method a few times. 51 | 52 |
53 | 54 | However, as you start calling the method many times for different words, your method will have to search the entire book on each call. 55 | 56 |
57 | 58 | Now it makes sense to do some preprocessing on the book so that each call to the method can be done faster. 59 | 60 |
61 | 62 | We can create a data structure that keeps track of the number of occurrences of every word in our book. This can be done with a hash table (or dictionary). 63 | 64 |
65 | 66 | This way, whenever we want to query the occurrence of a specific word, we can just search our hash table and return the answer in constant time. 67 | 68 |
69 | 70 | Creating the data structure is quite simple. We create our dictionary and then iterate through the book string. If a word is already in our dictionary, then we update it's count by 1. Otherwise, we add it to the dictionary with a count of one. 71 | 72 |
73 | 74 | Then, our `getWordFrequency` function will just query the dictionary for the number of occurrences of the given word and return the answer in $$O(1)$$ time. 75 | 76 | ```python 77 | class wordFrequency: 78 | def __init__(self, book): 79 | self.wordCounts = self.setupDictionary(book) 80 | 81 | def setupDictionary(self, book): 82 | wordCounts = {} 83 | for w in book: 84 | w = w.trim().lower() 85 | if w == "": 86 | continue 87 | if w in wordCounts: 88 | wordCounts[w] += 1 89 | else: 90 | wordCounts[w] = 1 91 | return wordCounts 92 | 93 | def getWordFrequency(self, word): 94 | word = word.lower() 95 | if word in self.wordCounts: 96 | return self.wordCounts[word] 97 | 98 | return 0 99 | ``` 100 |
101 | 102 |
103 | 104 |
105 | Further Enhancement 106 | 107 | 108 | Currently, the word counts for our books are stored in volatile memory. If our program crashes or we shut off the computer, we'll lose all the word counts. 109 | 110 |
111 | 112 | You can fix this by using a key-value database like Redis. Redis is in-memory (so reads and writes are quite fast) and can be configured to be persistent so it writes to disk at regular intervals. 113 | 114 |
115 | 116 | That way, you won't have to re-index your books if your program crashes. 117 | 118 |
119 | -------------------------------------------------------------------------------- /trees-and-graphs/check-balanced.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/check-balanced) 2 | 3 | # Check Balanced 4 | 5 | ## Cracking The Coding Interview 4.4 6 | 7 |
8 | 9 | # Question 10 | 11 | Implement a function to check if a binary tree is balanced. 12 | 13 |
14 | 15 | A balanced tree is a tree where the heights of the two subtrees of any node never differ by more than one. 16 | 17 | ``` 18 | Input - 6 19 | / \ 20 | 3 9 21 | / \ / \ 22 | 1 4 7 10 23 | \ \ \ 24 | 2 8 11 25 | Output - True 26 | 27 | Input - 6 28 | / \ 29 | 3 9 30 | / \ / 31 | 1 4 7 32 | \ \ 33 | 2 8 34 | Output - False 35 | Explanation - Node 9 is not balanced. 36 | ``` 37 | 38 |
39 | Brute Force Solution 40 | 41 | We traverse the entire tree. For each node we get the height of the left subtree and right subtree. 42 | 43 |
44 | 45 | We check to make sure the two heights do not differ by more than than one. 46 | 47 |
48 | 49 | The time complexity is $$O(n \log n)$$. This is because we are visiting every single node in the tree which takes $$O(n)$$ time. At **each** node, we're traversing it's children to get their heights. That takes an average of $$O(\log n)$$ time. 50 | 51 |
52 | 53 | The space complexity is $$O(h)$$ where $$h$$ is the height of the tree. This is due to the space the call stack uses as we recurse through our tree. 54 | 55 | 56 | ```python 57 | def isBalanced(root: BinaryTreeNode) -> bool: 58 | if not root: 59 | return True 60 | if abs(getHeight(root.left) - getHeight(root.right)) > 1: 61 | return False 62 | else: 63 | return isBalanced(root.left) and isBalanced(root.right) 64 | 65 | 66 | def getHeight(root): 67 | if not root: 68 | return -1 69 | else: 70 | return max(getHeight(root.left), getHeight(root.right)) + 1 71 | ``` 72 | 73 | Here's a Python 3 REPL with the code. 74 |
75 | 76 |
77 | 78 |
79 | Optimized Solution 80 | 81 | In the brute force solution, we visit every node in the tree and check the heights of the left and right subtree **for each node**. This results in a lot of repeated computation. 82 | 83 |
84 | 85 | We can write a more efficient solution if we keep track of the heights **as we're traversing through the tree**. 86 | 87 |
88 | 89 | We'll use a helper function, `_balanced` that takes in a node. Then, the helper function will find the if the left subtree is balanced **and** what the left subtree's height. The helper function will do the same for the right subtree. 90 | 91 |
92 | 93 | If both the left and right subtree are balanced, then the function will look at the heights of the left and right subtrees and make sure that they are within 1. 94 | 95 |
96 | 97 | If all these conditions are met, then the helper function knows that the current node is a balanced binary tree. **It will now calculate the height of the tree at the current node** by taking the `max` of the left subtree's height and the right subtree's height. Then, add `1` to account for the current node. 98 | 99 |
100 | 101 | Then our helper function will return a boolean representing whether the current tree is balanced **and the current tree's height**. 102 | 103 |
104 | 105 | The time complexity is $$O(n)$$ since we're visiting each node once. The space complexity is $$O(h)$$ where $$h$$ is the height of the tree. 106 | 107 | ```python 108 | def isBalanced(root): 109 | def _balanced(root): 110 | if root == None: 111 | return (True, 0) 112 | leftBalanced, leftHeight = _balanced(root.left) 113 | rightBalanced, rightHeight = _balanced(root.right) 114 | if leftBalanced and rightBalanced and abs(leftHeight - rightHeight) < 2: 115 | currentHeight = max(leftHeight, rightHeight) + 1 116 | return (True, currentHeight) 117 | return (False, None) 118 | return _balanced(root)[0] 119 | ``` 120 |
-------------------------------------------------------------------------------- /trees-and-graphs/validate-bst.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/validate-bst) 2 | 3 | # Validate BST 4 | 5 | ## Cracking The Coding Interview 4.5 6 | 7 |
8 | 9 | # Question 10 | 11 | Implement a function to check if a binary tree is a binary search tree 12 | 13 | ``` 14 | Input - 6 15 | / \ 16 | 3 9 17 | / \ / \ 18 | 1 4 7 10 19 | 20 | Output - True 21 | 22 | Input - 6 23 | / \ 24 | 3 9 25 | / \ / \ 26 | 1 8 7 10 27 | 28 | 29 | Output - False 30 | 31 | ``` 32 |
33 | Brute Force Solution 34 | 35 | Traverse the tree in-order copy the the data to an array and verify if the array is sorted. 36 | 37 | ```python 38 | 39 | class BinaryTreeNode: 40 | def __init__(self, data): 41 | self.data = data 42 | self.right = None 43 | self.left = None 44 | 45 | 46 | 47 | def is_bst(root: BinaryTreeNode) -> bool: 48 | def fill_sorted_items(root): 49 | if not root: 50 | return 51 | fill_sorted_items(root.left) 52 | sorted_items.append(root.data) 53 | fill_sorted_items(root.right) 54 | sorted_items = [] 55 | fill_sorted_items(root) 56 | i = 1 57 | while i < len(sorted_items): 58 | if sorted_items[i-1] >= sorted_items[i]: 59 | return False 60 | i += 1 61 | 62 | return True 63 | 64 | ``` 65 | Time complexity O(n). 66 | Space complexity O(n). 67 | this approach fails when duplicates are alowed on any one side of the root, 68 | let's say our bst property is (left <= root < right), inorder traversal produces same output in folowing cases. 69 | 70 | ``` 71 | Valid BST Invalid BST 72 | Input - 20 20 73 | / \ 74 | 20 20 75 | 76 | ``` 77 |
78 | 79 |
80 | 81 |
82 | Optimized Solution One 83 | 84 | if we look closely at brute force solution the list sorted_items is used only for comparing the element to previous element, 85 | we can do the same comparision while traversing the tree in-order by keeping track of previously seen node. 86 | 87 | 88 | ```python 89 | 90 | class BinaryTreeNode: 91 | def __init__(self, data): 92 | self.data = data 93 | self.right = None 94 | self.left = None 95 | 96 | 97 | 98 | class WrapPreviousNodeData: 99 | def __init__(self, data): 100 | self.data = data 101 | 102 | previous_node_data: WrapPreviousNodeData = WrapPreviousNodeData(float("-inf")) 103 | def is_bst(root: BinaryTreeNode) -> bool: 104 | if not root: 105 | return True 106 | if not is_bst(root.left): 107 | return False 108 | if root.data < previous_node_data.data: 109 | return False 110 | previous_node_data.data = root.data 111 | if not is_bst(root.right): 112 | return False 113 | return True 114 | 115 | ``` 116 | Time complexity O(n). we touch all nodes of the tree once. 117 | Space complexity O(h), because of recursion, recursion depth is equal to height of the tree. 118 |
119 | 120 |
121 | 122 |
123 | Optimized Solution Two 124 | 125 | For a binary tree to be valid bst every node should be greater than the max of its left subtree and less than the min of its right subtree, we can use this property 126 | to validate if the binary tree is indeed a bst. 127 | 128 | ```python 129 | 130 | class BinaryTreeNode: 131 | def __init__(self, data): 132 | self.data = data 133 | self.right = None 134 | self.left = None 135 | 136 | 137 | 138 | def is_bst(root: BinaryTreeNode) -> bool: 139 | def validate_bst(root, min, max): 140 | if not root: 141 | return True 142 | if root.data <= min or root.data >= max: 143 | return False 144 | if not ( 145 | validate_bst(root.left, min, root.data) 146 | and validate_bst(root.right, root.data, max) 147 | ): 148 | return False 149 | return True 150 | return validate_bst(root, float("-inf"), float("+inf")) 151 | 152 | ``` 153 | Time complexity O(n) we touch all the nodes of the tree once. 154 | Space complexity O(h) where h is height of the tree, recursion stack depth is equal to height of the tree. 155 |
-------------------------------------------------------------------------------- /trees-and-graphs/list-of-depths.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/list-of-depths) 2 | 3 | # List Of Depths 4 | 5 | ## Cracking The Coding Interview 4.3 6 | 7 |
8 | 9 | # Question 10 | 11 | Given a binary tree, design an algorithm which creates a linked list of all the nodes at each depth (if you have a tree with 12 | depth D, you will have D linked lists). 13 | 14 | ``` 15 | Input - 2 16 | / \ 17 | 1 3 18 | Output - [[1],[2 -> 3]] 19 | 20 | InPut - A 21 | / \ 22 | B C 23 | / \ \ 24 | D E F 25 | / / \ 26 | G H I 27 | 28 | Input - [[A],[B -> C],[D -> E -> F], [G -> H -> I]] 29 | 30 | ``` 31 | 32 |
33 | Solution One 34 | 35 | Simple way to think of it is we have to build the a linked list for nodes at each level so, traverse the tree level by level and build the 36 | linked list. linked list built for depth D can be used to build the linked list of nodes at depth D + 1. this solution is also a slight modification 37 | of BFS. 38 | 39 | ```python 40 | 41 | def get_list_of_depths(root: BinaryTreeNode) -> List[LinkedList]: 42 | list_of_depths, curr = [], LinkedList() 43 | curr.insert(root) 44 | while len(curr) > 0: 45 | list_of_depths.append(curr) 46 | parent, curr = curr.head, LinkedList() 47 | while parent: 48 | if parent.data.left: 49 | curr.insert(parent.data.left) 50 | if parent.data.right: 51 | curr.insert(parent.data.right) 52 | parent = parent.next 53 | 54 | return list_of_depths 55 | 56 | ``` 57 | Time complexity O(n), we touch each nodes once. 58 | Space complexity O(n) dominated by list_of_depths. 59 |
60 | 61 |
62 | 63 |
64 | Solution Two 65 | 66 | Since we have to build linked list for nodes at each level we can build it with any kind of traversal as long as we know wich level nodes are in. We can 67 | use a variable depth and pass depth+1 to next level to keep track of depth, this solution is combination of dfs pre-order travel. 68 | 69 | ```python 70 | 71 | def get_list_of_depths(root: BinaryTreeNode) -> List[LinkedList]: 72 | def dfs(root, depth, list_of_depths): 73 | if root == None: 74 | return 75 | depth_list = LinkedList() 76 | if len(list_of_depths) > depth: 77 | depth_list = list_of_depths[depth] 78 | else: 79 | list_of_depths.append(depth_list) 80 | depth_list.insert(root) 81 | dfs(root.left, depth + 1, list_of_depths) 82 | dfs(root.right, depth + 1, list_of_depths) 83 | 84 | list_of_depths = [] 85 | dfs(root, 0, list_of_depths) 86 | return list_of_depths 87 | 88 | ``` 89 | Time complexity O(n), we touch each nodes once. 90 | Space complexity O(n) dominated by list_of_depths. 91 |
92 | 93 |
94 | 95 |
96 | Driver Code 97 | 98 | ```python 99 | 100 | from typing import List 101 | 102 | 103 | class LinkedList: 104 | def __init__(self): 105 | self.head = None 106 | self.tail = None 107 | self.size = 0 108 | 109 | 110 | class ListNode: 111 | def __init__(self, data): 112 | self.data = data 113 | self.next = None 114 | 115 | 116 | def insert(self, data): 117 | if not self.head: 118 | self.head = self.ListNode(data) 119 | self.tail = self.head 120 | else: 121 | self.tail.next = self.ListNode(data) 122 | self.tail = self.tail.next 123 | self.size += 1 124 | 125 | 126 | def __len__(self): 127 | return self.size 128 | 129 | 130 | class BinaryTreeNode: 131 | def __init__(self, data, left=None, right=None): 132 | self.data = data 133 | self.right = right 134 | self.left = left 135 | 136 | 137 | root = BinaryTreeNode("A") 138 | root.left = BinaryTreeNode("B") 139 | root.right = BinaryTreeNode("C") 140 | root.left.left = BinaryTreeNode("D") 141 | root.left.right = BinaryTreeNode("E") 142 | root.left.left.left = BinaryTreeNode("G") 143 | root.right.right = BinaryTreeNode("C") 144 | root.right.right.left = BinaryTreeNode("H") 145 | root.right.right.right = BinaryTreeNode("I") 146 | 147 | for linked_list in get_list_of_depths_bfs(root): 148 | result = [] 149 | curr = linked_list.head 150 | while curr: 151 | result.append(curr.data.data) 152 | curr = curr.next 153 | 154 | print(result) 155 | 156 | ``` 157 | *Linked list implementation is naive 158 | 159 |
160 | 161 | -------------------------------------------------------------------------------- /arrays-and-strings/is-unique.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/is-unique) 2 | 3 | # Is Unique 4 | 5 | ## Cracking the Coding Interview (CTCI) 1.1 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given a string. Write a function to determine if all of the characters in the string appear only once. 12 | 13 | Your function shouldn't care about capitalization. `A` is the same as `a`. 14 | 15 | ``` 16 | Input - "heLlo" 17 | Output - False 18 | 19 | Input - "hey" 20 | Output - True 21 | ``` 22 | 23 |
24 | Brute Force Solution 25 | 26 |
27 | 28 | For each character in the input string, compare it with every other character in the string and make sure they're different. 29 | 30 |
31 | 32 | We implement this with two loops. The first loop iterates through every character in the string. 33 | 34 |
35 | 36 | For each iteration, we will check to make sure that this character does not appear again in the string. We do this with the nested loop. 37 | 38 |
39 | 40 | The time complexity is $$O(n^2)$$. The space complexity is $$O(n)$$ since we're creating a new string on line 2. 41 | 42 | ```python 43 | def isUnique(s): 44 | s = s.lower() 45 | # c -> counter, l -> letter 46 | for i1, c1 in enumerate(s): 47 | for i2, c2 in enumerate(s): 48 | if i1 == i2: # skip if same index 49 | continue 50 | if c1 == c2: # compare the characters 51 | return False 52 | return True 53 | ``` 54 | 55 |
56 | 57 | Here's a Python 3 REPL with the solution and test cases. 58 | 59 |
60 | 61 | 62 |
63 | 64 |
65 | Optimized Solution 66 | 67 | 68 | Instead of using two loops, we can use a set. As we iterate through the string, we check if the current letter is in the set. 69 | 70 |
71 | 72 | If it is, then we return `False`. 73 | 74 |
75 | 76 | Otherwise, we add the current letter to the set and continue iterating. 77 | 78 |
79 | 80 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$. 81 | 82 | ```python 83 | def isUnique(s): 84 | s = s.lower() 85 | seen = set() 86 | for c in s: 87 | if c in seen: 88 | return False 89 | else: 90 | seen.add(c) 91 | return True 92 | ``` 93 | 94 |
95 | 96 | Here's a Python 3 REPL with the solution and test cases. 97 |
98 | 99 | 100 |
101 | 102 | ## Follow Up 103 | 104 | What if you're not allowed to use any additional data structures? 105 | 106 |
107 | 108 |
109 | Follow Up Solution 110 | 111 | 112 | The brute force solution doesn't use any extra data structures, but can we do better than $$O(n^2)$$ time complexity? 113 | 114 |
115 | 116 | Instead of using two for loops, what if we sorted the string and then just checked if any of the characters were equivalent to the character before them? 117 | 118 |
119 | 120 | We're taking advantage of the fact that if you sort the string, all identical characters will be placed right next to each other. 121 | 122 |
123 | 124 | The time complexity is $$O(n \log n)$$ since that's how long it takes to sort the characters in a string. 125 | 126 |
127 | 128 | The space complexity is $$O(n)$$ since we're creating a new string. 129 | 130 | ```python 131 | def isUnique(s): 132 | s = sorted(s.lower()) 133 | for i in range(1, len(s)): 134 | if s[i] == s[i - 1]: 135 | return False 136 | return True 137 | ``` 138 | 139 | 140 | Here's a Python 3 REPL with the solution and test cases. 141 | 142 |
143 | 144 |
145 | 146 | 147 |
148 | Optimized Follow Up Solution 149 | 150 |
151 | 152 | 153 | Can we do even better than $$O(n \log n)$$ time complexity without using extra data structures? Yes we can! 154 | 155 |
156 | 157 | We can do this with bit manipulation. We mantain an integer, `checker`, that keeps track of which character we've already seen in our string. 158 | 159 |
160 | 161 | `checker` is acting like the set that we used in the previous **Optimized Solution**. 162 | 163 |
164 | 165 | Integers in Python occupy 32 bits of space, so we'll use 26 bits of `checker` to keep track of which characters we've seen. 166 | 167 |
168 | 169 | When we see a character, we flip the corresponding bit to 1 in `checker`. For `a` the corresponding bit would be bit 0. For `z` the corresponding bit would be bit 25. 170 | 171 |
172 | 173 | When we want to check if we've already seen a specific character, we need to see if that character's bit is set to a `1` in `checker`. We can do that with a bitmask. 174 | 175 |
176 | 177 | We start by setting `checker` to 0, because all the character bits should be set to `0` (we haven't seen any characters yet). 178 | 179 |
180 | 181 | Then, we iterate through the string and first check if the character is in `checker` with our bitmask. 182 | 183 |
184 | 185 | If it is, then we can return `False`. Otherwise, we set that character's bit to `1` in `checker` and continue iterating. 186 | 187 |
188 | 189 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$ 190 | 191 | ```python 192 | def isUnique(s): 193 | checker = 0 194 | for i in range(len(s)): 195 | index = ord(s[i]) 196 | if 65 <= index <= 90: # check capitalization 197 | index += 32 198 | value = (checker & ((1 << index))) 199 | if value > 0: 200 | return False 201 | checker = checker | (1 << index) 202 | return True 203 | ``` 204 | 205 | Here's a Python 3 REPL with the solution and test cases. 206 | 207 |
208 | -------------------------------------------------------------------------------- /linked-lists/intersection.mdx: -------------------------------------------------------------------------------- 1 | # Read the Solution [Here](https://quastor.org/cracking-the-coding-interview/linked-lists/intersection) 2 | 3 | # Intersection 4 | 5 | ## Cracking the Coding Interview (CTCI) 2.7 6 | 7 |
8 | 9 | ## Question 10 | 11 | You are given two singly linked lists. Write a function to determine if the two lists intersect. If they do intersect, return the node at which the two lists intersect. Otherwise, return `None`. 12 | 13 | ``` 14 | Input - 15 | 1 -> 2 -> 3 -> 16 | 4 -> 5 -> 6 17 | 2 -> 18 | 19 | Output - 4 -> 5 -> 6 20 | (the output is the node with the value 4) 21 | ``` 22 | 23 |
24 | Brute Force Solution 25 | 26 | 27 | We can solve this by using a set. We first iterate through the first linked list and put every node in the set. 28 | 29 |
30 | 31 | We then iterate through the second linked list and check if the node is in the set. If it is, then we return the node. 32 | 33 | ```python 34 | def intersection(l1, l2): 35 | nodeSet = set() 36 | cur = l1 37 | 38 | while cur: 39 | nodeSet.add(cur) 40 | cur = cur.next 41 | 42 | cur = l2 43 | 44 | while cur: 45 | if cur in nodeSet: 46 | return cur 47 | 48 | return None 49 | ``` 50 | 51 | The time complexity is $$O(n)$$ and the space complexity is $$O(n)$$. 52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | Optimzed Solution 60 | 61 | 62 | The unoptimzed solution requires linear space time. Can we do better? We can! 63 | 64 |
65 | 66 | We will use two pointers, `p1` and `p2`. `p1` points to the head of `l1` and `p2` points to the head of `l2`. We'll traverse both linked lists. When `p1` reaches the tail of `l1`, then we will redirect `p1` to the head of `l2`. We do the same for `p2`. When `p2` reaches the tail of `l2`, we will redirect it to `l1`. If the pointers are ever pointing to the same node (`p1 == p2`), then that is the intersecting node. Let's talk about why this approach works. 67 | 68 |

69 | 70 | Let's say we have two linked lists that intersect. `p1` points to the head of the first linked list and `p2` points to the head of the second. 71 | 72 | intersecting linked lists 80 | 81 | We move `p1` and `p2` to `p1.next` and `p2.next` and traverse both linked lists. `p1` has traveled 1 node and `p2` has traveled 1 node. 82 | 83 | intersecting linked lists 90 | 91 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 2 nodes and `p2` has traveled 2 nodes. 92 | 93 | intersecting linked lists 100 | 101 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 3 nodes and `p2` has traveled 3 nodes. 102 | 103 | intersecting linked lists 110 | 111 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 4 nodes and `p2` has traveled 4 nodes. 112 | 113 | intersecting linked lists 120 | 121 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 5 nodes and `p2` has traveled 5 nodes. 122 | 123 | intersecting linked lists 130 | 131 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 6 nodes and `p2` has traveled 6 nodes. 132 | 133 | intersecting linked lists 140 | 141 | We move `p1` and `p2` to `p1.next` and `p2.next` and continue traversing both linked lists. `p1` has traveled 7 nodes and `p2` has traveled 7 nodes. 142 | 143 | intersecting linked lists 150 | 151 | We move `p1` and `p2` to `p1.next` and `p2.next`. Now `p1` and `p2` point to the same node! This node is the intersection node of the two linked lists. Both nodes have traveled 8 nodes. 152 | 153 | intersecting linked lists 160 | 161 | Both `p1` and `p2` will be guaranteed to meet at the intersection point with this approach, since they're both traversing the same amount of nodes. If there is no intersection point, then both `p1` and `p2` will eventually be at `None` and we can return `None` (meaning there is no intersection point). 162 | 163 | ```python 164 | def intersection(l1, l2): 165 | p1 = l1 166 | p2 = l2 167 | p1Switch = False 168 | p2Switch = False 169 | 170 | while (p1 or not p1Switch) and (p2 or not p2Switch): 171 | if p1 == p2: 172 | return p1 173 | p1 = p1.next 174 | p2 = p2.next 175 | 176 | if p1 is None and not p1Switch: 177 | p1 = l2 178 | p1Switch = True 179 | 180 | if p2 is None and not p2Switch: 181 | p2 = l1 182 | p2Switch = True 183 | ``` 184 | 185 | The time complexity is $$O(n)$$ and the space complexity is $$O(1)$$. 186 | 187 |
188 | 189 | -------------------------------------------------------------------------------- /stacks-and-queues/stack-of-plates.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [here](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/stack-of-plates) 2 | 3 | # Stack of Plates 4 | 5 | ## Cracking the Coding Interview (CTCI) 3.3 6 | 7 |
8 | 9 | ## Question 10 | 11 | Imagine a stack of plates. If the stack gets too high then it will become unstable and topple over. Therefore, when stacking plates we create a new stack after a certain number of plates. 12 | 13 |
14 | 15 | Implement a data structure that mimics this behavior. `SetOfStacks` should be composed of several stacks and it should create a new stack once the previous one exceeds a certain threshold. 16 | 17 |
18 | 19 | `SetOfStacks.push()` and `SetOfStacks.pop()` should behave identically to if the data structure was just a single stack. 20 | 21 |
22 | 23 |
24 | Solution 25 | 26 | 27 | We'll design the class to have a `push` and `pop` method, so the API is identical to a normal stack. 28 | 29 |
30 | 31 | In order to instantiate the class, the user will have the option of providing the capacity of the stacks. The capacity must be a positive number (greater than 0). We set the default capacity to an arbitrary number (we picked 10). 32 | 33 |
34 | 35 | We will represent the data structure internally with a 2 dimensional list. Everytime one list reaches capacity, we'll add another. 36 | 37 |
38 | 39 | The list allows us to do `push` and `pop` operations in $$O(1)$$ time. 40 | 41 |
42 | 43 | With our `push` method, we first check to make sure that our current stack (the last list in our 2-D list) is not at capacity. 44 | 45 |
46 | 47 | If it is, then we add a new empty list as our current stack. 48 | 49 |
50 | 51 | Then, we can just append the value to our current stack. 52 | 53 |
54 | 55 | In `pop`, we first check to make sure that there is at least one value in our data structure. If not, we throw an `IndexError`. 56 | 57 |
58 | 59 | Otherwise, we can just pop the last value and return it. 60 | 61 |
62 | 63 | `push` and `pop` both take $$O(1)$$ time. 64 | 65 | ```python 66 | class SetOfStacks: 67 | def __init__(self, capacity = 10): 68 | if capacity < 1: 69 | raise ValueError("capacity must be at least 1") 70 | 71 | self.capacity = capacity 72 | self.stacks = [[]] 73 | 74 | def push(self, val): 75 | if len(self.stacks[-1]) == self.capacity: 76 | self.stacks.append([]) 77 | 78 | self.stacks[-1].append(val) 79 | 80 | def pop(self): 81 | if len(self.stacks[-1]) == 0: 82 | if len(self.stacks) == 1: 83 | raise IndexError("pop from empty stack") 84 | else: 85 | self.stacks.pop() 86 | 87 | return self.stacks[-1].pop() 88 | ``` 89 | 90 |
91 | 92 |
93 | 94 | ## Follow Up 95 | 96 | Implement a function `popAt(index)` which performs a pop operation on a specific stack in your set of stacks. 97 | 98 |
99 | 100 |
101 | Solution 102 | 103 | There are two main ways you can implement this. 104 | 105 |
106 | 107 | One way is to just pop the element out of the specific stack without performing a left shift operation (shifting over the elements in the stacks that come after this one left by 1). 108 | 109 |
110 | 111 | If we don't perform the left shift operation, this particular stack will be smaller than capacity while stacks to the left (stacks that come *after*) might be at full capacity. This may violate assumptions about the `SetOfStacks` data structure. 112 | 113 |
114 | 115 | THe other way of implementing this is with the left shift operation. Whenever we `popAt` a stack that is not the last stack, we shift over 1 element from all the stacks on the left so that all of the stacks (excluding the last one) are at full capacity. 116 | 117 |
118 | 119 | The issue with the left shift operation is that it makes our `popAt` function more computationally expensive. The left shift operation will take $$O(n)$$ time every time it's performed. 120 | 121 |
122 | 123 | We will implement our `popAt` function *with* a left shift operation. 124 | 125 |
126 | 127 | The left shift operation will be implmeneted with a `while` loop. We start off on the stack we just popped from and append the first element of the next stack (while popping the first element off that stack). We continue with the next stack until we've reached the last stack. 128 | 129 |
130 | 131 | If, after our left shift operation, the last stack in our `SetOfStacks` is empty, then we `pop` off that stack. 132 | 133 |
134 | 135 | `popAt` takes $$O(n)$$ time. 136 | 137 | ```python 138 | class SetOfStacks: 139 | def __init__(self, capacity = 10): 140 | if capacity < 1: 141 | raise ValueError("capacity must be at least 1") 142 | 143 | self.capacity = capacity 144 | self.stacks = [[]] 145 | 146 | def push(self, val): 147 | if len(self.stacks[-1]) == self.capacity: 148 | self.stacks.append([]) 149 | 150 | self.stacks[-1].append(val) 151 | 152 | def pop(self): 153 | if len(self.stacks[-1]) == 0: 154 | if len(self.stacks) == 1: 155 | raise IndexError("pop from empty stack") 156 | else: 157 | self.stacks.pop() 158 | 159 | return self.stacks[-1].pop() 160 | 161 | def _leftShift(self, stack): 162 | while stack + 1 < len(self.stacks): 163 | self.stacks[stack].append(self.stacks[stack + 1].pop(0)) 164 | stack += 1 165 | if len(self.stacks[-1]) == 0: 166 | self.stacks.pop() 167 | 168 | def popAt(self, stack): 169 | if stack >= len(self.stacks): 170 | raise IndexError("stack index out of range") 171 | 172 | if len(self.stacks[stack]) == 0: 173 | raise IndexError("pop from empty stack") 174 | 175 | returnValue = self.stacks[stack].pop() 176 | 177 | self._leftShift(stack) 178 | return returnValue 179 | ``` 180 |
-------------------------------------------------------------------------------- /trees-and-graphs/first-common-ancestor.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/first_common_ancestor) 2 | 3 | # First Common Ancestor 4 | 5 | ## Cracking The Coding Interview 4.8 6 | 7 |
8 | 9 | # Question 10 | 11 | Design and write code to find first common ancestor of two nodes in a binary tree. 12 | Avoid storing aditional nodes in a data structure. NOTE: this is not necessarily a binary search tree. 13 | 14 | 15 | ``` 16 | Tree - 6 17 | / \ 18 | 4 3 19 | / \ / \ 20 | 1 2 19 10 21 | \ \ \ 22 | 18 8 11 23 | 24 | InPut - p = 8, q = 11 25 | OutPut - 3 26 | 27 | InPut - p = 2, q = 4 28 | OutPut - 4 29 | 30 | InPut - p = 80, q = 11 31 | OutPut - None 32 | 33 | ``` 34 | 35 |
36 | Solution One: With Links to Parents 37 | 38 | Let's say both the nodes are at same depth, we can travel up the nodes using parent pointer in tandom and when nodes meet we can return 39 | the node as first common ancestor. But, this might not always be the case because nodes might be at different depth, if we can manage to 40 | bring them to same level then we have a simple problem at hand and this solution is based on that insight. 41 | 42 | 43 | ```python 44 | 45 | class BinaryTreeNode: 46 | def __init__(self, data): 47 | self.data = data 48 | self.left = None 49 | self.right = None 50 | self.parent = None 51 | 52 | 53 | def common_ancestor(p, q) -> int: 54 | delta = depth(p) - depth(q) 55 | # let p be the deeper node 56 | if delta < 0: 57 | p, q = q, p 58 | delta = abs(delta) 59 | while delta: 60 | p = p.parent 61 | delta -= 1 62 | while p != q: 63 | p, q = p.parent, q.parent 64 | return p 65 | 66 | 67 | def depth(node): 68 | depth = 0 69 | while node: 70 | depth += 1 71 | node = node.parent 72 | return depth 73 | 74 | 75 | ``` 76 | Time complexity O(d), where d is the depth of the tree. 77 | Space complexity O(n) extra parent pointer in the binary tree for each node. 78 |
79 | 80 |
81 | 82 |
83 | Solution Two: Better Worst-Case Runtime 84 | 85 | Travel up from one of the nodes(p), keep track of parent and sibling nodes, at each iteration parent gets set to 86 | parent.parent and sibling get's set to sibling.parent. We are done searching when the sibling node is or covers the other node(q) 87 | 88 | ```python 89 | 90 | class BinaryTreeNode: 91 | def __init__(self, data): 92 | self.data = data 93 | self.left = None 94 | self.right = None 95 | self.parent = None 96 | 97 | 98 | def common_ancestor(root, p, q): 99 | if not covers(root, p) or not covers(root, q): 100 | return False 101 | elif covers(p, q): 102 | return p 103 | elif covers(q, p): 104 | return q 105 | sibling = get_sibling(p) 106 | parent = p.parent 107 | while not covers(sibling, q): 108 | sibling = get_sibling(parent) 109 | parent = parent.parent 110 | return parent 111 | 112 | 113 | def covers(root, p): 114 | if not root: 115 | return False 116 | if root == p: 117 | return True 118 | return covers(root.left, p) or covers(root.right, p) 119 | 120 | 121 | def get_sibling(node): 122 | if not node or not node.parent: 123 | return None 124 | parent = node.parent 125 | return parent.right if parent.left == node else parent.left 126 | 127 | ``` 128 | Time complexity O(t), where t is the size of the subtree for the first common ancestor. 129 | Space complexity O(n) extra parent pointer in the binary tree for each node. 130 | 131 |
132 | 133 |
134 | 135 |
136 | Solution Three: Without Links to parent 137 | 138 | Start from the root, first check both the nodes are in the tree. then, 139 | check which side of the root covers both p and q and continue searching that side, 140 | do this recursively until p and q are not on the same side and return that root. 141 | This resembles a little bit to finding first common ancestor in a 142 | binary search tree, though run time is completely different. 143 | 144 | ```python 145 | 146 | class BinaryTreeNode: 147 | def __init__(self, data): 148 | self.data = data 149 | self.left = None 150 | self.right = None 151 | 152 | 153 | def common_ancestor(root, p, q): 154 | if not covers(root, p) or not covers(root, q): 155 | return None 156 | return ancestor_helper(root, p, q) 157 | 158 | 159 | def ancestor_helper(root, p, q): 160 | if not root or root == p or root == q: 161 | return root 162 | 163 | p_onleft = covers(root.left, p) 164 | q_onleft = covers(root.left, q) 165 | 166 | if p_onleft is not q_onleft: 167 | return root 168 | 169 | if p_onleft and q_onleft: 170 | return ancestor_helper(root.left, p, q) 171 | else: 172 | return ancestor_helper(root.left, p, q) 173 | 174 | 175 | def covers(root, p): 176 | if not root: 177 | return False 178 | if root == p: 179 | return True 180 | return covers(root.left, p) or covers(root.right, p) 181 | 182 | 183 | ``` 184 | Time complexity O(n), dominated by checking whether the nodes are in the tree. 185 | Space complexity O(d) where d is depth of the tree. 186 |
187 | 188 |
189 | 190 |
191 | Solution Four(Optimized) 192 | 193 | Solution three has optimal run time but it is ineffient because we check both the subtrees initialy 194 | then we move to one of the subtree and do the same check on the subtree all over again. we can get away with repeated checking 195 | by bubbling up the result when we find one node. Reading CTCI is recomended for better explaination. 196 | 197 | ```python 198 | 199 | class BinaryTreeNode: 200 | def __init__(self, data): 201 | self.data = data 202 | self.left = None 203 | self.right = None 204 | 205 | 206 | class Result: 207 | def __init__(self, node, is_ancestor): 208 | self.node: BinaryTreeNode = node 209 | self.is_ancestor: bool = is_ancestor 210 | 211 | 212 | def common_ancestor(root, p, q): 213 | result = common_ancestor_helper(root, p, q) 214 | if result.is_ancestor: 215 | return result.node 216 | return None 217 | 218 | 219 | def common_ancestor_helper(root, p, q): 220 | if not root: 221 | return Result(None, False) 222 | if root == p and root == q: 223 | return Result(root, True) 224 | r_left = common_ancestor_helper(root.left, p, q) 225 | if r_left.is_ancestor: 226 | return r_left 227 | r_right = common_ancestor_helper(root.right, p, q) 228 | if r_right.is_ancestor: 229 | return r_right 230 | if r_left.node and r_right.node: 231 | return Result(root, True) 232 | elif root == p or root == q: 233 | is_ancestor = r_left.node or r_right.node 234 | return Result(root, is_ancestor) 235 | else: 236 | return Result(r_left.node if r_left.node else r_right.node, False) 237 | 238 | ``` 239 | Time complexity O(n), we touch each nodes once in worst case. 240 | Space complexity O(d) where d is the depth of the tree, recursion stack depth is equal to depth of the tree. 241 |
242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cracking the Coding Interview in Python - Solutions with Explanations 2 | 3 | Detailed explanations to the coding interview questions in CTCI. The solutions are written in Python 3. 4 | 5 | **If you find this useful, a Github star would be much appreciated!!** ⭐ ⭐ ⭐ 6 | 7 | ## Arrays and Strings 8 | |Problem Number | Problem Name | Status| 9 | |--- | --- | --- | 10 | | 1.1 | Is Unique | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/is-unique) | 11 | | 1.2 | Check Permutation | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/check-permutation) | 12 | 1.3 | URLify | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/urlify) | 13 | 1.4 | Palindrome Permutation | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/palindrome-permutation) | 14 | 1.5 | One Away | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/one-away) | 15 | 1.6 | String Compression | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/string-compression) | 16 | 1.7 | Rotate Matrix | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/rotate-matrix) | 17 | 1.8 | Zero Matrix | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/zero-matrix) | 18 | 1.9 | String Rotation | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/arrays-and-strings/string-rotation) | 19 | 20 | ## Linked Lists 21 | 22 | | Problem Number | Problem Name | Status | 23 | | --- | --- | --- | 24 | | 2.1 | Remove Dups | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/remove-duplicates) | 25 | | 2.2 | Return Kth to Last | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/return-kth-to-last) | 26 | | 2.3 | Delete Middle Node | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/delete-middle-node) | 27 | | 2.4 | Partition | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/partition) | 28 | | 2.5 | Sum List | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/sum-list) | 29 | | 2.6 | Palindrome | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/palindrome) | 30 | | 2.7 | Intersection | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/intersection) | 31 | | 2.8 | Loop Detection | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/linked-lists/loop-detection) | 32 | 33 | ## Stacks and Queues 34 | 35 | | Problem Number | Problem Name | Status | 36 | | --- | --- | --- | 37 | | 3.1 | Three In One | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/three-in-one) | 38 | | 3.2 | Stack Min | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/stack-min) | 39 | | 3.3 | Stack of Plates | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/stack-of-plates)| 40 | | 3.4 | Queue via Stacks | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/stacks-and-queues/queue-via-stacks) | 41 | | 3.5 | Sort Stack | ❌ | 42 | | 3.6 | Animal Shelter | ❌ | 43 | 44 | ## Trees and Graphs 45 | 46 | | Problem Number | Problem Name | Status | 47 | | --- | --- | --- | 48 | | 4.1 | Route Between Nodes | ❌ | 49 | | 4.2 | Minimal Tree | ❌ | 50 | | 4.3 | List of Depths | ❌ | 51 | | 4.4 | Check Balanced | ❌ | 52 | | 4.5 | Validate BST | ❌ | 53 | | 4.6 | Successor | ❌ | 54 | | 4.7 | Build Order | ❌ | 55 | | 4.8 | First Common Ancestor | ❌ | 56 | | 4.9 | BST Sequence | ❌ | 57 | | 4.10 | Check Subtree | ❌ | 58 | | 4.11 | Random Node | ❌ | 59 | | 4.12 | Paths with Sum | ❌ | 60 | 61 | ## Bit Manipulation 62 | 63 | | Problem Number | Problem Name | Status | 64 | | --- | --- | --- | 65 | | 5.1 | Insertion | ❌ | 66 | | 5.2 | Binary to String | ❌ | 67 | | 5.3 | Flip Bit to Win | ❌ | 68 | | 5.4 | Next Number | ❌ | 69 | | 5.5 | Debugger | ❌ | 70 | | 5.6 | Conversion | ❌ | 71 | | 5.7 | Pairwise Swap | ❌ | 72 | | 5.8 | Draw Line | ❌ | 73 | 74 | ## Object Oriented Design 75 | 76 | | Problem Number | Problem Name | Status | 77 | | --- | --- | --- | 78 | | 7.1 | Deck of Cards | ❌ | 79 | | 7.2 | Call Center | ❌ | 80 | | 7.3 | Jukebox | ❌ | 81 | | 7.4 | Parking Lot | ❌ | 82 | | 7.5 | Online Book Reader | ❌ | 83 | | 7.6 | Jigsaw | ❌ | 84 | | 7.7 | Chat Server | ❌ | 85 | | 7.8 | Othello | ❌ | 86 | | 7.9 | Circular Array | ❌ | 87 | 88 | ## Recursion and Dynamic Programming 89 | 90 | | Problem Number | Problem Name | Status | 91 | | --- | --- | --- | 92 | | 8.1 | Triple Step | ❌ | 93 | | 8.2 | Robot in a Grid | ❌ | 94 | | 8.3 | Magic Index | ❌ | 95 | | 8.4 | Power Set | ❌ | 96 | | 8.5 | Recursive Multiply | ❌ | 97 | | 8.6 | Towers of Hanoi | ❌ | 98 | | 8.7 | Permutation without Dups | ❌ | 99 | | 8.8 | Permutation with Dups | ❌ | 100 | | 8.9 | Parens | ❌ | 101 | | 8.10 | Paint Fill | ❌ | 102 | | 8.11 | Coins | ❌ | 103 | | 8.12 | Eight Queens | ❌ | 104 | | 8.13 | Stack of Boxes | ❌ | 105 | | 8.14 | Boolean Evaluation | ❌ | 106 | 107 | ## Sorting and Searching 108 | 109 | | Problem Number | Problem Name | Status | 110 | | --- | --- | --- | 111 | | 10.1 | Sorted Merge | ❌ | 112 | | 10.2 | Group Anagram | ❌ | 113 | | 10.3 | Search in Rotated Array | ❌ | 114 | | 10.4 | Sorted Search, No Size | ❌ | 115 | | 10.5 | Sparse Search | ❌ | 116 | | 10.6 | Sort Big File | ❌ | 117 | | 10.7 | Missing Int | ❌ | 118 | | 10.8 | Find Duplicates | ❌ | 119 | | 10.9 | Sorted Matrix Search | ❌ | 120 | | 10.10 | Rank from Stream | ❌ | 121 | | 10.11 | Peaks and Valleys | ❌ | 122 | 123 | ## Moderate 124 | 125 | | Problem Number | Problem Name | Status | 126 | | --- | --- | --- | 127 | | 16.1 | Number Swapper | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/moderate/number-swapper) | 128 | | 16.2 | Word Frequencies | [Read Solution ✅](https://quastor.org/cracking-the-coding-interview/moderate/word-frequencies) | 129 | | 16.3 | Intersection | ❌ | 130 | | 16.4 | Tic Tac Win | ❌ | 131 | | 16.5 | Factorial Zeros | ❌ | 132 | | 16.6 | Smallest Difference | ❌ | 133 | | 16.7 | Number Max | ❌ | 134 | | 16.8 | English Int | ❌ | 135 | | 16.9 | Operations | ❌ | 136 | | 16.10 | Living People | ❌ | 137 | | 16.11 | Diving Board | ❌ | 138 | | 16.12 | XML Encoding | ❌ | 139 | | 16.13 | Bisect Squares | ❌ | 140 | | 16.14 | Best Line | ❌ | 141 | | 16.15 | Master Mind | ❌ | 142 | | 16.16 | Sub Sort | ❌ | 143 | | 16.17 | Contiguous Sequence | ❌ | 144 | | 16.18 | Pattern Matching | ❌ | 145 | | 16.19 | Pond Sizes | ❌ | 146 | | 16.20 | T9 | ❌ | 147 | | 16.21 | Sum Swap | ❌ | 148 | | 16.22 | Langton's Ant | ❌ | 149 | | 16.23 | Rand7 from Rand5 | ❌ | 150 | | 16.24 | Pairs with Sum | ❌ | 151 | | 16.25 | LRU Cache | ❌ | 152 | | 16.26 | Calculator| ❌ | 153 | 154 | ## Hard 155 | 156 | | Problem Number | Problem Name | Status | 157 | | --- | --- | --- | 158 | | 17.1 | Add Without Plus | ❌ | 159 | | 17.2 | Shuffle | ❌ | 160 | | 17.3 | Random Set | ❌ | 161 | | 17.4 | Missing Number | ❌ | 162 | | 17.5 | Letters and Numbers | ❌ | 163 | | 17.6 | Count of 2s | ❌ | 164 | | 17.7 | Baby Names | ❌ | 165 | | 17.8 | Circus Tower | ❌ | 166 | | 17.9 | Kth Multiple | ❌ | 167 | | 17.10 | Majority Element | ❌ | 168 | | 17.11 | Word Distance | ❌ | 169 | | 17.12 | BiNode | ❌ | 170 | | 17.13 | Re-Space | ❌ | 171 | | 17.14 | Smallest K | ❌ | 172 | | 17.15 | Longest Word | ❌ | 173 | | 17.16 | The Masseuse | ❌ | 174 | | 17.17 | Multi Search | ❌ | 175 | | 17.18 | Shortest Supersequence | ❌ | 176 | | 17.19 | Missing Two | ❌ | 177 | | 17.20 | Continuous Median | ❌ | 178 | | 17.21 | Volume of Histogram | ❌ | 179 | | 17.22 | Word Transformer | ❌ | 180 | | 17.23 | Max Black Square | ❌ | 181 | | 17.24 | Max Submatrix | ❌ | 182 | | 17.25 | Word Rectangle | ❌ | 183 | | 17.26 | Sparse Similarity | ❌ | 184 | 185 | **If you find this useful, a Github star would be much appreciated!!** ⭐ ⭐ ⭐ 186 | 187 | 188 | ## Contributing 189 | 190 | Pull requests are welcome. For major changes, please open an issue to discuss what you want to change. 191 | 192 | 193 | ## Contributors 194 | 195 | Huge thanks to our contributors! 196 | 197 | * [Gan3i](https://www.linkedin.com/in/ganeshnageshappa/) 198 | * [Farhan Juneja](https://www.linkedin.com/in/farhan-juneja/) 199 | 200 | -------------------------------------------------------------------------------- /trees-and-graphs/build-order.mdx: -------------------------------------------------------------------------------- 1 | # Read the solution [Here](https://quastor.org/cracking-the-coding-interview/trees-and-graph/build_order) 2 | 3 | # build_order 4 | 5 | ## Cracking The Coding Interview 4.7 6 | 7 |
8 | 9 | # Question 10 | 11 | you are given a list of projects and a list dependencies(which is a list of pairs of projects, 12 | where the second project is dependent on the first project). All of a project's dependencies must be built before 13 | the project is. Find a build order that will allow the project to be built If there is no valid build order. return an 14 | error. 15 | 16 | ``` 17 | 18 | Input - 19 | projects = [a, b, c, d, e, f] 20 | dependencies = [[a,d], [f,b], [b,d], [f,a], [d,c]] 21 | Output - [f, e, a, b, d, c] 22 | 23 | Input - 24 | projects = [a, b, c, d, e, f] 25 | dependencies = [[a,d], [f,b], [b,d], [f,a], [d,c],[c,d]] 26 | Output - None 27 | 28 | ``` 29 | 30 |
31 | Solution One 32 | 33 | This solution is based on the insight that build can start with the projects that have zero dependencies and 34 | check if any of their dependent projects can be processed next and repeate these steps. the code is 35 | converted as it is in CTCI to python. 36 | 37 | ```python 38 | 39 | from __future__ import annotations 40 | from typing import List 41 | 42 | 43 | def find_build_order(projects: List[str], dependencies: List[List[str]]) -> List[str]: 44 | graph: Graph = build_graph(projects, dependencies) 45 | return order_projects(graph.get_nodes()) 46 | 47 | 48 | def build_graph(projects: List[str], dependencies: List[List[str]]) -> Graph: 49 | graph: Graph = Graph() 50 | for project in projects: 51 | graph.get_or_create_node(project) 52 | for dependency in dependencies: 53 | start: str = dependency[0] 54 | end: str = dependency[1] 55 | graph.add_edge(start, end) 56 | return graph 57 | 58 | 59 | def order_projects(projects: List[Project]) -> List[Project]: 60 | order: List[Project] = [None] * len(projects) 61 | end_of_list: int = add_non_dependent(order, projects, 0) 62 | to_be_processed = 0 63 | while to_be_processed < len(order): 64 | current: Project = order[to_be_processed] 65 | if not current: 66 | return None 67 | children: List[Project] = current.get_children() 68 | for child in children: 69 | child.decrease_dependencies() 70 | end_of_list = add_non_dependent(order, children, end_of_list) 71 | to_be_processed += 1 72 | return order 73 | 74 | 75 | def add_non_dependent( 76 | order: List[Project], projects: List[Project], offset: int 77 | ) -> int: 78 | for project in projects: 79 | if project.get_number_of_dependencies() == 0: 80 | order[offset] = project 81 | offset += 1 82 | return offset 83 | 84 | 85 | class Project: 86 | def __init__(self, name: str): 87 | self._children: List[Project] = [] 88 | self._map: dict[str, Project] = {} 89 | self._name: str = name 90 | self._dependencies: int = 0 91 | 92 | def add_neighbor(self, node: Project) -> None: 93 | if not node.get_name() in self._map: 94 | self._children.append(node) 95 | self._map[node.get_name()] = node 96 | node.increase_dependencies() 97 | 98 | def increase_dependencies(self) -> None: 99 | self._dependencies += 1 100 | 101 | def decrease_dependencies(self) -> None: 102 | self._dependencies -= 1 103 | 104 | def get_name(self) -> str: 105 | return self._name 106 | 107 | def get_children(self) -> List[Project]: 108 | return self._children 109 | 110 | def get_number_of_dependencies(self) -> int: 111 | return self._dependencies 112 | 113 | 114 | class Graph: 115 | def __init__(self): 116 | self._nodes: List[Project] = [] 117 | self._map: dict[str, Project] = {} 118 | 119 | def get_or_create_node(self, name: str) -> Project: 120 | if not name in self._map: 121 | node: Project = Project(name) 122 | self._nodes.append(node) 123 | self._map[name] = node 124 | return self._map[name] 125 | 126 | def add_edge(self, start: str, end: str) -> None: 127 | start_node: Project = self.get_or_create_node(start) 128 | end_node: Project = self.get_or_create_node(end) 129 | start_node.add_neighbor(end_node) 130 | 131 | def get_nodes(self): 132 | return self._nodes 133 | 134 | ``` 135 | Time complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 136 | Space complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 137 |
138 | 139 |
140 | 141 |
142 | Solution Two 143 | 144 | This solution is based on depth-first-search. build a directed graph and then start a DFS from each node, 145 | keep track of visited nodes,when you hit the end of DFS start adding nodes to build order stack as you walk 146 | up the recursion stack. we should also have a way to track the nodes that are in current recursion stack, there is a cycle 147 | in the graph if we see the nodes that are in current resusion stack again, that means no valid build order. 148 | 149 | ```python 150 | from __future__ import annotations 151 | from typing import List 152 | from queue import LifoQueue 153 | from enum import Enum 154 | 155 | 156 | def find_build_order(projects: List[str], dependencies: List[List[str]]) -> List[str]: 157 | graph: Graph = build_graph(projects, dependencies) 158 | return order_projects(graph.get_nodes()) 159 | 160 | 161 | def order_projects(projects: List[Project]) -> List[Project]: 162 | order = LifoQueue() 163 | for project in projects: 164 | if project.get_state() == State.BLANK: 165 | if not doDFS(project, order): 166 | return None 167 | return order 168 | 169 | 170 | def doDFS(project, order): 171 | if project.get_state() == State.PARTIAL: 172 | return False 173 | if project.get_state() == State.BLANK: 174 | project.set_state(State.PARTIAL) 175 | for child in project.get_children(): 176 | if not doDFS(child, order): 177 | return False 178 | project.set_state(State.COMPLETE) 179 | order.put(project) 180 | return True 181 | 182 | 183 | # Same as previous Soltuion 184 | def build_graph(projects: List[str], dependencies: List[List[str]]) -> Graph: 185 | graph: Graph = Graph() 186 | for project in projects: 187 | graph.get_or_create_node(project) 188 | for dependency in dependencies: 189 | start: str = dependency[0] 190 | end: str = dependency[1] 191 | graph.add_edge(start, end) 192 | return graph 193 | 194 | 195 | class Graph: 196 | def __init__(self): 197 | self._nodes: List[Project] = [] 198 | self._map: dict[str, Project] = {} 199 | 200 | def get_or_create_node(self, name: str) -> Project: 201 | if not name in self._map: 202 | node: Project = Project(name) 203 | self._nodes.append(node) 204 | self._map[name] = node 205 | return self._map[name] 206 | 207 | def add_edge(self, start: str, end: str) -> None: 208 | start_node: Project = self.get_or_create_node(start) 209 | end_node: Project = self.get_or_create_node(end) 210 | start_node.add_neighbor(end_node) 211 | 212 | def get_nodes(self): 213 | return self._nodes 214 | 215 | 216 | class Project: 217 | def __init__(self, name: str): 218 | self._children: List[Project] = [] 219 | self._map: dict[str, Project] = {} 220 | self._name: str = name 221 | self._dependencies: int = 0 222 | self._state = State.BLANK 223 | 224 | def add_neighbor(self, node: Project) -> None: 225 | if not node.get_name() in self._map: 226 | self._children.append(node) 227 | self._map[node.get_name()] = node 228 | node.increase_dependencies() 229 | 230 | def increase_dependencies(self) -> None: 231 | self._dependencies += 1 232 | 233 | def decrease_dependencies(self) -> None: 234 | self._dependencies -= 1 235 | 236 | def get_name(self) -> str: 237 | return self._name 238 | 239 | def get_children(self) -> List[Project]: 240 | return self._children 241 | 242 | def get_number_of_dependencies(self) -> int: 243 | return self._dependencies 244 | 245 | def get_state(self): 246 | return self._state 247 | 248 | def set_state(self, state: State): 249 | self._state = state 250 | 251 | 252 | class State(Enum): 253 | COMPLETE = 1 # Processed node 254 | PARTIAL = 2 # Node is in current resusion stack 255 | BLANK = 3 # Not processed 256 | 257 | ``` 258 | Time complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 259 | Space complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 260 |
261 | 262 |
263 | 264 |
265 | Keeping It Simple 266 |
267 | Solution one 268 | 269 | Using defaultdict for adjucenncy list graph representation 270 | 271 | ```python 272 | 273 | from collections import defaultdict 274 | from queue import Queue 275 | 276 | 277 | def find_build_order(projects, dependencies): 278 | in_degree_map = defaultdict(int) 279 | graph, q = defaultdict(set), Queue() 280 | build_graph(dependencies, graph, in_degree_map) 281 | for project in in_degree_map: 282 | if not in_degree_map[project]: 283 | q.put(project) 284 | result, visited = [], set() 285 | while not q.empty(): 286 | curr = q.get() 287 | result.append(curr) 288 | visited.add(curr) 289 | for nhbr in graph[curr]: 290 | in_degree_map[nhbr] -= 1 291 | if not in_degree_map[nhbr] and nhbr not in visited: 292 | q.put(nhbr) 293 | return None if len(result) != len(projects) else result 294 | 295 | 296 | def build_graph(dependencies, graph, in_degree_map): 297 | for edge in dependencies: 298 | graph[edge[0]].add(edge[1]) 299 | in_degree_map[edge[0]] += 0 300 | in_degree_map[edge[1]] += 1 301 | 302 | ``` 303 | 304 | Time complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 305 | Space complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 306 |
307 | 308 |
309 | 310 |
311 | Solution one 312 | 313 | DFS solution, Using defaultdict for adjucenncy list graph representation. 314 | 315 | ```python 316 | 317 | from collections import defaultdict 318 | 319 | def find_build_order(projects, dependencies): 320 | visited, order = set(), [] 321 | graph = build_graph(dependencies) 322 | for project in projects: 323 | if project not in visited: 324 | visited.add(project) 325 | recursion_stack = set() 326 | if not dfs(graph, project, visited, order, recursion_stack): 327 | return None 328 | return list(reversed(order)) 329 | 330 | 331 | def dfs(graph, project, visited, order, recursion_stack): 332 | recursion_stack.add(project) 333 | for nhbr in graph[project]: 334 | if nhbr in recursion_stack: 335 | return False 336 | if nhbr not in visited: 337 | visited.add(nhbr) 338 | if not dfs(graph, nhbr, visited, order, recursion_stack): 339 | return False 340 | order.append(project) 341 | recursion_stack.remove(project) 342 | return True 343 | 344 | 345 | def build_graph(dependencies): 346 | graph = defaultdict(set) 347 | for edge in dependencies: 348 | start, end = edge[0], edge[1] 349 | graph[start].add(end) 350 | return graph 351 | 352 | ``` 353 | 354 | Time complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 355 | Space complexity O(P + D), where P is number of projects(vertices) and D is number of dependency pairs(edges). 356 |
357 |
358 | --------------------------------------------------------------------------------