This gist contains a bunch of algo solutions
-
-
Save vicradon/b32d44889e66184c1d615d810b9f93d6 to your computer and use it in GitHub Desktop.
# Definition for a binary tree node. | |
class TreeNode: | |
def __init__(self, val=0, left=None, right=None): | |
self.val = val | |
self.left = left | |
self.right = right | |
class Solution: | |
def isSymmetric(self, root: TreeNode) -> bool: | |
return self.checkSymmetry(root.left, root.right) | |
def checkSymmetry(self, left, right): | |
if not left and not right: | |
return True | |
if not left or not right: | |
return False | |
return (left.val == right.val) and self.checkSymmetry(left.left, right.right) and self.checkSymmetry(left.right, right.left) | |
if __name__ == "__main__": | |
solution = Solution() | |
# Example 1 | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
print(solution.isSymmetric(root)) # True | |
# Example 2 | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
# / \ | |
# 5 5 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
root.left.right.left = TreeNode(5) | |
root.right.right.right = TreeNode(5) | |
print(solution.isSymmetric(root)) # False | |
# Example 3 | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
# / \ | |
# 5 5 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
root.left.right.left = TreeNode(5) | |
root.right.left.right = TreeNode(5) | |
print(solution.isSymmetric(root)) # True | |
# Definition for a binary tree node. | |
class TreeNode: | |
def __init__(self, val=0, left=None, right=None): | |
self.val = val | |
self.left = left | |
self.right = right | |
class Solution: | |
def obtainTreeValues(self, root: TreeNode) -> bool: | |
if not root: | |
return | |
stack = [root] | |
result = [] | |
while stack: | |
top = stack.pop() | |
if not top: | |
continue | |
result.append(top.val) | |
stack.append(top.left) | |
stack.append(top.right) | |
return result | |
if __name__ == "__main__": | |
# Example usage | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
solution = Solution() | |
result = solution.obtainTreeValues(root) | |
print(result) # Output: [1, 2, 3, 4, 2, 4, 3] |
# Definition for a binary tree node. | |
class TreeNode: | |
def __init__(self, val=0, left=None, right=None): | |
self.val = val | |
self.left = left | |
self.right = right | |
class Solution: | |
def obtainTreeValues(self, root: TreeNode) -> bool: | |
if not root: | |
return | |
result = [] | |
self.dfs(root, result) | |
return result | |
def dfs(self, root: TreeNode, result): | |
if not root: | |
return | |
result.append(root.val) | |
self.dfs(root.left, result) | |
self.dfs(root.right, result) | |
if __name__ == "__main__": | |
# Example usage | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
solution = Solution() | |
result = solution.obtainTreeValues(root) | |
print(result) # Output: [1, 2, 3, 4, 2, 4, 3] |
''' | |
This solution focuses on level order traversal using the breadth-first search algorithm | |
It uses a for-loop to lock in on a particular level of the tree and work on all the nodes of that level | |
''' | |
# Definition for a binary tree node. | |
# class TreeNode: | |
# def __init__(self, val=0, left=None, right=None): | |
# self.val = val | |
# self.left = left | |
# self.right = right | |
from collections import defaultdict | |
class Solution: | |
def levelOrderBottom(self, root: Optional[TreeNode]) -> List[List[int]]: | |
results = [] | |
output = [] | |
queue = deque([root]) | |
level = 0 | |
while queue: | |
# process nodes at the current level | |
for i in range(len(queue)): | |
leftmost = queue.popleft() | |
if not leftmost: | |
continue | |
results.append((leftmost.val, level)) | |
if leftmost.left: | |
queue.append(leftmost.left) | |
if leftmost.right: | |
queue.append(leftmost.right) | |
level += 1 | |
hashmap = defaultdict(list) | |
for value,level in results: | |
hashmap[level].append(value) | |
for i in range(level, -1, -1): | |
if hashmap[i]: | |
output.append(hashmap[i]) | |
return output | |
''' | |
This is a recursive approach to breadth-first search. It is not very elegant as the iterative approach and you won't find it | |
in many places but it works anyways. | |
``` | |
# Definition for a binary tree node. | |
class TreeNode: | |
def __init__(self, val=0, left=None, right=None): | |
self.val = val | |
self.left = left | |
self.right = right | |
def recursive_bfs(queue, output): | |
if not queue: | |
return | |
new_queue = [] | |
for node in queue: | |
if not node: | |
continue | |
output.append(node.val) | |
new_queue.append(node.left) | |
new_queue.append(node.right) | |
recursive_bfs(new_queue, output) | |
if __name__ == "__main__": | |
# Example 1 | |
# 1 | |
# / \ | |
# 2 2 | |
# / \ / \ | |
# 3 4 4 3 | |
root = TreeNode(1) | |
root.left = TreeNode(2) | |
root.right = TreeNode(2) | |
root.left.left = TreeNode(3) | |
root.left.right = TreeNode(4) | |
root.right.left = TreeNode(4) | |
root.right.right = TreeNode(3) | |
queue = [root] | |
output = [] | |
recursive_bfs(queue, output) | |
print(output) # 1, 2, 2, 3, 4, 4, 3 | |
Stack problems involve using the stack data structure. This data structure is used when you want to store things for further processing. You add things to the stack and get to them later when you pop from the end of the stack.
new concept: monotonically decreasing stack - A stack that only allows smaller subsequent elements.
class Solution: | |
def dailyTemperatures(self, temperatures: List[int]) -> List[int]: | |
''' | |
higher -> lower | |
stack = [(76,6),(73,7)] | |
result = [1,1,4,2,1,1,0,0] | |
result[popedvalue[1]] = i - popedvalue[1] | |
[73,74,75,71,69,72,76,73] | |
i | |
stack is decreasing (higher to lower) | |
so if current value in loop is greater than top of stack | |
use while loop to pop from stack | |
else | |
add to the stack | |
''' | |
result = [0]*len(temperatures) | |
stack = [] | |
for i in range(len(temperatures)): | |
while stack and temperatures[i] > stack[-1][0]: | |
top = stack.pop() | |
result[top[1]] = i - top[1] | |
stack.append((temperatures[i], i)) | |
return result |
Linked list problems involve using a pointer to move through a linked list to work with
We deal with four problems here:
- Reverse a linked list
- Reorder a linked list such that the start and end nodes alternate
- Merge two sorted linked lists
- Remove the nth node from a linked list (use offset and a normal pointer)
Here's infographic for reversing a linked list
# Definition for singly-linked list. | |
# class ListNode: | |
# def __init__(self, val=0, next=None): | |
# self.val = val | |
# self.next = next | |
class Solution: | |
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: | |
''' | |
temp = 2 | |
iterr = 1 | |
temp.next = iterr | |
1 -> 2 -> 3 -> 4 -> 5 | |
tail = None | |
temp = None | |
iterr = head (1) | |
# first iteration (head = 1) | |
temp = 2 (iterr.next) | |
iterr.next = tail | |
tail = iterr | |
iterr = temp | |
1 -> None | |
2 -> 3 -> 4 -> 5 | |
# second iteration | |
temp = iterr.next (3) | |
iterr.next = tail | |
tail = iterr | |
iterr = temp | |
2 -> 1 -> None | |
3 -> 4 -> 5 | |
''' | |
tail = None | |
iterr = head | |
while iterr: | |
temp = iterr.next | |
iterr.next = tail | |
tail = iterr | |
iterr = temp | |
return tail | |
# Definition for singly-linked list. | |
# class ListNode: | |
# def __init__(self, val=0, next=None): | |
# self.val = val | |
# self.next = next | |
class Solution: | |
def reorderList(self, head: Optional[ListNode]) -> None: | |
""" | |
Do not return anything, modify head in-place instead. | |
- break list into two | |
- reverse the second list | |
- initialize pointers for the two halves | |
- alternatively point the nodes of the first list to values in the second list | |
- stop looping when either goes out of bound | |
""" | |
# find half the list | |
fast = head | |
slow = head | |
slowslow = ListNode(next=head) | |
while fast: | |
slow = slow.next | |
fast = fast.next | |
if fast: | |
fast = fast.next | |
slowslow = slowslow.next | |
slowslow.next = None | |
# reverse second list | |
tail = None | |
iterator = slow | |
while iterator: | |
temp = iterator.next | |
iterator.next = tail | |
tail = iterator | |
iterator = temp | |
list1ptr = head | |
list2ptr = tail | |
while list2ptr: | |
temp1 = list1ptr.next | |
list1ptr.next = list2ptr | |
temp2 = list2ptr.next | |
list2ptr.next = temp1 | |
list1ptr = temp1 | |
list2ptr = temp2 |
# Definition for singly-linked list. | |
# class ListNode: | |
# def __init__(self, val=0, next=None): | |
# self.val = val | |
# self.next = next | |
class Solution: | |
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: | |
dummy = ListNode() | |
list1ptr = list1 | |
list2ptr = list2 | |
dummyptr = dummy | |
while list1ptr and list2ptr: | |
if list1ptr.val >= list2ptr.val: | |
dummyptr.next = list2ptr | |
dummyptr = dummyptr.next | |
list2ptr = list2ptr.next | |
else: | |
dummyptr.next = list1ptr | |
dummyptr = dummyptr.next | |
list1ptr = list1ptr.next | |
if list1ptr: | |
dummyptr.next = list1ptr | |
elif list2ptr: | |
dummyptr.next = list2ptr | |
return dummy.next | |
# Definition for singly-linked list. | |
# class ListNode: | |
# def __init__(self, val=0, next=None): | |
# self.val = val | |
# self.next = next | |
class Solution: | |
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]: | |
# 1 -> 2 -> 3 -> 4 -> 5 | |
# O | |
# P | |
dummy = ListNode(next=head) | |
ptr = dummy | |
offset = head | |
for _ in range(n): | |
offset = offset.next | |
while offset: | |
ptr = ptr.next | |
offset = offset.next | |
ptr.next = ptr.next.next | |
return dummy.next |
Grid graph problems involve a sort of matrix or grid that has to be traversed in all four directions (up, left, bottom, right). These problems are usually solved using depth-first search. The general idea is to traverse the grid and store the coordinates that satisfy a condition.
This gist contains 2 problems:
- Pacific Atlantic Water Flow
- Word Search
def pacific_atlantic_waterflow(land): | |
ROWS, COLS = len(land), len(land[0]) | |
atl_visited, pac_visited = set(), set() | |
result = [] | |
def dfs(r, c, visited, prev_height): | |
# invalid conditions | |
if ( | |
r >= ROWS or | |
c >= COLS or | |
r < 0 or | |
c < 0 or | |
(r, c) in visited or | |
land[r][c] < prev_height | |
): | |
return | |
# condition is valid, add current cell to the visited set | |
visited.add((r, c)) | |
cur_height = land[r][c] | |
# use depth-first search to check the next four possible directions | |
dfs(r, c - 1, visited, cur_height) | |
dfs(r - 1, c, visited, cur_height) | |
dfs(r, c + 1, visited, cur_height) | |
dfs(r + 1, c, visited, cur_height) | |
# starting point for left and right... prev_height is the value of cell at this starting point | |
for r in range(ROWS): | |
dfs(r, 0, pac_visited, land[r][0]) | |
dfs(r, COLS - 1, atl_visited, land[r][COLS - 1]) | |
# starting point for top and bottom | |
for c in range(COLS): | |
dfs(0, c, pac_visited, land[0][c]) | |
dfs(ROWS - 1, c, atl_visited, land[ROWS - 1][c]) | |
# process results | |
for r in range(ROWS): | |
for c in range(COLS): | |
if (r, c) in atl_visited and (r, c) in pac_visited: | |
result.append([r, c]) | |
return result | |
if __name__ == "__main__": | |
land = [ | |
[1,2,2,3,5], | |
[3,2,3,4,4], | |
[2,4,5,3,1], | |
[6,7,1,4,5], | |
[5,1,1,2,4]] | |
print(pacific_atlantic_waterflow(land)) # [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] |
def word_search(matrix, word): | |
ROWS, COLS = len(matrix), len(matrix[0]) | |
def dfs(r, c, i): | |
if len(word) == i: | |
return True | |
if r >= ROWS or c >= COLS or r < 0 or c < 0 or matrix[r][c] != word[i] or matrix[r][c] == "#": | |
return False | |
temp, matrix[r][c] = matrix[r][c], "#" | |
found = ( | |
dfs(r + 1, c, i+1) or | |
dfs(r, c + 1, i+1) or | |
dfs(r - 1, c, i+1) or | |
dfs(r, c - 1, i+1) | |
) | |
matrix[r][c] = temp | |
return found | |
for r in range(ROWS): | |
for c in range(COLS): | |
if matrix[r][c] == word[0]: | |
found = dfs(r, c, 0) | |
if found: | |
return True | |
return False | |
if __name__ == "__main__": | |
matrix = [ | |
["A", "B", "D", "F"], | |
["E", "E", "F", "I"], | |
["E", "N", "I", "N"] | |
] | |
word1 = "FEB" | |
word2 = "BEN" | |
word3 = "FINER" | |
print(word_search(matrix, word1)) # TRUE | |
print(word_search(matrix, word2)) # TRUE | |
print(word_search(matrix, word3)) # FALSE | |
While sorting algorithms are not usually asked in interviews, it's helpful to know how to implement them. Here are the basic sort algorithms:
- Merge Sort - A divide and conquer algorithm that allows you to break an array into the smallest bits and build it back up. It takes O(n log(n)) time to execute and stores the subarrays in O(n log(n)) space.
class MergeSort(): | |
def sort(self, arr): | |
# base case | |
if len(arr) <= 1: | |
return arr | |
# divide | |
m = len(arr)//2 | |
left = arr[0:m] | |
right = arr[m:] | |
# sort | |
sorted_left = self.sort(left) | |
sorted_right = self.sort(right) | |
# merge | |
return self.merge(sorted_left, sorted_right) | |
def merge(self, left, right): | |
merged = [] | |
l, r = 0, 0 | |
# move the pointers and insert the smaller element into the merged array | |
while l < len(left) and r < len(right): | |
if left[l] < right[r]: | |
merged.append(left[l]) | |
l += 1 | |
else: | |
merged.append(right[r]) | |
r += 1 | |
# insert remaining elements into the merge array | |
merged.extend(left[l:]) | |
merged.extend(right[r:]) | |
return merged | |
if __name__ == "__main__": | |
arr = [9, 3, 1, 2, 4, 8, 16, 4, 3] | |
sorter = MergeSort() | |
print(sorter.sort(arr)) # [1, 2, 3, 3, 4, 4, 8, 9, 16] | |
# Simple example | |
# [5, 2, 3] - original array | |
# [5] [2, 3] - first division | |
# [5] [2] [3] - right sub array from previous step is further divided | |
# [5] [2, 3] - right sub array is then merged using the two pointers technique | |
# [2, 3, 5] - original left and right sub arrays are then merged using the two pointers technique | |
A Prefix Tree also known as Trie is a data structure that allows one to add strings in a big tree. It's basically a nested dictionary data structure. Search takes O(n) time and does not involve hashing like you'd do with a hashset. This is how it looks when pretty printed
{
"a": {
"p": {
"p": {}
}
}
}
import json | |
class TrieNode(): | |
def __init__(self): | |
self.children = {} | |
class PrefixTree(): | |
def __init__(self): | |
self.root = TrieNode() | |
def insert(self, string): | |
ptr = self.root | |
for char in string: | |
# if this character is not in the tree, initialize this character as a new node | |
if char not in ptr.children: | |
ptr.children[char] = TrieNode() | |
# then go one step into the node | |
ptr = ptr.children[char] | |
def search(self, string): | |
ptr = self.root | |
for char in string: | |
if char in ptr.children: | |
ptr = ptr.children[char] | |
else: | |
return False | |
return True | |
def dictrep(self, node): | |
rep = dict() | |
if not node.children: | |
return {} | |
for key in node.children: | |
rep[key] = self.dictrep(node.children[key]) | |
return rep | |
def pretty_print(self, indent=1): | |
''' | |
pretty-print rep | |
{ | |
"a": { | |
"p": { | |
"p": {} | |
} | |
} | |
} | |
''' | |
out = self.dictrep(self.root) | |
print(json.dumps(out, indent=indent)) | |
if __name__ == "__main__": | |
tree = PrefixTree() | |
tree.insert("boy") | |
tree.insert("brother") | |
tree.insert("man") | |
tree.insert("masculine") | |
tree.pretty_print() | |
print(tree.search("man")) # True | |
print(tree.search("manny")) # False | |
print(tree.search("da-boss")) # False | |
import heapq | |
class Solution: | |
def findKthLargest(self, nums: List[int], k: int) -> int: | |
nums = [-1 * num for num in nums] | |
heapq.heapify(nums) | |
for _ in range(k-1): | |
heapq.heappop(nums) | |
return -1 * nums[0] |
import heapq | |
class KthLargest: | |
def __init__(self, k: int, nums: List[int]): | |
self.heap = [num for num in nums] | |
self.k = k | |
heapq.heapify(self.heap) | |
while k < len(self.heap): | |
heapq.heappop(self.heap) | |
def add(self, val: int) -> int: | |
heapq.heappush(self.heap, val) | |
if len(self.heap) > self.k: | |
heapq.heappop(self.heap) | |
return self.heap[0] | |
# Your KthLargest object will be instantiated and called as such: | |
# obj = KthLargest(k, nums) | |
# param_1 = obj.add(val) |
class MedianFinder: | |
def __init__(self): | |
self.min_heap = [] | |
self.max_heap = [] | |
def addNum(self, num: int) -> None: | |
if self.min_heap and num < self.min_heap[0]: | |
self.maxheappush(num) | |
else: | |
heapq.heappush(self.min_heap, num) | |
if len(self.min_heap) - len(self.max_heap) == 2: | |
top = heapq.heappop(self.min_heap) | |
self.maxheappush(top) | |
elif len(self.max_heap) - len(self.min_heap) == 2: | |
top = self.maxheappop() | |
heapq.heappush(self.min_heap, top) | |
def maxheappop(self) -> int: | |
return -1 * heapq.heappop(self.max_heap) | |
def maxheappush(self, val): | |
heapq.heappush(self.max_heap, -1 * val) | |
def maxheaptop(self): | |
return -1 * self.max_heap[0] | |
def findMedian(self) -> float: | |
if len(self.min_heap) > len(self.max_heap): | |
return self.min_heap[0] | |
elif len(self.min_heap) < len(self.max_heap): | |
return self.maxheaptop() | |
return (self.min_heap[0] + self.maxheaptop())/2 | |
# Your MedianFinder object will be instantiated and called as such: | |
# obj = MedianFinder() | |
# obj.addNum(num) | |
# param_2 = obj.findMedian() |
def index_equals_value_search(arr): | |
left = 0 | |
right = len(arr) - 1 | |
last = -1 | |
currentIndex = 0 | |
while left < right: | |
currentIndex = (left + right)//2 | |
if arr[currentIndex] - currentIndex < 0: | |
left = currentIndex + 1 | |
elif arr[currentIndex] == currentIndex: | |
right = currentIndex - 1 | |
last = currentIndex | |
else: | |
right = currentIndex - 1 | |
if arr[left] == left: | |
return left | |
return last |
#!/bin/bash | |
array=(11 3 9 13 2 1 5 8) | |
target=8 | |
for i in "${!array[@]}"; do | |
for j in "${!array[@]}"; do | |
if [ $i -ne $j ]; then | |
if (( array[i] + array[j] == target )); then | |
echo $i, $j | |
exit 0 | |
fi | |
fi | |
done | |
done |