diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c791ff59a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] \ No newline at end of file diff --git a/01/NOTE.md b/01/NOTE.md new file mode 100644 index 000000000..95b924da3 --- /dev/null +++ b/01/NOTE.md @@ -0,0 +1,360 @@ +# 01 + +Array +----- + +> Array is a contiguous block of memory. It is usually used to represent sequences. + +- Arrays are lists, lists are **mutable** sequences, tuples are **immutable** sequences +- Lists are dynamically-resized, there is no upper bound +- Values can be deleted and inserted at arbitrary locations + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | O(1) | +| Search | O(1) | +| Insertion | O(n) | +| Deletion | O(n) | + +Common Sequence Operations (list, tuple, range, etc.) +```py +x in s # True if an item of s is equal to x, else False +x not in s # False if an item of s is equal to x, else True +s + t # the concatenation of s and t +s * n or n * s # equivalent to adding s to itself n times +s[i] # ith item of s, origin 0 +s[i:j] # slice of s from i to j +s[i:j:k] # slice of s from i to j with step k +len(s) # length of s +min(s) # smallest item of s +max(s) # largest item of s +reversed(s) # return a reverse iterator +sorted(s) # return a new sorted list from the items in iterable +``` + +List Operations +```py +a.append(x) # appends x to the end of the sequence (same as s[len(s):len(s)] = [x]) +a.extend(iterable) # extends s with the contents of t (same as s += t) +a.insert(i, x) # inserts x into s at the index given by i (same as s[i:i] = [x]) +a.remove(x) # remove the first item from s where s[i] is equal to x +a.pop([i]) # retrieves the item at i and also removes it from s +a.clear() # removes all items from s (same as del s[:]) +a.count(x) # total number of occurrences of x in s +a.reverse() # reverses the items of s in place +a.copy() # creates a shallow copy of s (same as s[:]) +a.index(x[, start[, end]]) # index of the first occurrence of x in s +a.sort(key=None, reverse=False) # Sort the items of the list in place +``` + +Coding Techniques +```py +# List comprehension +vec = [-4, -2, 0, 2, 4] +[x*2 for x in vec] # [-8, -4, 0, 4, 8] +[x for x in vec if x >= 0] # [0, 2, 4] +[abs(x) for x in vec] # [4, 2, 0, 2, 4] + +# Create a list of 2-tuples (number, square) +[(x, x**2) for x in range(4)] # [(0, 0), (1, 1), (2, 4), (3, 9)] + +# Flatten a 2-D list +vec = [[1,2,3], [4,5,6], [7,8,9]] +[num for elem in vec for num in elem] + +# String formatter +'Hello {name}'.format(name='World') + +# Useful functions +filter(lambda x: x % 2 != 0, [1, 2, 3, 4, 5, 6]) # [1, 3, 5] +map(lambda x: x * x, [1, 2, 3, 4, 5]) # [1, 4, 9, 16, 25] +map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6]) # [5, 7, 9] +any((False, False, True)) # True +all((False, True, True)) # False +sum([1, 2, 3, 4, 5]) # 15 +functools.reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) # calculates ((((1+2)+3)+4)+5) = 15 +``` + +Common Patterns +```py +# total combinations of two numbers (pairs) in an array (brute force) +nums = [1,2,3,4] +n = len(nums) +for i in range(n-1): + for j in range(i+1, n): + print((nums[i], nums[j])) # [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + +# traverse backwards +nums = [1,2,3,4,5] +n = len(nums) +for i in range(n-1, -1, -1): + print(nums[i]) # [5,4,3,2,1] + +# Two Pointers +nums = [1,2,3,4,5,6,7,8,9] +n = len(nums) +i, j = 0, n-1 +while i <= j: + print(nums[i], nums[j]) # [(1, 9), (2, 8), (3, 7), (4, 6), (5, 5)] + while i <= j and nums[i+1] == nums[i]: i += 1 # skip duplicates + while i <= j and nums[j-1] == nums[j]: j -= 1 # skip duplicates + i += 1 + j -= 1 + +# Get sliding windows of size k in an array of nums +nums, k = [1,2,3,4,5,6], 3 +n = len(nums) +windows = [nums[i:i+k] for i in range(n-k+1)] +print(windows) # [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]] + +# Rotate array by k times (right shift) +nums, k = [1,2,3,4,5,6,7], 3 +n = len(nums) +res = [0] * n +for i in range(n): + res[(i+k)%n] = nums[i] +print(res) # [5,6,7,1,2,3,4] +``` + +Leetcode Problems +- [283. Move Zeroes](https://leetcode.com/problems/move-zeroes/) +- [26. Remove Duplicates from Sorted Array ](https://leetcode.com/problems/remove-duplicates-from-sorted-array/) +- [66. Plus One](https://leetcode.com/problems/plus-one/) +- [1. Two Sum](https://leetcode.com/problems/two-sum/) +- [88. Merge Sorted Array](https://leetcode.com/problems/merge-sorted-array/) +- [189. Rotate Array](https://leetcode.com/problems/rotate-array/) +- [344. Reverse String](https://leetcode.com/problems/reverse-string/) +- [15. 3Sum](https://leetcode.com/problems/3sum/) +- [11. Container With Most Water](https://leetcode.com/problems/container-with-most-water/) + +Linked Lists +------------ + +> A list implements an ordered collection of values, which may include repetitions. + +Singly linked list: `2 -> 3 -> 5 -> 4 -> x` +- 2 is linked list head +- 4 is linked list tail +- 2's next is 3, 3's next is 5 and 5's next is 4, and 4's next is None +- L sometimes is used as a "dummy" head + +Doubly linked list: `x <- 2 <-> 3 <-> 5 <-> 4 -> x` +- 2's prev is None, 2's next is 3 +- 3's prev is 2 , 3's next is 5 +- 4's prev is 3 , 5's next is 4 +- 4's prev is 5 , 4's next is None + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | O(n) | +| Search | O(n) | +| Insertion | O(1) | +| Deletion | O(1) | + +```py +class ListNode: + def __init__(self, val = 0, next = None): + self.val = val + self.next = next +``` + +Two Pointers Template +```py +prev, curr = None, head +while curr: + # do something with prev and curr + if prev is None: + # when curr is head + else: + # when curr is not head + # move prev and curr + prev = curr + curr = curr.next + +return prev +``` + +Dummy Head (Sentry) Template +```py +dummy = ListNode(None) +dummy.next = head + +prev, curr = dummy, head +while curr: + # prev will never be None here + + # move prev and curr + prev = curr + curr = curr.next + +return dummy.next +``` + +Fast Slow Template +```py +fast = slow = head +while fast and fast.next: + # fast move 2 steps at a time + fast = fast.next.next + # slow move 1 step at a time + slow = slow.next + +# when num of the list is odd, slow is the mid +# when num of the list is even, slow is the first node after mid +return slow +``` + +Linked List Recursion +```py +def traverse(head): + # base case + if not head: + return head + # do something before (pre-order) + node = traverse(head.next) + # do something after (post-order) + return node +``` + +Leetcode Problems +- [203. Remove Linked List Elements](https://leetcode.com/problems/remove-linked-list-elements/) +- [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) +- [92. Reverse Linked List II](https://leetcode.com/problems/reverse-linked-list-ii/) +- [445. Add Two Numbers II](https://leetcode.com/problems/add-two-numbers-ii/) +- [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) +- [24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/) +- [876. Middle of the Linked List](https://leetcode.com/problems/middle-of-the-linked-list/) +- [83. Remove Duplicates from Sorted List](https://leetcode.com/problems/remove-duplicates-from-sorted-list/) +- [19. Remove Nth Node From End of List](https://leetcode.com/problems/remove-nth-node-from-end-of-list/) +- [141. Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/) +- [142. Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/) +- [148. Sort List](https://leetcode.com/problems/sort-list/) +- [25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) + +Stacks and Queues +----------------- + +> Stacks support first-in, last-out (FILO) for inserts and deletes, whereas queues are first-in first-out (FIFO) + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | O(n) | +| Search | O(n) | +| Insertion | O(1) | +| Deletion | O(1) | + +Stack Operations +```py +stack = [] # create a stack (nothing but a list) +stack.append(x) # push +stack.pop() # pop +stack[-1] # peek (top of the stack) +``` + +Queue Operations +```py +from collections import deque + +queue = deque() # create a double ended queue +queue.append(x) # add x to the right side of the deque +queue.appendleft(x) # add x to the left side of the deque +queue.pop() # remove and return an element from the right side of the deque +queue.popleft() # remove and return an element from the left side of the deque +queue[0] # peek left side of the deque +queue[-1] # peek right side of the deque +``` + +Mono stack +```py +n = len(nums) +stack = [] + +prevGreaterElement = [-1] * n +for i in range(n): # push into stack + while stack and stack[-1] <= nums[i]: # compare with stack top + stack.pop() # pop out numbers smaller than me + # now the top is the first element larger than me + prevGreaterElement[i] = stack[-1] if stack else -1 + # push myself in stack for the next round + stack.append(nums[i]) +print(prevGreaterElement) + +# Variation 1: push to stack backwards to get the rightMax +nextGreaterElement = [-1] * n +for i in range(n-1, -1, -1): + while stack and stack[-1] <= nums[i]: + stack.pop() + nextGreaterElement[i] = stack[-1] if stack else -1 + stack.append(nums[i]) +print(nextGreaterElement) + +# Variation 2: find min rather than max (change the compare part) +prevSmallerElement = [-1] * n +for i in range(n): + while stack and stack[-1] > nums[i]: + stack.pop() + prevSmallerElement[i] = stack[-1] if stack else -1 + stack.append(nums[i]) +print(prevSmallerElement) + +# Variation 3: push index to stack instead of numbers +prevGreaterIndex = [-1] * n +for i in range(n): + while stack and nums[stack[-1]] <= nums[i]: + stack.pop() + prevGreaterIndex[i] = stack[-1] if stack else -1 + stack.append(i) +print(prevGreaterIndex) +``` + +Mono Increasing Stack Template +```py +for i in range(n): + while stack and nums[stack[-1]] > nums[i]: + curr = stack.pop() # current index + if not stack: break + left = stack[-1] # prev smallest index + right = i # next smallest index + # do something with curr, left and right... + stack.append(i) +``` + +Mono Decreasing Stack Template +```py +for i in range(n): + while stack and nums[stack[-1]] < nums[i]: + curr = stack.pop() # current index + if not stack: break + left = stack[-1] # prev largest index + right = i # next largest index + # do something with curr, left and right... + stack.append(i) +``` + +Mono Queue +```py +class monoQueue: + def __init__(self): + self.queue = deque() + + def push(self, x): + while self.queue and self.queue[-1] < x: + self.queue.pop() + self.queue.append(x) + + def pop(self, x): + if self.queue and self.queue[0] == x: + self.queue.popleft() + + def max(self): + return self.queue[0] +``` + +Leetcode Problems +- [20. Valid Parentheses](https://leetcode.com/problems/valid-parentheses/) +- [155. Min Stack](https://leetcode.com/problems/min-stack/) +- [496. Next Greater Element I](https://leetcode.com/problems/next-greater-element-i/) +- [503. Next Greater Element II](https://leetcode.com/problems/next-greater-element-ii/) +- [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/) +- [84. Largest Rectangle in Histogram](https://leetcode.com/problems/largest-rectangle-in-histogram/) +- [239. Sliding Window Maximum](https://leetcode.com/problems/sliding-window-maximum/) diff --git a/01/linked_list.py b/01/linked_list.py new file mode 100644 index 000000000..79eb0095e --- /dev/null +++ b/01/linked_list.py @@ -0,0 +1,30 @@ +""" +Singly Linked List Data Structure +""" +class ListNode: + """ + Linked List Node + """ + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +def insert_node(node, new_node): + new_node.next = node.next + node.next = new_node + +def remove_node(node): + node.next = node.next.next + +def create_list(iterable = ()): + dummy = ListNode() + for elem in reversed(iterable): + insert_node(dummy, ListNode(elem)) + return dummy.next + +def print_list(head): + result, p = [], head + while p: + result.append(p.val) + p = p.next + print(result) diff --git a/01/merge-sorted-array.py b/01/merge-sorted-array.py new file mode 100644 index 000000000..46b8bc2bc --- /dev/null +++ b/01/merge-sorted-array.py @@ -0,0 +1,55 @@ +""" +88. Merge Sorted Array +https://leetcode.com/problems/merge-sorted-array/ +""" +from typing import List + +class Solution: + def merge1(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: + """ + Solution #1: Two pointers, forwards + Time: O(m+n) + Space O(m): need a copy for nums1 + """ + nums1_copy = nums1[:m] + nums1[:] = [] + p1 = p2 = 0 + while p1 < m and p2 < n: + if nums1_copy[p1] < nums2[p2]: + nums1.append(nums1_copy[p1]) + p1 += 1 + else: + nums1.append(nums2[p2]) + p2 += 1 + # reminder + if p1 < m: nums1[p1+p2:] = nums1_copy[p1:] + if p2 < n: nums1[p1+p2:] = nums2[p2:] + + def merge2(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: + """ + Solution #2: Three pointers, backwards + Time: O(m+n) + Space O(1) + """ + p1, p2, p = m-1, n-1, len(nums1) - 1 + while p1 >= 0 and p2 >= 0: + if nums1[p1] < nums2[p2]: + nums1[p] = nums2[p2] + p2 -= 1 + else: + nums1[p] = nums1[p1] + p1 -= 1 + p -= 1 + nums1[:p2+1] = nums2[:p2+1] + +solution = Solution() + +nums1 = [1,2,3,0,0,0] +nums2 = [2,5,6] +solution.merge1(nums1, 3, nums2, 3) +print(nums1) # [1, 2, 2, 3, 5, 6] + +nums1 = [1,2,3,4,0,0] +nums2 = [3,4] +solution.merge2(nums1, 4, nums2, 2) +print(nums1) # [1, 2, 3, 3, 4, 4] \ No newline at end of file diff --git a/01/merge-two-sorted-lists.py b/01/merge-two-sorted-lists.py new file mode 100644 index 000000000..7a16014b1 --- /dev/null +++ b/01/merge-two-sorted-lists.py @@ -0,0 +1,50 @@ +""" +21. Merge Two Sorted Lists +https://leetcode-cn.com/problems/merge-two-sorted-lists/ +""" +from linked_list import ListNode, create_list, print_list + +class Solution: + def mergeTwoLists1(self, l1: ListNode, l2: ListNode) -> ListNode: + """ + Solution #1: iteratively + Time: O(n) + Space: O(1) + """ + dummy = ListNode() + p = dummy + while l1 and l2: + if l1.val < l2.val: + p.next, l1 = l1, l1.next + else: + p.next, l2 = l2, l2.next + p = p.next + p.next = l1 or l2 + return dummy.next + + def mergeTwoLists2(self, l1: ListNode, l2: ListNode) -> ListNode: + """ + Solution #2: recursively + Time: O(n) + Space: O(1) + """ + if not l1: return l2 + if not l2: return l1 + if l1.val < l2.val: + l1.next = self.mergeTwoLists2(l1.next, l2) + return l1 + else: + l2.next = self.mergeTwoLists2(l1, l2.next) + return l2 + +solution = Solution() + +l1 = create_list([1,2,3]) +l2 = create_list([1,2,4]) +ans1 = solution.mergeTwoLists1(l1, l2) +print_list(ans1) # [1,1,2,2,3,4] + +l1 = create_list([1,2,3]) +l2 = create_list([3,4,5,6,7]) +ans2 = solution.mergeTwoLists2(l1, l2) +print_list(ans2) # [1, 2, 3, 3, 4, 5, 6, 7] \ No newline at end of file diff --git a/01/move-zeroes.py b/01/move-zeroes.py new file mode 100644 index 000000000..7ed30a329 --- /dev/null +++ b/01/move-zeroes.py @@ -0,0 +1,57 @@ +""" +283. Move Zeroes +https://leetcode.com/problems/move-zeroes/ +""" +from typing import List + +class Solution: + def moveZeroes1(self, nums: List[int]) -> None: + """ + Solution #1: use append & remove API + Time: O(n^2): append O(1) + remove O(n) worst case + Space: O(1) + """ + for elem in nums: + if elem == 0: + nums.append(0) + nums.remove(0) + + def moveZeroes2(self, nums: List[int]) -> None: + """ + Solution #2: remember the next zero position and swap + Time: O(n) + Space: O(1) + """ + next_zero_spot = 0 + for i in range(len(nums)): + if nums[i] != 0: + nums[i], nums[next_zero_spot] = nums[next_zero_spot], nums[i] + next_zero_spot += 1 + + def moveZeroes3(self, nums: List[int]) -> None: + """ + Solution #3: snowball solution by https://leetcode.com/olsh + Time: O(n) + Space: O(1) + """ + snowBallSize = 0 + for i in range(len(nums)): + if nums[i] == 0: + snowBallSize += 1 # the snowball gets bigger + elif snowBallSize > 0: + # swap the most left 0 with the element + nums[i], nums[i - snowBallSize] = nums[i - snowBallSize], nums[i] + +solution = Solution() + +nums = [0,1,0,3,12] +solution.moveZeroes1(nums) +print(nums) # [1,3,12,0,0] + +nums = [0,0,0,0,1] +solution.moveZeroes2(nums) +print(nums) # [1, 0, 0, 0, 0] + +nums = [1,0,2,0,3,0] +solution.moveZeroes3(nums) +print(nums) # [1, 2, 3, 0, 0, 0] \ No newline at end of file diff --git a/01/plus-one.py b/01/plus-one.py new file mode 100644 index 000000000..075e22634 --- /dev/null +++ b/01/plus-one.py @@ -0,0 +1,27 @@ +""" +66. Plus One +https://leetcode.com/problems/plus-one/ +""" +from typing import List + +class Solution: + def plusOne(self, digits: List[int]) -> List[int]: + """ + Solution #1 + Time: O(n) + Space: O(1) + """ + n = len(digits) - 1 + while n >= 0: + if digits[n] == 9: + digits[n] = 0 + n -= 1 + else: + digits[n] += 1 + break + + return [1] + digits[:] if digits[0] == 0 else digits + +solution = Solution() +ans = solution.plusOne([9,9,9]) +print(ans) # [1,0,0,0] \ No newline at end of file diff --git a/01/remove-duplicates.py b/01/remove-duplicates.py new file mode 100644 index 000000000..52c904f0f --- /dev/null +++ b/01/remove-duplicates.py @@ -0,0 +1,26 @@ +""" +26. Remove Duplicates from Sorted Array +https://leetcode.com/problems/remove-duplicates-from-sorted-array/ +""" +from typing import List + +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + """ + Solution #1: Two Pointers + Time: O(n) + Space: O(1) + """ + if len(nums) == 0: + return 0 + p = 0 + for q in range(1, len(nums)): + if nums[q] > nums[p]: + p += 1 + nums[p] = nums[q] + return p + 1 + +solution = Solution() +nums = [0,0,1,1,1,2,2,3,3,4] +ans = solution.removeDuplicates(nums) +print(nums[:ans]) # [0, 1, 2, 3, 4] \ No newline at end of file diff --git a/01/rotate-array.py b/01/rotate-array.py new file mode 100644 index 000000000..f6b27368f --- /dev/null +++ b/01/rotate-array.py @@ -0,0 +1,85 @@ +""" +189. Rotate Array +https://leetcode.com/problems/rotate-array/ +""" +from typing import List + +class Solution: + def rotate1(self, nums: List[int], k: int) -> None: + """ + Solution #1: Brute Force, rotate k times + Time: O(n*k) + Space: O(1) + """ + for i in range(k): + prev = nums[len(nums) - 1] + for j in range(len(nums)): + nums[j], prev = prev, nums[j] + + def rotate2(self, nums: List[int], k: int) -> None: + """ + Solution #2: Extra Array + Time: O(n) + Space: O(n) + """ + n = len(nums) + res = [0] * n + for i in range(n): + res[(i+k) % n] = nums[i] + for i in range(n): + nums[i] = res[i] + + def rotate3(self, nums: List[int], k: int) -> None: + """ + Solution #3: Cyclic Replacement + Time: O(n) + Space: O(1) + """ + n = len(nums) + k %= n + start = count = 0 + while count < n: + curr, prev = start, nums[start] + while True: + next_idx = (curr + k) % n + nums[next_idx], prev = prev, nums[next_idx] + curr = next_idx + count += 1 + if start == curr: + break + start += 1 + + def rotate4(self, nums: List[int], k: int) -> None: + """ + Solution #3: Reverse + Time: O(n): n elements are reversed a total of three times. + Space: O(1) + """ + n = len(nums) + k %= n + self.reverse(nums, 0, n - 1) + self.reverse(nums, 0, k - 1) + self.reverse(nums, k, n - 1) + + def reverse(self, nums: list, start: int, end: int) -> None: + while start < end: + nums[start], nums[end] = nums[end], nums[start] + start, end = start + 1, end - 1 + +solution = Solution() + +nums = [1,2,3,4,5,6,7] +solution.rotate1(nums, 1) +print(nums) # [7, 1, 2, 3, 4, 5, 6] + +nums = [1,2,3,4,5,6,7] +solution.rotate2(nums, 2) +print(nums) # [6, 7, 1, 2, 3, 4, 5] + +nums = [1,2,3,4,5,6,7] +solution.rotate3(nums, 3) +print(nums) # [5, 6, 7, 1, 2, 3, 4] + +nums = [1,2,3,4,5,6,7] +solution.rotate4(nums, 4) +print(nums) # [4, 5, 6, 7, 1, 2, 3] diff --git a/01/two-sum.py b/01/two-sum.py new file mode 100644 index 000000000..3376eb510 --- /dev/null +++ b/01/two-sum.py @@ -0,0 +1,56 @@ +""" +1. Two Sum +https://leetcode.com/problems/two-sum/ +""" +from typing import List + +class Solution: + def twoSum1(self, nums: List[int], target: int) -> List[int]: + """ + Solution #1: brute-force + Time: O(n^2) + Space: O(1) + """ + l = len(nums) + for i in range(0, l - 1): + for j in range(i + 1, l): + if nums[i] + nums[j] == target: + return [i, j] + + def twoSum2(self, nums: List[int], target: int) -> List[int]: + """ + Solution #2: cache, 2 iteration + Time: O(n) + Space: O(n) + """ + dic = {} + for i in range(len(nums)): + dic[target - nums[i]] = i + + for i, num in enumerate(nums): + if num in dic and dic[num] != i: + return [i, dic[num]] + + def twoSum3(self, nums: List[int], target: int) -> List[int]: + """ + Solution #3: cache, 1 iteration + Time: O(n) + Space: O(n) + """ + dic = {} + for i, n in enumerate(nums): + if n in dic: + return [dic.get(n), i] + else: + dic[target - n] = i + +solution = Solution() +lst = [2, 7, 11, 15] +target = 9 +ans1 = solution.twoSum1(lst, target) +ans2 = solution.twoSum1(lst, target) +ans3 = solution.twoSum1(lst, target) + +print(ans1) # [0, 1] +print(ans2) # [0, 1] +print(ans3) # [0, 1] \ No newline at end of file diff --git a/02/NOTE.md b/02/NOTE.md new file mode 100644 index 000000000..d7d79b307 --- /dev/null +++ b/02/NOTE.md @@ -0,0 +1,352 @@ +# 02 + +Hash Table +---------- + +> A hash table is a data structure used to store vals, optionally, with corresponding values. + +- A mapping object maps hashable values to arbitrary objects. Mappings are **mutable** objects. The only standard mapping type in python is `Dictionary`. +- A `dict` vals are almost arbitrary values. Values that are not hashable (like lists, dictionaries or other mutable types) may not be used as vals. +- A `set` is an unordered collection with no duplicate elements. Curly braces or the set() function can be used to create sets. (empty set must use set()) + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | N/A | +| Search | O(1) | +| Insertion | O(1) | +| Deletion | O(1) | + +Dictionary Operations +```py +d[val] # Return the item of d with val val. Raises a valError if val is not in the map. +d[val] = value # Set d[val] to value. +del d[val] # Remove d[val] from d. Raises a valError if val is not in the map. +val in d # Return True if d has a val val, else False. +val not in d # Equivalent to not val in d. +d.clear() # Remove all items from the dictionary. +d.get(val[, default]) # Return the value for val if val is in the dictionary, else default. +d.keys() # Return a new view of the dictionary’s keys. +d.values() # Return a new view of the dictionary’s values. +d.items() # Return a new view of the dictionary’s items ((val, value) pairs). +``` + +Useful functions +```py +from collections import defaultdict, Counter + +# A defaultdict is initialized with a function ("default factory") that takes no arguments and provides the default value for a nonexistent key. +dic = {} # build-in dictionary +dic['a'] # KeyError, no such key + +dic = defaultdict(int) # using int() as default factory +dic['a'] # defaultdict(, {'a': 0}) + +# A Counter is a dict subclass for counting hashable objects. +cnt = Counter(['red', 'blue', 'red', 'green', 'blue', 'blue']) +print(cnt) # Counter({'blue': 3, 'red': 2, 'green': 1}) +``` + +Leetcode Problems +- [1. Two Sum](https://leetcode.com/problems/two-sum/) +- [242. Valid Anagram](https://leetcode.com/problems/valid-anagram/description/) +- [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) + +Binary Tree +----------- + +> A binary tree is either empty, or a root node together with a left binary tree and a right binary tree. + +``` + Height Depth Level + __A__ ----> 4 0 1 + / \ + __B C ----> 3 1 2 + / \ / \ + D E F G ----> 2 2 3 + / \ +H I ----> 1 3 4 +``` +- Node `A` is Root +- Node `A` has 2 children: Node `B` and Node `C` +- Node `B` is Node `A`'s left child +- Node `C` is Node `A`'s right child +- Node `A` is Node `B` and Node `C`'s parent +- Node `H` and Node `I` are a Leaf Nodes +- Node `A` is Node `H`'s ancestor +- Node `I` is Node `A`'s decedent + +Binary tree +> A tree has a root node and every node has at most 2 children + +Full Binary Tree +> A tree in which every node has either 0 or 2 children + +Perfect Binary Tree +> A full binary tree in which all leaves are at the same depth, and in which every parent has 2 children + +Complete Binary Tree +> A binary tree in which every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible. + +- A complete binary tree has `2^k` nodes at every depth `k < n` and between `2^n` and `2^n+1 - 1` nodes altogether. +- It can be efficiently implemented as an array, where a node at index `i` has children at indexes `2i` and `2i+1` and a parent at index `i/2`, with 1-based indexing (`2i+1` and `2i+2` for 0-based indexing) + +``` + __A__ + / \ + B C + / \ / \ + D E F G + / +H + +[_,A,B,C,D,E,F,G,H] +``` + +Below are **NOT** Complete Binary Trees +``` + __A__ __A__ ______A______ + / \ / \ / \ + B C B C __B__ __C__ + / \ / \ / \ / \ / \ + D E D E F G D E F G + / \ \ \ \ / \ / \ / \ / \ +F G H H I H I J K L M N O +``` + +BinaryTree Node +```py +class BinaryTreeNode: + def __init__(self, val, left=None, right=None): + self.val = val + self.left = left + self.right = right +``` + +Binary Tree Traversal + +Pre-order Traversal: `root -> left -> right` +```py +def preorder(self, root): + if root: + visit(root) + preorder(left) + preorder(right) +``` + +In-order Traversal: `left -> root -> right` +```py +def inorder(self, root): + if root: + inorder(left) + visit(root) + inorder(right) +``` + +Post-order Traversal: `left -> right -> root` +```py +def postorder(self, root): + if root: + postorder(left) + postorder(right) + visit(root) +``` + +Level Order Traversal: `top -> bottom, left -> right` +```py +def levelorder(root): + queue = collections.deque() + queue.append(root) + while queue: + node = queue.popleft() + self.visit(node) + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) +``` + +LeetCode Problems +- [144. Binary Tree Preorder Traversal](https://leetcode.com/problems/binary-tree-preorder-traversal/) +- [94. Binary Tree Inorder Traversal](https://leetcode.com/problems/binary-tree-inorder-traversal/) +- [145. Binary Tree Postorder Traversal](https://leetcode.com/problems/binary-tree-postorder-traversal/) +- [102. Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/) +- [589. N-ary Tree Preorder Traversal](https://leetcode.com/problems/n-ary-tree-preorder-traversal/) +- [590. N-ary Tree Postorder Traversal](https://leetcode.com/problems/n-ary-tree-postorder-traversal/) +- [429. N-ary Tree Level Order Traversal](https://leetcode.com/problems/n-ary-tree-level-order-traversal/) + +Binary Search Tree (BST) +------------------------ + +> A BST is a rooted binary tree whose internal nodes each store a val greater than all the vals in the node's left subtree and less than those in its right subtree. + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | O(log n) | +| Search | O(log n) | +| Insertion | O(log n) | +| Deletion | O(log n) | + +BST Template +```py +def BST(root, target): + if root.val == target: + # found it, do something... + + if val < root.val: + BST(root.left, val) # find in left tree + + if val > root.val: + BST(root.right, val) # find in right tree +``` + +Search +```py +def search(root, val): + if root is None: + return None + if val < root.val: + return self.search(root.left, val) + elif val > root.val: + return self.search(root.right, val) + return root +``` + +Insert +``` + 100 100 + / \ Insert(40) / \ + 20 500 ---------> 20 500 + / \ / \ +10 30 10 30 + \ + 40 +``` + +```py +def insert(root, val): + if root is None: + return BinaryTreeNode(val) + if val < root.val: + root.left = self.insert(root.left, val) + elif val > root.val: + root.right = self.insert(node.right, val) + return root +``` + +Delete +``` +1) Node to be deleted is leaf: Simply remove from the tree. + + 50 50 + / \ Delete(20) / \ + 30 70 ---------> 30 70 + / \ / \ \ / \ +20 40 60 80 40 60 80 + +2) Node to be deleted has only one child: Copy the child to the node and delete the child. + + 50 50 + / \ Delete(30) / \ + 30 70 ---------> 40 70 + \ / \ / \ + 40 60 80 60 80 + +3) Node to be deleted has two children: Find inorder successor of the node. + Copy contents of the inorder successor to the node and delete the inorder successor. + + 50 60 + / \ Delete(50) / \ + 40 70 ---------> 40 70 + / \ \ + 60 80 80 +``` + +```py +def deleteNode(self, root: TreeNode, val: int) -> TreeNode: + if root is None: + return root + + if val < root.val: + root.left = self.deleteNode(root.left, val) + elif val > root.val: + root.right = self.deleteNode(root.right, val) + else: + # has one child + if root.left is None: # only right child + return root.right + if root.right is None: # only left child + return root.left + + # has two children + minNode = self.getMin(root.right) + root.val = minNode.val + root.right = self.deleteNode(root.right, minNode.val) + + return root + +def getMin(self, node: TreeNode) -> TreeNode: + # left most child is the smallest + while node.left is not None: + node = node.left + return node +``` + +LeetCode Problems +- [100. Same Tree](https://leetcode.com/problems/same-tree/) +- [700. Search in a Binary Search Tree](https://leetcode.com/problems/search-in-a-binary-search-tree/) +- [701. Insert into a Binary Search Tree](https://leetcode.com/problems/insert-into-a-binary-search-tree/) +- [450. Delete Node in a BST](https://leetcode.com/problems/delete-node-in-a-bst/) +- [98. Validate Binary Search Tree](https://leetcode.com/problems/validate-binary-search-tree/) + +Heap +---- + +> A heap is a complete binary tree, and is represent by array. The children of the node at index i are at indices 2i+1 and 2i+2. + +Given element in a heap at position `i`: +- parent position: `(i-1) >> 1` or `i // 2` +- left child position: `2*i + 1` +- right child position: `2*i + 2` + +| Operation | Time Complexity | +| ---------- | :-------------: | +| find-min | O(1) | +| delete-min | O(logn) | +| Insert | O(logn) | +| K largest | O(nlogK) | +| K smallest | O(nlogK) | + +max-heap: the val at each node is at least as great as the vals at it's children. +``` + _____561_____ + / \ + ___314_ _401_ + / \ / \ + _28 156 359 271 + / \ +11 3 +``` + +min-heap: the val at each node is at least as small as the vals at it's children. +``` + _____3____ + / \ + _____11_ _28_ + / \ / \ + _156_ 314 561 401 + / \ +359 271 +``` + +**heapq** Operations +```py +heap = [] # creates an empty heap +heap[0] # smallest element on the heap without popping it +heapq.heapify(L) # transforms list into a heap, in-place, in linear time +heapq.heappush(h, e) # pushes a new element on the heap +heapq.heappop(h) # pops the smallest item from the heap +heapq.heappushpop(h, a) # pushes a on the heap and then pops and returns the smallest element +heapq.heapreplace(h, e) # pops and returns smallest item, and adds new item; the heap size is unchanged +heapq.nlargest(n, L) # Find the n largest elements in a dataset. +heapq.nsmallest(n, L) # Find the n smallest elements in a dataset. +``` \ No newline at end of file diff --git a/02/binary-tree-traversal.py b/02/binary-tree-traversal.py new file mode 100644 index 000000000..d3a4be64b --- /dev/null +++ b/02/binary-tree-traversal.py @@ -0,0 +1,225 @@ +""" +144. Binary Tree Preorder Traversal +https://leetcode.com/problems/binary-tree-preorder-traversal/ + +94. Binary Tree Inorder Traversal +https://leetcode.com/problems/binary-tree-inorder-traversal/ + +145. Binary Tree Postorder Traversal +https://leetcode.com/problems/binary-tree-postorder-traversal/ + +102. Binary Tree Level Order Traversal +https://leetcode.com/problems/binary-tree-level-order-traversal/ +""" +from typing import List +from binary_tree import BinaryTreeNode, create_binary_tree, print_binary_tree +from collections import deque + +class Solution: + def preorderTraversal1(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #1: Recursively + """ + def traverse(root): + if not root: return + res.append(root.val) + traverse(root.left) + traverse(root.right) + + res = [] + traverse(root) + return res + + def preorderTraversal2(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #2: Iteratively + """ + if not root: return [] + res = [] + stack = [root] + while stack: + node = stack.pop() + if node is not None: + res.append(node.val) + stack.append(node.right) + stack.append(node.left) + return res + + def preorderTraversal3(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #3: White/Grey Tagging + """ + WHITE, GREY = 0, 1 + res = [] + stack = [(WHITE, root)] + while stack: + color, node = stack.pop() + if not node: + continue + if color == WHITE: + stack.append((WHITE, node.right)) + stack.append((WHITE, node.left)) + stack.append((GREY, node)) + else: + res.append(node.val) + return res + + def inorderTraversal1(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #1: Recursively + """ + def traverse(root): + if not root: return + traverse(root.left) + res.append(root.val) + traverse(root.right) + + res = [] + traverse(root) + return res + + def inorderTraversal2(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #2: Iteratively + """ + if not root: return [] + res = [] + stack = [] + while True: + while root: + stack.append(root) + root = root.left + if not stack: + return res + node = stack.pop() + res.append(node.val) + root = node.right + return res + + def inorderTraversal3(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #3: White/Grey Tagging + """ + WHITE, GREY = 0, 1 + res = [] + stack = [(WHITE, root)] + while stack: + color, node = stack.pop() + if not node: + continue + if color == WHITE: + stack.append((WHITE, node.right)) + stack.append((GREY, node)) + stack.append((WHITE, node.left)) + else: + res.append(node.val) + return res + + def postorderTraversal1(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #1: Recursively + """ + def traverse(root): + if not root: return + traverse(root.left) + traverse(root.right) + res.append(root.val) + + res = [] + traverse(root) + return res + + def postorderTraversal2(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #2: Iteratively + """ + if not root: return root + res = [] + stack = [root] + while stack: + node = stack.pop() + if node is not None: + res.append(node.val) + stack.append(node.left) + stack.append(node.right) + return res[::-1] + + def postorderTraversal3(self, root: BinaryTreeNode) -> List[int]: + """ + Solution #3: White/Grey Tagging + """ + WHITE, GREY = 0, 1 + res = [] + stack = [(WHITE, root)] + while stack: + color, node = stack.pop() + if not node: + continue + if color == WHITE: + stack.append((GREY, node)) + stack.append((WHITE, node.right)) + stack.append((WHITE, node.left)) + else: + res.append(node.val) + return res + + def levelOrder1(self, root: BinaryTreeNode) -> List[List[int]]: + """ + Solution #1: Recursively + """ + def traverse(root, level): + if not root: + return + if len(res) == level: + res.append([]) + res[level].append(root.val) + if root.left: + traverse(root.left, level+1) + if root.right: + traverse(root.right, level+1) + + res = [] + traverse(root, 0) + return res + + def levelOrder2(self, root: BinaryTreeNode) -> List[List[int]]: + """ + Solution #2: Iteratively + """ + if not root: + return [] + res = [] + queue = deque([root]) + while queue: + n = len(queue) + level = [] + for _ in range(n): + root = queue.popleft() + level.append(root.val) + if root.left: + queue.append(root.left) + if root.right: + queue.append(root.right) + res.append(level) + return res + +t = create_binary_tree([0,1,2,3,4,5,6,7,8,9]) +print_binary_tree(t) + +solution = Solution() +ans = solution.preorderTraversal1(t) +print('Binary Tree Preorder Traversal Recursively: ', ans) +ans = solution.preorderTraversal2(t) +print('Binary Tree Preorder Traversal Iteratively: ', ans) +ans = solution.inorderTraversal1(t) +print('Binary Tree Inorder Traversal Recursively: ', ans) +ans = solution.inorderTraversal2(t) +print('Binary Tree Inorder Traversal Iteratively: ', ans) +ans = solution.postorderTraversal1(t) +print('Binary Tree Postorder Traversal Recursively: ', ans) +ans = solution.postorderTraversal2(t) +print('Binary Tree Postorder Traversal Iteratively: ', ans) +ans = solution.levelOrder1(t) +print('Binary Tree Level Order Traversal Recursively: ', ans) +ans = solution.levelOrder2(t) +print('Binary Tree Level Order Traversal Iteratively: ', ans) \ No newline at end of file diff --git a/02/binary_search_tree.py b/02/binary_search_tree.py new file mode 100644 index 000000000..e24ea1f8a --- /dev/null +++ b/02/binary_search_tree.py @@ -0,0 +1,125 @@ +""" +700. Search in a Binary Search Tree +https://leetcode.com/problems/search-in-a-binary-search-tree/ + +701. Insert into a Binary Search Tree +https://leetcode.com/problems/insert-into-a-binary-search-tree/ + +450. Delete Node in a BST +https://leetcode.com/problems/delete-node-in-a-bst/ +""" +from binary_tree import BinaryTreeNode, create_binary_tree, print_binary_tree + +class Solution: + def searchBST1(self, root: BinaryTreeNode, val: int) -> BinaryTreeNode: + """ + Solution #1: Recursively + """ + if root is None: + return root + if val < root.val: + return self.searchBST1(root.left, val) + if val > root.val: + return self.searchBST1(root.right, val) + # val == root.val + return root + + def searchBST2(self, root: BinaryTreeNode, val: int) -> BinaryTreeNode: + """ + Solution #2: Iteratively + """ + curr = root + while curr: + if val < curr.val: + curr = curr.left + elif val > curr.val: + curr = curr.right + else: # val == root.val + break + return curr + + def insertIntoBST1(self, root: BinaryTreeNode, val: int) -> BinaryTreeNode: + if root is None: + return BinaryTreeNode(val) + if val < root.val: + root.left = self.insertIntoBST1(root.left, val) + else: # val > root.val + root.right = self.insertIntoBST1(root.right, val) + return root + + def insertIntoBST2(self, root: BinaryTreeNode, val: int) -> BinaryTreeNode: + if root is None: + return BinaryTreeNode(val) + parent, curr = None, root + while curr: + parent = curr + if val < curr.val: + curr = curr.left + elif val > curr.val: + curr = curr.right + else: # val == root.val + break + if val < parent.val: + parent.left = BinaryTreeNode(val) + elif val > parent.val: + parent.right = BinaryTreeNode(val) + return root + + def deleteNode(self, root: BinaryTreeNode, key: int) -> BinaryTreeNode: + if root is None: + return None + if key < root.val: + root.left = self.deleteNode(root.left, key) + elif key > root.val: + root.right = self.deleteNode(root.right, key) + else: + if root.left is None: + # has only right child + child = root.right + root = None + return child + elif root.right is None: + # has only left child + child = root.left + root = None + return child + # has two children + child = self.minChild(root.right) + root.val = child.val + root.right = self.deleteNode(root.right , child.val) + return root + + def minChild(self, node: BinaryTreeNode) -> BinaryTreeNode: + curr = node + while curr.left is not None: + curr = curr.left + return curr + +t = create_binary_tree([5,3,7,2,4,6]) +print_binary_tree(t) + +solution = Solution() +print('Search BST Recursively: ') +ans = solution.searchBST1(t, 3) +print_binary_tree(ans) +print('Search BST Iteratively: ') +ans = solution.searchBST2(t, 7) +print_binary_tree(ans) +print('Insert into BST Recursively: ') +ans = solution.insertIntoBST1(t, 1) +print_binary_tree(ans) +print('Insert into BST Iteratively: ') +ans = solution.insertIntoBST2(t, 8) +print_binary_tree(ans) +print('Delete Node (leaf): ') +ans = solution.deleteNode(t, 1) +print_binary_tree(ans) +print('Delete Node (leaf): ') +ans = solution.deleteNode(t, 4) +print_binary_tree(ans) +print('Delete Node (one child): ') +ans = solution.deleteNode(t, 3) +print_binary_tree(ans) +print('Delete Node (two children): ') +ans = solution.deleteNode(t, 5) +print_binary_tree(ans) diff --git a/02/binary_tree.py b/02/binary_tree.py new file mode 100644 index 000000000..a053ed7bd --- /dev/null +++ b/02/binary_tree.py @@ -0,0 +1,93 @@ +""" +Binary Tree Data Structure +""" +class BinaryTreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val + self.left = None + self.right = None + +def create_binary_tree(iterable = ()): + """ Given in iterable, create a binary tree in level order + Example: create_tree([0,1,2,3,4,5,6,7,8,9]) + + ____0__ + / \ + __1__ 2 + / \ / \ + 3 4 5 6 + / \ / + 7 8 9 + """ + def insert(node, i): + if i < len(iterable) and iterable[i] is not None: + node = BinaryTreeNode(iterable[i]) + node.left = insert(node.left, 2 * i + 1) + node.right = insert(node.right, 2 * i + 2) + return node + return insert(None, 0) + +def print_binary_tree(root, index=False): + """ Pretty-print the binary tree. + Inspired by https://pypi.org/project/binarytree/ + """ + def build_tree_string(root, current, index=False, delimiter='-'): + if root is None: + return [], 0, 0, 0 + + line1 = [] + line2 = [] + if index: + node_repr = '{}{}{}'.format(current, delimiter, root.val) + else: + node_repr = str(root.val) + + new_root_width = gap_size = len(node_repr) + + # Get the left and right sub-boxes, their widths, and root repr positions + l_box, l_box_width, l_root_start, l_root_end = \ + build_tree_string(root.left, 2 * current + 1, index, delimiter) + r_box, r_box_width, r_root_start, r_root_end = \ + build_tree_string(root.right, 2 * current + 2, index, delimiter) + + # Draw the branch connecting the current root node to the left sub-box + # Pad the line with whitespaces where necessary + if l_box_width > 0: + l_root = (l_root_start + l_root_end) // 2 + 1 + line1.append(' ' * (l_root + 1)) + line1.append('_' * (l_box_width - l_root)) + line2.append(' ' * l_root + '/') + line2.append(' ' * (l_box_width - l_root)) + new_root_start = l_box_width + 1 + gap_size += 1 + else: + new_root_start = 0 + + # Draw the representation of the current root node + line1.append(node_repr) + line2.append(' ' * new_root_width) + + # Draw the branch connecting the current root node to the right sub-box + # Pad the line with whitespaces where necessary + if r_box_width > 0: + r_root = (r_root_start + r_root_end) // 2 + line1.append('_' * r_root) + line1.append(' ' * (r_box_width - r_root + 1)) + line2.append(' ' * r_root + '\\') + line2.append(' ' * (r_box_width - r_root)) + gap_size += 1 + new_root_end = new_root_start + new_root_width - 1 + + # Combine the left and right sub-boxes with the branches drawn above + gap = ' ' * gap_size + new_box = [''.join(line1), ''.join(line2)] + for i in range(max(len(l_box), len(r_box))): + l_line = l_box[i] if i < len(l_box) else ' ' * l_box_width + r_line = r_box[i] if i < len(r_box) else ' ' * r_box_width + new_box.append(l_line + gap + r_line) + + # Return the new box, its width and its root repr positions + return new_box, len(new_box[0]), new_root_start, new_root_end + + lines = build_tree_string(root, 0, index)[0] + print('\n' + '\n'.join((line.rstrip() for line in lines))) diff --git a/02/group_anagrams.py b/02/group_anagrams.py new file mode 100644 index 000000000..d94ba3791 --- /dev/null +++ b/02/group_anagrams.py @@ -0,0 +1,37 @@ +""" +49. Group Anagrams +https://leetcode.com/problems/valid-anagram/ +""" +from typing import List +from collections import defaultdict + +class Solution: + def groupAnagrams1(self, strs: List[str]) -> List[List[str]]: + """ + Solution #1: Sort + Time: O(N*K*logK): N = len(strs), K = len(longest string in strs) + Space: O(N*K): the dictionary + """ + ans = defaultdict(list) + for s in strs: + ans[tuple(sorted(s))].append(s) + return ans.values() + + def groupAnagrams2(self, strs: List[str]) -> List[List[str]]: + """ + Solution #2: Count + Time: O(N*K): N = len(strs), K = len(longest string in strs) + Space: O(N*K): the dictionary + """ + ans = defaultdict(list) + for s in strs: + count = [0] * 26 + for c in s: + count[ord(c) - ord('a')] += 1 + ans[tuple(count)].append(s) + return ans.values() + +solution = Solution() +print(solution.groupAnagrams1(["eat", "tea", "tan", "ate", "nat", "bat"])) +print(solution.groupAnagrams2(["eat", "tea", "tan", "ate", "nat", "bat"])) +# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']] diff --git a/02/heap.py b/02/heap.py new file mode 100644 index 000000000..ccca6f1c6 --- /dev/null +++ b/02/heap.py @@ -0,0 +1,121 @@ +""" +Max Heap Data Structure +""" +class MaxHeap: + def __init__(self, capacity = 10): + self.heapsize = 0 + self.heap = [-1] * capacity + + def is_empty(self): + return self.heapsize == 0 + + def is_full(self): + return self.heapsize == len(self.heap) + + def insert(self, elem): + """ + Inserts new element in to heap + Time Complexity: O(log N) + """ + if self.is_full(): + raise Exception("Heap is full, No space to insert new element") + # 1. append new element to the end of heap. + self.heap[self.heapsize] = elem + # 2. increase the heapsize by 1. + self.heapsize += 1 + # 3. bubble up the larger child until hitting root. + endpos = self.heapsize - 1 + self._siftup(endpos) + + def _siftup(self, i): + """ + Maintains the heap property while inserting an element at position i + """ + while i > 0: + newitem = self.heap[i] + parentpos = (i - 1) >> 1 + parent = self.heap[parentpos] + if newitem > parent: + # newitem is bigger, move it up. + newitem, parent = parent, newitem + # process the next element (parentpos). + i = parentpos + continue + # parent is bigger, we are done. + break + + def delete(self, i): + """ + Remove an element at position i from the heap + Time Complexity: O(log N) + """ + if self.is_empty(): + raise Exception("Heap is empty, No element to delete") + delitem = self.heap[i] + endpos = self.heapsize - 1 + # 1. replace the element at position i with the last element. + self.heap[i] = self.heap[endpos] + # 2. decrease heapsize by 1 (so the last item is removed). + self.heapsize -= 1 + # 3. move down the new element until the end of heap. + self._siftdown(i) + return delitem + + def _siftdown(self, i): + """ + Maintains the heap property while deleting an element. + """ + leftpos = 2 * i + 1 + while leftpos < self.heapsize: + delitem = self.heap[i] + # select the bigger one from leftchild and rightchild. + childpos = leftpos + rightpos = leftpos + 1 + if rightpos < self.heapsize and self.heap[rightpos] > self.heap[leftpos]: + childpos = rightpos + # delitem is bigger than child, we are done. + if delitem >= self.heap[childpos]: + break + # delitem is smaller, move it down. + self.heap[i], self.heap[childpos] = self.heap[childpos], self.heap[i] + # process the next element (childpos). + i = childpos + leftpos = 2 * i + 1 + + def find_max(self): + """ + Return the Max Element (Heap Top) + Time Complexity: O(1) + """ + if self.is_empty(): + raise Exception("Heap is empty.") + return self.heap[0] + + def printheap(self): + n = self.heapsize + depth = n // 2 + line = '' + for i in range(depth + 1): + j = 0 + while j < 2**i and (j + 2**i - 1) < n: + padding = ' ' * (2**(depth - i) - 1) + line += padding + str(self.heap[j + 2**i - 1]) + padding + ' ' + j += 1 + line = line[:-1] + '\n' + print(line) + +max_heap = MaxHeap(10) +max_heap.insert(10) +max_heap.insert(4) +max_heap.insert(9) +max_heap.insert(1) +max_heap.insert(7) +max_heap.insert(5) +max_heap.insert(3) + +max_heap.printheap() +max_heap.delete(5) +max_heap.printheap() +max_heap.delete(0) +max_heap.printheap() +print("heapmax = ", max_heap.find_max()) \ No newline at end of file diff --git a/02/n-ary_tree_traversal.py b/02/n-ary_tree_traversal.py new file mode 100644 index 000000000..896ac4736 --- /dev/null +++ b/02/n-ary_tree_traversal.py @@ -0,0 +1,141 @@ +""" +589. N-ary Tree Preorder Traversal +https://leetcode.com/problems/n-ary-tree-preorder-traversal/ + +590. N-ary Tree Postorder Traversal +https://leetcode.com/problems/n-ary-tree-postorder-traversal/ + +429. N-ary Tree Level Order Traversal +https://leetcode.com/problems/n-ary-tree-level-order-traversal/ +""" +from typing import List +from collections import deque + +class TreeNode: + def __init__(self, val=None, children=None): + self.val = val + self.children = children + +class Solution: + def preorder1(self, root: TreeNode) -> List[int]: + """ + Solution #1: Recursively + """ + def traverse(root): + if not root: return + res.append(root.val) + if root.children is not None: + for child in root.children: + traverse(child) + + res = [] + traverse(root) + return res + + def preorder2(self, root: TreeNode) -> List[int]: + """ + Solution #2: Iteratively + """ + if not root: return [] + res = [] + stack = [root] + while stack: + node = stack.pop() + if not node: continue + res.append(node.val) + if node.children: + for child in node.children[::-1]: + stack.append(child) + return res + + def postorder1(self, root: TreeNode) -> List[int]: + """ + Solution #1: Recursively + """ + def traverse(root): + if not root: + return + if root.children is not None: + for child in root.children: + traverse(child) + res.append(root.val) + + res = [] + traverse(root) + return res + + def postorder2(self, root: TreeNode) -> List[int]: + """ + Solution #2: Iteratively + """ + if not root: + return [] + res = [] + stack = [root] + while stack: + root = stack.pop() + if root is not None: + res.append(root.val) + if root.children is not None: + # from left to right append child + for child in root.children: + stack.append(child) + return res[::-1] # the result is reversed + + def levelOrder1(self, root: TreeNode) -> List[List[int]]: + """ + Solution #1: Recursively + """ + def traverse(root, level): + if not root: + return + if len(res) == level: + res.append([]) + res[level].append(root.val) + if root.children is not None: + for child in root.children: + traverse(child, level + 1) + + res = [] + traverse(root, 0) + return res + + def levelOrder2(self, root: TreeNode) -> List[List[int]]: + if not root: + return [] + res = [] + queue = deque([root]) + while queue: + n = len(queue) + level = [] + for _ in range(n): + node = queue.popleft() + level.append(node.val) + if node.children is not None: + for child in node.children: + queue.append(child) + res.append(level) + return res + +t = TreeNode(1, [TreeNode(3, [TreeNode(5), TreeNode(6)]), TreeNode(2), TreeNode(4)]) +""" + ___1___ + / | \ + __3__ 2 4 + / \ +5 6 +""" + +solution = Solution() +ans = solution.preorder1(t) +print('N-ary Tree Preorder Traversal Recursively: ', ans) +ans = solution.preorder2(t) +print('N-ary Tree Preorder Traversal Iteratively: ', ans) +ans = solution.postorder1(t) +print('N-ary Tree Postorder Traversal Recursively: ', ans) +ans = solution.postorder2(t) +print('N-ary Tree Postorder Traversal Iteratively: ', ans) +ans = solution.levelOrder1(t) +print('N-ary Tree Level Order Traversal Recursively: ', ans) +ans = solution.levelOrder2(t) +print('N-ary Tree Level Order Traversal Iteratively: ', ans) \ No newline at end of file diff --git a/02/valid_anagram.py b/02/valid_anagram.py new file mode 100644 index 000000000..c7a560e75 --- /dev/null +++ b/02/valid_anagram.py @@ -0,0 +1,34 @@ +""" +242. Valid Anagram +https://leetcode.com/problems/valid-anagram/ +""" +from typing import List + +class Solution: + def isAnagram1(self, s: str, t: str) -> bool: + """ + Solution #1: Sort + Time: O(N*logN): sort + Space: O(1) + """ + return sorted(s) == sorted(t) + + def isAnagram2(self, s: str, t: str) -> bool: + """ + Solution #2: Dictionary + Time: O(1) + Space: O(K): K is the common character in s and t + """ + if len(s) != len(t): return False + d = {} + for c in s: + d[ord(c)] = d.get(ord(c), 0) + 1 + for c in t: + d[ord(c)] = d.get(ord(c), 0) - 1 + return not [x for x in d if d[x] != 0] + +solution = Solution() +print(solution.isAnagram1("anagram", "nagaram")) # True +print(solution.isAnagram1("anagram", "nagara")) # False +print(solution.isAnagram2("anagram", "nagaram")) # True +print(solution.isAnagram2("anagram", "nagara")) # False diff --git a/03/NOTE.md b/03/NOTE.md new file mode 100644 index 000000000..685243f5f --- /dev/null +++ b/03/NOTE.md @@ -0,0 +1,120 @@ +# 03 + +Recursion +--------- + +> A recursive function is defined in terms of base cases and recursive steps. + +- In a base case, we compute the result immediately given the inputs to the function call. +- In a recursive step, we compute the result with the help of one or more recursive calls to this same function, but with the inputs somehow **reduced** in size or complexity, closer to a base case. + +To calculate Factorial of **n**: +``` +n! = n x (n-1) x ... x 2 x 1 +``` + +Iterative Implementation +```py +def factorial(n: int) -> int: + fact = 1 + for i in range(n): + fact = fact * i + } + return fact +``` + +Recurrence relation +``` +n! = { + 1 if n = 0 + (n-1)! x n if n > 0 +} +``` + +Recursive Implementation +```py +def factorial(n: int) -> int: + if n = 0: + return 1 + return n * factorial(n-1) +``` + +Divide & Conquer +---------------- + +- Divide the problem into a number of subproblems that are smaller instances of the same problem. +- Conquer the subproblems by solving them recursively. If they are small enough, solve the subproblems as base cases. +- Combine the solutions to the subproblems into the solution for the original problem. + +```py +def divide_conquer(problem, param1, param2, ...): + # recursion terminator + if problem is None: + process_result + return + + # prepare data (key is to how to split the problem) + data = prepare_data(problem) + subproblems = split_problem(problem, data) + + # conquer subproblems + subresult1 = self.divide_conquer(subproblems[0], p1, ...) + subresult2 = self.divide_conquer(subproblems[1], p1, ...) + subresult3 = self.divide_conquer(subproblems[2], p1, ...) + ... + + # process and generate the final result + result = process_result(subresult1, subresult2, subresult3, ...) + + # revert the current level states +``` + +- Sometimes we need to return multiple results (tuple) +- Sometimes we need global variables to easily update the final result + +Leetcode Problems +- [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) +- [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) +- [105. Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) +- [236. Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/) +- [169. Majority Element](https://leetcode.com/problems/majority-element/) + +Backtrack +--------- + +> Backtracking can be defined as a general algorithmic technique that considers searching every possible combination in order to solve a computational problem. + +```py +result = [] +def backtrack(path): + if end condition: + result.add(path[:]) # param pass by reference + return + + if some condition: # Pruning if necessary + return + + # Get the choice list + for choice in choices: + # get rid of the illegal choices + if exclusive condition: + continue + + path.append(choice) # Make the choice + backtrack(path, choices) # enter the next decision tree + path.pop() # Remove the choice (since it's already made) +``` + +- Time complexity for backtrack algorithm is at least O(N!) +- Backtrack is a decision tree, updating the result is actually a preorder and/or postorder recursion (DFS) +- Sometimes we don't need to explicitly maintain the choice list, we **derive** it using other parameters (e.g. index) +- Sometimes path can be a string instead of an array, and we use `path += 'choice'` and `path = path[:-1]` to make and remove choice + +Leetcode Problems +- [22. Generate Parentheses](https://leetcode.com/problems/generate-parentheses/) +- [78. Subsets](https://leetcode.com/problems/subsets/) +- [46. Permutations](https://leetcode.com/problems/permutations/) +- [47. Permutations II](https://leetcode.com/problems/permutations-ii/) +- [77. Combinations](https://leetcode.com/problems/combinations/) +- [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/) +- [51. N-Queens](https://leetcode.com/problems/n-queens/) \ No newline at end of file diff --git a/03/binary_tree.py b/03/binary_tree.py new file mode 100644 index 000000000..c24d41d5a --- /dev/null +++ b/03/binary_tree.py @@ -0,0 +1,98 @@ +""" +Binary Tree Data Structure +""" +class BinaryTreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val + self.left = None + self.right = None + +def create_binary_tree(iterable = ()): + """ Given in iterable, create a binary tree in level order + Example: create_tree([0,1,2,3,4,5,6,7,8,9]) + + ____0__ + / \ + __1__ 2 + / \ / \ + 3 4 5 6 + / \ / + 7 8 9 + """ + def insert(node, i): + if i < len(iterable) and iterable[i] is not None: + node = BinaryTreeNode(iterable[i]) + node.left = insert(node.left, 2 * i + 1) + node.right = insert(node.right, 2 * i + 2) + return node + return insert(None, 0) + +def cherry_pick(root, val): + if root is None or root.val == val: + return root + return cherry_pick(root.left, val) or cherry_pick(root.right, val) + +def print_binary_tree(root, index=False): + """ Pretty-print the binary tree. + Inspired by https://pypi.org/project/binarytree/ + """ + def build_tree_string(root, current, index=False, delimiter='-'): + if root is None: + return [], 0, 0, 0 + + line1 = [] + line2 = [] + if index: + node_repr = '{}{}{}'.format(current, delimiter, root.val) + else: + node_repr = str(root.val) + + new_root_width = gap_size = len(node_repr) + + # Get the left and right sub-boxes, their widths, and root repr positions + l_box, l_box_width, l_root_start, l_root_end = \ + build_tree_string(root.left, 2 * current + 1, index, delimiter) + r_box, r_box_width, r_root_start, r_root_end = \ + build_tree_string(root.right, 2 * current + 2, index, delimiter) + + # Draw the branch connecting the current root node to the left sub-box + # Pad the line with whitespaces where necessary + if l_box_width > 0: + l_root = (l_root_start + l_root_end) // 2 + 1 + line1.append(' ' * (l_root + 1)) + line1.append('_' * (l_box_width - l_root)) + line2.append(' ' * l_root + '/') + line2.append(' ' * (l_box_width - l_root)) + new_root_start = l_box_width + 1 + gap_size += 1 + else: + new_root_start = 0 + + # Draw the representation of the current root node + line1.append(node_repr) + line2.append(' ' * new_root_width) + + # Draw the branch connecting the current root node to the right sub-box + # Pad the line with whitespaces where necessary + if r_box_width > 0: + r_root = (r_root_start + r_root_end) // 2 + line1.append('_' * r_root) + line1.append(' ' * (r_box_width - r_root + 1)) + line2.append(' ' * r_root + '\\') + line2.append(' ' * (r_box_width - r_root)) + gap_size += 1 + new_root_end = new_root_start + new_root_width - 1 + + # Combine the left and right sub-boxes with the branches drawn above + gap = ' ' * gap_size + new_box = [''.join(line1), ''.join(line2)] + for i in range(max(len(l_box), len(r_box))): + l_line = l_box[i] if i < len(l_box) else ' ' * l_box_width + r_line = r_box[i] if i < len(r_box) else ' ' * r_box_width + new_box.append(l_line + gap + r_line) + + # Return the new box, its width and its root repr positions + return new_box, len(new_box[0]), new_root_start, new_root_end + + lines = build_tree_string(root, 0, index)[0] + print('\n' + '\n'.join((line.rstrip() for line in lines))) diff --git a/03/combinations.py b/03/combinations.py new file mode 100644 index 000000000..3b9369c93 --- /dev/null +++ b/03/combinations.py @@ -0,0 +1,27 @@ +""" +77. Combinations +https://leetcode.com/problems/combinations/ +""" +from typing import List + +class Solution: + def combine(self, n: int, k: int) -> List[List[int]]: + res = [] + if k <= 0 or n <= 0: + return res + + def backtrack(start = 1, path = []): + if len(path) == k: + res.append(path[:]) + return + + for i in range(start, n + 1): + path.append(i) + backtrack(i + 1, path) + path.pop() + + backtrack() + return res + +solution = Solution() +print(solution.combine(4, 2)) \ No newline at end of file diff --git a/03/construct_binary_tree.py b/03/construct_binary_tree.py new file mode 100644 index 000000000..f4e92cc9b --- /dev/null +++ b/03/construct_binary_tree.py @@ -0,0 +1,41 @@ +""" +105. Construct Binary Tree from Preorder and Inorder Traversal +https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ +""" +from typing import List +from binary_tree import BinaryTreeNode, print_binary_tree + +class Solution: + def buildTree(self, preorder: List[int], inorder: List[int]) -> BinaryTreeNode: + if not preorder or not inorder or len(preorder) != len(inorder): + return None + + def build(preorder_left, preorder_right, inorder_left, inorder_right): + # base + if preorder_left > preorder_right: + return None + + # process + preorder_root = preorder_left + inorder_root = index[preorder[preorder_root]] + root = BinaryTreeNode(preorder[preorder_root]) + + # drill down + size_left_subtree = inorder_root - inorder_left + root.left = build(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root + 1) + root.right = build(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right) + + # return + return root + + # preorder = [root, [left subtree], [right subtree]] + # inorder = [[left subtree], root, [right subtree]] + n = len(preorder) + index = { elem: i for i, elem in enumerate(inorder) } + return build(0, n-1, 0, n-1) + +solution = Solution() +preorder = [3,9,20,15,7] +inorder = [9,3,15,20,7] +ans = solution.buildTree(preorder, inorder) +print_binary_tree(ans) diff --git a/03/generate_parenthesis.py b/03/generate_parenthesis.py new file mode 100644 index 000000000..ab80debee --- /dev/null +++ b/03/generate_parenthesis.py @@ -0,0 +1,48 @@ +""" +22. Generate Parentheses +https://leetcode.com/problems/generate-parentheses/ +""" +from typing import List + +class Solution: + def generateParenthesis1(self, n: int) -> List[str]: + res = [] + + def generate(left = 0, right = 0, s = ''): + if left == n and right == n: + res.append(s) + return + + # process and drill down + if left < n: + generate(left + 1, right, s + '(') + if left > right: + generate(left, right + 1, s + ')') + + generate() + return res + + def generateParenthesis2(self, n: int) -> List[str]: + res = [] + + def backtrack(left = n, right = n, path = ''): + if left < 0 or right < 0: return + if right < left: return + if left == 0 and right == 0: + res.append(path) + return + + path = path + '(' + backtrack(left - 1, right, path) + path = path[:-1] + + path = path + ')' + backtrack(left, right - 1, path) + path = path[:-1] + + backtrack() + return res + +solution = Solution() +print(solution.generateParenthesis1(3)) +print(solution.generateParenthesis2(3)) \ No newline at end of file diff --git a/03/lowest_common_ancestor.py b/03/lowest_common_ancestor.py new file mode 100644 index 000000000..4dbca0eba --- /dev/null +++ b/03/lowest_common_ancestor.py @@ -0,0 +1,86 @@ +""" +236. Lowest Common Ancestor of a Binary Tree +https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/ +""" +from binary_tree import BinaryTreeNode, create_binary_tree, print_binary_tree, cherry_pick + +class Solution: + def lowestCommonAncestor1(self, root: BinaryTreeNode, p: BinaryTreeNode, q: BinaryTreeNode) -> BinaryTreeNode: + if root is None: + return None + + def helper(root: BinaryTreeNode, p: BinaryTreeNode, q: BinaryTreeNode) -> BinaryTreeNode: + if root is None: + return None + + p_in_left = nodesIndex[p.val] <= nodesIndex[root.val] + p_in_right = nodesIndex[p.val] >= nodesIndex[root.val] + + q_in_left = nodesIndex[q.val] <= nodesIndex[root.val] + q_in_right = nodesIndex[q.val] >= nodesIndex[root.val] + + if (p_in_left and q_in_right) or (p_in_right and q_in_left): + # found LCA + return root + + if p_in_left and q_in_left: + # LCA must in the left subtree, keep finding + return helper(root.left, p, q) + + if p_in_right and q_in_right: + # LCA must in the right subtree, keep finding + return helper(root.right, p, q) + + return None + + def inorder(root: BinaryTreeNode): + if root is None: + return [] + return inorder(root.left) + [root.val] + inorder(root.right) + + nodesInorder = inorder(root) + nodesIndex = {} # based on assumption that all nodes values are unique + for i, elem in enumerate(nodesInorder): + nodesIndex[elem] = i + return helper(root, p, q) + + + def lowestCommonAncestor2(self, root: BinaryTreeNode, p: BinaryTreeNode, q: BinaryTreeNode) -> BinaryTreeNode: + # terminator + if root is None: + return None + if root == p or root == q: + return root + + # drill down + lca_left = self.lowestCommonAncestor2(root.left, p, q) + lca_right = self.lowestCommonAncestor2(root.right, p, q) + + # process + if lca_left is None: + return lca_right + if lca_right is None: + return lca_left + if lca_left and lca_right: + return root + + # return + return None + +solution = Solution() +tree = create_binary_tree([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4]) +print_binary_tree(tree) + +p = cherry_pick(tree, 5) +q = cherry_pick(tree, 1) +ans = solution.lowestCommonAncestor1(tree, p, q) +print('lowestCommonAncestor1 of', '(', p.val, ') and (', q.val, ') is (', ans.val, ')') +ans = solution.lowestCommonAncestor2(tree, p, q) +print('lowestCommonAncestor2 of', '(', p.val, ') and (', q.val, ') is (', ans.val, ')') + +p = cherry_pick(tree, 5) +q = cherry_pick(tree, 4) +ans = solution.lowestCommonAncestor1(tree, p, q) +print('lowestCommonAncestor1 of', '(', p.val, ') and (', q.val, ') is (', ans.val, ')') +ans = solution.lowestCommonAncestor2(tree, p, q) +print('lowestCommonAncestor2 of', '(', p.val, ') and (', q.val, ') is (', ans.val, ')') diff --git a/03/permutations.py b/03/permutations.py new file mode 100644 index 000000000..57ca17031 --- /dev/null +++ b/03/permutations.py @@ -0,0 +1,99 @@ +""" +46. Permutations +https://leetcode.com/problems/permutations/ + +47. Permutations II +https://leetcode.com/problems/permutations-ii/ +""" +from typing import List +from collections import Counter + +class Solution: + def permute1(self, nums: List[int]) -> List[List[int]]: + res = [] + n = len(nums) + + def backtrack(path = []): + if len(path) == n: + res.append(path[:]) + return + + for i in range(n): + if nums[i] in path: + continue + path.append(nums[i]) + backtrack(path) + path.pop() + + backtrack() + return res + + def permute2(self, nums: List[int]) -> List[List[int]]: + res = [] + n = len(nums) + + # [0, first - 1] = filled numbers + # [first, n - 1] = numbers to be filled + # swap nums[first] and nums[i] before backtrack to maintain the dynamic array + def backtrack(first = 0): + if first == n: + res.append(nums[:]) + return + + for i in range(first, n): + nums[first], nums[i] = nums[i], nums[first] + backtrack(first + 1) + nums[first], nums[i] = nums[i], nums[first] + + backtrack() + return res + + def permuteUnique1(self, nums: List[int]) -> List[List[int]]: + res = [] + n = len(nums) + used = [False] * n + nums.sort() + + def backtrack(path = []): + if len(path) == n: + res.append(path[:]) + return + + for i in range(n): + if not used[i]: + if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: + continue + path.append(nums[i]) + used[i] = True + backtrack(path) + path.pop() + used[i] = False + + backtrack() + return res + + def permuteUnique2(self, nums: List[int]) -> List[List[int]]: + res = [] + n = len(nums) + + def backtrack(path = [], unused = Counter(nums)): + if len(path) == n: + res.append(path[:]) + return + + for x in unused: + if unused[x] > 0: + path.append(x) + unused[x] -= 1 + backtrack(path) + path.pop() + unused[x] += 1 + + backtrack() + return res + +solution = Solution() +print(solution.permute1([1,2,3])) +print(solution.permute2([1,2,3])) +print(solution.permuteUnique1([1,1,2])) +print(solution.permuteUnique2([3,3,0,3])) \ No newline at end of file diff --git a/03/subsets.py b/03/subsets.py new file mode 100644 index 000000000..3d382b4f6 --- /dev/null +++ b/03/subsets.py @@ -0,0 +1,24 @@ +""" +78. Subsets +https://leetcode.com/problems/subsets/ +""" +from typing import List + +class Solution: + def subsets(self, nums: List[int]) -> List[List[int]]: + res = [] + n = len(nums) + + def backtrack(start = 0, track = []): + res.append(track[:]) + + for i in range(start, n): + track.append(nums[i]) + backtrack(i + 1, track) + track.pop() + + backtrack() + return res + +solution = Solution() +print(solution.subsets([1,2,3])) \ No newline at end of file diff --git a/04/NOTE.md b/04/NOTE.md new file mode 100644 index 000000000..5949efec4 --- /dev/null +++ b/04/NOTE.md @@ -0,0 +1,286 @@ +# 04 + +Graph +----- + +> A graph is a finite set of vertices and connected by a set of edges. + +``` +(1:M)---(0:E) + | \ | + | \ | + | \ | +(2:B)---(3:L) + \ + \ + (4:P) +``` + +Types of graphs: +- Undirected Graph: nodes are connected by edges that are all bidirectional. +- Directed Graph: nodes are connected by directed edges – they only go in one direction. + +Graph Adjacency List Representation +- The size of the array is equal to the number of nodes. +- A single index, array[i] represents the list of nodes adjacent to the ith node. +``` +0 -> 1 -> 3# +1 -> 0 -> 2 -> 3# +2 -> 1 -> 3 -> 4# +3 -> 0 -> 1 -> 2# +4 -> 2# +``` + +Graph Adjacency Matrix Representation +- An Adjacency Matrix is a 2D array of size V x V where V is the number of nodes in a graph. +- A slot matrix[i][j] = 1 indicates that there is an edge from node i to node j. + +| | 0 | 1 | 2 | 3 | 4 | +|-------|---|---|---|---|---| +| **0** | 0 | 1 | 0 | 1 | 0 | +| **1** | 1 | 0 | 1 | 1 | 0 | +| **2** | 0 | 1 | 0 | 1 | 1 | +| **3** | 1 | 1 | 1 | 0 | 0 | +| **4** | 0 | 0 | 1 | 0 | 0 | + +Graph Python Implementation +```py +graph = { + 'A': ['B', 'C'], # A -> B, A -> C + 'B': ['C', 'D'], # B -> C, B -> D + 'C': ['D'], # C -> D + 'D': ['C'], # D -> C + 'E': ['F'], # E -> F + 'F': ['C'] # F -> C +} +``` + +Depth-first Search (DFS) +------------------------ + +> Depth-first search is an algorithm for traversing or searching tree or graph data structures. The algorithm starts at the root node and explores as far as possible along each branch before backtracking. + +```py +visited = set() + +def dfs(node): + # base case + if not node: + return + + # already visited + if node in visited: + return + + # process current node + process(node.val) + + # add to visited + visited.add(node) + + # process neighbors / children + for child in node.children: + dfs(child) +``` + +Island Problem +```py +def island(self, grid: List[List[int]]): + # length of row and column + m, n = len(grid), len(grid[0]) + + def dfs(r, c): + # base case: grid[r][c] is out of bound + if not inArea(r, c): + return + + # current node is ocean, or it's already visited + if grid[r][c] == 0 or grid[r][c] == -1: + return + + # mark as visited + grid[r][c] = -1 + + # visit neighbor nodes + dfs(r+1, c) # UP + dfs(r-1, c) # DOWN + dfs(r, c-1) # LEFT + dfs(r, c+1) # RIGHT + + def inArea(r, c): + return 0 <= r < m and 0 <= c < n + + for r in range(m): + for c in range(n): + # start dfs for each element in grid + dfs(r, c) + +``` + +Leetcode Problems +- [200. Number of Islands](https://leetcode.com/problems/number-of-islands/) +- [695. Max Area of Island](https://leetcode.com/problems/max-area-of-island/) +- [463. Island Perimeter](https://leetcode.com/problems/island-perimeter/) +- [827. Making A Large Island](https://leetcode.com/problems/making-a-large-island/) +- [36. Valid Sudoku](https://leetcode.com/problems/valid-sudoku/) +- [37. Sudoku Solver](https://leetcode.com/problems/sudoku-solver/) + +Breadth-first Search (BFS) +-------------------------- + +> Breadth-first search is an algorithm for searching a tree data structure for a node that satisfies a given property. It starts at the tree root and explores all nodes at the present depth prior to moving on to the nodes at the next depth level. + +```py +def bfs(root): + queue = collections.deque([root]) + visited = set() + + # Loop until queue is empty + while queue: + # get current node from queue + node = queue.popleft() + + # already visited + if node in visited: + continue + + # process current node + process(node.val) + + # add to visited + visited.add(node) + + # process neighbors / children + for child in node.children: + queue.append(child) +``` + +Level Order +```py +def levelOrder(root): + queue = collections.deque([root]) + visited = set() + res = [] + + # Loop until queue is empty + while queue: + # process all nodes from the current level + level_nodes = [] + + for _ in range(len(queue)): + # get current node from queue + node = queue.popleft() + + # check if the node is already visited + if node in visited: + continue + + # process current node + level_nodes.append(node.val) + + # add to visited + visited.add(node) + + # process children + if node.children: + for child in node.children: + if child not in visited: + queue.append(child) + + res.append(level_nodes) + + return res +``` + +Shortest Path +```py +def shortestPath(start, target): # any two nodes, doesn't have to start from the root + queue = collections.deque([start]) + visited = set([start]) + step = 0 + + # Loop until queue is empty + while queue: + # spread the search from the current level + for _ in range(len(queue)): + # get current node from queue + node = queue.popleft() + + # see if we reach the target + if node is target: + return step + + # process children + if node.children: + for child in node.children: + if child not in visited: + queue.append(child) + visited.add(child) + + step += 1 + + return 0 # not found +``` + +Bidirectional BFS +```py +def biBfs(source, target): + sourceQueue = collections.deque([source]) + targetQueue = collections.deque([target]) + visited = set([source]) + step = 0 + + while sourceQueue and targetQueue: + # choose the smaller queue to spread + if len(sourceQueue) > len(targetQueue): + sourceQueue, targetQueue = targetQueue, sourceQueue + + for _ in range(len(sourceQueue)): + node = sourceQueue.popleft() + + for child in node.children: + # source and target meet + if child in targetQueue: + return step + 1 + + if child not in visited: + sourceQueue.append(child) + visited.add(child) + + step += 1 + + return 0 # not found +``` + +Leetcode Problems +- [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/) +- [515. Find Largest Value in Each Tree Row](https://leetcode.com/problems/find-largest-value-in-each-tree-row/) +- [127. Word Ladder](https://leetcode.com/problems/word-ladder/) +- [126. Word Ladder II](https://leetcode.com/problems/word-ladder-ii/) +- [752. Open the Lock](https://leetcode.com/problems/open-the-lock/) +- [433. Minimum Genetic Mutation](https://leetcode.com/problems/minimum-genetic-mutation/) +- [529. Minesweeper](https://leetcode.com/problems/minesweeper/) +- [773. Sliding Puzzle](https://leetcode.com/problems/sliding-puzzle/) + +A-Star (A*) Search +------------------ + +> A-star is a graph traversal and path search algorithm, which is often used in computer science due to its completeness, optimality, and optimal efficiency. One major practical drawback is its O space complexity, as it stores all generated nodes in memory. + +```py +def AstarSearch(graph, start, end): + pq = collections.priority_queue() # priority —> heuristic function + pq.append([start]) + visited.add(start) + while pq: + node = pq.pop() # can we add more intelligence here? + visited.add(node) + process(node) + nodes = generate_related_nodes(node) + unvisited = [node for node in nodes if node not in visited] + pq.push(unvisited) +``` + +Leetcode Problems +- [126. Word Ladder II](https://leetcode.com/problems/word-ladder-ii/) +- [1091. Shortest Path in Binary Matrix](https://leetcode.com/problems/shortest-path-in-binary-matrix/) diff --git a/04/bfs_dfs.py b/04/bfs_dfs.py new file mode 100644 index 000000000..fef528273 --- /dev/null +++ b/04/bfs_dfs.py @@ -0,0 +1,125 @@ +import collections + +class TreeNode: + def __init__(self, val=None, children=None): + self.val = val + self.children = children + +def DFS1(root: TreeNode): + """ + Recursively + """ + def _dfs(node, visited = set()): + # terminator + if not node: + return + # already visited + if node in visited: + return + # process current node + res.append(node.val) + # add to visited + visited.add(node) + # process children (drill down) + if node.children: + for child in node.children: + if child not in visited: + _dfs(child, visited) + res = [] + _dfs(root) + return res + +def DFS2(root: TreeNode): + """ + Iteratively + """ + if not root: + return [] + res = [] + visited = set() + stack = [root] + # Loop until stack is empty + while stack: + # get current node from stack + node = stack.pop() + # already visited + if node in visited: + continue + # process current node + res.append(node.val) + # add to visited + visited.add(node) + # process children + if node.children: + for child in node.children[::-1]: + if child not in visited: + stack.append(child) + return res + +def BFS1(root: TreeNode): + """ + Recursively + """ + def _bfs(node, level = 0, visited = set()): + # terminator + if not node: + return + # already visited + if node in visited: + return + # process all nodes from the current level + if len(res) == level: + res.append([]) + res[level].append(node.val) + # add to visited + visited.add(node) + # process children (drill down) + if node.children: + for child in node.children: + if child not in visited: + _bfs(child, level + 1, visited) + res = [] + _bfs(root) + return [node for level in res for node in level] # flatten + +def BFS2(root: TreeNode): + """ + Iteratively + """ + if not root: + return [] + res = [] + visited = set() + queue = collections.deque([root]) + # Loop until queue is empty + while queue: + n = len(queue) + level_nodes = [] + # process all nodes from the current level + for _ in range(n): + # get current node from queue + node = queue.popleft() + # process current node + level_nodes.append(node.val) + # add to visited + visited.add(node) + # process children + if node.children: + for child in node.children: + if child not in visited: + queue.append(child) + res.append(level_nodes) + return [node for level in res for node in level] # flatten + +tree = TreeNode(1, [TreeNode(3, [TreeNode(5), TreeNode(6)]), TreeNode(2), TreeNode(4)]) +""" + ___1___ + / | \ + __3__ 2 4 + / \ +5 6 +""" +print(DFS1(tree)) +print(DFS2(tree)) +print(BFS1(tree)) +print(BFS2(tree)) diff --git a/04/graph.py b/04/graph.py new file mode 100644 index 000000000..539a0b7f4 --- /dev/null +++ b/04/graph.py @@ -0,0 +1,51 @@ +""" +Graph Data Structure +""" +graph = { + 'A': ['B', 'C'], + 'B': ['C', 'D'], + 'C': ['D'], + 'D': ['C'], + 'E': ['F'], + 'F': ['C'] +} + +def find_path(graph, start, end, path = []): + path = path + [start] + + if start == end: + return path + + if start not in graph: + return None + + for node in graph[start]: + if node not in path: + newpath = find_path(graph, node, end, path) + if newpath: + return newpath + + return node + +def find_all_paths(graph, start, end, path = []): + path = path + [start] + + if start == end: + return [path] + + if start not in graph: + return [] + + paths = [] + for node in graph[start]: + if node not in path: + newpaths = find_all_paths(graph, node, end, path) + for newpath in newpaths: + paths.append(newpath) + + return paths + +print(find_path(graph, 'A', 'D')) +print(find_path(graph, 'E', 'D')) +print(find_all_paths(graph, 'A', 'D')) +print(find_all_paths(graph, 'E', 'D')) \ No newline at end of file diff --git a/04/lsland_problems.py b/04/lsland_problems.py new file mode 100644 index 000000000..84b3afb8c --- /dev/null +++ b/04/lsland_problems.py @@ -0,0 +1,176 @@ +""" +200. Number of Islands +https://leetcode.com/problems/number-of-islands/ + +695. Max Area of Island +https://leetcode.com/problems/max-area-of-island/ + +463. Island Perimeter +https://leetcode.com/problems/island-perimeter/ + +827. Making A Large Island +https://leetcode.com/problems/making-a-large-island/ +""" +from typing import List + +class Solution: + def numIslands(self, grid: List[List[str]]) -> int: + if not grid: + return 0 + + m, n = len(grid), len(grid[0]) + + def countIsland(r, c): + if 0 <= r < m and 0 <= c < n and grid[r][c] == '1': + grid[r][c] = '0' + countIsland(r+1, c) + countIsland(r-1, c) + countIsland(r, c+1) + countIsland(r, c-1) + + count = 0 + for r in range(m): + for c in range(n): + if grid[r][c] == '1': + count += 1 + countIsland(r, c) + + return count + + def maxAreaOfIsland(self, grid: List[List[int]]) -> int: + def area(grid, r, c): + if not inArea(grid, r, c): + return 0 + if grid[r][c] != 1: # ocean or visited + return 0 + grid[r][c] = -1 # mark as visited + return 1 \ + + area(grid, r, c-1) \ + + area(grid, r+1, c) \ + + area(grid, r, c+1) \ + + area(grid, r-1, c) + + def inArea(grid, r, c): + return 0 <= r < len(grid) and 0 <= c < len(grid[0]) + + maxArea = 0 + for r in range(len(grid)): + for c in range(len(grid[0])): + if grid[r][c] == 1: + islandArea = area(grid, r, c) + maxArea = max(maxArea, islandArea) + + return maxArea + + def islandPerimeter(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + + def perimeter(r, c): + if not (0 <= r < m and 0 <= c < n): # boundary + return 1 + if grid[r][c] == 0: # ocean + return 1 + if grid[r][c] != 1: # not land or visited + return 0 + grid[r][c] = -1 + return perimeter(r+1, c) \ + + perimeter(r-1, c) \ + + perimeter(r, c+1) \ + + perimeter(r, c-1) + + for r in range(m): + for c in range(n): + if grid[r][c] == 1: + return perimeter(r, c) + + return 0 + + def largestIsland(self, grid: List[List[int]]) -> int: + m, n = len(grid), len(grid[0]) + + def area(r, c, index): + if 0 <= r < m and 0 <= c < n and grid[r][c] == 1: + grid[r][c] = index + return 1 \ + + area(r+1, c, index) \ + + area(r-1, c, index) \ + + area(r, c+1, index) \ + + area(r, c-1, index) + return 0 + + areas = {} + index = 2 + for r in range(m): + for c in range(n): + if grid[r][c] == 1: + areas[index] = area(r, c, index) + index += 1 + + def connect(r, c): + if 0 <= r < m and 0 <= c < n and grid[r][c] > 1: + return grid[r][c] + return -1 + + res = max(areas.values() or [0]) + for r in range(m): + for c in range(n): + if grid[r][c] == 0: + surroundings = set([ + connect(r+1, c), + connect(r-1, c), + connect(r, c+1), + connect(r, c-1) + ]) + res = max(res, sum(areas[index] for index in surroundings if index > 0) + 1) + + return res + +solution = Solution() + +grid = [ + ['1', '1', '1', '1', '0'], + ['1', '1', '0', '1', '0'], + ['1', '1', '0', '0', '0'], + ['0', '0', '0', '0', '0'] +] +print('numIslands:', solution.numIslands(grid)) # 1 + +grid = [ + ['1', '1', '0', '0', '0'], + ['1', '1', '0', '0', '0'], + ['0', '0', '1', '0', '0'], + ['0', '0', '0', '1', '1'] +] +print('numIslands:', solution.numIslands(grid)) # 3 + +grid = [ + [0,0,1,0,0,0,0,1,0,0,0,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,1,1,0,1,0,0,0,0,0,0,0,0], + [0,1,0,0,1,1,0,0,1,0,1,0,0], + [0,1,0,0,1,1,0,0,1,1,1,0,0], + [0,0,0,0,0,0,0,0,0,0,1,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,0,0,0,0,0,0,1,1,0,0,0,0] +] +print('maxAreaOfIsland:', solution.maxAreaOfIsland(grid)) # 6 + +grid = [[0,0,0,0,0,0,0,0]] +print('maxAreaOfIsland:', solution.maxAreaOfIsland(grid)) # 0 + +grid = [ + [0,1,0,0], + [1,1,1,0], + [0,1,0,0], + [1,1,0,0] +] +print('islandPerimeter:', solution.islandPerimeter(grid)) # 16 + +grid = [[1, 0], [0, 1]] +print('largestIsland', solution.largestIsland(grid)) # 3 + +grid = [[1, 1], [1, 0]] +print('largestIsland', solution.largestIsland(grid)) # 4 + +grid = [[1, 1], [1, 1]] +print('largestIsland', solution.largestIsland(grid)) # 4 diff --git a/04/minimum_genetic_mutation.py b/04/minimum_genetic_mutation.py new file mode 100644 index 000000000..a0c6d1e52 --- /dev/null +++ b/04/minimum_genetic_mutation.py @@ -0,0 +1,48 @@ +""" +433. Minimum Genetic Mutation +https://leetcode.com/problems/minimum-genetic-mutation/ +""" +from typing import List +from collections import deque + +class Solution: + def minMutation(self, start: str, end: str, bank: List[str]) -> int: + bank = set(bank) + if end not in bank: + return -1 + + step = 0 + queue = deque([start]) + while queue: + n = len(queue) + for _ in range(n): + gene = queue.popleft() + if gene == end: + return step + mutations = self.mutate(gene) + for mutation in mutations: + if mutation in bank: + queue.append(mutation) + bank.remove(mutation) + step += 1 + + return -1 + + def mutate(self, gene: str) -> List[str]: + mutation_map = { + "A": "CGT", + "C": "AGT", + "G": "CAT", + "T": "CGA", + } + res = [] + for i, s in enumerate(gene): + for c in mutation_map[s]: + mutation = gene[:i] + c + gene[i+1:] + res.append(mutation) + return res + +solution = Solution() +print(solution.minMutation("AACCGGTT", "AACCGGTA", ["AACCGGTA"])) # 1 +print(solution.minMutation("AACCGGTT", "AAACGGTA", ["AACCGGTA", "AACCGCTA", "AAACGGTA"])) # 2 +print(solution.minMutation("AAAAACCC", "AACCCCCC", ["AAAACCCC", "AAACCCCC", "AACCCCCC"])) # 3 \ No newline at end of file diff --git a/04/sudoku.py b/04/sudoku.py new file mode 100644 index 000000000..245e490b2 --- /dev/null +++ b/04/sudoku.py @@ -0,0 +1,111 @@ +""" +36. Valid Sudoku +https://leetcode.com/problems/valid-sudoku/ + +37. Sudoku Solver +https://leetcode.com/problems/sudoku-solver/ +""" +from typing import List +from collections import Counter + +class Solution: + def isValidSudoku(self, board: List[List[str]]) -> bool: + row = [Counter() for _ in range(9)] + col = [Counter() for _ in range(9)] + box = [Counter() for _ in range(9)] + + for i in range(9): + for j in range(9): + num = board[i][j] + if num != '.': + num = int(num) + k = (i // 3) * 3 + (j // 3) # box index + + row[i][num] += 1 + col[j][num] += 1 + box[k][num] += 1 + + if row[i][num] > 1 or col[j][num] > 1 or box[k][num] > 1: + return False + return True + + def solveSudoku(self, board: List[List[str]]) -> None: + """ + Do not return anything, modify board in-place instead. + """ + nums = ['1','2','3','4','5','6','7','8','9'] + + def backtrack(row = 0, col = 0): + if col == 9: + return backtrack(row + 1, 0) # next row + + if row == 9: + return True # end of board + + if board[row][col] != '.': + return backtrack(row, col + 1) # next number + + for num in nums: + if not isValid(row, col, num): + continue + + board[row][col] = num + if backtrack(row, col + 1): + return True # found an solution + board[row][col] = '.' + + return False + + def isValid(r, c, n) -> bool: + for i in range(9): + if board[r][i] == n: + return False + if board[i][c] == n: + return False + if board[(r//3)*3 + i//3][(c//3)*3 + i%3] == n: + return False + return True + + backtrack() + + +solution = Solution() + +board1 = [ + ["5","3",".",".","7",".",".",".","."], + ["6",".",".","1","9","5",".",".","."], + [".","9","8",".",".",".",".","6","."], + ["8",".",".",".","6",".",".",".","3"], + ["4",".",".","8",".","3",".",".","1"], + ["7",".",".",".","2",".",".",".","6"], + [".","6",".",".",".",".","2","8","."], + [".",".",".","4","1","9",".",".","5"], + [".",".",".",".","8",".",".","7","9"] +] + +board2 = [ + ["8","3",".",".","7",".",".",".","."], + ["6",".",".","1","9","5",".",".","."], + [".","9","8",".",".",".",".","6","."], + ["8",".",".",".","6",".",".",".","3"], + ["4",".",".","8",".","3",".",".","1"], + ["7",".",".",".","2",".",".",".","6"], + [".","6",".",".",".",".","2","8","."], + [".",".",".","4","1","9",".",".","5"], + [".",".",".",".","8",".",".","7","9"] +] + +print(solution.isValidSudoku(board1)) # True +print(solution.isValidSudoku(board2)) # True + +solution.solveSudoku(board1) +print(board1) +# ['5', '3', '4', '6', '7', '8', '9', '1', '2'], +# ['6', '7', '2', '1', '9', '5', '3', '4', '8'], +# ['1', '9', '8', '3', '4', '2', '5', '6', '7'], +# ['8', '5', '9', '7', '6', '1', '4', '2', '3'], +# ['4', '2', '6', '8', '5', '3', '7', '9', '1'], +# ['7', '1', '3', '9', '2', '4', '8', '5', '6'], +# ['9', '6', '1', '5', '3', '7', '2', '8', '4'], +# ['2', '8', '7', '4', '1', '9', '6', '3', '5'], +# ['3', '4', '5', '2', '8', '6', '1', '7', '9'], \ No newline at end of file diff --git a/04/word_ladder.py b/04/word_ladder.py new file mode 100644 index 000000000..8c3e8220b --- /dev/null +++ b/04/word_ladder.py @@ -0,0 +1,98 @@ +""" +127. Word Ladder +https://leetcode.com/problems/word-ladder/description/ +""" +from typing import List +import collections +import string + +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + wordList = set(wordList) + if not wordList or endWord not in wordList: + return 0 + + queue = collections.deque([beginWord]) + visited = set([beginWord]) + step = 1 + + while queue: + for _ in range(len(queue)): + word = queue.popleft() + + if word == endWord: + return step + + for i in range(len(word)): + for c in string.ascii_lowercase: + if c == word[i]: + continue + + new_word = word[:i] + c + word[i+1:] + if new_word not in visited and new_word in wordList: + queue.append(new_word) + visited.add(new_word) + step += 1 + + return 0 + + def ladderLength2(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + wordList = set(wordList) + if not wordList or endWord not in wordList: + return 0 + + # Bidirectional BFS + beginQueue = collections.deque([beginWord]) + endQueue = collections.deque([endWord]) + visited = set([beginWord, endWord]) + step = 1 + + while beginQueue and endQueue: + # choose the smaller queue to spread + if len(beginQueue) > len(endQueue): + beginQueue, endQueue = endQueue, beginQueue + + # spread the search from the current level + for _ in range(len(beginQueue)): + word = beginQueue.popleft() + + # process each character in the current word + for i in range(len(word)): + for c in string.ascii_lowercase: # a-z + if c == word[i]: + continue + + # see if we reach the target + newWord = word[:i] + c + word[i+1:] + if newWord in endQueue: + return step + 1 + + # process children, if not visited + if newWord not in visited and newWord in wordList: + beginQueue.append(newWord) + visited.add(newWord) + step += 1 + + return 0 # not found + +solution = Solution() + +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log","cog"] +print(solution.ladderLength(beginWord, endWord, wordList)) # 5 + +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log"] +print(solution.ladderLength(beginWord, endWord, wordList)) # 0 + +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log","cog"] +print(solution.ladderLength2(beginWord, endWord, wordList)) # 5 + +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log"] +print(solution.ladderLength2(beginWord, endWord, wordList)) # 0 \ No newline at end of file diff --git a/05/NOTE.md b/05/NOTE.md new file mode 100644 index 000000000..2fba0e4d1 --- /dev/null +++ b/05/NOTE.md @@ -0,0 +1,272 @@ +# 05 + +Sorting +------- + +### Simple Sorts (Comparison based) + +- **Bubble Sort**: compares two adjacent elements and swaps them if they are not in the intended order. +``` +bubbleSort(array) + for i <- 1 to indexOfLastUnsortedElement-1 + if leftElement > rightElement + swap leftElement and rightElement +end bubbleSort +``` + +- **Insertion Sort**: places an unsorted element at its suitable place in each iteration. +``` +insertionSort(array) + mark first element as sorted + for each unsorted element X + 'extract' the element X + for j <- lastSortedIndex down to 0 + if current element j > X + move sorted element to the right by 1 + break loop and insert X here +end insertionSort +``` + +- **Selection Sort**: selects the smallest element from an unsorted list in each iteration and places that element at the beginning of the unsorted list. +``` +selectionSort(array, size) + repeat (size - 1) times + set the first unsorted element as the minimum + for each of the unsorted elements + if element < currentMinimum + set element as new minimum + swap minimum with first unsorted position +end selectionSort +``` + +| Sorting | Average | Worst | Best | Space | Stability | +| :------------- | :-------- | :-------- | :-------- | :-------- | :-------- | +| Bubble Sort | O(N^2) | O(N^2) | O(N) | O(1) | YES | +| Insertion Sort | O(N^2) | O(N^2) | O(N) | O(1) | YES | +| Selection Sort | O(N^2) | O(N^2) | O(N^2) | O(1) | NO | + +### Efficient Sorts (Comparison based) + +- **Merge Sort**: the MergeSort function repeatedly divides the array into two halves until we reach a stage where we try to perform MergeSort on a subarray of size 1 (i.e. left == right). After that, the merge function comes into play and combines the sorted arrays into larger arrays until the whole array is merged. +``` +MergeSort(A, l, r): + if l > r + return + m = (l+r)/2 + mergeSort(A, l, m) + mergeSort(A, m+1, r) + merge(A, l, m, r) +``` + +- **Quick Sort**: An array is divided into sub-arrays by selecting a pivot element from the array. The pivot element should be positioned in such a way that elements less than pivot are kept on the left side and elements greater than pivot are on the right side of the pivot. The left and right sub-arrays are also divided using the same approach. This process continues until each subarray contains a single element. At this point, elements are already sorted. Finally, elements are combined to form a sorted array. +``` +quickSort(array, leftmostIndex, rightmostIndex) + if (leftmostIndex < rightmostIndex) + pivotIndex <- partition(array,leftmostIndex, rightmostIndex) + quickSort(array, leftmostIndex, pivotIndex - 1) + quickSort(array, pivotIndex, rightmostIndex) + +partition(array, leftmostIndex, rightmostIndex) + set rightmostIndex as pivotIndex + storeIndex <- leftmostIndex - 1 + for i <- leftmostIndex + 1 to rightmostIndex + if element[i] < pivotElement + swap element[i] and element[storeIndex] + storeIndex++ + swap pivotElement and element[storeIndex+1] +return storeIndex + 1 +``` + +- **Heap Sort**: Build a max-heap based on the array (heapify), the largest item is stored at the root node. Remove the root element and put at the end of the array (nth position) Put the last item of the tree (heap) at the vacant place. Reduce the size of the heap by 1. Heapify the root element again so that we have the highest element at root. The process is repeated until all the items of the list are sorted. +``` +heapify(array, index) + Root = array[index] + Largest = largest(root, left child, right child) + if(Root != Largest) + Swap(Root, Largest) + heapify(array, Largest) + +heapSort(array) + for index <- n//2-1 to 0 + heapify the index + for index <- n-1 to 0 + Swap(array[0], array[index]) + heapify(array, index, 0) +``` + +| Sorting | Average | Worst | Best | Space | Stability | +| :------------- | :-------- | :-------- | :-------- | :-------- | :-------- | +| Merge Sort | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | YES | +| Quick Sort | O(N*logN) | O(N^2) | O(N*logN) | O(N*logN) | NO | +| Heap Sort | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | YES | + +### Distribution Sorts (Non-comparison based) +- Counting Sort +- Bucket Sort +- Radix Sort + +| Sorting | Average | Worst | Best | Space | Stability | +| :------------- | :-------- | :-------- | :-------- | :-------- | :-------- | +| Counting Sort | O(N+k) | O(N+k) | O(N+k) | O(N+k) | YES | +| Bucket Sort | O(N+k) | O(N^2) | O(N) | O(N+k) | YES | +| Radix Sort | O(N*k) | O(N*k) | O(N*k) | O(N+k) | YES | + +Searching +--------- + +### Linear Search + +> Linear search is a sequential searching algorithm where we start from one end and check every element of the list until the desired element is found. It is the simplest searching algorithm. +``` +LinearSearch(array, key) + for each item in the array + if item == value + return its index +``` + +### Binary Search + +> Binary Search is a searching algorithm for finding an element's position in a sorted array. Binary search can be implemented only when the array is: +- Monotonically increasing/decreasing +- Bounded (have upper and lower bound) +- Index accessible + +Iterative Method +```py +def binary_search(nums, target) + left, right = 0, len(nums) - 1 + while left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid - 1 + else: # found + return mid + return -1 +``` + +Recursive Method +```py +def binary_search(nums, target): + def helper(left, right): + if left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] < target: + return helper(mid + 1, right) + elif nums[mid] > target: + return helper(left, mid - 1) + else: # found + return mid + return -1 + return helper(0, len(nums) - 1) +``` + +Variation 1: Find the first match (array contains duplicates) +```py +def binary_search1(nums, target): + left, right = 0, len(nums) - 1 + while left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid - 1 + else: + if mid == 0 or a[mid - 1] != target: + return mid # the first match + else: + right = mid - 1 # keep searching + return -1 +``` + +Variation 2: Find the last match (array contains duplicates) +```py +def binary_search2(nums, target): + left, right = 0, len(nums) - 1 + while left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + elif nums[mid] > target: + right = mid - 1 + else: + if mid == n - 1 or a[mid + 1] != target: + return mid # the last match + else: + left = mid + 1 # keep searching + return -1 +``` + +Variation 3: Find first number greater than target (array contains duplicates) +```py +def binary_search3(nums, target): + left, right = 0, len(nums) - 1 + while left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + else: + if mid == 0 or a[mid - 1] < target: + return mid # the first number greater than target + else: + right = mid - 1 # keep searching + return -1 +``` + +Variation 4: Find first number smaller than target (array contains duplicates) +```py +def binary_search4(nums, target): + left, right = 0, len(nums) - 1 + while left <= right: + # use (left + right) // 2 might be out of bound + mid = left + (right - left) // 2 + if nums[mid] > target: + right = mid - 1 + else: + if mid == n - 1 or a[mid + 1] > target: + return mid # the first number smaller than target + else: + left = mid + 1 # keep searching + return -1 +``` + +Target Function g(m) +```py +def binary_search(l, r): + """ + Returns the smallest number m in range [l, r] such that g(m) is true. + Returns r+1 if not found. + + Time Complexity: O(log(r - l) * (f(m) + g(m))) + Space Complexity: O(1) + """ + while l <= r: + m = l + (r - l) // 2 + if f(m): # optional: if somehow we can determine m is the answer, return it + return m + if g(m): + r = m - 1 # new range [l, m-1] + else: + l = m + 1 # new range [m+1, r] + return l # or not found +``` + +| Searching | Average | Worst | Best | Space | +| :------------- | :------- | :------- | :------- | :-------- | +| Linear Search | O(n) | O(n) | O(n) | O(1) | +| Binary Search | O(log n) | O(log n) | O(1) | O(1) | + +Leetcode Problems +- [704. Binary Search](https://leetcode.com/problems/binary-search/) +- [167. Two Sum II - Input array is sorted](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/) +- [33. Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/) +- [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) +- [69. Sqrt(x)](https://leetcode.com/problems/sqrtx/) +- [74. Search a 2D Matrix](https://leetcode.com/problems/search-a-2d-matrix/) +- [367. Valid Perfect Square](https://leetcode.com/problems/valid-perfect-square/) \ No newline at end of file diff --git a/05/search_2d_matrix.py b/05/search_2d_matrix.py new file mode 100644 index 000000000..e0f0ca67e --- /dev/null +++ b/05/search_2d_matrix.py @@ -0,0 +1,111 @@ +""" +74. Search a 2D Matrix +https://leetcode.com/problems/making-a-large-island/ +""" +from typing import List + +class Solution: + def searchMatrix1(self, matrix: List[List[int]], target: int) -> bool: + """ + 1. 2D Binary Search + Treat the (m x n) 2D Matrix as sorted 1D array (length = m x n) + For any element in the array we have: x = matrix[m // n][m % n] + """ + m = len(matrix) + if m == 0: + return False + + n = len(matrix[0]) + if n == 0: + return False + + l, r = 0, m * n - 1 + while l <= r: + m = (l + r) // 2 + x = matrix[m // n][m % n] + if x < target: + l = m + 1 + elif x > target: + r = m - 1 + elif x == target: + return True + + return False + + def searchMatrix2(self, matrix: List[List[int]], target: int) -> bool: + """ + 2. Diagonal Binary Search + """ + m = len(matrix) + if m == 0: + return False + + n = len(matrix[0]) + if n == 0: + return False + + # Start from the bottom-left corner to up-right corner + x, y = m-1, 0 + while x >= 0 and y < n: + if matrix[x][y] > target: + x -= 1 # move up + elif matrix[x][y] < target: + y += 1 # move right + else: + return True + + return False + + def searchMatrix3(self, matrix: List[List[int]], target: int) -> bool: + """ + 3. Twice Binary Search + """ + m = len(matrix) + if m == 0: + return False + + n = len(matrix[0]) + if n == 0: + return False + + l, r = 0, m - 1 + while l <= r: + mid = (l + r) // 2 + if matrix[mid][-1] < target: + l = mid + 1 + elif matrix[mid][0] > target: + r = mid - 1 + else: + break + + row = matrix[mid] + + l, r = 0, n - 1 + while l <= r: + mid = (l + r) // 2 + if row[mid] < target: + l = mid + 1 + elif row[mid] > target: + r = mid - 1 + elif row[mid] == target: + return True + + return False + +solution = Solution() + +matrix = [ + [ 1, 3, 5, 7], + [10, 11, 16, 20], + [23, 30, 34, 50] +] + +target = 3 # found +print('searchMatrix1:', solution.searchMatrix1(matrix, target)) +print('searchMatrix2:', solution.searchMatrix1(matrix, target)) +print('searchMatrix3:', solution.searchMatrix1(matrix, target)) + +target = 13 # not found +print('searchMatrix1:', solution.searchMatrix1(matrix, target)) +print('searchMatrix2:', solution.searchMatrix1(matrix, target)) +print('searchMatrix3:', solution.searchMatrix1(matrix, target)) diff --git a/05/sort.py b/05/sort.py new file mode 100644 index 000000000..86b2cf75f --- /dev/null +++ b/05/sort.py @@ -0,0 +1,252 @@ +""" +Sorting Algorithms +""" +class Sort: + def BubbleSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + for i in range(n-1): + for j in range(n-1-i): + if nums[j] > nums[j+1]: + nums[j], nums[j+1] = nums[j+1], nums[j] + + return nums + + def InsertionSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + for i in range(1, n): + key = nums[i] + j = i - 1 + while j >= 0 and nums[j] > key: + nums[j+1] = nums[j] + j -= 1 + nums[j+1] = key + + return nums + + def SelectionSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + for i in range(n-1): + min_j = i + for j in range(i+1, n): + if nums[j] < nums[min_j]: + min_j = j + nums[i], nums[min_j] = nums[min_j], nums[i] + + return nums + + def MergeSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + def merge(left, right): + res = [] + while left and right: + if left[0] <= right[0]: + res.append(left.pop(0)) + else: + res.append(right.pop(0)) + while left: res.append(left.pop(0)) + while right: res.append(right.pop(0)) + return res + + mid = n // 2 + leftSorted = self.MergeSort(nums[:mid]) + rightSorted = self.MergeSort(nums[mid:]) + return merge(leftSorted, rightSorted) + + def QuickSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + def partition(left, right): + pivot = nums[left] + i, j = left + 1, right + while i <= j: + while i <= j and nums[i] <= pivot: i += 1 + while i <= j and nums[j] >= pivot: j -= 1 + if i <= j: + nums[i], nums[j] = nums[j], nums[i] + nums[left], nums[j] = nums[j], nums[left] + return j + + def quickSort(left, right): + if left < right: + p = partition(left, right) + quickSort(right, p-1) + quickSort(p+1, right) + + quickSort(0, n-1) + return nums + + def HeapSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + def heapify(n, i): + largest = i + left = 2 * i + 1 + right = 2 * i + 2 + + if left < n and nums[left] > nums[largest]: + largest = left + if right < n and nums[right] > nums[largest]: + largest = right + + if largest != i: + nums[largest], nums[i] = nums[i], nums[largest] + heapify(n, largest) + + for i in range(n//2 - 1, -1, -1): + heapify(n, i) + + for i in range(n-1, 0, -1): + nums[i], nums[0] = nums[0], nums[i] + heapify(i, 0) + + return nums + + def CountingSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + res = [0] * n + + # Store the count of each elements in count array + max_elem = max(nums) + count = [0] * (max_elem + 1) + for i in range(n): + count[nums[i]] += 1 + + # Store the cumulative count + for i in range(1, max_elem + 1): + count[i] += count[i-1] + + # Find the index of each element of the original array in count array + # place the elements in output array + i = n - 1 + while i >= 0: + res[count[nums[i]] - 1] = nums[i] + count[nums[i]] -= 1 + i -= 1 + + # Copy the sorted elements into original aay + nums[:] = res[:] + + return nums + + def RadixSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + def countSort(place): + res = [0] * n + count = [0] * (max_elem + 1) + + # Calculate count of elements + for i in range(n): + index = nums[i] // place + count[index % 10] += 1 + + # Calculate cumulative count + for i in range(1, max_elem + 1): + count[i] += count[i-1] + + # Place the elements in sorted order + i = n - 1 + while i >= 0: + index = nums[i] // place + res[count[index % 10] - 1] = nums[i] + count[index % 10] -= 1 + i -= 1 + + nums[:] = res[:] + + # Get maximum element + max_elem = max(nums) + + # Apply counting sort to sort elements based on place value. + place = 1 + while max_elem // place > 0: + countSort(place) + place *= 10 + + return nums + + def BucketSort(self, nums): + if nums is None: + return nums + + n = len(nums) + if n < 2: + return nums + + # Create empty buckets + bucket = [[] for i in range(n)] + + # Insert elements into their respective buckets + for x in nums: + index = int(10 * x) + bucket[index].append(x) + + # Sort the elements of each bucket + for i in range(n): + bucket[i] = sorted(bucket[i]) + + # Get the sorted elements + k = 0 + for i in range(n): + for j in range(len(bucket[i])): + nums[k] = bucket[i][j] + k += 1 + + return nums + +nums = [3, 48, 15, 42, 26, 50, 27, 4, 34, 19, 2, 13, 36, 5, 47, 17] +sort = Sort() +print('Bubble Sort ', sort.BubbleSort(nums[:])) +print('Selection Sort', sort.SelectionSort(nums[:])) +print('Insertion Sort', sort.InsertionSort(nums[:])) +print('Merge Sort ', sort.MergeSort(nums[:])) +print('Quick Sort ', sort.QuickSort(nums[:])) +print('Heap Sort ', sort.HeapSort(nums[:])) +print('Counting Sort ', sort.CountingSort(nums[:])) +print('Radix Sort ', sort.RadixSort(nums[:])) +print('Bucket Sort ', [int(x*100) for x in sort.BucketSort([x/100 for x in nums])]) \ No newline at end of file diff --git a/06/LCS.py b/06/LCS.py new file mode 100644 index 000000000..177f8aaf0 --- /dev/null +++ b/06/LCS.py @@ -0,0 +1,33 @@ +""" +1143. Longest Common Subsequence +https://leetcode.com/problems/longest-common-subsequence/ +""" +class Solution: + def longestCommonSubsequence(self, s: str, t: str) -> int: + """ + f(0, *) = 0 + f(*, 0) = 0 + f(i, j) = { + f(i-1, j-1) + 1 , when s[i-1] == t[j-1] + max(f(i-1, j), f(i, j-1)) , when s[i-1] != t[j-1] + } + """ + if not s or not t: + return 0 + + m, n = len(s), len(t) + dp = [[0 for _ in range(n+1)] for _ in range(m+1)] + + for i in range(1, m+1): + for j in range(1, n+1): + if s[i-1] == t[j-1]: + dp[i][j] = dp[i-1][j-1] + 1 + else: + dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + + return dp[m][n] + +solution = Solution() +print(solution.longestCommonSubsequence('abcde', 'ace')) # 3 +print(solution.longestCommonSubsequence('abc', 'abc')) # 3 +print(solution.longestCommonSubsequence('abc', 'def')) # 0 diff --git a/06/NOTE.md b/06/NOTE.md new file mode 100644 index 000000000..6a764fe14 --- /dev/null +++ b/06/NOTE.md @@ -0,0 +1,145 @@ +# 06 + +Dynamic Programming +------------------- +- **Optimal Substructure**: the solution to a given optimization problem can be obtained by the combination of optimal solutions to its sub-problems. Such optimal substructures are usually described by means of "_recursion_". + +- **Overlapping Subproblems**: the space of sub-problems must be small, that is, any recursive algorithm solving the problem should solve the same sub-problems over and over, rather than generating new sub-problems. If a problem can be solved by combining optimal solutions to non-overlapping sub-problems, the strategy is called _"divide and conquer"_ instead. + +This can be achieved in either of two ways: +1. **Top-down**: Recursion + Memo +2. **Bottom-up**: Iteration + DP Table (states) + +Fibonacci sequence +------------------ + +Top-down +``` + _________________f(5)________________ + / \ + _______f(4)______ _______f(3)_ + / \ / \ + _______f(3)_ __f(2)_ __f(2)_ f(1) + / \ / \ / \ + __f(2)_ f(1) f(1) f(1) f(1) f(1) + / \ +f(1) f(1) +``` + +Recursion +```py +def fib(N): + if N < 2: return N + return fib(N-1) + fib(N-2) +``` + +Space Optimization (Memo) +```py +memo = {} +def fib(N): + if N < 2: return N + if N not in memo: + memo[N] = fib(N-1) + fib(N-2) + return memo[N] +``` + +Bottom-up +``` +f(0) f(1) f(2) f(3) f(4) f(5) + 1 2 3 5 8 13 +---------------------------> +``` + +Dynamic Programming +```py +def fib(N): + """ + dp(N) = { + N, N < 2 + dp(N-1) + dp(N-2), N >= 2 + } + """ + if N < 2: return N + dp = [0] * (N+1) + dp[0] = 0 + dp[1] = 1 + for i in range(2, N+1): + dp[i] = dp[i-1] + dp[i-2] + return dp[-1] +``` + +Space Optimization (Reduce States) +```py +def fib(self): + if N < 2: return N + f, f1, f2 = 0, 0, 1 + for i in range(N): + f = f1 + f2 + f2 = f1 + f1 = f + return f +``` + +DP Framework +------------ +```py +# initialize base case +dp[0][0][...] = base + +# status transfer +for status_1 in all_values_in_status_1: + for status_2 in all_values_in_status_2: + for ... + dp[status_1][status_2][...] = choose(choice_1,choice_2...) +``` + +Leetcode Problems +----------------- + +Basics +- [509. Fibonacci Number](https://leetcode.com/problems/fibonacci-number/) +- [70. Climbing Stairs](https://leetcode.com/problems/climbing-stairs/) +- [120. Triangle](https://leetcode.com/problems/triangle/) +- [322. Coin Change](https://leetcode.com/problems/coin-change/) +- [518. Coin Change 2](https://leetcode.com/problems/coin-change-2/) + +House Robber +- [198. House Robber](https://leetcode.com/problems/house-robber/) +- [213. House Robber II](https://leetcode.com/problems/house-robber-ii/) +- [337. House Robber III](https://leetcode.com/problems/house-robber-iii/) + +Unique Paths +- [62. Unique Paths](https://leetcode.com/problems/unique-paths/) +- [63. Unique Paths II](https://leetcode.com/problems/unique-paths-ii/) +- [980. Unique Paths III](https://leetcode.com/problems/unique-paths-iii/) + +Sub Array / Sub Subsequence +- [53. Maximum Subarray](https://leetcode.com/problems/maximum-subarray/) +- [152. Maximum Product Subarray](https://leetcode.com/problems/maximum-product-subarray/description/) +- [718. Maximum Length of Repeated Subarray](https://leetcode.com/problems/maximum-length-of-repeated-subarray/) +- [416. Partition Equal Subset Sum](https://leetcode.com/problems/partition-equal-subset-sum/) +- [300. Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/) +- [516. Longest Palindromic Subsequence](https://leetcode.com/problems/longest-palindromic-subsequence/) +- [1143. Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) +- [1035. Uncrossed Lines](https://leetcode.com/problems/uncrossed-lines/) + +Buy and Sell Stocks +- [121. Best Time to Buy and Sell Stock](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/) +- [122. Best Time to Buy and Sell Stock II](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/) +- [123. Best Time to Buy and Sell Stock III](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/) +- [188. Best Time to Buy and Sell Stock IV](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/) +- [309. Best Time to Buy and Sell Stock with Cooldown](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/) +- [714. Best Time to Buy and Sell Stock with Transaction Fee](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) + +Greedy Algorithm +---------------- + +> A greedy algorithm is any algorithm that follows the problem-solving heuristic of making the locally optimal choice at each stage. + +Leetcode Problems +- [860. Lemonade Change](https://leetcode.com/problems/lemonade-change/) +- [455. Assign Cookies](https://leetcode.com/problems/assign-cookies/) +- [874. Walking Robot Simulation](https://leetcode.com/problems/walking-robot-simulation) +- [322. Coin Change](https://leetcode.com/problems/coin-change/) +- [55. Jump Game](https://leetcode.com/problems/jump-game/) +- [45. Jump Game II](https://leetcode.com/problems/jump-game-ii/) \ No newline at end of file diff --git a/06/buy-sells-stocks.py b/06/buy-sells-stocks.py new file mode 100644 index 000000000..9603eef5c --- /dev/null +++ b/06/buy-sells-stocks.py @@ -0,0 +1,192 @@ +""" +121. Best Time to Buy and Sell Stock +https://leetcode.com/problems/best-time-to-buy-and-sell-stock/ + +122. Best Time to Buy and Sell Stock II +https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/ + +309. Best Time to Buy and Sell Stock with Cooldown +https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/ + +714. Best Time to Buy and Sell Stock with Transaction Fee +https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/ + +123. Best Time to Buy and Sell Stock III +https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/ + +188. Best Time to Buy and Sell Stock IV +https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/ +""" +from typing import List +import math + +class Solution: + def maxProfit1(self, prices: List[int]) -> int: + """ + dp[i][0] = max ( + dp[i-1][0] + dp[i-1][1] + prices[i] + ) + dp[i][1] = max ( + dp[i-1][1] + 0 - prices[i] + ) + """ + n = len(prices) + if n <= 1: return 0 + + dp = [[None, None] for _ in range(n)] + dp[0][0] = 0 + dp[0][1] = -prices[0] + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp[i][1] = max(dp[i-1][1], -prices[i]) + + return dp[-1][0] + + def maxProfit2(self, prices: List[int]) -> int: + """ + dp[i][0] = max( + dp[i-1][0] + dp[i-1][1] + prices[i] + ) + dp[i][1] = max( + dp[i-1][1] + dp[i-1][0] - prices[i] + ) + """ + n = len(prices) + if n <= 1: return 0 + + dp = [[None, None] for _ in range(n)] + dp[0][0] = 0 + dp[0][1] = -prices[0] + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) + + return dp[-1][0] + + def maxProfit3(self, prices: List[int]) -> int: + """ + dp[i][0] = max( + dp[i-1][0] + dp[i-1][1] + price[i] + ) + dp[i][1] = max( + dp[i-1][1] + dp[i-2][0] - price[i] + ) + """ + n = len(prices) + if n <= 1: return 0 + + dp = [[0 for _ in range(2)] for _ in range(n)] + dp[0][0] = 0 + dp[0][1] = -prices[0] + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]) + + return dp[n-1][0] + + def maxProfit4(self, prices: List[int], fee: int) -> int: + """ + dp[i][0] = max( + dp[i-1][0] + dp[i-1][1] + prices[i] + ) + dp[i][1] = max( + dp[i-1][1] + dp[i-1][0] - prices[i] - fee + ) + """ + n = len(prices) + if n <= 1: return 0 + + dp = [[0 for _ in range(2)] for _ in range(n)] + dp[0][0] = 0 + dp[0][1] = -prices[0] - fee + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee) + + return dp[n-1][0] + + def maxProfit5(self, prices: List[int]) -> int: + """ + dp[i][k][0] = max( + dp[i-1][k][0] + dp[i-1][k][1] + prices[i] + ) + dp[i][k][1] = max( + dp[i-1][k][1] + dp[i-1][k-1][0] - prices[i] + ) + """ + n = len(prices) + if n <= 1: return 0 + + dp = [[[None, None] for _ in range(3)] for _ in range(n)] # (n, k+1, 2) + for i in range(n): + dp[i][0][0] = 0 + dp[i][0][1] = -math.inf + for j in range(1, 3): + dp[0][j][0] = 0 + dp[0][j][1] = -prices[0] + + for i in range(1, n): + for j in range(1, 3): + dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]) + dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]) + + return dp[-1][-1][0] + + def maxProfit6(self, k: int, prices: List[int]) -> int: + """ + dp[i][k][0] = max( + dp[i-1][k][0] + dp[i-1][k][1] + prices[i] + ) + dp[i][k][1] = max( + dp[i-1][k][1] + dp[i-1][k-1][0] - prices[i] + ) + """ + n = len(prices) + if n <= 1: return 0 + + if k > n // 2: # back to unlimited situation (maxProfit3) + profit = 0 + for i in range(1, n): + if prices[i] > prices[i - 1]: + profit += prices[i] - prices[i - 1] + return profit + + dp = [[[None, None] for _ in range(k+1)] for _ in range(n)] # (n, k+1, 2) + for i in range(n): + dp[i][0][0] = 0 + dp[i][0][1] = -math.inf + for j in range(1, k+1): + dp[0][j][0] = 0 + dp[0][j][1] = -prices[0] + + for i in range(1, n): + for j in range(1, k+1): + dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]) + dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]) + + return dp[-1][-1][0] + + +solution = Solution() +print("Maximum 1 Transaction:", solution.maxProfit1([7,1,5,3,6,4])) # 5 +print("Unlimited Transactions:", solution.maxProfit2([7,1,5,3,6,4])) # 7 +print("Unlimited Transactions with Cooldown:", solution.maxProfit3([1,2,3,0,2])) # 3 +print("Unlimited Transactions with Fee:", solution.maxProfit4([1,3,2,8,4,9], 2)) # 8 +print("Maximum 2 Transactions:", solution.maxProfit5([3,3,5,0,0,3,1,4])) # 6 +print("Maximum k Transactions:", solution.maxProfit6(2, [3,2,6,5,0,3])) # 7 \ No newline at end of file diff --git a/06/find-length.py b/06/find-length.py new file mode 100644 index 000000000..fd747f022 --- /dev/null +++ b/06/find-length.py @@ -0,0 +1,27 @@ +""" +718. Maximum Length of Repeated Subarray +https://leetcode.com/problems/maximum-length-of-repeated-subarray/ +""" +from typing import List + +class Solution: + def findLength(self, A: List[int], B: List[int]) -> int: + """ + status: { + dp[i][j] = 0, i = 0 + dp[i][j] = 0, j = 0 + dp[i][j] = dp[i-1][j-1] + 1, if A[i] == B[j] + } + """ + m, n = len(A), len(B) + dp = [[0 for _ in range(n+1)] for _ in range(m+1)] + res = 0 + for i in range(1, m+1): + for j in range(1, n+1): + if A[i-1] == B[j-1]: + dp[i][j] = dp[i-1][j-1] + 1 + res = max(res, dp[i][j]) + return res + +solution = Solution() +print(solution.findLength([1,2,3,2,1], [3,2,1,4,7])) # 3 \ No newline at end of file diff --git a/06/house-robber.py b/06/house-robber.py new file mode 100644 index 000000000..cb7559eca --- /dev/null +++ b/06/house-robber.py @@ -0,0 +1,132 @@ +""" +198. House Robber +https://leetcode.com/problems/house-robber/ + +213. House Robber II +https://leetcode.com/problems/house-robber-ii/ + +337. House Robber III +https://leetcode.com/problems/house-robber-iii/ +""" +from typing import List + +class TreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val + self.left = None + self.right = None + +def createTree(iterable = ()): + def insert(node, i): + if i < len(iterable) and iterable[i] is not None: + node = TreeNode(iterable[i]) + node.left = insert(node.left, 2 * i + 1) + node.right = insert(node.right, 2 * i + 2) + return node + return insert(None, 0) + +class Solution: + def rob1(self, nums: List[int]) -> int: + """ + choices = { + 0: not rob + 1: rob + } + + status = { + dp[i][0] = max(dp[i-1][0], dp[i-1][1]) + dp[i][1] = dp[i-1][0] + nums[i] + } + + base case = { + dp[0][0] = 0 + dp[0][1] = nums[0] + } + + answer = max(dp[-1][0], dp[-1][1]) + """ + n = len(nums) + if n == 0: return 0 + if n == 1: return nums[0] + + dp = [[0, 0] for _ in range(n)] + dp[0][0] = 0 + dp[0][1] = nums[0] + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1]) + dp[i][1] = dp[i-1][0] + nums[i] + + return max(dp[-1][0], dp[-1][1]) + + def rob1(self, nums: List[int]) -> int: + """ + dp[i] = { + dp[0] = nums[0], n = 0 + dp[1] = max(nums[0], nums[1]), n = 1 + dp[i] = max(dp[i-1], dp[i-2] + nums[i]), n >= 2 + } + """ + n = len(nums) + if n == 0: return 0 + if n == 1: return nums[0] + + dp = [0 for _ in range(n)] + dp[0] = nums[0] + dp[1] = max(nums[0], nums[1]) + + for i in range(2, n): + dp[i] = max(dp[i-1], dp[i-2] + nums[i]) + + return dp[-1] + + def rob1(self, nums: List[int]) -> int: + """ + Space Optimization + """ + n = len(nums) + if n == 0: return 0 + if n == 1: return nums[0] + + f = 0 # dp[i] + f1 = 0 # dp[i-1] + f2 = 0 # dp[i-2] + + for i in range(n): + f = max(f1, f2 + nums[i]) + f2 = f1 + f1 = f + + return f + + def rob2(self, nums: List[int]) -> int: + n = len(nums) + if n == 0: return 0 + if n == 1: return nums[0] + return max(self.rob1(nums[1:]), self.rob1(nums[:-1])) + + def rob3(self, root: TreeNode, memo = {}) -> int: + def dp(root): + if not root: + return 0, 0 + + rob_left, not_rob_left = dp(root.left) + rob_right, not_rob_right = dp(root.right) + + rob = root.val + not_rob_left + not_rob_right + not_rob = max(rob_left, not_rob_left) + max(rob_right, not_rob_right) + return rob, not_rob + + rob, not_rob = dp(root) + return max(rob, not_rob) + + +solution = Solution() +print(solution.rob1([1,2,3,1])) # 4 +print(solution.rob2([2,3,2])) # 3 + +tree = createTree([3,2,3,None,3,None,1]) +print(solution.rob3(tree)) # 7 + +tree = createTree([3,4,5,1,3,None,1]) +print(solution.rob3(tree)) # 9 \ No newline at end of file diff --git a/06/triangle.py b/06/triangle.py new file mode 100644 index 000000000..15f730587 --- /dev/null +++ b/06/triangle.py @@ -0,0 +1,41 @@ +""" +120. Triangle +https://leetcode.com/problems/triangle/ +""" +from typing import List + +class Solution: + def minimumTotal(self, triangle: List[List[int]]) -> int: + """ + 1. state definition: + dp[i][j] is the min total from (i,j) to bottom + + 2. state transform: + dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j] + """ + m = len(triangle) + dp = [[0 for _ in range(m+1)] for _ in range(m+1)] + for i in range(m-1, -1, -1): + for j in range(i+1): + dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j] + return dp[0][0] + + def minimumTotal(self, triangle: List[List[int]]) -> int: + """ + dp[j] = min(dp[j], dp[j+1]) + triangle[i][j] + """ + m = len(triangle) + dp = [0 for _ in range(m+1)] + for i in range(m-1, -1, -1): + for j in range(i+1): + dp[j] = min(dp[j], dp[j+1]) + triangle[i][j] + return dp[0] + +solution = Solution() +ans = solution.minimumTotal([ + [2], + [3,4], + [6,5,7], + [4,1,8,3] +]) +print(ans) # 2 + 3 + 5 = 11 \ No newline at end of file diff --git a/06/unique-paths.py b/06/unique-paths.py new file mode 100644 index 000000000..055504175 --- /dev/null +++ b/06/unique-paths.py @@ -0,0 +1,63 @@ +""" +62. Unique Paths +https://leetcode.com/problems/unique-paths/ +""" +from typing import List + +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + """ + status = { + dp[i][j] = 1, i = 0 + dp[i][j] = 1, j = 0 + dp[i][j] = dp[i-1][j] + dp[i][j-1], i >= 1, j >= 1 + } + """ + dp = [[1]*n] + [[1] + [0]*(n-1) for _ in range(m-1)] + + for i in range(1, m): + for j in range(1, n): + dp[i][j] = dp[i-1][j] + dp[i][j-1] + + return dp[-1][-1] + + def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: + """ + status = { + dp[i][j] = 0, i = 0, obstacleGrid[i][j] == 0 + dp[i][j] = 1, j = 0, obstacleGrid[i][j] == 1 + dp[i][j] = 0, i = 0, obstacleGrid[i][j] == 0 + dp[i][j] = 1, j = 0, obstacleGrid[i][j] == 1 + dp[i][j] = dp[i-1][j] + dp[i][j-1], i >= 1, j >= 1 + } + """ + if obstacleGrid[0][0] == 1: + return 0 + + m, n = len(obstacleGrid), len(obstacleGrid[0]) + dp = [[0 for _ in range(n)] for _ in range(m)] + dp[0][0] = 1 + + for i in range(1, m): + if obstacleGrid[i][0] == 1: break + else: dp[i][0] = dp[i-1][0] + + for j in range(1, n): + if obstacleGrid[0][j] == 1: break + else: dp[0][j] = dp[0][j-1] + + for i in range(1, m): + for j in range(1, n): + if obstacleGrid[i][j] == 0: + dp[i][j] = dp[i-1][j] + dp[i][j-1] + + return dp[-1][-1] + +solution = Solution() +print(solution.uniquePaths(3,2)) # 3 +print(solution.uniquePaths(7,3)) # 28 +print(solution.uniquePathsWithObstacles([ + [0,0,0], + [0,1,0], + [0,0,0] +])) # 2 \ No newline at end of file diff --git a/07/NOTE.md b/07/NOTE.md new file mode 100644 index 000000000..43a34d263 --- /dev/null +++ b/07/NOTE.md @@ -0,0 +1,239 @@ +# 07 + +Skip List +--------- + +> The skip list is a probabilisitc data structure that is built upon the general idea of a linked list. The skip list uses probability to build subsequent layers of linked lists upon an original linked list. Each additional layer of links contains fewer elements, but no new elements. + +``` + 1 10 + o---> o---------------------------------------------------------> o Top level + 1 3 2 5 + o---> o---------------> o---------> o---------------------------> o Level 3 + 1 2 1 2 3 2 + o---> o---------> o---> o---------> o---------------> o---------> o Level 2 + 1 1 1 1 1 1 1 1 1 1 1 + o---> o---> o---> o---> o---> o---> o---> o---> o---> o---> o---> o Bottom level +Head 1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th NIL + Node Node Node Node Node Node Node Node Node Node +``` + +| Operation | Time Complexity | +| ---------- | :-------------: | +| Access | O(log n) | +| Search | O(log n) | +| Insertion | O(log n) | +| Deletion | O(log n) | + +```py +def search(key): + p = topLeftNode() + while p.below: # Scan down + p = p.below + while key >= p.next: # Scan forward + p = p.next + return p + +def insert(key): + p, q = search(key), None + i = 1 + while CoinFlip() != 'Tails': + i = i + 1 # Height of tower for new element + if i >= h: + h = h + 1 + createNewLevel() # Creates new linked list level + while p.above is None: + p = p.prev # Scan backwards until you can go up + p = p.above + q = insertAfter(key, p) # Insert our key after position p + n = n + 1 + return q + +def delete(key): + # Search for all positions p_0, ..., p_i where key exists + if none are found: + return + # Delete all positions p_0, ..., p_i + # Remove all empty layers of skip list +``` + +Trie +---- +> A trie is a tree-like data structure whose nodes store the letters of an alphabet. By structuring the nodes in a particular way, words and strings can be retrieved from the structure by traversing down a branch path of the tree. + +```py +def buildTrie(words): + trie = {} + for word in words: + node = trie + for char in word: + node = node.setdefault(char, {}) + node['#'] = True + return trie +``` + +``` +trie = buildTrie(["app", "apple", "bar", "ball"]) + + () + / \ + a b + | | + p a + | / \ + p r l +/ \ | | +# l # l + | | + e # + | + # +``` + +Leetcode Problems +- [208. Implement Trie (Prefix Tree)](https://leetcode.com/problems/implement-trie-prefix-tree/) +- [212. Word Search II](https://leetcode.com/problems/word-search-ii/) + +Disjoint-set (Union-find set) +----------------------------- +> A disjoint-set data structure is a data structure that tracks a set of elements partitioned into a number of disjoint (non-overlapping) subsets. It provides near-constant-time operations to add new sets, to merge existing sets, and to determine whether elements are in the same set. + +```py +ds = DisjointSet(5) + +(0) (1) (2) (3) (4) + +ds.union(1, 2) +ds.union(3, 4) + +(0) (1) (3) + | | + (2) (4) +``` + +Leetcode Problems +- [547. Friend Circles](https://leetcode.com/problems/friend-circles/) +- [130. Surrounded Regions](https://leetcode.com/problems/surrounded-regions/) +- [200. Number of Islands](https://leetcode.com/problems/number-of-islands/) + +Self-balancing BST +------------------ + +> A self-balancing (or height-balanced) BST is any node-based BST that automatically keeps its height (maximal number of levels below the root) small in the face of arbitrary item insertions and deletions. + +### AVL Tree + +> In an AVL tree, the heights of the two child subtrees of any node differ by at most one; if at any time they differ by more than one, re-balancing is done to restore this property. Lookup, insertion, and deletion all take O(log n) time in both the average and worst cases. + +```py +BalanceFactor(node) = Height(RightSubtree(node)) - Height(LeftSubtree(node)) +BalanceFactor(node) = {-1, 0, 1} +``` + +4 Types of Rotations +``` +Left-Left Case, right rotation: + + A (-2) B (0) + / / \ + B (-1) C (0) A (0) + / +C (0) + +Right Right Case, left rotation: + +A (2) B (0) + \ / \ + B (1) A (0) C (0) + \ + C (0) + +Left-Right Case, right rotation -> left rotation: + + A (-2) A (-2) C (0) + / / / \ +B (1) C (-1) B (0) A (0) + \ / + C (0) B (0) + +Right-Left Case, left rotation -> right rotation: + +A (-2) A (-2) C (0) + \ \ / \ + B (-1) C (-1) A (0) B (0) + / \ +C (0) B (0) +``` + +With Subtree +``` + right rotation + ------------------> + Y X + / \ / \ + X T3 T1 Y + / \ / \ + T1 T2 T2 T3 + <------------------ + left rotation + + Case 1 Case 4 + Z Y | Y Z + / \ / \ | / \ / \ + Y T4 X Z | Z X T1 Y + / \ / \ / \ | / \ / \ / \ + X T3 T1 T2 T3 T4 | T1 T2 T3 T4 T2 X + / \ | / \ + T1 T2 | T3 T4 + + Case 2 Case 3 + Z Z X | Y Z Z + / \ / \ / \ | / \ / \ / \ + Y T4 X T4 Y Z | Z X T1 Y T1 Y + / \ / \ / \ / \ | / \ / \ / \ / \ + T1 X Y T3 T1 T2 T3 T4 | T1 T2 T3 T4 T2 X X T4 + / \ / \ | / \ / \ + T1 T2 T1 T2 | T3 T4 T2 T3 +``` + +### Red-Black Tree + +> In a red–black tree, each node stores an extra bit representing color, used to ensure that the tree remains approximately balanced during insertions and deletions. + +Properties +1. Every node is either red or black. +2. The root is black. +3. All leaves (NIL) are black. +4. Red nodes have only black children (no two red nodes are connected) +5. Every path from a given node to any of its descendant NIL nodes goes through the same number of black nodes. + +Operations +1. Change Color +2. Rotate Left +3. Rotate Right + +``` + G G - Grand Parent + / \ + P U P - Parent, U - Uncle + / \ +S N S - Sibling, N - Current +``` + +RED-BLACK repair procedure Operations: +- All newly inserted nodes are considered as **RED** by default +- Case 1: current node N is root + - set current node N to **BLACK** +- Case 2: current node N's parent node P is **BLACK** + - Do Nothing since tree is still valid +- Case 3: current node N's parent node P is **RED** and it's Uncle node U is also **RED** + - set parent node P to **BLACK** + - set Uncle node U to **BLACK** + - set Grand parent node G to **RED** + - rerun on the RED-BLACK repair procedure G +- Case 4: current node N's parent node P is **RED** but it's uncle node U is **BLACK** + - Current node N is right sub-tree, rotate left parent node P + - current node N is left sub-tree + - Set parent node P to **BLACK** + - Set grand parent node G to **RED** + - Rotate right on grand parent node G \ No newline at end of file diff --git a/07/disjoint_set.py b/07/disjoint_set.py new file mode 100644 index 000000000..973638c1c --- /dev/null +++ b/07/disjoint_set.py @@ -0,0 +1,82 @@ +""" +Disjoint-set Data Structure +""" +class DisjointSet: + def __init__(self, size): + self.parents = [i for i in range(size)] + + def find(self, x): + """ + Worse Case: O(N) + """ + parents = self.parents + while x != parents[x]: + x = parents[x] + return x + + def union(self, x, y): + """ + Worse Case: O(N) + """ + parent_x, parent_y = self.find(x), self.find(y) + if parent_x != parent_y: + self.parents[parent_x] = parent_y + + def connected(self, x, y): + return self.find(x) == self.find(y) + +ds = DisjointSet(5) +print(ds.find(1)) # 1 +print(ds.connected(1, 2)) # False +ds.union(1, 2) +print(ds.find(1)) # 2 +print(ds.find(2)) # 2 +print(ds.connected(1, 2)) # True + +""" +Disjoint-set Data Structure Optimized +""" +class DisjointSetOptimized: + def __init__(self, size): + self.count = size + self.parents = [i for i in range(size)] + self.weights = [1 for _ in range(size)] + + def find(self, x): + """ + Close to O(1) + """ + parents = self.parents + while x != parents[x]: + # Compression: reduce the path to the root + parents[x] = parents[parents[x]] + x = parents[x] + return x + + def union(self, x, y): + """ + Average Case: O(logN) + """ + parent_x, parent_y = self.find(x), self.find(y) + if parent_x != parent_y: + weights = self.weights + parents = self.parents + # balancing: connect "lighter" tree onto the "heavier" tree + if weights[parent_x] > weights[parent_y]: + parents[parent_y] = parent_x + weights[parent_x] += weights[parent_y] + else: + parents[parent_x] = parent_y + weights[parent_y] += weights[parent_x] + self.count -= 1 + + def connected(self, x, y): + return self.find(x) == self.find(y) + +ds = DisjointSetOptimized(5) +print(ds.find(1)) # 1 +print(ds.connected(1, 2)) # False +ds.union(1, 2) +print(ds.find(1)) # 2 +print(ds.find(2)) # 2 +print(ds.connected(1, 2)) # True \ No newline at end of file diff --git a/07/friend-circles.py b/07/friend-circles.py new file mode 100644 index 000000000..318da60a2 --- /dev/null +++ b/07/friend-circles.py @@ -0,0 +1,53 @@ +""" +547. Friend Circles [Medium] +https://leetcode.com/problems/friend-circles/ +""" +from typing import List + +class DisjointSet: + def __init__(self, size): + self.count = size + self.parents = [i for i in range(size)] + self.weights = [1 for _ in range(size)] + + def find(self, x): + parents = self.parents + while x != parents[x]: + # Compression: reduce the path to the root + parents[x] = parents[parents[x]] + x = parents[x] + return x + + def union(self, x, y): + parent_x, parent_y = self.find(x), self.find(y) + if parent_x != parent_y: + weights = self.weights + parents = self.parents + # balancing: connect "lighter" tree onto the "heavier" tree + if weights[parent_x] > weights[parent_y]: + parents[parent_y] = parent_x + weights[parent_x] += weights[parent_y] + else: + parents[parent_x] = parent_y + weights[parent_y] += weights[parent_x] + self.count -= 1 + + def connected(self, x, y): + return self.find(x) == self.find(y) + +class Solution: + def findCircleNum(self, M: List[List[int]]) -> int: + N = len(M) + ds = DisjointSet(N) + for i in range(N): + for j in range(N): + if M[i][j] == 1 and i != j: + ds.union(i, j) + + return ds.count + +solution = Solution() +ans = solution.findCircleNum([[1,1,0], [1,1,0], [0,0,1]]) +print(ans) # 2 +ans = solution.findCircleNum([[1,1,0], [1,1,1], [0,1,1]]) +print(ans) # 1 \ No newline at end of file diff --git a/07/similarity.py b/07/similarity.py new file mode 100644 index 000000000..a6d45c620 --- /dev/null +++ b/07/similarity.py @@ -0,0 +1,65 @@ +""" +Five Most Popular Similarity Measures in Python + +https://dataaspirant.com/five-most-popular-similarity-measures-implementation-in-python/ +""" +from math import * +from decimal import Decimal + +class Similarity(): + """ + Five similarity measures function + """ + def euclidean_distance(self, x, y): + """ + return euclidean distance between two lists + """ + return sqrt(sum(pow(a-b, 2) for a, b in zip(x, y))) + + def manhattan_distance(self, x, y): + """ + return manhattan distance between two lists + """ + return sum(abs(a-b) for a, b in zip(x,y)) + + def minkowski_distance(self, x, y, p_value): + """ + return minkowski distance between two lists + """ + return self.nth_root(sum(pow(abs(a-b), p_value) for a,b in zip(x, y)), p_value) + + def nth_root(self, value, n_root): + """ + returns the n_root of an value + """ + root_value = 1/float(n_root) + return round (Decimal(value) ** Decimal(root_value),3) + + def cosine_similarity(self, x, y): + """ + return cosine similarity between two lists + """ + numerator = sum(a*b for a,b in zip(x,y)) + denominator = self.square_rooted(x)*self.square_rooted(y) + return round(numerator/float(denominator),3) + + def square_rooted(self, x): + """ + return 3 rounded square rooted value + """ + return round(sqrt(sum([a*a for a in x])),3) + + def jaccard_similarity(self, x, y): + """ + returns the jaccard similarity between two lists + """ + intersection_cardinality = len(set.intersection(*[set(x), set(y)])) + union_cardinality = len(set.union(*[set(x), set(y)])) + return intersection_cardinality/float(union_cardinality) + +measures = Similarity() +print(measures.euclidean_distance([0,3,4,5], [7,6,3,-1])) +print(measures.manhattan_distance([10,20,10], [10,20,20])) +print(measures.minkowski_distance([0,3,4,5], [7,6,3,-1], 3)) +print(measures.cosine_similarity([3, 45, 7, 2], [2, 54, 13, 15])) +print(measures.jaccard_similarity([0,1,2,5,6], [0,2,3,5,7,9])) \ No newline at end of file diff --git a/07/trie.py b/07/trie.py new file mode 100644 index 000000000..811cde90a --- /dev/null +++ b/07/trie.py @@ -0,0 +1,52 @@ +""" +Trie Data Structure +""" +import collections + +class Trie: + def __init__(self): + self.root = {} + + def insert(self, word: str) -> None: + """ + Inserts a word into the trie. + """ + root = self.root + for letter in word: + root = root.setdefault(letter, {}) + root['#'] = True + + def search(self, word: str) -> bool: + """ + Returns if the word is in the trie. + """ + root = self.root + for letter in word: + root = root.get(letter) + if root is None: + return False + return '#' in root + + def startsWith(self, prefix: str) -> bool: + """ + Returns if there is any word in the trie that starts with the given prefix. + """ + root = self.root + for letter in prefix: + root = root.get(letter) + if root is None: + return False + return True + + def __str__(self): + root = self.root + return str(root) + +trie = Trie() +trie.insert('apple') +print(trie) +print(trie.search('apple')) # True +print(trie.search('app')) # False +print(trie.startsWith('app')) # True +trie.insert("app") +print(trie.search("app")) # True \ No newline at end of file diff --git a/08/LRU_cache.py b/08/LRU_cache.py new file mode 100644 index 000000000..a70328006 --- /dev/null +++ b/08/LRU_cache.py @@ -0,0 +1,108 @@ +""" +146. LRU Cache +https://leetcode.com/problems/lru-cache/ +""" +import collections + +class LRUCache: + def __init__(self, capacity): + self.dic = collections.OrderedDict() + self.remain = capacity + + def get(self, key): + if key not in self.dic: + return -1 + value = self.dic.pop(key) + self.dic[key] = value # key is now the latest one + return value + + def put(self, key, value): + if key in self.dic: + self.dic.pop(key) + else: + if self.remain > 0: + self.remain -= 1 + else: # self.dic is full + self.dic.popitem(last=False) + self.dic[key] = value + +class DLinkedNode: + def __init__(self, key=0, value=0): + self.key = key + self.value = value + self.prev = None + self.next = None + +class LRUCache2: + def __init__(self, capacity: int): + self.cache = {} + self.head = DLinkedNode() + self.tail = DLinkedNode() + self.head.next = self.tail + self.tail.prev = self.head + self.capacity = capacity + self.size = 0 + + def get(self, key: int) -> int: + if key not in self.cache: + return -1 + # if key exists, locate node in cache, then move to head + node = self.cache[key] # key is now least recently used + self.moveToHead(node) + return node.value + + def put(self, key: int, value: int) -> None: + if key not in self.cache: + node = DLinkedNode(key, value) + self.cache[key] = node + self.addToHead(node) + self.size += 1 + if self.size > self.capacity: + removed = self.removeTail() + self.cache.pop(removed.key) + self.size -= 1 + else: + node = self.cache[key] + node.value = value + self.moveToHead(node) + + def addToHead(self, node): + node.prev = self.head + node.next = self.head.next + self.head.next.prev = node + self.head.next = node + + def removeNode(self, node): + node.prev.next = node.next + node.next.prev = node.prev + + def moveToHead(self, node): + self.removeNode(node) + self.addToHead(node) + + def removeTail(self): + node = self.tail.prev + self.removeNode(node) + return node + +cache = LRUCache(2) +print(cache.put(1, 1)) +print(cache.put(2, 2)) +print(cache.get(1)) # returns 1 +print(cache.put(3, 3)) # evicts key 2 +print(cache.get(2)) # returns -1 (not found) +print(cache.put(4, 4)) # evicts key 1 +print(cache.get(1)) # returns -1 (not found) +print(cache.get(3)) # returns 3 +print(cache.get(4)) # returns 4 + +cache2 = LRUCache2(2) +print(cache2.put(1, 1)) +print(cache2.put(2, 2)) +print(cache2.get(1)) # returns 1 +print(cache2.put(3, 3)) # evicts key 2 +print(cache2.get(2)) # returns -1 (not found) +print(cache2.put(4, 4)) # evicts key 1 +print(cache2.get(1)) # returns -1 (not found) +print(cache2.get(3)) # returns 3 +print(cache2.get(4)) # returns 4 \ No newline at end of file diff --git a/08/NOTE.md b/08/NOTE.md new file mode 100644 index 000000000..eee568dd6 --- /dev/null +++ b/08/NOTE.md @@ -0,0 +1,124 @@ +# 08 + +Primitives +---------- + +Twos Complement +```py +-8 4 2 1 +[1 0 0 0] -8 +[1 0 0 1] -7 +[1 0 1 0] -6 +[1 0 1 1] -5 +[1 1 0 0] -4 +[1 1 0 1] -3 +[1 1 1 0] -2 +[1 1 1 1] -1 +[0 0 0 0] 0 +[0 0 0 1] 1 +[0 0 1 0] 2 +[0 0 1 1] 3 +[0 1 0 0] 4 +[0 1 0 1] 5 +[0 1 1 0] 6 +[0 1 1 1] 7 +``` + +How to Convert Negative Number +```py + [0 1 0 1] 5 +Invert [1 0 1 0] +Plus 1 [1 0 1 0] -5 + + [0 0 0 1 0 0 0 0] 16 +Invert [1 1 1 0 1 1 1 1] +Plus 1 [1 1 1 1 0 0 0 0] -16 +``` + +Bit-wise operators +```py +6 & 4 # 0110 & 0100 = 0100 (4) AND +1 | 2 # 0001 | 0010 = 0011 (3) OR +15 ^ 1 # 00001111 ^ 00000001 = 00001110 (14) XOR +8 >> 1 # 00001000 >> 1 = 00000100 (4) x >> y = x // 2^y +1 << 10 # 000000000001 << 10 = 010000000000 (1024) x << y = x * 2^y +-16 >> 2 # 11110000 >> 2 = 11111100 (-4) negative right shifting +-16 << 2 # 11110000 << 2 = 11000000 (-64) negative left shifting +~0 # ~0000 = 1111 (-1) ~x = -x - 1 +``` + +Bit Operation Tricks +```py +x & 1 == 1 # Odd number, same as x % 2 == 1 +x & 1 == 0 # Even number, same as x % 2 == 0 +x >> 1 # Same as x / 2 +x & 1 # Extract the last bit +(x >> k) & 1 # Extract the Kth bit +x |= 1 # Set the last bit +x |= (1 << k) # Set the Kth bit +x ^= 1 # Flip the last bit +x ^= (1 << k) # Flip the Kth bit +x & (x - 1) # Drop the lowest set bit of x +x & ~(x - 1) # Extract the lowest set bit of x +x & (-x) # Keep the lowest set bit and sets all the other bits to 0 +``` + +Leetcode Problems +- [191. Number of 1 Bits](https://leetcode.com/problems/number-of-1-bits/) +- [231. Power of Two](https://leetcode.com/problems/power-of-two/) +- [190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) +- [338. Counting Bits](https://leetcode.com/problems/counting-bits/) + +Bloom Filter +------------ + +> A Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set. The price for efficiency is that it is probabilistic in nature that means, there might be some False Positive results. + +``` +INITIAL BITS +0 0 0 0 0 0 0 0 0 0 + +INSERT X {0, 3, 7} +(1) 0 0 (1) 0 0 0 (1) 0 0 + +INSERT Y {2, 3, 4} +1 0 (1) (1) (1) 0 0 1 0 0 + +SEARCH Z {1, 4, 7} +1 (0) 1 1 (1) 0 0 (1) 0 0 <== Probably Present + +SEARCH W {1, 5, 6} +1 (0) 1 1 1 (0) (0) 1 0 0 <== Definitely NOT Present +``` + +Properties +- Unlike a standard hash table, a Bloom filter of a fixed size can represent a set with an arbitrarily large number of elements. +- Bloom filters never generate false negative result +- Adding an element never fails. However, the false positive rate increases steadily as elements are added until all bits in the filter are set to 1, at which point all queries yield a positive result. +- Deleting elements from filter is not possible because, if we delete a single element by clearing bits at indices generated by k hash functions, it might cause deletion of few other elements. + +LRU Cache +--------- + +> Discards the least recently used items first. This algorithm requires keeping track of what was used when, which is expensive if one wants to make sure the algorithm always discards the least recently used item. + +``` + A B C D E F C G +-------------------------------------------- +[ ] [A] [B] [C] [D] [E] [F] [C] [G] +[ ] [ ] [A] [B] [C] [D] [E] [F] [C] +[ ] [ ] [ ] [A] [B] [C] [D] [E] [F] +[ ] [ ] [ ] [ ] [A] [B] [C] [D] [E] +[ ] [ ] [ ] [ ] [ ] [A] [B] [B] [D] +-------------------------------------------- + [A] [B] +``` + +Properties +- Native Implementation: Hash Table + Doubly LinkedList +- Python API Implementation: `collections.OrderedDict()` +- O(1) lookup +- O(1) update + +Leetcode Problems +- [146. LRU Cache](https://leetcode.com/problems/lru-cache/) diff --git a/08/bloom_filter.py b/08/bloom_filter.py new file mode 100644 index 000000000..36e25d554 --- /dev/null +++ b/08/bloom_filter.py @@ -0,0 +1,29 @@ +""" +Bloom Filter +""" +from bitarray import bitarray +import mmh3 + +class BloomFilter: + def __init__(self, size, hashes): + self.size = size + self.hashes = hashes + self.bit_array = bitarray(size) + self.bit_array.setall(0) + + def add(self, s): + for seed in range(self.hashes): + result = mmh3.hash(s, seed) % self.size + self.bit_array[result] = 1 + + def lookup(self, s): + for seed in range(self.hashes): + result = mmh3.hash(s, seed) % self.size + if self.bit_array[result] == 0: + return 'Nope' + return 'Probably' + +bf = BloomFilter(500000, 7) +bf.add("bloom") +print(bf.lookup("bloom")) +print(bf.lookup("filter")) \ No newline at end of file diff --git a/08/hamming_weight.py b/08/hamming_weight.py new file mode 100644 index 000000000..473cf9722 --- /dev/null +++ b/08/hamming_weight.py @@ -0,0 +1,28 @@ +""" +191. Number of 1 Bits +https://leetcode.com/problems/number-of-1-bits/ +""" +class Solution: + def hammingWeight1(self, n: int) -> int: + bits = 0 + while n != 0: + bits += 1 + n &= (n - 1) # drop lowest set bit + return bits + + def hammingWeight2(self, n: int) -> int: + bits = 0 + mask = 1 + for i in range(32): + if (n & mask) != 0: + bits += 1 + mask <<= 1 + return bits + +solution = Solution() +print(solution.hammingWeight1(0b00000000000000000000000000001011)) # 3 +print(solution.hammingWeight1(0b00000000000000000000000010000000)) # 1 +print(solution.hammingWeight1(0b11111111111111111111111111111101)) # 31 +print(solution.hammingWeight2(0b00000000000000000000000000001011)) # 3 +print(solution.hammingWeight2(0b00000000000000000000000010000000)) # 1 +print(solution.hammingWeight2(0b11111111111111111111111111111101)) # 31 \ No newline at end of file diff --git a/08/power_of_two.py b/08/power_of_two.py new file mode 100644 index 000000000..137e25ae8 --- /dev/null +++ b/08/power_of_two.py @@ -0,0 +1,14 @@ +""" +231. Power of Two +https://leetcode.com/problems/power-of-two/ +""" +class Solution: + def isPowerOfTwo(self, n: int) -> bool: + if n == 0: + return False + return n & (-n) == n + +solution = Solution() +print(solution.isPowerOfTwo(1)) # True +print(solution.isPowerOfTwo(16)) # True +print(solution.isPowerOfTwo(218)) # False \ No newline at end of file diff --git a/08/reverse_bits.py b/08/reverse_bits.py new file mode 100644 index 000000000..f1c5740e7 --- /dev/null +++ b/08/reverse_bits.py @@ -0,0 +1,27 @@ +""" +190. Reverse Bits +https://leetcode.com/problems/reverse-bits/ +""" +class Solution: + def reverseBits1(self, x: int) -> int: + result = 0 + power = 31 + while x: + result += (x & 1) << power + x >>= 1 + power -= 1 + return result + + def reverseBits2(self, n: int) -> int: + n = (n >> 16) | (n << 16) + n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8) + n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4) + n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2) + n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1) + return n + +solution = Solution() +print(bin(solution.reverseBits1(0b00000010100101000001111010011111))) +print(bin(solution.reverseBits1(0b11111111111111111111111111111101))) +print(bin(solution.reverseBits2(0b00000010100101000001111010011111))) +print(bin(solution.reverseBits2(0b11111111111111111111111111111101))) \ No newline at end of file diff --git a/09/NOTE.md b/09/NOTE.md new file mode 100644 index 000000000..43dd3a717 --- /dev/null +++ b/09/NOTE.md @@ -0,0 +1,115 @@ +# 09 + +String +------ + +String Operations +```py +s.strip([chars]) # return a copy of the string with the leading and trailing characters removed. +s.startswith(prefix) # return True if string starts with the prefix, False otherwise. +s.endswith(prefix) # return True if string starts with the prefix, False otherwise. +s.slipt(delimiter) # return a list of the words of the string s. +s.lower() # return a copy of the string with all the lowercase characters +s.upper() # return a copy of the string with all the uppercase characters +ord(c) # the unicode code representation of the char +ord(c) - ord('a') # the position of the char in 26 letters +chr(i) # string representation of the char unicode code +``` + +String Constants +```py +import string + +string.digits # the string '0123456789' +string.hexdigits # the string '0123456789abcdefABCDEF' +string.octdigits # the string '01234567' +string.ascii_lowercase # the uppercase letters 'abcdefghijklmnopqrstuvwxyz' +string.ascii_letters # The lowercase letters 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +string.letters # The concatenation of the ascii_lowercase and ascii_uppercase +``` + +Sliding Window Template +```py +from collections import Counter + +def slidingWindow(s, t): + window = Counter() + target = Counter(t) + + valid = 0 + left = right = 0 + while right < len(s): + # c is the element to be inserted into the window + c = s[right] + # if we insert it, is the window still valid? + if c in target: + window[c] += 1 + if window[c] == target[c]: + valid += 1 + # expand the current window + right += 1 + + print(s[left: right]) + + # when we found a valid window + while right - left >= len(t): + # check the answer or update the result + if valid == len(target): + ... + + # d is the element to be removed from the window + d = s[left] + # if we remove it, is the window still valid? + if d in target: + if window[d] == target[d]: + valid -= 1 + window[d] -= 1 + # shrink the current window + left += 1 +``` + +String Basics +- [709. To Lower Case](https://leetcode.com/problems/to-lower-case/) +- [58. Length of Last Word](https://leetcode.com/problems/length-of-last-word/) +- [771. Jewels and Stones](https://leetcode.com/problems/jewels-and-stones/) +- [387. First Unique Character in a String](https://leetcode.com/problems/first-unique-character-in-a-string/) +- [8. String to Integer (atoi)](https://leetcode.com/problems/string-to-integer-atoi/) + +String Operations +- [14. Longest Common Prefix](https://leetcode.com/problems/longest-common-prefix/) +- [344. Reverse String](https://leetcode.com/problems/reverse-string/) +- [541. Reverse String II](https://leetcode.com/problems/reverse-string-ii/) +- [151. Reverse Words in a String](https://leetcode.com/problems/reverse-words-in-a-string/) +- [557. Reverse Words in a String III](https://leetcode.com/problems/reverse-words-in-a-string-iii/) +- [917. Reverse Only Letters](https://leetcode.com/problems/reverse-only-letters/) + +Palindrome +- [125. Valid Palindrome](https://leetcode.com/problems/valid-palindrome/) +- [680. Valid Palindrome II](https://leetcode.com/problems/valid-palindrome-ii/) +- [5. Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring/) + +Anagram +- [242. Valid Anagram](https://leetcode.com/problems/valid-anagram/) +- [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) +- [438. Find All Anagrams in a String](https://leetcode.com/problems/find-all-anagrams-in-a-string/) + +Sliding Window +- [3. Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/) +- [76. Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/) +- [438. Find All Anagrams in a String](https://leetcode.com/problems/find-all-anagrams-in-a-string/) +- [567. Permutation in String](https://leetcode.com/problems/permutation-in-string/) + +Advanced String Problems +- [10. Regular Expression Matching](https://leetcode.com/problems/regular-expression-matching/) +- [44. Wildcard Matching](https://leetcode.com/problems/wildcard-matching/) +- [115. Distinct Subsequences](https://leetcode.com/problems/distinct-subsequences/) + +String-searching algorithms +----------- + +| algorithm | Preprocessing Time | Matching Time | Space | +| ---------- | :----------------: | :-----------: | :---: | +| Naive | None | O(mn) | None | +| Rabin-Karp | O(m) | O(n+m) | O(1) | +| KMP | O(m) | O(n) | O(m) | +| Boyer-Moore | O(m+k) | O(mn) | O(k) | \ No newline at end of file diff --git a/09/first-unique-character.py b/09/first-unique-character.py new file mode 100644 index 000000000..b41d1692f --- /dev/null +++ b/09/first-unique-character.py @@ -0,0 +1,18 @@ +""" +387. First Unique Character in a String +https://leetcode.com/problems/first-unique-character-in-a-string/ +""" +from collections import Counter + +class Solution: + def firstUniqChar(self, s: str) -> int: + if not s: return -1 + cnt = Counter(s) + for i, c in enumerate(s): + if cnt[c] == 1: + return i + return -1 + +solution = Solution() +print(solution.firstUniqChar("leetcode")) +print(solution.firstUniqChar("loveleetcode")) \ No newline at end of file diff --git a/09/jewels-and-stones.py b/09/jewels-and-stones.py new file mode 100644 index 000000000..2df0d0628 --- /dev/null +++ b/09/jewels-and-stones.py @@ -0,0 +1,14 @@ +""" +771. Jewels and Stones +https://leetcode.com/problems/jewels-and-stones/ +""" +from collections import Counter + +class Solution: + def numJewelsInStones(self, J: str, S: str) -> int: + if not J or not S: return 0 + return sum([1 for s in S if s in Counter(J)]) + +solution = Solution() +print(solution.numJewelsInStones("aA", "aAAbbbb")) +print(solution.numJewelsInStones("z", "ZZ")) \ No newline at end of file diff --git a/09/length-of-last-word.py b/09/length-of-last-word.py new file mode 100644 index 000000000..df7d62e15 --- /dev/null +++ b/09/length-of-last-word.py @@ -0,0 +1,20 @@ +""" +58. Length of Last Word +https://leetcode.com/problems/length-of-last-word/ +""" +class Solution: + def lengthOfLastWord(self, s: str) -> int: + if not s: return 0 + count = 0 + i = len(s) - 1 + while i >= 0 and s[i] == ' ': + i -= 1 + while i >= 0 and s[i] != ' ': + count += 1 + i -= 1 + return count + +solution = Solution() +print(solution.lengthOfLastWord("Hello World")) +print(solution.lengthOfLastWord("")) +print(solution.lengthOfLastWord(" test ")) \ No newline at end of file diff --git a/09/longest-common-prefix.py b/09/longest-common-prefix.py new file mode 100644 index 000000000..c4e46fdfa --- /dev/null +++ b/09/longest-common-prefix.py @@ -0,0 +1,21 @@ +""" +14. Longest Common Prefix +https://leetcode.com/problems/longest-common-prefix/ +""" +from typing import List + +class Solution: + def longestCommonPrefix(self, strs: List[str]) -> str: + if not strs: return '' + m, n = len(strs[0]), len(strs) + for i in range(m): + c = strs[0][i] + if any(i == len(strs[j]) or strs[j][i] != c for j in range(1, n)): + return strs[0][:i] + return strs[0] + +solution = Solution() +print(solution.longestCommonPrefix(["flower","flow","flight"])) # "fl" +print(solution.longestCommonPrefix(["dog","racecar","car"])) # "" +print(solution.longestCommonPrefix(["a"])) # "a" +print(solution.longestCommonPrefix(["aa", "a"])) # "a" \ No newline at end of file diff --git a/09/string-to-integer-atoi.py b/09/string-to-integer-atoi.py new file mode 100644 index 000000000..936ab4116 --- /dev/null +++ b/09/string-to-integer-atoi.py @@ -0,0 +1,56 @@ +""" +8. String to Integer (atoi) +https://leetcode.com/problems/string-to-integer-atoi/ +""" +import string + +class Solution: + def myAtoi(self, str: str) -> int: + if not str: + return 0 + + INT_MIN = -2**31 + INT_MAX = 2**31 - 1 + + n = len(str) + i = 0 + while i < n and str[i] == ' ': + i += 1 + if i >= n: return 0 + + sign = 1 + if str[i] == '+': + i += 1 + elif str[i] == '-': + sign = -1 + i += 1 + if i >= n: return 0 + + start = i + while i < n and str[i] != ' ': + if str[i] not in string.digits: + break + i += 1 + + k = 0 + res = 0 + for j in range(i - 1, start - 1, -1): + res += (ord(str[j]) - ord('0')) * (10**k) + k += 1 + + res = res * sign + if INT_MIN <= res <= INT_MAX: + return res + return INT_MAX if res > 0 else INT_MIN + +solution = Solution() +print(solution.myAtoi("42")) +print(solution.myAtoi(" -42")) +print(solution.myAtoi("4193 with words")) +print(solution.myAtoi("words and 987")) +print(solution.myAtoi("-91283472332")) +print(solution.myAtoi("")) +print(solution.myAtoi(" ")) +print(solution.myAtoi("+")) +print(solution.myAtoi(" -0012a99")) +print(solution.myAtoi(" 3.1415926")) \ No newline at end of file diff --git a/09/to-lower-case.py b/09/to-lower-case.py new file mode 100644 index 000000000..f7f9822fb --- /dev/null +++ b/09/to-lower-case.py @@ -0,0 +1,21 @@ +""" +709. To Lower Case +https://leetcode-cn.com/problems/to-lower-case/ +""" +import string + +class Solution: + def toLowerCase(self, str: str) -> str: + if not str: return str + res = [''] * len(str) + for i, c in enumerate(str): + if c in string.ascii_uppercase: + res[i] = chr(ord(c) + 32) + else: + res[i] = c + return ''.join(res) + +solution = Solution() +print(solution.toLowerCase("Hello")) +print(solution.toLowerCase("here")) +print(solution.toLowerCase("LOVELY")) \ No newline at end of file diff --git a/10/NOTE.md b/10/NOTE.md new file mode 100644 index 000000000..927344293 --- /dev/null +++ b/10/NOTE.md @@ -0,0 +1,72 @@ +# 10 System Design + +## 1. Requirement Clarifications + +- Ask Questions! Define System Scope +- Clarify what parts of system to focus on +- Functional Requirements + - From an end user perspective +- None Functional Requirements + - Highly Available + - Acceptable Latency (realtime?) + - Data Consistency + - Highly Reliable (no data lost) + +## 2. Capacity Estimation + +- User Estimates + - total Users (500M) + - daily active users (1M) +- Traffic/Bandwidth Estimates + - read/write ratio (100:1) + - number of *entities* generated per month (writes) + - transaction per second (TPS) +- Storage Estimates + - how long will the data store? (5 years? 10 years?) + - size of each stored object + - calculate total storage + +## 3. API Design + +- postData(api_key, user_id, data, data_location, user_location, timestamp, …) +- generateTimeline(user_id, current_time, user_location, ...) +- markFavorite(user_id, item_id, timestamp, ...) + +## 4. Define Data Model + +What database to use? NoSQL? Relational? + +- **User**: UserID (PK), Name, Email, DoB, CreationDate, LastLogin, etc. +- **Tweet**: TweetID (PK), Content, TweetLocation, NumberOfLikes, TimeStamp, etc. +- **UserFollow**: UserID1, UserID2 (combined PK) +- **FavoriteTweets**: UserID, TweetID (combined PK), TimeStamp + +## 5. High-Level Design + +``` + | App Server | + / | ... | --> cache --> Databases +client --> load Balancer - | App Server | + \ | ... | --> cache --> File Storage + | App Server | +``` + +## 6. Detailed Design + +Dig Deeper into two or three major components. + +- Data Partitioning (Sharding based on user_id? item_id? creation_date?) +- Data Flow (Pull? Push? or Hybrid?) +- Offline Generation Data (pre generate aggregate data for fast retrieval) +- Key Generation Service (pre generate keys instead of realtime hashing) +- Caching +- Load Balancer +- Fault Tolerance + +## 7. Identify and Resolve Bottleneck + +- Do we have Single point of failure? how to mitigate it? +- Do we have replicas of the data? +- Do we have Failover services available? +- How to handle "hot" users? +- How to monitoring the performance of our service? Do we get fail alerts? diff --git a/README.md b/README.md index c2f794952..49d3e4bf2 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,49 @@ -# 极客大学「算法训练营-第10期」作业提交仓库 - - -## 讲师课件下载地址 - -请大家通过该链接查看讲师课件并进行下载,链接:https://pan.baidu.com/s/1GOuXJfnlERQs8bI8HNwPrQ 密码:zlua - - -## 仓库目录结构说明 - -1. `week01/` 代表第一周作业提交目录,以此类推。 -2. 请在对应周的目录下新建或修改自己的代码作业。 -2. 每周均有一个 `REDAME.md` 文档,你可以将自己当周的学习心得以及做题过程中的思考记录在该文档中。 - -## 作业提交规则 - -1. 先将本仓库 Fork 到自己 GitHub 账号下。 -2. 将 Fork 后的仓库 Clone 到本地,然后在本地仓库中对应周的目录下新建或修改自己的代码作业,当周的学习总结写在对应周的README.md文件里。 -3. 在本地仓库完成作业后,push 到自己的 GitHub 远程仓库。 -4. 最后将远程仓库中当周的作业链接,按格式贴到班级仓库对应学习周的issue下面。 -5. 提交issue请务必按照规定格式进行提交,否则作业统计工具将抓取不到你的作业提交记录。 - -详细的作业提交流程可以查阅:https://shimo.im/docs/m5rtM8K8rNsjw5jk/ - - -## 注意事项 - - 如果对 Git 和 GitHub 不太了解,请参考 [Git 官方文档](https://git-scm.com/book/zh/v2) 或者极客时间的[《玩转 Git 三剑客》](https://time.geekbang.org/course/intro/145)视频课程。 +# Data Structure and Algorithm Notes + +[NOTE 01](01/NOTE.md) +- Array +- Linked List +- Stack and Queue + +[NOTE 02](02/NOTE.md) +- Hash Table +- Binary Tree +- Binary Search Tree (BST) +- Heap + +[NOTE 03](03/NOTE.md) +- Recursion +- Divide and Conquer +- Backtrack + +[NOTE 04](04/NOTE.md) +- Graph +- Depth-first search (DFS) +- Breadth-first search (BFS) +- A-Star Search + +[NOTE 05](05/NOTE.md) +- Sorting Algorithms +- Searching Algorithms + +[NOTE 06](06/NOTE.md) +- Dynamic Programming +- Greedy + +[NOTE 07](07/NOTE.md) +- Skip List +- Trie +- Disjoint-set +- Self-balancing BST (AVL tree, Red-Black tree, B- tree, B+ tree) + +[NOTE 08](08/NOTE.md) +- Primitives +- Bloom Filter +- LRU Cache + +[NOTE 09](09/NOTE.md) +- String +- String Searching algorithms (Naive, Rabin-Karp, KMP, Boyer-Moore) + +[NOTE 10](10/NOTES.md) +- System Design \ No newline at end of file diff --git a/Week01/NOTE.md b/Week01/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week01/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week02/NOTE.md b/Week02/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week02/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week03/NOTE.md b/Week03/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week03/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week04/NOTE.md b/Week04/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week04/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week06/NOTE.md b/Week06/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week06/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week07/NOTE.md b/Week07/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week07/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week08/NOTE.md b/Week08/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week08/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/Week09/NOTE.md b/Week09/NOTE.md deleted file mode 100644 index 50de30414..000000000 --- a/Week09/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file