├── .gitignore ├── sieve_prime.py ├── fizzbuzz.py ├── year_max_pop.py ├── priority_queue.py ├── lc_reverse_int.py ├── lc_two_sum.py ├── lc_longest_nonrepeat_substring.py ├── binary_tree_algos.py ├── lc_add_number_reverse.py ├── reverse_words.py ├── substring_two_chars.py ├── subarray_sum.py ├── lc_longest_palindrome.py ├── anagrams.py ├── lc_zigzag_convert.py ├── delim_balanced.py ├── linked_list.py ├── binary_heap.py ├── lc_median_arrays.py ├── binary_search_tree.py ├── README.md ├── trie.py ├── sort.py └── ghost.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /sieve_prime.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Using Sieve of Eratosthenes algorithm to return a generator of 3 | all primes under a threshold. 4 | ''' 5 | 6 | 7 | def _test_div(n): 8 | ''' 9 | Check if in the input m is divisible by n 10 | ''' 11 | def _divisible_by_n(m): 12 | return m % n == 0 13 | return _divisible_by_n 14 | 15 | 16 | def prime_under(threshold): 17 | div_tests = [] 18 | 19 | for n in range(2, threshold): 20 | if not any(map(lambda test: test(n), div_tests)): 21 | div_tests.append(_test_div(n)) 22 | yield n 23 | 24 | 25 | primes = prime_under(1000) 26 | for prime in primes: 27 | print(prime) 28 | -------------------------------------------------------------------------------- /fizzbuzz.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Write a program that prints the numbers from 1 to 100. 3 | But for multiples of three print “Fizz” instead of the number and 4 | for the multiples of five print “Buzz”. For numbers which are multiples 5 | of both three and five print “FizzBuzz”. 6 | ''' 7 | 8 | 9 | def fizzbuzz(): 10 | results = [] 11 | for i in range(1, 101): 12 | if i % 15 == 0: 13 | results.append('fizzbuzz') 14 | elif i % 3 == 0: 15 | results.append('fizz') 16 | elif i % 5 == 0: 17 | results.append('buzz') 18 | else: 19 | results.append(str(i)) 20 | return ' '.join(results) 21 | 22 | 23 | print(fizzbuzz()) 24 | -------------------------------------------------------------------------------- /year_max_pop.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Given a list of people with their birth and end years, 3 | between a specific start year and end year, 4 | find the year with the most number of people alive. 5 | ''' 6 | import numpy as np 7 | 8 | 9 | def year_max_pop(people, start_year, end_year): 10 | n_years = end_year - start_year + 1 11 | pops = np.zeros(n_years) 12 | for birth, death in people: 13 | assert birth <= death 14 | idx = [i - 1900 for i in range(birth, death + 1)] 15 | pops[idx] += 1 16 | return np.argmax(pops) + 1900 17 | 18 | 19 | people = [[1900, 1988], [1932, 2000], [1957, 1957], 20 | [1956, 1999], [1935, 1945], [1920, 1944], 21 | [1998, 2000]] 22 | assert year_max_pop(people, 1900, 2000) == 1935 23 | -------------------------------------------------------------------------------- /priority_queue.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from binary_heap import BinaryHeap 4 | 5 | 6 | class PriorityQueue(object): 7 | def __init__(self): 8 | self._heap = BinaryHeap() 9 | 10 | def peek(self): 11 | return self._heap.peek_min() 12 | 13 | def is_empty(self): 14 | return self._heap.is_empty() 15 | 16 | def enqueue(self, value): 17 | self._heap.insert(value) 18 | 19 | def dequeue(self): 20 | self._heap.extract_min() 21 | 22 | def __iter__(self): 23 | yield from iter(self._heap) 24 | 25 | def __len__(self): 26 | return len(self._heap) 27 | 28 | 29 | def test_priority_queue(): 30 | pq = PriorityQueue() 31 | values = random.sample(range(-15, 15), 30) 32 | for v in values: 33 | pq.enqueue(v) 34 | print(list(pq)) 35 | 36 | for v in iter(pq): 37 | print(v) 38 | 39 | 40 | test_priority_queue() 41 | -------------------------------------------------------------------------------- /lc_reverse_int.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is from leetcode.com 3 | https://leetcode.com/problems/reverse-integer/ 4 | 5 | Given a 32-bit signed integer, reverse digits of an integer. 6 | 7 | Example 1: 8 | Input: 123 9 | Output: 321 10 | 11 | Example 2: 12 | Input: -123 13 | Output: -321 14 | 15 | Example 3: 16 | Input: 120 17 | Output: 21 18 | 19 | Note: 20 | Assume we are dealing with an environment which could only hold integers 21 | within the 32-bit signed integer range. For the purpose of this problem, 22 | assume that your function returns 0 when the reversed integer overflows. 23 | ''' 24 | 25 | 26 | class Solution(object): 27 | def helper(self, x): 28 | if x < 10: 29 | return str(x) 30 | return str(x % 10) + self.helper(x // 10) 31 | 32 | def reverse(self, x): 33 | """ 34 | :type x: int 35 | :rtype: int 36 | """ 37 | r = int(self.helper(abs(x))) 38 | return (r < 2 ** 31) * ((x > 0) - (x < 0)) * r 39 | 40 | 41 | solver = Solution() 42 | assert solver.reverse(-123) == -321 43 | assert solver.reverse(120) == 21 44 | assert solver.reverse(10) == 1 45 | assert solver.reverse(-1) == -1 46 | -------------------------------------------------------------------------------- /lc_two_sum.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Problem on leetcode.com 3 | https://leetcode.com/problems/two-sum/description/ 4 | 5 | Given an array of integers, return indices of the two numbers such that 6 | they add up to a specific target. 7 | 8 | You may assume that each input would have exactly one solution, 9 | and you may not use the same element twice. 10 | 11 | Example: 12 | Given nums = [2, 7, 11, 15], target = 9, 13 | 14 | Because nums[0] + nums[1] = 2 + 7 = 9, 15 | return [0, 1]. 16 | 17 | Note: I didn't come up with the solution. 18 | This elegant solution was posted as a comment by xiaohua on leetcode 19 | ''' 20 | 21 | 22 | def two_sum(nums, target): 23 | """ 24 | :type nums: List[int] 25 | :type target: int 26 | :rtype: List[int] 27 | """ 28 | seen = {} 29 | for idx, value in enumerate(nums): 30 | compl = target - value 31 | 32 | if compl in seen: 33 | return [seen[compl], idx] 34 | 35 | seen[value] = idx 36 | return [] 37 | 38 | 39 | assert two_sum([2, 3, 5, 1], 6) == [2, 3] 40 | assert two_sum([2, 3, 5, 1], 8) == [1, 2] 41 | assert two_sum([2, 3, 5, 1], 10) == [] 42 | assert two_sum([2, 3, 3, 1], 6) == [1, 2] 43 | -------------------------------------------------------------------------------- /lc_longest_nonrepeat_substring.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is from leetcode 3 | https://leetcode.com/problems/longest-substring-without-repeating-characters/description/ 4 | 5 | Given a string, find the length of the longest substring without 6 | repeating characters. 7 | 8 | Examples: 9 | Given "abcabcbb", the answer is "abc", which the length is 3. 10 | Given "bbbbb", the answer is "b", with the length of 1. 11 | Given "pwwkew", the answer is "wke", with the length of 3. 12 | 13 | Note that the answer must be a substring, "pwke" is a subsequence and not 14 | a substring. 15 | ''' 16 | 17 | 18 | class Solution(object): 19 | def lengthOfLongestSubstring(self, s): 20 | """ 21 | :type s: str 22 | :rtype: int 23 | """ 24 | if len(s) <= 1: 25 | return len(s) 26 | max_str = curr_blob = s[:1] 27 | for char in s[1:]: 28 | if char not in set(list(curr_blob)): 29 | curr_blob += char 30 | else: 31 | idx = curr_blob.find(char) 32 | curr_blob = curr_blob[idx + 1:] + char 33 | if len(curr_blob) > len(max_str): 34 | max_str = curr_blob 35 | return len(max_str) 36 | -------------------------------------------------------------------------------- /binary_tree_algos.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Algorithms available: 3 | is_bst(root): 4 | input: root of a binary tree 5 | output: True if the binary tree is a binary search tree. 6 | False otherwise 7 | The idea is that if it's a binary search tree, when you traverse 8 | the tree in-order, the values will always be non-decreasing. 9 | 10 | Note that there are two versions of this function. 11 | The first one is how it should be. 12 | The second one is the one accepted by HackerRank. 13 | ''' 14 | 15 | 16 | def is_bst(root): 17 | return _is_bst_helper(root, None) 18 | 19 | 20 | def _is_bst_helper(node, prev): 21 | if not node: 22 | return True 23 | if not _is_bst_helper(node.left, prev): 24 | return False 25 | if prev and prev.data >= node.data: 26 | return False 27 | return _is_bst_helper(node.right, node) 28 | 29 | 30 | def is_bst_hr(root): 31 | ''' The solution accepted by HackerRank''' 32 | nodes = _in_order_traversal(root, []) 33 | prev = None 34 | seen = set() 35 | for node in nodes: 36 | if node.data in seen: 37 | return False 38 | if prev and prev >= node.data: 39 | return False 40 | seen.add(node.data) 41 | 42 | 43 | def _in_order_traversal(root, nodes): 44 | if not root: 45 | return nodes 46 | new_nodes = _in_order_traversal(root.left, nodes) 47 | new_nodes.append(root) 48 | new_nodes = _in_order_traversal(root.right, new_nodes) 49 | return new_nodes 50 | -------------------------------------------------------------------------------- /lc_add_number_reverse.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a problem on leetcode.com 3 | https://leetcode.com/problems/add-two-numbers/description/ 4 | 5 | You are given two non-empty linked lists of non-negative integers. 6 | The digits are stored in reverse order and each of their nodes 7 | contain a single digit. 8 | 9 | Add the two numbers and return it as a linked list. 10 | 11 | You may assume the two numbers do not contain any leading zero, 12 | except the number 0 itself. 13 | 14 | Input: (2 -> 4 -> 3) + (5 -> 6 -> 4) 15 | Output: 7 -> 0 -> 8 16 | ''' 17 | 18 | 19 | class ListNode(object): 20 | def __init__(self, x): 21 | self.val = x 22 | self.next = None 23 | 24 | 25 | class Solution(object): 26 | def _addHelper(self, l1, l2, head, curr, carry_over): 27 | if not l1 and not l2: 28 | if carry_over == 1: 29 | curr.next = ListNode(1) 30 | return head 31 | 32 | if not l1: 33 | val = l2.val + carry_over 34 | l2 = l2.next 35 | elif not l2: 36 | val = l1.val + carry_over 37 | l1 = l1.next 38 | else: 39 | val = l1.val + l2.val + carry_over 40 | l2 = l2.next 41 | l1 = l1.next 42 | 43 | next_node = ListNode(val % 10) 44 | 45 | if not head: 46 | head = next_node 47 | curr = head 48 | else: 49 | curr.next = next_node 50 | curr = next_node 51 | 52 | return self._addHelper(l1, l2, head, curr, val // 10) 53 | 54 | def addTwoNumbers(self, l1, l2): 55 | """ 56 | :type l1: ListNode 57 | :type l2: ListNode 58 | :rtype: ListNode 59 | """ 60 | return self._addHelper(l1, l2, None, None, 0) 61 | -------------------------------------------------------------------------------- /reverse_words.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is taken from Stanford's CS 9: Problem-Solving for the CS Technical Interview. 3 | 4 | Question statement: 5 | Reverse all the words in a string of words and spaces / tabs 6 | while preserving the word order and spacing. 7 | Example 8 | 'moo cow bark dog' -> 'oom woc krab god' 9 | 10 | Potential pitfalls: 11 | Don't reverse the entire string -- preserve the word ordering and the spaces. 12 | We make no guarantee about how many spaces / tabs there are between words, 13 | so you can't split (using the .split() method) and then reverse 14 | the words individually. 15 | 16 | My solution: 17 | Start with current word to be None. 18 | Iterate over each character in the string. 19 | + if it's space/tabs: 20 | + if the current word is not None, reverse it and add it to result. 21 | + add current character to the result. 22 | + else: 23 | + if the previous character is space/tab, make it the first 24 | character of current word. 25 | + else, add the current character to the current word 26 | 27 | ''' 28 | 29 | 30 | def reverse_string(string): 31 | if len(string) <= 1: 32 | return string 33 | return string[-1] + reverse_string(string[:-1]) 34 | 35 | 36 | def reverse_words(string): 37 | curr_word = '' 38 | results = '' 39 | for char in string: 40 | if char == ' ' or char == '\t': 41 | if not curr_word == '': 42 | results += reverse_string(curr_word) 43 | curr_word = '' 44 | results += char 45 | else: 46 | curr_word += char 47 | results += reverse_string(curr_word) 48 | return results 49 | 50 | 51 | assert reverse_words('moo cow bark dog') == 'oom woc krab god' 52 | assert reverse_words('moo ') == 'oom ' 53 | assert reverse_words(' moo moo') == ' oom oom' 54 | assert reverse_words(' ') == ' ' 55 | -------------------------------------------------------------------------------- /substring_two_chars.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Question statement: 3 | Return the longest contiguous substring of 2 distinct characters from an 4 | input string. 5 | 6 | Example 7 | input: abbaacab 8 | output : abbaa 9 | 10 | input: abcefabbabaabefghghfa 11 | return: abbabaab 12 | 13 | input: aabceddddcdccecabceftg 14 | return: ddddcdcc 15 | 16 | input: acbabbcbca 17 | return : bbcbc 18 | ''' 19 | 20 | 21 | def max_contiguous(string): 22 | if len(string) <= 2: 23 | return string 24 | 25 | curr_max_str = curr_blob = string[:2] 26 | char0, char1 = string[0], string[1] 27 | last_char = char1 28 | if char0 == char1: 29 | last_contiguous_idx = 0 30 | last_last_char = None 31 | else: 32 | last_contiguous_idx = 1 33 | last_last_char = char0 34 | 35 | for i, char in enumerate(string[2:]): 36 | if last_last_char is None: 37 | curr_blob += char 38 | 39 | if char != last_char: 40 | last_contiguous_idx = i + 2 41 | last_last_char = last_char 42 | last_char = char 43 | else: 44 | if char == last_char: 45 | curr_blob += char 46 | elif char == last_last_char: 47 | curr_blob += char 48 | last_contiguous_idx = i + 2 49 | last_last_char = last_char 50 | last_char = char 51 | else: 52 | curr_blob = string[last_contiguous_idx: i + 3] 53 | last_contiguous_idx = i + 2 54 | last_last_char = last_char 55 | last_char = char 56 | 57 | if len(curr_blob) > len(curr_max_str): 58 | curr_max_str = curr_blob[:] 59 | 60 | return curr_max_str 61 | 62 | 63 | assert max_contiguous('abbaacab') == 'abbaa' 64 | assert max_contiguous('abcefabbabaabefghghfa') == 'abbabaab' 65 | assert max_contiguous('aabceddddcdccecabceftg') == 'ddddcdcc' 66 | assert max_contiguous('acbabbcbca') == 'bbcbc' 67 | assert max_contiguous('') == '' 68 | assert max_contiguous('aaaaaaaa') == 'aaaaaaaa' 69 | assert max_contiguous( 70 | 'aaaaabbbbb3dasfa938209320202020202020202') == '20202020202020202' 71 | assert max_contiguous( 72 | 'aaaaabbbbb3dasfa938209320202020202020202afdafsfasdweeweeeeee') == '20202020202020202' 73 | -------------------------------------------------------------------------------- /subarray_sum.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is taken from Stanford's CS 9: Problem-Solving for 3 | the CS Technical Interview. 4 | http://web.stanford.edu/class/cs9/sample_probs/SubarraySums.pdf 5 | 6 | A subarray of an array is a consecutive sequence of zero or more values taken 7 | out of that array. For example, the array [1, 3, 7] has seven subarrays: 8 | [ ] [1] [3] [7] [1, 3] [3, 7] [1, 3, 7] 9 | 10 | Notice that [1, 7] is not a subarray of [1, 3, 7], because even though the 11 | values 1 and 7 appear in the array, they're not consecutive in the array. 12 | 13 | Similarly, the array [7, 3] isn't a subarray of the original array, 14 | because these values are in the wrong order. 15 | 16 | The sum of an array is the sum of all the values in that array. Your task is 17 | to write a function that takes as input an array and outputs the sum of all of 18 | its subarrays. 19 | 20 | For example, given [1, 3, 7], you'd output 36, because: 21 | 22 | [ ] + [1] + [3] + [7] + [1, 3] + [3, 7] + [1, 3, 7] 23 | = 0 + 1 + 3 + 7 + 4 + 10 + 11 = 36 24 | 25 | My solution: 26 | subarray_sum_slow: the easy but slow way to do it. Just enumberate all the 27 | subarrays and sum them up. 28 | subarray_sum: the fast way to do it. We can calculate the number of times 29 | each element occurs and sum them up. 30 | ''' 31 | 32 | 33 | def get_subarrays(arr): 34 | subarrays = [] 35 | for i in range(len(arr)): 36 | for j in range(i, len(arr)): 37 | subarrays.append(arr[i:j + 1]) 38 | return subarrays 39 | 40 | 41 | def subarray_sum_slow(arr): 42 | subarrays = get_subarrays(arr) 43 | return sum([sum(arr) for arr in subarrays]) 44 | 45 | 46 | def subarray_sum(arr): 47 | total = 0 48 | for i, num in enumerate(arr): 49 | total += num * (i + 1) * (len(arr) - i) 50 | return total 51 | 52 | 53 | assert subarray_sum_slow([1]) == subarray_sum([1]) 54 | assert subarray_sum_slow([1, 2]) == subarray_sum([1, 2]) 55 | assert subarray_sum_slow([1, 2, 3]) == subarray_sum([1, 2, 3]) 56 | assert subarray_sum_slow([1, 3, 7]) == subarray_sum([1, 3, 7]) 57 | assert subarray_sum_slow([1, 3, 7, 9]) == subarray_sum([1, 3, 7, 9]) 58 | assert subarray_sum_slow([1, 3, 7, 9, 11]) == subarray_sum([1, 3, 7, 9, 11]) 59 | assert subarray_sum_slow( 60 | [1, 3, 7, 9, 11, 13]) == subarray_sum([1, 3, 7, 9, 11, 13]) 61 | -------------------------------------------------------------------------------- /lc_longest_palindrome.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a medium problem from leetcode. 3 | https://leetcode.com/problems/longest-palindromic-substring/ 4 | 5 | Given a string s, find the longest palindromic substring in s. 6 | You may assume that the maximum length of s is 1000. 7 | 8 | Example: 9 | Input: "babad" 10 | Output: "bab" or 11 | Note: "aba" is also a valid answer. 12 | 13 | Example: 14 | Input: "cbbd" 15 | Output: "bb" 16 | 17 | Note: should solve this in O(n^2) 18 | 19 | Solution idea: 20 | Go over each character in the string and find the longest palindrome 21 | that is centered around that character. 22 | A palindrome that centers around that character can either has odd length 23 | or even length. 24 | If it has odd length, the character is the exact center. 25 | If it has even length, there are two cases: 26 | if the next character is the same as this, their combination is the center. 27 | if not, the longest palindrome with even length centered around 28 | that character is '' 29 | 30 | ''' 31 | 32 | 33 | class Solution(object): 34 | def _helper(self, s, start, end, palin): 35 | while start >= 0 and end <= len(s) - 1: 36 | if s[start] != s[end]: 37 | break 38 | palin = s[start] + palin + s[end] 39 | start -= 1 40 | end += 1 41 | return palin 42 | 43 | def _get_palin(self, s, idx): 44 | palin1 = self._helper(s, idx - 1, idx + 1, s[idx]) 45 | if idx == len(s) - 1 or s[idx] != s[idx + 1]: 46 | return palin1 47 | palin2 = self._helper(s, idx, idx + 1, '') 48 | 49 | return palin2 if len(palin1) <= len(palin2) else palin1 50 | 51 | def longest_palindrome(self, s): 52 | """ 53 | :type s: str 54 | :rtype: str 55 | """ 56 | if len(s) <= 1: 57 | return s 58 | best_palin = s[:1] 59 | for i, char in enumerate(s): 60 | palin = self._get_palin(s, i) 61 | if len(palin) > len(best_palin): 62 | best_palin = palin 63 | return best_palin 64 | 65 | 66 | def test(): 67 | solver = Solution() 68 | assert solver.longest_palindrome('babad') in set(['bab', 'aba']) 69 | assert solver.longest_palindrome('cbbd') == 'bb' 70 | assert solver.longest_palindrome('') == '' 71 | assert solver.longest_palindrome('safljkl23kljaaa') == 'aaa' 72 | assert solver.longest_palindrome( 73 | 'safasdfbbbabbl23kljaaa') in set(['bbabb', 'safas']) 74 | 75 | 76 | test() 77 | -------------------------------------------------------------------------------- /anagrams.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Two strings are said to be anagrams of one another if you can turn the 3 | first string into the second by rearranging its letters. 4 | 5 | For example, "table" and "bleat" are anagrams, as are "tear" and "rate." 6 | Your job is to write a function that takes in two strings as input and 7 | determines whether they're anagrams of one another. 8 | 9 | Solution should run in O(n1 + n2) 10 | There are two solutions: 11 | one is using counter 12 | another one is also using dictionary. I thought it'd be faster, 13 | but thanks @shubham8111 for pointing out, it's faster for strings 14 | of less than 100 characters, then its performance gets worse. 15 | 16 | ''' 17 | from collections import Counter 18 | import random 19 | import string 20 | import time 21 | 22 | 23 | def are_anagrams(str1, str2): 24 | if len(str1) != len(str2): 25 | return False 26 | dict1 = Counter(list(str1)) 27 | dict2 = Counter(list(str2)) 28 | for char in dict1: 29 | if char not in dict2 or dict2[char] != dict1[char]: 30 | return False 31 | return True 32 | 33 | 34 | def are_anagrams_fast(str1, str2): 35 | if len(str1) != len(str2): 36 | return False 37 | 38 | char_dict = {} 39 | for char in str1: 40 | if char not in char_dict: 41 | char_dict[char] = 0 42 | char_dict[char] += 1 43 | 44 | for char in str2: 45 | if char not in char_dict or char_dict[char] <= 0: 46 | return False 47 | char_dict[char] -= 1 48 | return True 49 | 50 | 51 | def test(anagram_fn): 52 | print('Testing ', anagram_fn) 53 | assert anagram_fn('table', 'bleat') 54 | assert not anagram_fn('table', 'bleate') 55 | assert anagram_fn('honey', 'eyhon') 56 | assert not anagram_fn('area', 'are3') 57 | assert not anagram_fn('', ' ') 58 | 59 | 60 | def create_random_anagrams(n=10000): 61 | characters = [] 62 | for c in range(n): 63 | characters.append(random.choice(string.ascii_letters)) 64 | str1 = ''.join(characters) 65 | random.shuffle(characters) 66 | str2 = ''.join(characters) 67 | return str1, str2 68 | 69 | 70 | def time_test(str1, str2): 71 | start = time.time() 72 | assert are_anagrams(str1, str2) 73 | print('Time:', time.time() - start) 74 | start = time.time() 75 | assert are_anagrams_fast(str1, str2) 76 | print('Time:', time.time() - start) 77 | 78 | 79 | test(are_anagrams) 80 | test(are_anagrams_fast) 81 | str1, str2 = create_random_anagrams() 82 | time_test(str1, str2) 83 | -------------------------------------------------------------------------------- /lc_zigzag_convert.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is from leetcode.com 3 | https://leetcode.com/problems/zigzag-conversion/description/ 4 | 5 | The string "PAYPALISHIRING" is written in a zigzag pattern on 6 | a given number of rows like this: 7 | (you may want to display this pattern in a fixed font for better legibility) 8 | 9 | P A H N 10 | A P L S I I G 11 | Y I R 12 | And then read line by line: "PAHNAPLSIIGYIR" 13 | Write the code that will take a string and make this conversion 14 | given a number of rows: 15 | 16 | string convert(string text, int nRows); 17 | 18 | convert("PAYPALISHIRING", 3) should return "PAHNAPLSIIGYIR". 19 | 20 | My solution: 21 | I just worked out the formula to convert the index of the original string 22 | to the index of the new zigzag string. 23 | 24 | There are two versions: 25 | The first one I like, but is rejected by leetcode for being too slow. 26 | The secone one is the hacky one that is accepted by leetcode 27 | ''' 28 | 29 | 30 | class Solution(object): 31 | ''' 32 | This solution is not accepted by leetcode for being too slow. 33 | ''' 34 | 35 | def convert(self, s, numRows): 36 | """ 37 | :type s: str 38 | :type numRows: int 39 | :rtype: str 40 | """ 41 | line = '' 42 | skip = 2 * numRows - 2 43 | for i in range(numRows): 44 | idx = set([i]) 45 | if i != 0 and i != numRows - 1: 46 | idx.add(skip - i) 47 | line += ''.join([s[j] for j in range(len(s)) if j % skip in idx]) 48 | return line 49 | 50 | 51 | class Solution(object): 52 | ''' 53 | This solution is accepted by leetcode. 54 | ''' 55 | 56 | def convert(self, s, numRows): 57 | """ 58 | :type s: str 59 | :type numRows: int 60 | :rtype: str 61 | """ 62 | if numRows <= 1: 63 | return s 64 | if len(s) <= numRows: 65 | return s 66 | line = [] 67 | skip = 2 * numRows - 2 68 | intervals = len(s) / skip 69 | for i in range(numRows): 70 | mods = [i] 71 | if i != 0 and i != numRows - 1: 72 | if skip - i < len(s): 73 | mods.append(skip - i) 74 | idx = [j * skip + mod for j in range(intervals) for mod in mods] 75 | for mod in mods: 76 | if skip * intervals + mod < len(s): 77 | idx.append(skip * intervals + mod) 78 | line += [s[j] for j in idx] 79 | return ''.join(line) 80 | -------------------------------------------------------------------------------- /delim_balanced.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is taken from 3 | Stanford's CS 9: Problem-Solving for the CS Technical Interview. 4 | 5 | Write a function that takes as input a string and returns 6 | whether the parenthesis are "balanced". 7 | 8 | If the candidate solves this question quickly, add the following difficulty: 9 | the string may also contain "[]" or "{}". Return whether all three types of 10 | brackets are balanced. 11 | 12 | Example 13 | "(())", "(()())", or "()(()())" should return true 14 | "(()" or "())" should return false 15 | 16 | My solution: 17 | You can pass in a list of opening delimiters with 18 | a list of their corresponding closing delimiters. 19 | Returns True if all delims are balanced, False otherwise. 20 | 21 | The idea here is that everytime you encounter an opening delim of a 22 | certain type, increase the count for that delim by 1. 23 | When you encouter a closing delim: 24 | + decrease the count by 1 25 | + if the count now is negative, it's not balanced. 26 | 27 | ''' 28 | 29 | 30 | def is_balanced(string, delims=[], closes=[]): 31 | offsets = [0 for _ in delims] 32 | delims_map = {delims[i]: i for i in range(len(delims))} 33 | closes_map = {closes[i]: i for i in range(len(closes))} 34 | 35 | for char in string: 36 | if char in delims_map: 37 | offsets[delims_map[char]] += 1 38 | elif char in closes_map: 39 | offsets[closes_map[char]] -= 1 40 | if offsets[closes_map[char]] < 0: 41 | return False 42 | for i in offsets: 43 | if i != 0: 44 | return False 45 | return True 46 | 47 | 48 | assert not is_balanced('(()', delims=['('], closes=[')']) 49 | assert is_balanced('(())', delims=['('], closes=[')']) 50 | assert is_balanced('()(()())', delims=['('], closes=[')']) 51 | assert is_balanced('()(()())()', delims=['('], closes=[')']) 52 | assert is_balanced('(()())', delims=['('], closes=[')']) 53 | assert not is_balanced('(()))', delims=['('], closes=[')']) 54 | assert not is_balanced('(()))', delims=['(', '{'], closes=[')', '}']) 55 | assert not is_balanced('(())){', delims=['(', '{'], closes=[')', '}']) 56 | assert not is_balanced('(({)))}', delims=['(', '{'], closes=[')', '}']) 57 | assert is_balanced('(({))()}', delims=['(', '{'], closes=[')', '}']) 58 | assert not is_balanced('[(', delims=['(', '{', '['], closes=[')', '}', ']']) 59 | assert is_balanced('()({(})([]))[()]', 60 | delims=['(', '{', '['], 61 | closes=[')', '}', ']']) 62 | assert not is_balanced('()({(}{)([]))[()]', 63 | delims=['(', '{', '['], 64 | closes=[')', '}', ']']) 65 | -------------------------------------------------------------------------------- /linked_list.py: -------------------------------------------------------------------------------- 1 | ''' 2 | An unsorted linked list. Nothing fancy here. 3 | 4 | Methods available: 5 | insert(value): insert a value into the linked list 6 | remove(value): remove the first occurrence of the value in the list. 7 | Raise ValueError if that value doesn't exist 8 | Extra usage: 9 | value in ll: check if a value is in the list 10 | list(ll): list all values in the list 11 | 12 | ''' 13 | 14 | 15 | class Node(object): 16 | __slots__ = ('value', 'next') 17 | 18 | def __init__(self, value, next=None): 19 | self.value = value 20 | self.next = next 21 | 22 | 23 | class LinkedList(object): 24 | def __init__(self): 25 | self._head = None 26 | self._tail = None 27 | self._len = 0 28 | 29 | def insert(self, value): 30 | node = Node(value) 31 | if not self._tail: 32 | self._head = self._tail = node 33 | else: 34 | self._tail.next = node 35 | self._tail = node 36 | self._len += 1 37 | 38 | def remove(self, value): 39 | node, prev, found = self._find_value(self._head, None, value) 40 | if not node: 41 | raise ValueError() 42 | if prev: 43 | prev.next = node.next 44 | else: # we're removing the head 45 | self._head = node.next 46 | 47 | if not node.next: # the node we remove is the tail of the list 48 | self._tail = prev 49 | 50 | self._len -= 1 51 | 52 | def __contains__(self, value): 53 | _, _, found = self._find_value(self._head, None, value) 54 | return found 55 | 56 | def __len__(self): 57 | return self._len 58 | 59 | def __iter__(self): 60 | yield from self._iter(self._head) 61 | 62 | def _iter(self, node): 63 | if node: 64 | yield node.value 65 | yield from self._iter(node.next) 66 | 67 | def _find_value(self, curr, prev, value): 68 | while curr: 69 | if curr.value == value: 70 | return curr, prev, True 71 | prev = curr 72 | curr = curr.next 73 | return curr, prev, False 74 | 75 | 76 | def test_linkedlist(): 77 | ll = LinkedList() 78 | print(len(ll)) 79 | values = [2, 3, 2, 3, 5, -10] 80 | for value in values: 81 | ll.insert(value) 82 | print(list(ll)) 83 | print(len(ll)) 84 | 85 | for value in values: 86 | ll.remove(value) 87 | print(list(ll)) 88 | print(len(ll)) 89 | 90 | values = [-100, 23, 3, 2, 1, -10] 91 | for value in values: 92 | ll.insert(value) 93 | print(list(ll)) 94 | print(len(ll)) 95 | 96 | 97 | test_linkedlist() 98 | -------------------------------------------------------------------------------- /binary_heap.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A binary heap is a complete binary tree which satisfies 3 | the min-heap ordering property. 4 | The value of each node is greater than or equal to the value of its parent, 5 | with the minimum-value element at the root. 6 | 7 | Methods available: 8 | insert(value): insert a value into the binary heap 9 | peek_min(): peek the next smallest value 10 | extract_min(): return the next smallest value and remove it from the heap. 11 | Raise ValueError if that value doesn't exist 12 | is_empty(): returns True if the heap is empty, False otherwise 13 | 14 | Extra usage: 15 | iter(bh): return iteration to bh 16 | list(bh): list all values in the heap 17 | in a pre-order traversal (root, left, right) 18 | 19 | BinaryHeap can be used to build a priority queue and to do heap sort algorithm 20 | 21 | insert extract_min peek_min 22 | binary heap O(log n) O(log n) O(1) 23 | ''' 24 | import random 25 | 26 | 27 | class BinaryHeap(object): 28 | def __init__(self, arr=None): 29 | self._list = [0] 30 | if arr: 31 | for value in arr: 32 | self.insert(value) 33 | 34 | def insert(self, value): 35 | self._list.append(value) 36 | self._bubble_up(len(self._list) - 1) 37 | 38 | def peek_min(self): 39 | if len(self._list) == 1: 40 | raise ValueError('Empty') 41 | return self._list[1] 42 | 43 | def extract_min(self): 44 | if len(self._list) == 1: 45 | raise ValueError('Empty') 46 | value = self._list[1] 47 | self._swap(1, -1) 48 | self._list = self._list[:-1] 49 | self._bubble_down(1) 50 | return value 51 | 52 | def is_empty(self): 53 | return len(self._list) == 1 54 | 55 | def __len__(self): 56 | return len(self._list) - 1 57 | 58 | def __iter__(self): 59 | yield from iter(self._list[1:]) 60 | 61 | def _swap(self, idx1, idx2): 62 | temp = self._list[idx1] 63 | self._list[idx1] = self._list[idx2] 64 | self._list[idx2] = temp 65 | 66 | def _bubble_down(self, idx): 67 | while 2 * idx < len(self._list): # has at least one child 68 | if len(self._list) == 2 * idx + 1: 69 | min_child = 2 * idx 70 | else: 71 | if self._list[2 * idx] < self._list[2 * idx + 1]: 72 | min_child = 2 * idx 73 | else: 74 | min_child = 2 * idx + 1 75 | self._swap(min_child, idx) 76 | idx = min_child 77 | 78 | def _bubble_up(self, idx): 79 | parent = idx // 2 80 | while idx > 1 and self._list[idx] < self._list[parent]: 81 | self._swap(parent, idx) 82 | idx = parent 83 | parent = idx // 2 84 | 85 | 86 | def test_heap(): 87 | bh = BinaryHeap() 88 | values = random.sample(range(-15, 15), 30) 89 | for v in values: 90 | bh.insert(v) 91 | print(list(bh)) 92 | 93 | for v in iter(bh): 94 | print(v) 95 | 96 | 97 | test_heap() 98 | -------------------------------------------------------------------------------- /lc_median_arrays.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This problem is from leetcode 3 | https://leetcode.com/problems/median-of-two-sorted-arrays/description/ 4 | 5 | There are two sorted arrays nums1 and nums2 of size m and n respectively. 6 | 7 | Find the median of the two sorted arrays. The overall run time complexity 8 | should be O(log (m+n)). 9 | 10 | Example 1: 11 | nums1 = [1, 3] 12 | nums2 = [2] 13 | The median is 2.0 14 | 15 | Example 2: 16 | nums1 = [1, 2] 17 | nums2 = [3, 4] 18 | The median is (2 + 3)/2 = 2.5 19 | 20 | My solution: 21 | I know it's super ugly. I will update it when I can come up 22 | with a better solution. 23 | This solution was accepted by leetcode 24 | ''' 25 | 26 | import math 27 | 28 | 29 | class Solution(object): 30 | def median(self, nums): 31 | n = len(nums) 32 | if n == 0: 33 | return None 34 | if n % 2 == 0: 35 | return (nums[n // 2] + nums[n // 2 - 1]) / 2 36 | else: 37 | return nums[n // 2] 38 | 39 | def median3(self, num1, num2, num3): 40 | if num1 <= num2: 41 | return num2 42 | if num1 <= num3: 43 | return num1 44 | return num3 45 | 46 | def median4(self, num1, num2, num3, num4): 47 | if num1 <= num2: 48 | return (num2 + num3) / 2 49 | if num1 <= num4: 50 | return (num1 + num3) / 2 51 | return (num3 + num4) / 2 52 | 53 | def median6(self, a1, a2, b1, b2, b3, b4): 54 | if a2 <= b1: 55 | return (b1 + b2) / 2 56 | if a1 <= b1 and a2 <= b3: 57 | return (a2 + b2) / 2 58 | if (a1 <= b2 and a2 > b3): 59 | return (b2 + b3) / 2 60 | if a1 < b2 and a2 <= b3: 61 | return (a2 + b2) / 2 62 | if a1 > b2 and a2 <= b3: 63 | return (a1 + a2) / 2 64 | if a1 <= b4 and a2 > b3: 65 | return (a1 + b3) / 2 66 | return (b3 + b4) / 2 67 | 68 | def findMedianHelper(self, nums1, nums2): 69 | m = len(nums1) 70 | n = len(nums2) 71 | if m == 0: 72 | return self.median(nums2) 73 | if m == 1: 74 | if n == 1: 75 | return (nums1[0] + nums2[0]) / 2 76 | if n % 2 == 0: 77 | return self.median3(nums1[0], nums2[n // 2 - 1], nums2[n // 2]) 78 | return self.median4(nums1[0], 79 | nums2[n // 2 - 1], 80 | nums2[n // 2], 81 | nums2[n // 2 + 1]) 82 | 83 | if m <= 2: 84 | if n <= 3: 85 | return self.median(sorted(nums1 + nums2)) 86 | else: 87 | if n % 2 == 1: 88 | temp = sorted(nums1 + [nums2[n // 2 - 1], 89 | nums2[n // 2], 90 | nums2[n // 2 + 1]]) 91 | return self.median(temp) 92 | else: 93 | return self.median6(nums1[0], 94 | nums1[1], 95 | nums2[n // 2 - 2], 96 | nums2[n // 2 - 1], 97 | nums2[n // 2], 98 | nums2[n // 2 + 1]) 99 | 100 | mid1 = int(math.ceil(m / 2) - 1) 101 | mid2 = n // 2 102 | 103 | if nums1[mid1] <= nums2[mid2]: 104 | return self.findMedianHelper(nums1[mid1:], nums2[:-mid1]) 105 | else: 106 | return self.findMedianHelper( 107 | nums1[:mid1 + 1], nums2[m - mid1 - 1:]) 108 | 109 | def findMedianSortedArrays(self, nums1, nums2): 110 | """ 111 | :type nums1: List[int] 112 | :type nums2: List[int] 113 | :rtype: float 114 | """ 115 | if len(nums1) <= len(nums2): 116 | return self.findMedianHelper(nums1, nums2) 117 | else: 118 | return self.findMedianHelper(nums2, nums1) 119 | 120 | 121 | solver = Solution() 122 | assert solver.findMedianSortedArrays([1, 3], [2]) == 2 123 | assert solver.findMedianSortedArrays([1, 3, 5], [2]) == 2.5 124 | assert solver.findMedianSortedArrays([1], [2]) == 1.5 125 | assert solver.findMedianSortedArrays([1, 3, 5], [2, 6]) == 3 126 | assert solver.findMedianSortedArrays([1, 3, 3, 10, 11], [1, 2]) == 3 127 | -------------------------------------------------------------------------------- /binary_search_tree.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This binary search tree allows duplicate values 3 | All values on the left subtree are less than or equal to the root's value. 4 | All values on the right subtree are greater than the root's value. 5 | 6 | Methods available: 7 | insert(value): insert a value into the binary search tree 8 | remove(value): remove the first occurrence of the value in the bst. 9 | Raise ValueError if that value doesn't exist 10 | 11 | Extra usage: 12 | value in bst: check if a value is in the binary search tree 13 | list(bst): list all values of the tree in an in-order traversal 14 | ''' 15 | 16 | import random 17 | 18 | 19 | class Node(object): 20 | __slots__ = ('value', 'left', 'right') 21 | 22 | def __init__(self, value, left=None, right=None): 23 | self.value = value 24 | self.left = left 25 | self.right = right 26 | 27 | 28 | class BST(object): 29 | def __init__(self): 30 | self._root = None 31 | self._count = 0 32 | 33 | def insert(self, value): 34 | parent, prev = self._find_parent(self._root, None, value) 35 | if not parent: 36 | self._root = Node(value) 37 | else: 38 | if value <= parent.value: 39 | parent.left = Node(value) 40 | else: 41 | parent.right = Node(value) 42 | self._count += 1 43 | 44 | def remove(self, value): 45 | curr, parent = self._find_parent(self._root, None, value, True) 46 | if (not curr or curr.value != value): 47 | raise ValueError() 48 | if not parent: # it's the root 49 | self._root = self._remove_root(self._root, True) 50 | else: 51 | if parent.left and parent.left.value == value: 52 | parent.left = self._remove_root(curr, True) 53 | else: 54 | parent.right = self._remove_root(curr, False) 55 | self._count -= 1 56 | 57 | def __contains__(self, value): 58 | curr, _ = self._find_parent(self._root, None, value, True) 59 | return curr and curr.value == value 60 | 61 | def __len__(self): 62 | return self._count 63 | 64 | def __iter__(self): 65 | ''' 66 | Traverse the tree in order 67 | ''' 68 | yield from self._iter(self._root) 69 | 70 | def _remove_root(self, root, left=True): 71 | if not root.left and not root.right: 72 | return None 73 | if not root.left: 74 | return root.right 75 | if not root.right: 76 | return root.left 77 | 78 | prev = root 79 | if left: 80 | curr = root.left 81 | if not curr.right: 82 | root.value = curr.value 83 | root.left = curr.left 84 | else: 85 | while curr.right: 86 | prev = curr 87 | curr = curr.right 88 | root.value = curr.value 89 | prev.right = curr.left 90 | else: 91 | curr = root.right 92 | if not curr.left: 93 | root.value = curr.value 94 | root.right = curr.right 95 | else: 96 | while curr.left: 97 | prev = curr 98 | curr = curr.left 99 | root.value = curr.value 100 | prev.left = curr.right 101 | return root 102 | 103 | def _find_parent(self, node, prev, value, to_remove=False): 104 | if not node: 105 | return prev, None 106 | 107 | if to_remove: 108 | if value == node.value: 109 | return node, prev 110 | 111 | if value <= node.value: 112 | if node.left: 113 | node, prev = self._find_parent( 114 | node.left, node, value, to_remove) 115 | return node, prev 116 | 117 | if node.right: 118 | node, prev = self._find_parent(node.right, node, value, to_remove) 119 | 120 | return node, prev 121 | 122 | def _iter(self, node): 123 | if node: 124 | yield from self._iter(node.left) 125 | yield node.value 126 | yield from self._iter(node.right) 127 | 128 | 129 | def test_bst(): 130 | bst = BST() 131 | values = [] 132 | for i in range(100): 133 | value = random.randint(-10, 30) 134 | values.append(value) 135 | bst.insert(value) 136 | assert list(bst) == sorted(values) 137 | 138 | nums = random.sample(values, 40) 139 | for num in nums: 140 | bst.remove(num) 141 | assert len(bst) == len(values) - 1 142 | bst.insert(num) 143 | assert len(bst) == len(values) 144 | 145 | assert list(bst) == sorted(values) 146 | 147 | 148 | test_bst() 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains my implementation of useful data structures, algorithms, 2 | games, as well as my solutions to programming puzzles. 3 | 4 | Each item is marked with a difficulty level. 5 | 1 - easy 6 | 2 - medium 7 | 3 - hard 8 | 9 | If a file name starts with 'lc', it's a problem from leetcode.com 10 | 11 | Written in python3. Thanks Danijar Hafner (@danijar) for the inspiration. 12 | 13 | Data structures 14 | --------------- 15 | 16 | ### Linked lists 17 | 18 | - [x] List class (linked_list.py) - 1 19 | - [ ] Remove duplicates (linked_list_algos.py) 20 | - [ ] Find nth last element (linked_list_algos.py) 21 | - [ ] Remove node given only its object (linked_list_algos.py) 22 | - [ ] Sum linked lists with one digit per node (linked_list_algos.py) 23 | - [ ] Find beginning of circle (linked_list_algos.py) 24 | 25 | ### Trees 26 | 27 | - [x] Binary heap class (binary_heap.py) - 2 28 | - [ ] Binary tree class (binary_tree.py) - 1 29 | - [x] Binary search tree class that allows duplicate values (binary_search_tree.py) - 2 30 | 31 | BST class inherits binary tree class 32 | 33 | - [ ] Red black tree class (red_black_tree.py) 34 | - [ ] B+ tree class (b_tree.py) - 3 35 | - [x] Trie class that allows word removal (trie.py) - 2 36 | - [x] Check if a binary tree is a binary search tree (binary_tree_algos.py) - 2 37 | - [ ] Check if a binary tree is balanced (binary_tree_algos.py) 38 | - [ ] Find first common ancestor of two nodes in a binary tree (binary_tree_algos.py) 39 | - [ ] Get all nodes of depth n in a binary tree (binary_tree_algos.py) 40 | - [ ] Given two large trees, check if the first is a subtree of the second (binary_tree_algos.py) 41 | - [ ] Find all sub paths in a binary tree that sum up to x (binary_tree_algos.py) 42 | - [ ] Create a balanced binary search tree from a sorted array (bst_algos.py) 43 | 44 | ### Graphs 45 | 46 | - [ ] Undirected graph class 47 | - [ ] Directed graph class 48 | - [ ] Breadth first search (search.py) 49 | - [ ] Depth first search (search.py) 50 | - [ ] A-star (search.py) 51 | 52 | ### Stacks and queues 53 | 54 | - [ ] Queue class (queue.py) 55 | - [x] Heap priority queue (priority_queue.py) - 1 56 | - [ ] Stack class (stack.py) 57 | - [ ] Stack that finds min in O(1) (min_stack.py) 58 | - [ ] Solve Hanoi towers using stacks (stack_algos.py) 59 | - [ ] Sort stack using only push, pop, peek and empty (stack_algos.py) 60 | - [ ] Build a queue using two stacks (stack_algos.py) 61 | 62 | Algorithms 63 | ---------- 64 | 65 | ### Sorting 66 | - [x] Insertion sort (sort.py) - 1 67 | - [x] Selection sort (sort.py) - 1 68 | - [x] Merge sort (sort.py) - 2 69 | - [x] Heap sort (sort.py) - 2 70 | - [x] Quick sort (sort.py) - 2 71 | - [x] Counting sort (sort.py) - 2 72 | - [x] Radix sort (sort.py) - 2 73 | - [x] Bucket sort (sort.py) - 2 74 | 75 | ### Dynamic Programming 76 | - [ ] Computing a Fibonacci sequence 77 | - [ ] Find the longest common subsequence between two strings 78 | 79 | ### Recursion 80 | 81 | - [ ] Find all permutations of a string 82 | - [ ] Find all subsets of a set 83 | - [ ] Find all proper combinations of n parentheses 84 | - [ ] Bucket fill 85 | - [ ] Check if it's possible to make a change with a set of coins 86 | - [ ] Check if it's possible to weight two objects with a set of weights 87 | - [ ] Eight queen 88 | 89 | Programming Puzzles 90 | ------------------- 91 | 92 | ### String Manipulation 93 | - [x] Check if two strings are anagrams in O(n + m) time (anagrams.py) - 1 94 | - [x] Find the longest palindromic substring in O(n^2) time (lc_longest_palindrome.py) - 2 95 | - [x] Check if a string has balanced delimiters in O(n) time (delim_balanced.py) - 2 96 | - [x] Reverse words while maintaining spaces and tabs in O(n) time (reverse_words.py) - 2 97 | - [x] Longest substring without repeating characters in O(n) time (lc_longest_nonrepeat_substring.py) - 2 98 | - [x] Longest contiguous substring of 2 distinct characters in O(n) time (substring_two_chars.py) - 2 99 | - [ ] Remove duplicate chars in a string 100 | - [ ] Encode and decode Caesar cipher (caesar.py) 101 | - [ ] Check if a string is a rotation of another 102 | 103 | ### Mathematical 104 | - [x] Reverse integers (lc_reverse_int.py) - 1 105 | - [x] Sieve of Eratosthenes (sieve_prime.py) - 1 106 | - [x] Two sum in O(n) time (lc_two_sum.py) - 1 107 | 108 | Given an array of integers, return indices of the two numbers 109 | such that they add up to a specific target. 110 | 111 | - [x] Year with maximum population (year_max_pop.py) - 1 112 | 113 | Given a list of people with their years of birth and death, 114 | find the year with max population 115 | 116 | - [x] FizzBuzz (fizzbuzz.py) - 1 117 | - [x] ZigZag conversion (lc_zigzag_convert.py) - 2 118 | - [x] Find sum of all subarrays of an array (subarray_sum.py) - 2 119 | - [x] Add two numbers, each represented by a reverse linked list (lc_add_number_reverse.py) - 2 120 | - [x] Find the median of two sorted arrays in O(log(m+n)) time (lc_median_arrays.py) - 3 121 | - [ ] Find nth smallest number that can be created using a list of prime factors 122 | - [ ] Count occurences of given digit in all numbers up to n 123 | - [ ] Rotate N x N matrix in place 124 | 125 | ### Games 126 | - [x] Game of ghost (ghost.py) - 3 127 | 128 | Ghost is a word game in which players take turns adding letters to a 129 | growing word fragment, trying not to be the one to complete a valid word. 130 | 131 | This implementation uses minimax with alpha-beta pruning to make sure the computer always wins. 132 | It uses a Trie to keep track of the dictionary. 133 | -------------------------------------------------------------------------------- /trie.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implementation of a trie that allows you to remove words 3 | 4 | If initialized with a dictionary file, it will create a trie 5 | and insert all the words in the file into the trie. 6 | 7 | Methods available: 8 | insert(word): insert a value into the trie 9 | remove(word): remove a word from the trie and remove redundant nodes 10 | Raise ValueError if that value doesn't exist 11 | If word is only a prefix (not a word), do nothing. 12 | has_prefix(prefix): return True if prefix is a prefix in the trie 13 | has_word(word): return True if word is a word in the trie 14 | words_with_prefix(prefix): return a generator of words with the prefix 15 | 16 | Extra usage: 17 | list(trie): list all the words in the trie 18 | ''' 19 | __all__ = ['Node', 'Trie'] 20 | import os 21 | 22 | 23 | class Node(object): 24 | __slots__ = ('value', 'marks_end', 'children', 'parent') 25 | 26 | def __init__(self, char='', end=False, parent=None): 27 | self.value = char 28 | self.marks_end = end 29 | self.children = dict() 30 | self.parent = parent 31 | 32 | def add_child(self, char, end=False): 33 | self.children[char] = Node(char, end, self) 34 | 35 | def remove_child(self, char): 36 | del self.children[char] 37 | 38 | @property 39 | def num_children(self): 40 | return len(self.children) 41 | 42 | def is_root(self): 43 | return not self.parent 44 | 45 | def __repr__(self): 46 | if self.is_root(): 47 | strings = ['Root rode'] 48 | else: 49 | strings = [f'Char: {self.value}'] 50 | strings.append(f'Parent: {self.parent.value}') 51 | if self.num_children > 0: 52 | strings.append('Children: ' + ', '.join(self.children.keys())) 53 | return '. '.join(strings) 54 | 55 | def __getitem__(self, char): 56 | return self.children[char] 57 | 58 | 59 | class Trie(object): 60 | def __init__(self, dict_file=None): 61 | self._root = Node() 62 | self.count = 0 63 | if dict_file is not None: 64 | if not os.path.isfile(dict_file): 65 | raise ValueError(f"{dict_file} doesn't exist") 66 | lines = open(dict_file, 'r').readlines() 67 | words = [line.strip() for line in lines] 68 | for word in words: 69 | self.insert(word) 70 | 71 | def insert(self, word): 72 | curr = self._root 73 | for char in word: 74 | if char not in curr.children: 75 | curr.add_child(char) 76 | curr = curr[char] 77 | 78 | if curr.marks_end: 79 | print(f'{word} already exists.') 80 | else: 81 | curr.marks_end = True 82 | self.count += 1 83 | 84 | def remove(self, word): 85 | found, node = self._find_end_node(word) 86 | if found and node.marks_end: 87 | node.marks_end = False 88 | self._remove_nodes(node) 89 | self.count -= 1 90 | else: 91 | print(f"{word} doesn't exist") 92 | 93 | def has_prefix(self, prefix): 94 | found, _ = self._find_end_node(prefix) 95 | return found 96 | 97 | def has_word(self, word): 98 | found, node = self._find_end_node(word) 99 | return found and node.marks_end 100 | 101 | def get_children(self, prefix): 102 | found, node = self._find_end_node(prefix) 103 | if not found or not node: 104 | return [] 105 | return list(node.children.keys()) 106 | 107 | def words_with_prefix(self, prefix): 108 | found, node = self._find_end_node(prefix) 109 | if not found: 110 | print(f'Prefix {prefix} does not exist.') 111 | return [] 112 | return list(self._iter(node, prefix)) 113 | 114 | def __len__(self): 115 | return self.count 116 | 117 | def __iter__(self): 118 | yield from self._iter(self._root, '') 119 | 120 | def _iter(self, node, prefix): 121 | if node.marks_end: 122 | yield prefix 123 | for child in node.children: 124 | yield from self._iter(node[child], prefix + child) 125 | 126 | def _find_end_node(self, token): 127 | curr = self._root 128 | for char in token: 129 | if char not in curr.children: 130 | return False, curr 131 | curr = curr[char] 132 | return True, curr 133 | 134 | def _remove_nodes(self, curr): 135 | while curr: 136 | if curr.is_root or curr.num_children > 1 or curr.marks_end: 137 | return 138 | curr.parent.remove_child(curr.value) 139 | curr = curr.parent 140 | 141 | 142 | def test_trie(): 143 | trie = Trie() 144 | words = ['haha', 'you', 'a', 'no', 'hit', 'hi', 'this', 145 | 'harm', 'horn', 'hotel', 'verylongword', 'cs2014', 146 | 'hotel', 'hot'] 147 | print(len(words)) 148 | for word in words: 149 | trie.insert(word) 150 | print(len(trie), list(trie)) 151 | print(trie.has_prefix('hah')) 152 | print(trie.has_word('hah')) 153 | print(trie.has_word('haha')) 154 | print(trie.has_prefix('ab')) 155 | print(trie.has_prefix('')) 156 | trie.remove('ho') 157 | print('removed ho', len(trie), list(trie)) 158 | trie.remove('haha') 159 | print('removed haha', len(trie), list(trie)) 160 | trie.remove('cs2014') 161 | print('remove cs2014', len(trie), list(trie)) 162 | trie.remove('hit') 163 | print('removed hit', len(trie), list(trie)) 164 | trie.remove('hot') 165 | print('remove hot', len(trie), list(trie)) 166 | print('has prefix hah', trie.has_prefix('hah')) 167 | print('has prefix haha', trie.has_word('haha')) 168 | print('has prefix hi', trie.has_prefix('hi')) 169 | print('has prefix hot', trie.has_prefix('hot')) 170 | print('has word hot', trie.has_word('hot')) 171 | print('words with prefix h', trie.words_with_prefix('h')) 172 | print('words with prefix a', trie.words_with_prefix('a')) 173 | print('words with prefix hong', trie.words_with_prefix('hong')) 174 | 175 | trie = Trie('data/dictionary.txt') 176 | print('words with prefix hong', trie.words_with_prefix('hong')) 177 | print('words with prefix bet', len(trie.words_with_prefix('bet'))) 178 | print('next chars with prefix bet', trie.get_children('bet')) 179 | 180 | 181 | if __name__ == '__main__': 182 | test_trie() 183 | -------------------------------------------------------------------------------- /sort.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import sys 4 | 5 | from binary_heap import BinaryHeap 6 | 7 | 8 | def insertion_sort(arr): 9 | for j in range(1, len(arr)): 10 | key = arr[j] 11 | i = j - 1 12 | while i >= 0 and arr[i] > key: 13 | arr[i + 1] = arr[i] 14 | i -= 1 15 | arr[i + 1] = key 16 | return arr 17 | 18 | 19 | def selection_sort(arr): 20 | for i in range(len(arr) - 1): 21 | min_idx = i 22 | for j in range(i + 1, len(arr)): 23 | if arr[j] < arr[min_idx]: 24 | min_idx = j 25 | arr = _swap(arr, i, min_idx) 26 | return arr 27 | 28 | 29 | def merge_sort(arr): 30 | ''' 31 | As described in "Introduction to Algorithms" (CLRS book) 32 | Our MERGE procedure takes time O(n), 33 | where n = r - p + 1 is the number of elements being merged. 34 | merge_sort runs in O(nlogn) 35 | ''' 36 | return _merge_sort_helper(arr, 0, len(arr) - 1) 37 | 38 | 39 | def heap_sort(arr): 40 | ''' 41 | This consists of 2 steps: 42 | 1. build a min heap, which is O(nlogn) 43 | 2. extract all n elements of the heap, which is O(nlogn) 44 | Overall, this takes O(nlogn) 45 | ''' 46 | heap = BinaryHeap(arr) 47 | result = [] 48 | while not heap.is_empty(): 49 | result.append(heap.extract_min()) 50 | return result 51 | 52 | 53 | def quick_sort(arr): 54 | ''' 55 | As described in "Introduction to Algorithms" (CLRS book) 56 | ''' 57 | return _quick_sort_helper(arr, 0, len(arr) - 1) 58 | 59 | 60 | def counting_sort(arr, upper=None, lower=0): 61 | ''' 62 | As described in "Introduction to Algorithms" (CLRS book) 63 | Only works for arrays whose values are within a range (min, max) 64 | O(n) 65 | ''' 66 | if len(arr) <= 1: 67 | return arr 68 | if not upper: 69 | lower, upper = _find_bounds(arr) 70 | c = [0 for _ in range(lower, upper + 1)] 71 | for value in arr: 72 | c[value - lower] += 1 73 | for i in range(1, upper - lower + 1): 74 | c[i] += c[i - 1] 75 | b = arr[:] 76 | for i in range(len(arr) - 1, -1, -1): 77 | b[c[arr[i] - lower] - 1] = arr[i] 78 | c[arr[i] - lower] -= 1 79 | return b 80 | 81 | 82 | def radix_sort(arr, d): 83 | ''' 84 | As described in "Introduction to Algorithms" (CLRS book) 85 | the following procedure assumes that each element inthe n-element array A 86 | has d digits, where digit 1 is the lowest-order digit and digit d is the 87 | highest-order digit. 88 | 89 | We use counting sort as a stable subroutine for radix sort. 90 | ''' 91 | i = 10 92 | for i in range(1, d + 1): 93 | arr = _counting_sort_on_digit(arr, i) 94 | return arr 95 | 96 | 97 | def bucket_sort(arr, buckets=10): 98 | ''' 99 | As described in "Introduction to Algorithms" (CLRS book) 100 | Bucket sort runs in O(n) when input is drawn from a uniform distribution. 101 | 102 | The idea of bucket sort is to divide the interval [0, 1) into n equal-sized 103 | buckets, and then distribute the n input numbers into the buckets. 104 | Since the inputs are uniformly distributed over [0, 1), we don't expect 105 | many numbers to fall into each bucket. 106 | 107 | We then simply sort the numbers in each bucket and go through the buckets 108 | in order, listing the elements in each. 109 | ''' 110 | b = [[] for _ in range(buckets)] 111 | for value in arr: 112 | b[int(value * buckets)].append(value) 113 | 114 | result = [] 115 | for i in range(buckets): 116 | b[i] = insertion_sort(b[i]) 117 | result.extend(b[i]) 118 | 119 | return result 120 | 121 | 122 | def _swap(arr, i, j): 123 | temp = arr[i] 124 | arr[i] = arr[j] 125 | arr[j] = temp 126 | return arr 127 | 128 | 129 | def _partition(arr, p, r): 130 | x = arr[r] 131 | i = p - 1 132 | for j in range(p, r): 133 | if arr[j] <= x: 134 | i += 1 135 | arr = _swap(arr, i, j) 136 | arr = _swap(arr, i + 1, r) 137 | 138 | return i + 1, arr 139 | 140 | 141 | def _quick_sort_helper(arr, p, r): 142 | if p < r: 143 | q, arr = _partition(arr, p, r) 144 | arr = _quick_sort_helper(arr, p, q - 1) 145 | arr = _quick_sort_helper(arr, q + 1, r) 146 | return arr 147 | 148 | 149 | def _merge(arr, p, q, r): 150 | left = arr[p: q + 1] + [float('inf')] 151 | right = arr[q + 1: r + 1] + [float('inf')] 152 | i = j = 0 153 | for k in range(p, r + 1): 154 | if left[i] < right[j]: 155 | arr[k] = left[i] 156 | i += 1 157 | else: 158 | arr[k] = right[j] 159 | j += 1 160 | return arr 161 | 162 | 163 | def _merge_sort_helper(arr, p, r): 164 | if p < r: 165 | q = (p + r) // 2 166 | arr = _merge_sort_helper(arr, p, q) 167 | arr = _merge_sort_helper(arr, q + 1, r) 168 | arr = _merge(arr, p, q, r) 169 | return arr 170 | 171 | 172 | def _find_bounds(arr): 173 | lower = float('inf') 174 | upper = float('-inf') 175 | for value in arr: 176 | if value < lower: 177 | lower = value 178 | if value > upper: 179 | upper = value 180 | return lower, upper 181 | 182 | 183 | def _counting_sort_on_digit(arr, digit): 184 | div = 10 ** (digit - 1) 185 | c = [0 for _ in range(10)] 186 | for value in arr: 187 | digit = (value // div) % 10 188 | c[digit] += 1 189 | for i in range(1, 10): 190 | c[i] += c[i - 1] 191 | 192 | b = arr[:] 193 | for i in range(len(arr) - 1, -1, -1): 194 | digit = (arr[i] // div) % 10 195 | b[c[digit] - 1] = arr[i] 196 | c[digit] -= 1 197 | return b 198 | 199 | 200 | arrs = [[1, -2, 2, 30, 2, 10, 2, 2, 1], 201 | [], 202 | [1], 203 | [1, 3, -1], 204 | [2, 3, 2, 5, 6, 5], 205 | [10], 206 | [100, 123, 880, 231, 239, 293, 591, 942, 704, 101, 809]] 207 | 208 | 209 | def test(): 210 | for arr in arrs: 211 | print(insertion_sort(arr)) 212 | print(selection_sort(arr)) 213 | print(merge_sort(arr)) 214 | print(heap_sort(arr)) 215 | print(quick_sort(arr)) 216 | print(counting_sort(arr)) 217 | print(radix_sort(arrs[4], 3)) 218 | print(radix_sort(arrs[6], 1)) 219 | arr = [random.random() for _ in range(100)] 220 | a = bucket_sort(arr, 12) 221 | for i in range(1, len(a)): 222 | assert a[i - 1] <= a[i] 223 | 224 | 225 | test() 226 | -------------------------------------------------------------------------------- /ghost.py: -------------------------------------------------------------------------------- 1 | """ Ghost is a word game in which players take turns adding letters to a 2 | growing word fragment, trying not to be the one to complete a valid word. 3 | 4 | You lose if you have made a complete word or the letter you added 5 | makes the current word fragment a prefix that doesn't exist. 6 | 7 | This algorithm uses min max strategy with alpha-beta pruning to always 8 | beat humans in the game of Ghost. 9 | 10 | This is a fun exercise to use Trie. 11 | 12 | I wrote this in my 2nd year in college for fun so 13 | please don't judge me too harsh. 14 | """ 15 | 16 | import random 17 | import sys 18 | 19 | from trie import Trie 20 | 21 | HUMAN = 0 22 | COMP = 1 23 | DISCOUNT = 2 24 | REWARD = 100 25 | 26 | 27 | class Ghost: 28 | def __init__(self, dictionary): 29 | self.dictionary = dictionary 30 | self.curr_word = '' 31 | 32 | def reset(self): 33 | self.curr_word = '' 34 | 35 | def human_play(self): 36 | char = input("Enter your letter: ").strip() 37 | while not char or len(char) > 1 or not char.isalpha(): 38 | char = input("Please enter a valid character: ").strip() 39 | self.curr_word += char 40 | 41 | def computer_play(self): 42 | char = self.get_char() 43 | print(f'Current string: {self.curr_word}\nComputer picked {char}') 44 | self.curr_word += char 45 | 46 | def get_char(self): 47 | """ Pick the most promising next character to play for computer 48 | """ 49 | def recurse(curr_word, dictionary, player, alpha, beta): 50 | if self.must_end(curr_word): 51 | if player == HUMAN: 52 | return (-REWARD, None) 53 | return (REWARD, None) 54 | 55 | legal_chars = dictionary.get_children(curr_word) 56 | if not legal_chars: 57 | if player == HUMAN: 58 | return (REWARD, None) 59 | return (-REWARD, None) 60 | 61 | best_indices = [] 62 | # This is where the min max happens 63 | if player == COMP: # maximize the computer's utility 64 | next_word = curr_word + legal_chars[0] 65 | alpha = recurse(next_word, dictionary, HUMAN, alpha, beta)[0] 66 | best_indices.append(0) 67 | for index in range(1, len(legal_chars)): 68 | next_word = curr_word + legal_chars[index] 69 | score = recurse(next_word, dictionary, 70 | HUMAN, alpha, beta)[0] 71 | if score > alpha: 72 | best_indices = [index] 73 | alpha = score 74 | if alpha > beta: 75 | break 76 | if score == alpha: 77 | best_indices.append(index) 78 | best_idx = random.choice(best_indices) 79 | return (alpha, legal_chars[best_idx]) 80 | else: # minimize the computer's utility 81 | next_word = ''.join((curr_word, legal_chars[0])) 82 | """ add DISCOUNT to score so that if computer must lose, 83 | it would take more steps to lose (make it harder for users to win) 84 | """ 85 | beta = DISCOUNT + recurse(next_word, 86 | dictionary, 87 | COMP, 88 | alpha, 89 | beta)[0] 90 | best_indices.append(0) 91 | for index in range(1, len(legal_chars)): 92 | next_word = curr_word + legal_chars[index] 93 | score = DISCOUNT + recurse(next_word, 94 | dictionary, 95 | COMP, 96 | alpha, 97 | beta)[0] 98 | if score < beta: 99 | best_indices = [index] 100 | beta = score 101 | if alpha > beta: 102 | break 103 | if score == beta: 104 | best_indices.append(index) 105 | best_idx = random.choice(best_indices) 106 | return (beta, legal_chars[best_idx]) 107 | 108 | _, char = recurse(self.curr_word, 109 | self.dictionary, 110 | COMP, 111 | -float('inf'), 112 | float('inf')) 113 | return char 114 | 115 | def check_result(self, prefix, player): 116 | result = self.must_end(prefix) 117 | if not result: 118 | return 0 119 | if result == -1: 120 | print(f'No word starts with {prefix}.') 121 | elif result == 1: 122 | print(f'{prefix} is a legitimate word.') 123 | if player == HUMAN: 124 | print('Puny human you lost!') 125 | else: 126 | print('This is impossible. You won!') 127 | return result 128 | 129 | def must_end(self, prefix): 130 | if not self.dictionary.has_prefix(prefix): 131 | return -1 132 | if len(prefix) > 2 and self.dictionary.has_word(prefix): 133 | return 1 134 | return 0 135 | 136 | 137 | def intro(): 138 | print("Welcome to the game of Ghost.\n" 139 | "Players take turns adding letters to a growing word fragment, " 140 | "trying not to be the first one to complete a valid word.\n" 141 | "You lose if you have made a complete word or the letter you added " 142 | "makes the current word fragment a prefix that doesn't exist.") 143 | 144 | 145 | def main(): 146 | dictionary = Trie('data/dictionary.txt') 147 | intro() 148 | replay = True 149 | ghost = Ghost(dictionary) 150 | 151 | while replay: 152 | print('-' * 60) 153 | print("Let's start!\nYou go first.") 154 | ghost.human_play() 155 | while True: 156 | ghost.computer_play() 157 | if ghost.check_result(ghost.curr_word, COMP): 158 | break 159 | 160 | print("------------------------") 161 | print("Your turn\nCurrent string:", ghost.curr_word) 162 | ghost.human_play() 163 | if ghost.check_result(ghost.curr_word, HUMAN): 164 | break 165 | 166 | # Enter nothing or anything starts with 'n' to quit. 167 | ans = input('Do you want to play another game? (Y/N) ').lower().strip() 168 | if not ans or ans[0] == 'n': 169 | print('Thanks for playing!') 170 | replay = False 171 | ghost.reset() 172 | 173 | 174 | # run the program 175 | if __name__ == '__main__': 176 | main() 177 | --------------------------------------------------------------------------------