├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------